Skip to content

Connections

Your coding agent — Claude Code or Cursor — can call create_pull_request directly today, the moment its raw output looks plausible, no test evidence required. Connections are how you put that move behind a tests-passed gate.

A connection is a backend the gateway talks to: a CLI that runs npm test, the GitHub MCP server that opens the PR. You declare each backend once in the connections block, then a guarded capability wires them together — the PR move only fires when the test move has produced its evidence. Here’s the shape of it before the syntax:

proxy:
expose:
- name: safe.create_pr
description: Open a PR — only when tests have passed.
guards:
- kind: evidence
requires: [tests_passed] # no green suite, no PR
executor:
kind: mcp
connection: github # declared below
tool: create_pull_request

The agent can’t open the PR by asking nicely or by being confident. The tests_passed evidence has to exist first, and that evidence comes from a different connection — a CLI that ran your real suite. The rest of this guide is the three connection kinds that make that wiring possible: MCP servers, CLI commands, and REST APIs.

MCP connections speak the Model Context Protocol to another server. You can connect via stdio (launching a child process) or via Streamable HTTP (connecting to a running server).

The gateway spawns the process and communicates over stdin/stdout. This works with any MCP server — native binaries, npm packages via npx, Python packages via uvx, Docker containers, anything.

connections:
# Native binary on PATH
github:
kind: mcp
command: github-mcp-server
# npm-distributed MCP server via npx
filesystem:
kind: mcp
command: npx
args: [-y, "@modelcontextprotocol/server-filesystem", "/tmp"]
# Python-distributed MCP server via uvx
fetch:
kind: mcp
command: uvx
args: [mcp-server-fetch]
# Containerized MCP server
postgres:
kind: mcp
command: docker
args: [run, -i, --rm, -e, DATABASE_URL, mcp/postgres:latest]
env:
DATABASE_URL: postgres://localhost/app

The env block passes environment variables to the child process. The gateway doesn’t care what’s inside the container or package — if it speaks MCP over stdio, it works.

For MCP servers already running somewhere, connect via URL:

connections:
remote_server:
kind: mcp
url: https://mcp.example.com/v1

No command needed — the gateway connects to the existing server. Use this for shared MCP services, cloud-hosted servers, or anything you don’t want the gateway to manage the lifecycle of.

CLI connections run shell commands. The gateway spawns the process, passes arguments, and captures stdout. This is the kind that runs your test suite — the move that earns the evidence to open a PR.

connections:
cargo:
kind: cli
command: cargo
workingDirectory: /repo
shell:
kind: cli
command: /bin/bash
env:
PATH: /usr/local/bin:/usr/bin

The command is the binary to run (cargo, npm, pytest, /bin/bash). args on the executor (not the connection) supply the specific arguments for each call — so one cargo connection serves both cargo test and cargo build. workingDirectory sets where the process runs. env passes environment variables.

REST connections make HTTP requests to any API.

connections:
github_api:
kind: rest
baseUrl: https://api.github.com
headers:
Authorization: "Bearer ${GITHUB_TOKEN}"
Accept: application/vnd.github+json
ci:
kind: rest
baseUrl: https://api.your-ci.example.com
headers:
Authorization: "Bearer ${CI_TOKEN}"

The baseUrl is the root of the API. headers are sent with every request. Notice ${GITHUB_TOKEN} — environment variable interpolation works in header values, so you don’t put secrets directly in your config file.

The proxy.import block discovers tools from MCP connections at startup and turns each one into a proxy capability automatically. You don’t have to declare each tool by hand.

proxy:
import:
- connection: github
prefix: github
include: [list_issues, create_issue, create_pull_request]
tags: [github, source-control]
- connection: filesystem
prefix: fs
tags: [filesystem]
- connection: postgres
prefix: pg
include: [query, schema]
exclude: [drop_table]
tags: [database, sql]

The gateway calls tools/list on each connection at startup and creates proxy exposures for the matching tools. Each imported tool gets:

  • A name like github.list_issues (prefix + original tool name)
  • The original tool’s description and input schema
  • The tags you declare on the import block

include — only import these tools (allowlist). If omitted, all tools are imported.

exclude — skip these tools (blocklist). Applied after include.

prefix — prepended to each tool name with a dot separator. Keeps names from colliding when you import from multiple servers.

tags — applied to all imported tools. Makes them findable in flowgate.query({query}) search.

You can mix imports with explicit declarations. Declared capabilities can carry guards, reliability policies, and audit hooks that imports don’t have by default:

proxy:
import:
- connection: github
prefix: github
tags: [github]
expose:
- name: safe.create_pr
description: Create a PR with test evidence required.
guards:
- kind: evidence
requires: [tests_passed]
executor:
kind: mcp
connection: github
tool: create_pull_request

The proxy.expose block lets you declare capabilities with full control over their schema, guards, reliability, and executor.

proxy:
expose:
- name: github.list_issues
title: List GitHub issues
description: List issues from a GitHub repository.
tags: [github, issues, read]
aliases: [issues, bugs]
inputSchema:
type: object
required: [repo]
properties:
repo: { type: string }
executor:
kind: mcp
connection: github
tool: list_issues

Define the capability once, expose it (potentially with extra guards or a different name):

capabilities:
raw.create_pr:
title: Create GitHub PR
executor:
kind: mcp
connection: github
tool: create_pull_request
safe.create_pr:
wraps: raw.create_pr
guards:
- kind: evidence
requires: [tests_passed]
proxy:
expose:
- capability: safe.create_pr
as: github.create_pr
tags: [github, write]
aliases: [pr, pull-request]

The wraps keyword stacks guards — safe.create_pr inherits the executor from raw.create_pr and adds its own evidence guard on top. The as field renames it for the proxy surface.

Executors are the actual work that happens when a transition fires. They live inside proxy.expose[].executor, transition executor, state onEnter.executor, and fallback executors[]. The full set of kinds includes mcp, cli, rest, human, noop, workflow, script, parallel, and llm — see the Executors reference for the complete catalog. This section covers the most common ones.

Calls a tool on a connected MCP server.

executor:
kind: mcp
connection: github
tool: create_pull_request
map:
title: "$.arguments.title"
head: "$.context.branch_name"
base: main

The map block feeds values into the tool call — paths resolve from context (the shared state your workflow carries across moves) and from the call’s arguments; bare strings are literals. The first call to a connection opens it and the gateway holds it open, so repeated calls to the same MCP server reuse that one connection instead of reconnecting each time.

Runs a shell command and captures stdout as JSON.

executor:
kind: cli
connection: dotnet
args:
- test
- "$.arguments.project"

Arguments with $. prefixes are interpolated from the workflow’s scopes. Everything else is passed verbatim. The executor captures stdout and attempts to parse it as JSON for output mapping.

The treatNonZeroAsFailure flag (default true) controls what happens on non-zero exit codes. Set it to false when the exit code is data, not an error — useful for test runners and validators where you want to branch on the result:

executor:
kind: cli
connection: shell
args: ["-c", "cargo test"]
treatNonZeroAsFailure: false

Makes HTTP requests to any API.

executor:
kind: rest
connection: github_api
method: POST
path: "/repos/{owner}/{repo}/pulls"
query: { state: open }
headers: { X-Custom: value }
body:
title: "$.arguments.title"
head: "$.arguments.head"
base: main

path supports {var} templating — variables pull from arguments, then context, then workflow input. query, headers, and body all support path expressions. Status codes 408, 429, and 5xx are classified as retryable errors.

Stops the chain and queues for human approval — the way you keep “merge to main” a move the agent can open but never fire on its own.

executor:
kind: human
queue: pr-reviews

Doesn’t execute anything. Records a human.approval.requested audit event, returns a pending status, and waits. A person resolves it through your approval integration. The queue label tells your tooling where to route the request.

Returns immediately with an empty result.

executor:
kind: noop

Useful for testing, stubs, and transitions that just move state without doing work. When a proxy.expose entry has no executor, noop is the default.

Starts a sub-workflow and waits for it to complete.

executor:
kind: workflow
definitionId: validation_pipeline
input:
artifact: "$.context.artifactId"
timeoutMs: 300000

The sub-workflow runs to completion, and its final context becomes the executor’s output. This lets you compose workflows — a ship workflow can kick off a validation sub-workflow (lint, test, type-check) and use its results to decide whether the PR move becomes reachable.

Back to the opening problem. Here’s a complete config that gates your agent’s create_pull_request behind a real test run, using all three connection kinds — a cli that runs the suite, a github MCP server that opens the PR, and a rest connection to GitHub’s API for a status check:

version: "1.0.0"
connections:
# CLI: runs your real test suite (npm, cargo, pytest …)
shell:
kind: cli
command: /bin/bash
# MCP: the GitHub MCP server that can open PRs
github:
kind: mcp
command: github-mcp-server
# REST: GitHub's HTTP API, for a commit-status read
github_api:
kind: rest
baseUrl: https://api.github.com
headers:
Authorization: "Bearer ${GITHUB_TOKEN}"
audit:
sink: stderr
proxy:
expose:
# The test gate. treatNonZeroAsFailure keeps a red suite a failure,
# so this move only produces tests_passed evidence when it's green.
- name: run_tests
description: Run the test suite for the current branch.
tags: [tests, ci]
executor:
kind: cli
connection: shell
args: ["-c", "npm test"]
reliability:
timeoutMs: 120000
# The guarded write. No tests_passed evidence → GUARD_REJECTED.
- name: safe.create_pr
description: Open a PR — only when tests have passed.
tags: [github, write]
inputSchema:
type: object
required: [title, head]
properties:
title: { type: string }
head: { type: string }
guards:
- kind: evidence
requires: [tests_passed]
executor:
kind: mcp
connection: github
tool: create_pull_request
map:
title: "$.arguments.title"
head: "$.arguments.head"
base: main

Three connection kinds — cli, mcp, rest — wired through the same two tools the agent ever sees. The agent searches for “open a pull request” through flowgate.query({query}), finds safe.create_pr, and tries to fire it. With no tests_passed evidence on the workflow, that comes back GUARD_REJECTED — distinct from the INVALID_TRANSITION you’d get for a move that doesn’t exist in the current state at all. It can’t talk its way past the guard; it has to run run_tests green first. The whole point: the wrong order isn’t a thing the agent can do, no matter what the system prompt does or doesn’t say. (See How to run it for how airtight that is in each run mode.)