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_requestThe 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
Section titled “MCP connections”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).
Stdio (child process)
Section titled “Stdio (child process)”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/appThe 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.
Streamable HTTP
Section titled “Streamable HTTP”For MCP servers already running somewhere, connect via URL:
connections: remote_server: kind: mcp url: https://mcp.example.com/v1No 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
Section titled “CLI connections”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/binThe 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
Section titled “REST connections”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.
Importing tools from MCP servers
Section titled “Importing tools from MCP servers”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_requestExposing capabilities manually
Section titled “Exposing capabilities manually”The proxy.expose block lets you declare capabilities with full control over their schema, guards, reliability, and executor.
With an inline executor
Section titled “With an inline 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_issuesWith a capability reference
Section titled “With a capability reference”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.
Executor kinds
Section titled “Executor kinds”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.
MCP executor
Section titled “MCP executor”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: mainThe 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.
CLI executor
Section titled “CLI executor”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: falseREST executor
Section titled “REST executor”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: mainpath 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.
Human executor
Section titled “Human executor”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-reviewsDoesn’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.
Noop executor
Section titled “Noop executor”Returns immediately with an empty result.
executor: kind: noopUseful for testing, stubs, and transitions that just move state without doing work. When a proxy.expose entry has no executor, noop is the default.
Workflow executor
Section titled “Workflow executor”Starts a sub-workflow and waits for it to complete.
executor: kind: workflow definitionId: validation_pipeline input: artifact: "$.context.artifactId" timeoutMs: 300000The 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.
Putting it together
Section titled “Putting it together”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: mainThree 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.)