MCP tools
No matter how many capabilities you wire in, the model always sees exactly two tools: flowgate.query for all reads and flowgate.command for all state-changing writes. The split follows CQRS — if it has side effects, it’s a command; if it doesn’t, it’s a query.
The tool name carries nearly no decision weight for the model. What does the work is the links[] array every response carries: pre-filled { method, args } objects the model copies verbatim to make the next call. The schema declares what’s valid; the response declares what’s valid right now.
flowgate.query
Section titled “flowgate.query”Side-effect-free reads. Covers: catalog browsing (home), full-text search, fragment describe, workflow get, workflow explain, and lexicon reads.
Schema — all fields optional; the combination of fields present selects the operation:
{ "query": "string", "kind": "string", "subject": "string", "workflowId": "string", "transition": "string", "limit": "integer"}Dispatch table
Section titled “Dispatch table”| Args present | Operation |
|---|---|
| (none) | home — entry point; links to search and list operations |
query | search — full-text scored results |
subject only | describe — live-config fragment or capability |
subject + workflowId | describe — against the instance’s pinned snapshot (§8.2) |
workflowId alone | get — current workflow state |
workflowId + transition | explain — inspect a transition definition |
kind and limit are optional modifiers on search; they don’t change which dispatch row matches. kind filters the result set. The always-available values are workflow, capability, and connection. lexicon is also accepted and routes to a lexicon search. skill and script are authoring-time only and flag-gated — they return INVALID_PARAMS unless the server was started with with_skills_search(true) / with_scripts_search(true). limit caps the result count.
Audit: every describe call emits a guidance.describe_requested audit event — including browse-time describe (subject alone), where the event’s workflowId is null. When the call includes both subject AND workflowId, the event carries that workflowId — i.e., “guidance fetched for use in a running workflow.”
Returns the full capability catalog. Entry point for a cold-start model.
Example request:
{ "name": "flowgate.query", "arguments": {}}Example response:
{ "resource": { "type": "gateway", "id": "home" }, "result": { "status": "ready", "message": "Available workflows and proxy capabilities can be discovered here." }, "links": [ { "rel": "search", "title": "Search workflows and capabilities", "method": "flowgate.query", "args": { "query": "" }, "inputSchema": { "type": "object", "required": ["query"], "properties": { "query": { "type": "string" }, "kind": { "type": "string", "enum": ["workflow", "capability", "connection"] }, "limit": { "type": "integer", "default": 10 } }, "additionalProperties": false } }, { "rel": "list_workflows", "title": "List configured workflows", "method": "flowgate.query", "args": { "query": "", "kind": "workflow" } }, { "rel": "list_capabilities", "title": "List proxy capabilities", "method": "flowgate.query", "args": { "query": "", "kind": "capability" } } ]}home is a thin entry point, not a catalog dump. It hands the model three links — search, list_workflows, list_capabilities — and the model follows one to enumerate what’s actually available. The model chains these instead of guessing call shapes.
search
Section titled “search”Full-text search across all indexed capabilities, workflows, skills, scripts, and lexicon entries.
Example request:
{ "name": "flowgate.query", "arguments": { "query": "deploy", "kind": "workflow", "limit": 5 }}Example response:
{ "query": "deploy", "kind": "workflow", "items": [ { "score": 14.0, "item": { "id": "deploy_pipeline", "kind": "workflow", "title": "Deploy Pipeline", "description": "Lint, test, build, and deploy a service.", "tags": ["deploy", "pipeline"], "links": [ { "rel": "start", "method": "flowgate.command", "args": { "definitionId": "deploy_pipeline", "input": {} } } ] } } ], "links": [ { "rel": "home", "method": "flowgate.query", "args": {} } ]}Scoring weights:
| Field | Weight | What it matches |
|---|---|---|
title | 6.0 | Capability or workflow title |
id | 5.0 | The identifier |
tags | 3.0 | Tags array |
aliases | 3.0 | Search synonyms declared in config |
description | 2.0 | Description text |
text | 1.0 | State names, transition names, guidance text |
Lexicon search — pass kind: "lexicon" to restrict to the lexicon:
{ "query": "evidence", "kind": "lexicon" }Returns { "hits": [...] }, where each hit is the full stored lexicon entry with term added — e.g. { "term", "definition_short", "bounded_context", "refs"?, "governance"?, "state"? }. The exact keys depend on what the entry was authored with (an aliases array, for instance, appears only when the entry declares one); a hit is not a fixed subset.
describe
Section titled “describe”Returns the full details for a single capability, workflow definition, skill, script, or lexicon entry.
Example request:
{ "name": "flowgate.query", "arguments": { "subject": "github.list_issues" }}Example response:
For a workflow, capability, or connection, describe wraps the matched DiscoveryItem under item and attaches navigation links:
{ "id": "github.list_issues", "item": { "id": "github.list_issues", "kind": "capability", "title": "List GitHub issues", "description": "List issues from a GitHub repository.", "tags": ["github", "issues", "read"], "links": [ { "rel": "start", "method": "flowgate.command", "args": { "definitionId": "proxy_default", "input": { "capability": "github.list_issues" } }, "input_schema": { "type": "object", "required": ["repo"], "properties": { "repo": { "type": "string" } } } } ] }, "links": [ { "rel": "home", "method": "flowgate.query", "args": {} }, { "rel": "search", "method": "flowgate.query", "args": { "query": "" } } ]}Guidance (skill) and script subjects return a flatter wire shape instead — { "kind", "subject", "verb", "body", "links", "lexicon"? } — since they carry a guidance body rather than a HATEOAS-driven DiscoveryItem.
Lexicon lookup — use the lexicon: subject prefix to look up a term:
{ "subject": "lexicon:evidence-pack" }Returns { "term", "entry" } where entry may be null if the term has no definition yet. A lookup against any registered alias returns the same entry as the canonical term.
Check the current state of a running workflow.
Example request:
{ "name": "flowgate.query", "arguments": { "workflowId": "wf_a1b2c3d4e5f6" }}Example response:
{ "workflow": { "id": "wf_a1b2c3d4e5f6", "definitionId": "deploy_pipeline", "definitionVersion": 1, "state": "ready_to_deploy", "version": 4 }, "result": { "status": "waiting_for_action" }, "context": { "lintPassed": true, "testsPassed": true, "coverage": 87.5, "artifactId": "art_xyz789" }, "links": [ { "rel": "deploy", "title": "Deploy to environment", "description": null, "method": "flowgate.command", "actor": "agent", "args": { "workflowId": "wf_a1b2c3d4e5f6", "expectedVersion": 4, "transition": "deploy" }, "inputSchema": { "type": "object" } }, { "rel": "abort", "title": "Abort deployment", "description": null, "method": "flowgate.command", "actor": "agent", "args": { "workflowId": "wf_a1b2c3d4e5f6", "expectedVersion": 4, "transition": "abort" }, "inputSchema": { "type": "object" } } ], "evidence": []}The instance lives under workflow — workflow.id, workflow.state, workflow.version. The context accumulates data from previous transitions. Each link’s rel is the transition name (deploy, abort), and args.transition repeats it. The links show every legal next move; a link only carries args.arguments when the transition declares prefill.
explain
Section titled “explain”Inspect a workflow definition or a specific transition without starting anything.
Example request (specific transition):
{ "name": "flowgate.query", "arguments": { "workflowId": "wf_a1b2c3d4e5f6", "transition": "deploy" }}Example response:
{ "workflowId": "wf_a1b2c3d4e5f6", "currentState": "ready_to_deploy", "transition": "deploy", "allowedFromCurrentState": true, "actor": "agent", "deterministic": false, "legalTransitionsNow": ["deploy", "abort"]}explain answers “can I fire this transition from where the instance is right now, and what else is legal?” — allowedFromCurrentState is the verdict, legalTransitionsNow lists the transition names available from currentState, and deterministic is true when the transition’s actor is deterministic (the runtime would chain through it automatically). It does not expose the transition’s guards, target state, input schema, or executor.
Subject namespace
Section titled “Subject namespace”The subject field admits a colon-prefixed namespace. Only the lexicon: prefix is interpreted specially by query-time describe; everything else is resolved by an exact match on the discovery index id:
| Prefix | Resolves to |
|---|---|
| (none) | guidance fragment, workflow definition, capability, script, or connection — matched by exact id against the discovery index |
lexicon:<term> | a single lexicon entry (the only prefix the query path special-cases) |
workflow:<id> | reserved; not resolved as a prefix — the runtime matches workflow:<id> literally as an id and finds nothing unless an item is named that |
script:<subject> | reserved; not resolved as a prefix (same literal-id behaviour) |
skill:<subject> | reserved; not resolved as a prefix (same literal-id behaviour) |
Pass unprefixed subjects for workflows, capabilities, connections, scripts, and guidance — the discovery index resolves them by id. The lexicon: prefix is the only one that switches resolution path.
flowgate.command
Section titled “flowgate.command”State-changing writes. Covers: workflow start, workflow transition submit, and lexicon define (when enabled).
Schema — all fields optional; the combination of fields present selects the operation:
{ "definitionId": "string", "input": "object", "workflowId": "string", "expectedVersion": "integer", "transition": "string", "arguments": "object", "subject": "string", "definition": "object", "summary": "string", "traceId": "string", "runId": "string", "intent": "string", "unknown_subject": "string"}Dispatch table
Section titled “Dispatch table”| Args present | Operation |
|---|---|
definitionId, no workflowId, no subject | start — create a new workflow instance |
workflowId + transition + expectedVersion | submit — fire a transition |
subject (namespaced) + definition | define — write a lexicon entry |
input and runId are modifiers on start. arguments and summary are modifiers on submit. traceId and runId thread through to audit on any operation. intent plus unknown_subject drive the out-of-band cancel_pending_subject resolution (note unknown_subject keeps its snake_case wire key to match the cancel link emitted by SUBJECT_NEEDS_DEFINITION).
Create a new workflow instance at its initial state.
Example request:
{ "name": "flowgate.command", "arguments": { "definitionId": "deploy_pipeline", "input": { "service": "payments", "environment": "staging" } }}Example response:
{ "workflow": { "id": "wf_a1b2c3d4e5f6", "definitionId": "deploy_pipeline", "definitionVersion": 1, "state": "lint", "version": 1 }, "result": { "status": "started" }, "context": {}, "links": [ { "rel": "run_lint", "title": "Run linter", "description": null, "method": "flowgate.command", "actor": "agent", "args": { "workflowId": "wf_a1b2c3d4e5f6", "expectedVersion": 1, "transition": "run_lint" }, "inputSchema": { "type": "object" } } ], "evidence": []}Each link is a pre-filled flowgate.command submit call — the model copies args verbatim. The link rel is the transition name (run_lint).
If the initial state has actor: deterministic transitions, the runtime chains through them automatically and returns the first state that needs a decision (the chain steps appear under a chain array when any fired).
runId uniqueness: supply runId to assert uniqueness. If an instance with that runId already exists, the call returns RUN_ID_ALREADY_RUNNING (see error shapes below) rather than creating a duplicate. Omit runId to let the runtime mint one.
submit
Section titled “submit”Advance a workflow by firing a transition.
Example request:
{ "name": "flowgate.command", "arguments": { "workflowId": "wf_a1b2c3d4e5f6", "expectedVersion": 4, "transition": "deploy", "arguments": { "confirm": true } }}Example response (success):
{ "workflow": { "id": "wf_a1b2c3d4e5f6", "definitionId": "deploy_pipeline", "definitionVersion": 1, "state": "deployed", "version": 5 }, "result": { "status": "completed" }, "context": { "lintPassed": true, "testsPassed": true, "coverage": 87.5, "artifactId": "art_xyz789", "deploymentId": "dep_abc123" }, "links": [], "evidence": []}There is no top-level terminal flag. A workflow is terminal when result.status is "completed" and links is empty — the runtime forces result.status to completed for any terminal state. A non-terminal submit returns result.status "executed" (and a non-empty links array of legal next transitions).
Submit error codes:
| Code | Meaning |
|---|---|
STALE_WORKFLOW_VERSION | expectedVersion doesn’t match. Fetch current state with flowgate.query({workflowId}) and retry. |
ACTOR_MISMATCH | Transition requires a human principal but submitter is not human. |
GUARD_REJECTED | One or more guards rejected the transition. |
INPUT_SCHEMA_VIOLATION | Arguments don’t match the transition’s input schema. |
INVALID_TRANSITION | Transition doesn’t exist or isn’t legal from the current state. |
EXECUTOR_FAILED | Executor ran but failed. |
define
Section titled “define”Write a lexicon entry. Requires with_lexicon_writes(true) on the server (default off in production deployments).
Example request:
{ "name": "flowgate.command", "arguments": { "subject": "lexicon:evidence-pack", "definition": { "definition_short": "A structured set of artefacts that justify a decision.", "boundedContext": "swe-agent", "refs": ["evidence-packs", "evidence pack"], "governance": "human-only" } }}The definition body reads four fields: definition_short (required), boundedContext (camelCase on input), refs, and governance. Synonyms are carried as refs. Any other keys — including a definition_long or aliases — are ignored.
Example response:
{ "term": "evidence-pack", "entry": { "definition_short": "A structured set of artefacts that justify a decision.", "bounded_context": "swe-agent", "refs": ["evidence-packs", "evidence pack"], "governance": "human-only" }, "persisted_to": "overlay"}Note the echoed entry stores bounded_context in snake_case — the dispatcher reshapes the camelCase input key on the way in.
Emits a lexicon.defined audit event on success. Defines land in a gateway-lifetime in-memory overlay. Persist them by editing flowgate.yaml and reloading.
Define error codes:
| Code | Meaning |
|---|---|
LEXICON_DEFINE_REQUIRES_HUMAN | Caller is an agent; term governance is human-only. Route through an actor: human transition to commit. |
LEXICON_WRITES_DISABLED | Server was started without lexicon write support. See operator path in the error’s links[]. |
INVALID_LEXICON_ENTRY | Definition shape is malformed. |
LEXICON_ALIAS_COLLISION | An alias collides with another entry’s term or alias in the same bounded context. |
Error response shape
Section titled “Error response shape”Ambiguous argument combinations return a structured error — never an MCP protocol error:
AMBIGUOUS_INTENT
Section titled “AMBIGUOUS_INTENT”Fired when the args don’t match any known dispatch shape (e.g., conflicting dispatch keys are present). The flowgate.command variant returns three example links — one per command shape:
{ "error": { "code": "AMBIGUOUS_INTENT", "message": "flowgate.command args do not match a known dispatch shape", "hint": "see §32 dispatch table: start (definitionId only), submit (workflowId+expectedVersion+transition), define (subject namespaced + definition)" }, "links": [ { "rel": "start_example", "method": "flowgate.command", "args": { "definitionId": "<your-workflow>" } }, { "rel": "submit_example", "method": "flowgate.command", "args": { "workflowId": "<id>", "expectedVersion": 0, "transition": "<name>" } }, { "rel": "define_example", "method": "flowgate.command", "args": { "subject": "lexicon:<term>", "definition": { "definition_short": "..." } } } ]}The flowgate.query variant carries the message "flowgate.query args do not match a known dispatch shape" and two links — { rel: "home" } and { rel: "search" }.
LEXICON_WRITES_DISABLED
Section titled “LEXICON_WRITES_DISABLED”Fired when define is called on a server with lexicon writes off.
{ "error": { "code": "LEXICON_WRITES_DISABLED", "message": "This runtime does not accept lexicon define commands.", "hint": "Operators add lexicon terms via the `flowgate lexicon define` CLI subcommand." }, "links": [ { "rel": "operator_path", "method": "cli", "args": { "command": "flowgate lexicon define <term> <definition>" } }, { "rel": "lookup", "method": "flowgate.query", "args": { "subject": "lexicon:<term>" } } ]}The cli rel is informational — it names the operator-facing alternative, not a tool the model can call. The lookup rel keeps the read path discoverable.
RUN_ID_ALREADY_RUNNING
Section titled “RUN_ID_ALREADY_RUNNING”Fired when a start command supplies a run_id that already exists.
{ "error": { "code": "RUN_ID_ALREADY_RUNNING", "message": "An instance already exists with run_id 'r-abc123'.", "hint": "Each run_id is single-use. Fetch the existing instance with the linked get, or retry with a fresh run_id." }, "links": [ { "rel": "get", "method": "flowgate.query", "args": { "workflowId": "<existing>" } } ]}SUBJECT_NEEDS_DEFINITION
Section titled “SUBJECT_NEEDS_DEFINITION”Fired when a workflow start (or mid-workflow transition) encounters a subject that has no lexicon entry. The runtime pauses the command and returns a structured interaction instead of executing:
{ "interaction": { "kind": "SUBJECT_NEEDS_DEFINITION", "unknown_subject": "evidence-foo", "context": { "encountered_in": "workflow:swe_agent state:retrieving", "bounded_context": "swe-agent" }, "candidates": [ { "term": "evidence-pack", "distance": 2, "match_kind": "fuzzy_close", "definition_preview": "A bundle of facts the editor uses…" } ] }, "queued_command": { "method": "flowgate.command", "args": { } }, "links": [ { "rel": "link_as_alias", "method": "flowgate.command", "args": { "subject": "lexicon:evidence-pack", "definition": { "aliases_add": ["evidence-foo"] } }, "hint": "Use this if 'evidence-foo' is a synonym for 'evidence-pack'." }, { "rel": "define_new", "method": "flowgate.command", "args": { "subject": "lexicon:evidence-foo", "definition": { "definition_short": "<fill in>", "boundedContext": "swe-agent" } }, "hint": "Use this if 'evidence-foo' is a genuinely new concept." }, { "rel": "cancel", "method": "flowgate.command", "args": { "intent": "cancel_pending_subject", "unknown_subject": "evidence-foo" }, "hint": "Abandon the original command — the subject was a mistake." } ]}Follow one link. Resolution updates the live lexicon (or drops the placeholder for cancel). The client retries the original command. See Lexicon for the full PENDING_DEFINITION / SUBJECT_NEEDS_DEFINITION flow.
HATEOAS contract
Section titled “HATEOAS contract”Every response — success or error — carries a links[] array:
{ "rel": "string", "method": "flowgate.query" | "flowgate.command", "args": { ... } }The args object is pre-filled with the exact shape the next operation needs. Models chain by copying link.args verbatim — they never derive the next call from the schema. Static schema declares what’s valid; dynamic links[] declares what’s valid right now.