chant serve mcp
Synopsis
Section titled “Synopsis”chant serve mcp [path]Description
Section titled “Description”chant serve mcp starts an MCP (Model Context Protocol) server over stdio. Agents that speak MCP, such as Claude Code, Cursor and Windsurf, use it to work with your chant project.
You rarely run this yourself. chant init detects your IDE and writes the MCP config to its global config file (~/.claude/mcp.json for Claude Code, ~/.cursor/mcp.json for Cursor, or ~/.config/mcp/mcp.json otherwise), and your agent picks it up. See Agent Integration.
At a workspace root
Section titled “At a workspace root”A project with no lexicon refuses to start with “No lexicon detected”. The root of a declared workspace (chant.workspace.json) with no lexicon of its own is the exception, as in a repo made from chud’s template, whose lexicons are all in delivery/. There the server starts and serves the core tools and resources below, with the lexicons of the workspace’s members of kind chant, each read from that member’s own chant.config.ts. A member whose config does not load, or a lexicon this chant cannot import, is left out and the rest are served, down to core alone.
The initialize result then carries instructions. They say which lexicons loaded and which did not, and that the tools taking a path want a member’s directory.
These are the core tools the server registers. Lexicon plugins contribute more.
A test ties this list to the server’s own registrations, so a tool added or removed in packages/core/src/cli/mcp/server.ts fails the build until this page names it.
Build chant infrastructure code and generate output for the target lexicon.
| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | Yes | Path to the infrastructure directory or file |
output | string | No | Accepted by the schema and currently ignored; the built output always comes back in the response |
format | "json" | "yaml" | No | Output format (default: json) |
Returns { success, resourceCount, output, format }. A build that reports errors fails the call instead of returning them.
Lint chant infrastructure code and report issues.
| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | Yes | Path to the infrastructure directory or file |
fix | boolean | No | Auto-fix fixable issues (default: false) |
Returns { success, errorCount, warningCount, diagnostics, output }.
import
Section titled “import”Import external templates and convert them to chant TypeScript.
| Parameter | Type | Required | Description |
|---|---|---|---|
source | string | Yes | Path to the template file to import |
output | string | No | Output directory (default: ./infra/) |
The response is { success, lexicon, generatedFiles, warnings }. A failed import fails the call.
explain
Section titled “explain”Summarize the resources in a chant project.
| Parameter | Type | Required | Description |
|---|---|---|---|
path | string | Yes | Path to the infrastructure directory |
format | "markdown" | "json" | "okf" | No | Output format (default: markdown) |
markdown returns a prose summary. json returns { sourceFiles, totalEntities, lexicons, dependencies, errors }, carrying an extra knowledge key when the project’s OKF bundle bound a concept to a discovered entity, and omitting that key entirely when nothing bound. okf returns { okf_version, files, errors }, where files maps each bundle-relative path to its content, which is the same bundle chant explain --format okf writes to a directory.
scaffold
Section titled “scaffold”Generate starter files for a chant project.
| Parameter | Type | Required | Description |
|---|---|---|---|
pattern | string | Yes | Resource pattern to scaffold (e.g. "s3-bucket", "pipeline") |
lexicon | string | No | Lexicon to use for templates. Auto-detected if omitted |
template | string | No | Named init template (e.g. "node-pipeline"). Given one, the template’s own source files come back instead of a pattern match |
Returns { lexicon, pattern, files }, where files is [{ filename, content }]. A named template that matched adds template. When no lexicon template matched at all, the response is a generic skeleton with a note saying so.
search
Section titled “search”Search the resource types and the composites of the loaded lexicons.
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | Yes | Search query (case-insensitive substring match) |
lexicon | string | No | Filter to a specific lexicon |
limit | number | No | Maximum results (default: 20) |
The response is { query, total, results }, with prefix matches ahead of substring matches and total counting the matches before limit was applied. A resource type matches on its type, class name or kind and comes back as { className, resourceType, kind, lexicon }. A composite matches on its name, its description or a resource kind it bundles, and comes back as { kind: "composite", name, description, bundles, lexicon }, so a query for queue returns the Queue resource and the LambdaSqs composite that bundles one. Among matches of equal rank, composites come before resource types. The composites tool has the composite’s parameters.
composites
Section titled “composites”List the composites the loaded lexicons export. “What composites do you have for aws?” is a call with lexicon: "aws".
| Parameter | Type | Required | Description |
|---|---|---|---|
lexicon | string | No | Only composites from this lexicon |
query | string | No | Keyword matched against the name, description, bundled resource kinds and parameter names |
limit | number | No | Maximum composites (default: 50) |
Returns { lexicons, total, composites }, plus the lexicon and query that were given. lexicons names every loaded lexicon, so an empty list for a lexicon it names means that lexicon exports no composites. Each composite is { name, lexicon, description, bundles, params }. Its bundles are the resource kinds it creates, nested composites included, and each entry in params is { name, type, required, description }. With a query, name matches come first. The data is the catalog each lexicon ships in its package. Reading it loads no composite and calls no provider.
lifecycle-snapshot
Section titled “lifecycle-snapshot”Capture deployed state for an environment, reading every loaded lexicon that implements describeResources.
| Parameter | Type | Required | Description |
|---|---|---|---|
environment | string | Yes | Target environment |
lexicon | string | No | Restrict to one lexicon; snapshots all of them if omitted |
Returns { snapshots, warnings, errors }, where snapshots is how many were captured. Two conditions come back as a plain string rather than that object: No plugins implement describeResources when nothing loaded can describe live state, and Build failed when the project did not build.
lifecycle-diff
Section titled “lifecycle-diff”Compare the current build’s digest against the digest stored in an environment’s last snapshot.
| Parameter | Type | Required | Description |
|---|---|---|---|
environment | string | Yes | Target environment |
lexicon | string | No | Restrict to one lexicon; diffs all of them if omitted |
The response is an object keyed by lexicon name, each value that lexicon’s digest diff. A lexicon with no stored snapshot for the environment diffs against nothing, which reads as everything being new. Build failed comes back as a plain string when the project did not build.
Every Op tool takes an optional runtime, the same choice chant run --on makes on the command line. Omitted, it is the built-in local runtime, which executes the Op in this process.
op-list
Section titled “op-list”List every Op discovered from *.op.ts files with the run state the runtime reports.
| Parameter | Type | Required | Description |
|---|---|---|---|
runtime | string | No | Runtime to address: a lexicon name with an opRuntime, or local (the default) |
Returns { ops, errors }. Each entry carries { name, overview, phases, labels, depends, runState }, and an unreachable runtime leaves runState as a dash rather than failing the call.
op-run
Section titled “op-run”Run an Op on a runtime and return its result.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Op name (must match an *.op.ts file) |
env | string | No | Environment the run targets, as chant run --env names it; omitted, the runtime uses its own default |
runtime | string | No | Runtime to address: a lexicon name with an opRuntime, or local (the default) |
A finished run comes back as { op, runId, state, startedAt, endedAt }, plus records and gate when the run produced them.
An Op that declares neverOverMcp: true is refused here, and nothing starts. See Ops kept from agents.
op-status
Section titled “op-status”Show the state the runtime reports for an Op’s latest run.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Op name |
runtime | string | No | Runtime to address: a lexicon name with an opRuntime, or local (the default) |
Returns { op, runtime, runId, state, startedAt, endedAt, records, gate }. records is the per-phase StepRecord[] array (present when the Op’s *.op.ts is discoverable; see Progress and gate state), the same shape --progress-json streams. gate is the pending gate ({ name, since }), null when none is standing.
op-approve
Section titled “op-approve”Record a gate’s resolution on the gate ledger, then wake the runtime hosting the gated run. This is the rename of op-signal, and a different act: a gate is resolved by recording the fact, not by sending a message to a waiting workflow.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Op name |
gate | string | Yes | Gate name (must match the gate’s name in the Op) |
note | string | No | Free-text context recorded on the resolution |
url | string | No | Absolute http/https URL this resolution happened at |
env | string | No | Environment the woken run runs in, as chant run approve --env names it; omitted, the runtime keeps the gated run’s |
runtime | string | No | Runtime to address: a lexicon name with an opRuntime, or local (the default) |
Returns { op, gate, resolvedBy, origin, timestamp, runtimeNotified }.
A gate reached over MCP cannot be resolved over MCP
Section titled “A gate reached over MCP cannot be resolved over MCP”Every gate fact records the channel it came through, one of cli, mcp or acp. When a run reaches a gate over MCP, the pending fact says mcp, and this tool then refuses to answer it. op-run wrote the first half and op-approve would write the second, so one caller in one session would have written both, and the gate would have no second party in it.
The refusal names the way through. Approve from a channel the run did not arrive on, which is normally chant approve <op> <gate> at a shell. A gate reached at a shell or over ACP is resolvable here, because that is the separation the gate exists for. chant approve carries an --allow-same-origin override for the operator who wants both halves in one session; this tool has no equivalent.
There is no approver parameter, and the resolution records resolvedBy as unattested. MCP cannot verify a name, so a name supplied here would sit on the ledger looking exactly like a name a person gave. A shell approval takes --actor and can be believed.
The approval is for the plan the gated run recorded, so a gate with no pending fact standing has nothing for this tool to approve and the call fails. Naming a plan digest by hand is a shell-only affordance, chant approve --plan <digest>.
Ops kept from agents
Section titled “Ops kept from agents”An Op can declare neverOverMcp: true. Approvals for such an Op belong to people, so an agent on this server neither runs it nor resolves its gates.
| Tool | On a declared Op |
|---|---|
op-run | Refused. The Op does not start. |
op-approve | Refused for every gate of the Op, whichever channel reached the gate. Nothing is written to the gate ledger and no runtime is woken. |
op-list, op-status, op-report | Answer as usual, so an agent can read where a run stands. |
The rule above only covers a gate reached over MCP. A gate reached on forge CI or at a shell is otherwise resolvable here, and the resolution is recorded as unattested. For an Op that must never be approved by an agent, that is too much, so the declaration closes it. Each refusal names the rule and the shell command, chant run <op> or chant approve <op> <gate>, that does the same thing for a person. Those commands are unchanged.
An Op that is not discovered from a *.op.ts file, such as fan-out, workspace-upgrade or the pull-request gate pr-<number>, has no declaration to read. Its pending gate record carries neverOverMcp: true instead, and op-approve refuses it the same way. The ACP surface refuses the same gates, because it resolves a gate through the same code.
op-report
Section titled “op-report”Return a markdown report for an Op’s latest run.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Op name |
runtime | string | No | Runtime to address: a lexicon name with an opRuntime, or local (the default) |
Plugin Tools
Section titled “Plugin Tools”Lexicon plugins can add tools. Plugin tools are namespaced as {pluginName}:{toolName} to avoid conflicts with core tools.
Workspace tools
Section titled “Workspace tools”Started at or inside a declared workspace (chant.workspace.json in the directory or above it, up to the git root), the server also serves the workspace’s read contract and its record writes (#2707). A harness that speaks MCP but has no shell can then read decisions and propose them. The same test ties these sections to the tools a server inside a workspace registers.
Each tool is a thin call into what the CLI runs. A read runs chant workspace <command> ... --json with the chant the server runs as, in the server’s directory, and returns the document it printed, unchanged. The document follows that command’s output schema in the read contract, reason codes included, and an error document is returned as a document. A read handed to the workspace root’s pinned chant by the CLI is handed on the same way. The reader conformance suite runs over MCP as well, and requires each tool’s document to equal the command’s.
A write calls the function chant workspace records new, amend, review or close calls, and returns the same JSON result, refusals included. It keeps the CLI’s rules. The kind’s schema judges the record, and a closed record never changes. An approved record changes only in its state, evidence and reviews, and a dissent needs a note. Through MCP, two things are added. A new record opens in the kind’s first state, proposed for a decision, and any other state is refused with record-state-not-initial. The record’s source block gets via: "mcp" and client, the client’s clientInfo from initialize or from the request’s _meta. An amendment gets them only when its fields set the source block, since the block says where the proposal came from. Computed provenance, from git, is unchanged. sign: true seals with this host’s configured key, git’s user.signingkey with gpg.format ssh, and is refused with the CLI’s remedy when none is set.
points-answer calls what chant workspace points answer calls (ws-058). It records people’s answer to an open decision point question once the point’s quorum is met, with their note. The quorum never counts anyone holding the agent role, and since every call on the server is the model’s, it counts nobody by names: an answer here is refused with quorum-not-met, and one to a question whose gate is neverOverMcp with answer-never-over-mcp (#3817). People answer at a shell or through hud. points-retract calls what chant workspace points retract calls, and takes an answer back under the same quorum, keeping it in the record’s retractions (#3351). points-withdraw calls what chant workspace points withdraw calls, and closes an open question that no longer applies (#2888). It answers nothing, so it needs no quorum. No tool asks a point’s deciders: chant calls no model, so an ask runs in the operations layer.
Records are proposals until they are reviewed, and by must name the person or agent that actually proposed or decided it. Each write tool’s description says so, for the model that reads it. The server listens on no port and authenticates nobody: it speaks over stdio, and by is recorded as given, as --by is.
A server started with CHANT_AGENT set writes in that agent session, bound to its one member (#2548). Each write is judged against the session’s write scope, read from the declaration at base, and refused with the CLI’s write-scope-member, write-scope-kind or agent-unknown. A write whose by names a principal a session lists is judged as that session too.
A value that starts with - is refused before any command runs, so no argument can be read as a flag.
| Tool | Parameters | Returns |
|---|---|---|
workspace-ls | at | the document of chant workspace ls --json (ls.schema.json) |
workspace-status | env (required), compareTo | the document of chant workspace status <env> --json (status.schema.json) |
workspace-graph | kind (a list), intent, composites, at | the document of chant workspace graph --json (graph.schema.json); with intent, the intent graph over that region (intent.schema.json); with composites, the composites joined to components (composites.schema.json) |
workspace-changes | range (required), work, severity, kind (a list) | the document of chant workspace check --changes <range> --json (changes.schema.json): each changed path with the records covering it, and the change-uncovered and change-out-of-scope findings (#2773), and in links whether each trunk commit names its work item (#4029) |
workspace-points | open, kind, at | the document of chant workspace points --json (points.schema.json): the decision points and the questions asked of them, each open one with any model’s answer and confidence (ws-058) |
workspace-agent | name | the document of chant workspace agent <name> --json (agent.schema.json): the session’s member, its write scope and the spec it reloads from; without name, this server’s session (#2548) |
workspace-work-history | item (required), kind | the document of chant workspace work history <item> --json (work-history.schema.json): the work item’s claims, each with its token, holder and how it ended (#2785) |
workspace-runs | unit, decision, by, since | the document of chant workspace runs --json (runs.schema.json): the agent runs with their model, tokens, cost and commits, and totals per work item, decision and principal (#3033) |
workspace-records | kind, current, uncommitted, since, id, at | the document of chant workspace records --json (records.schema.json), or with since the changes since a revision or session (records-since.schema.json); with uncommitted, only the records the working tree holds modified or new against HEAD (--uncommitted, not with at or since); with id, only that record is kept in records, and summary stays the command’s |
records-new | record (required), kind, prefix, by, sign, dryRun | the result of chant workspace records new (records-new.schema.json) |
records-amend | id and fields (required), kind, by, sign, dryRun | the result of chant workspace records amend (records-amend.schema.json) |
records-review | id, verdict and by (required), kind, note, session, sign, dryRun | the result of chant workspace records review (records-review.schema.json), whose verdict moves the record’s quorum |
records-close | id (required), kind, dryRun | the result of chant workspace records close (records-close.schema.json) |
points-answer | id, answer, by (all required), note, kind, dryRun | the result of chant workspace points answer (points-write.schema.json): people’s answer to an open question, refused with quorum-not-met until the point’s quorum is met |
points-retract | id, by (both required), note, kind, dryRun | the result of chant workspace points retract (points-write.schema.json): the question escalated to people again, refused with answer-not-answered when it has no answer |
points-withdraw | id, by (both required), note, kind, dryRun | the result of chant workspace points withdraw (points-write.schema.json): the question withdrawn and no longer open, refused with record-closed when it is answered or withdrawn already |
Each parameter stands for the CLI flag of the same name, written in camel case, so compareTo is --compare-to. kind names a kind file from the server’s directory or a kind the declaration names. Without kind, a write goes through the one kind the declaration names, as the CLI’s does. record and fields are JSON objects, the text --from and --set would read. Without state, records-new gives the record the kind’s first.
by on records-new names a proposer when the kind declares proposedBy (#2756): it fills that field, proposed_by for a decision, whenever the record opens in the kind’s first state. A kind with no proposedBy skips that and fills the decider field, decided_by for a decision, the way it always has; so does a record whose own fields set its state past the first one. Signing needs a decider. by on records-amend always writes the decider field, unchanged from before.
Resources
Section titled “Resources”Agents can read these resources:
| URI | MIME Type | Description |
|---|---|---|
chant://context | text/markdown | Lexicon-specific instructions, patterns, and best practices |
chant://examples/list | application/json | JSON array of available examples ([{ name, description }]) |
chant://ops | application/json | Every Op discovered from *.op.ts, with overview, phase count, labels and dependencies |
chant://ops/{name}/runs | application/json | Run history for a named Op, newest first |
chant://ops/{name}/runs/latest | application/json | Latest run state for a named Op |
chant://composites | application/json | Every composite the loaded lexicons export, the same entries the composites tool returns, unfiltered |
chant://knowledge | application/json | The project’s OKF knowledge bundle as { dir, index, concepts }, empty when the project has authored none |
chant://state/{environment} | application/json | Every lexicon’s snapshot for an environment, keyed by lexicon |
chant://state/{environment}/{lexicon} | application/json | One lexicon’s snapshot for an environment |
chant://examples/{name} reads too, although resources/list does not advertise it. It resolves to whichever plugin resource has a URI ending in /examples/{name}.
Plugin Resources
Section titled “Plugin Resources”Lexicon plugins can add resources, namespaced as chant://{pluginName}/{path}. Plugin resources with /examples/ in their URI are included in the examples list.
Configuration
Section titled “Configuration”Automatic Setup
Section titled “Automatic Setup”chant init detects your IDE and adds the MCP server to that IDE’s global config file:
| IDE | Config path |
|---|---|
| Claude Code | ~/.claude/mcp.json |
| Cursor | ~/.cursor/mcp.json |
| Other | ~/.config/mcp/mcp.json |
The config looks like:
{ "mcpServers": { "chant": { "command": "npx", "args": ["chant", "serve", "mcp"] } }}Manual Setup
Section titled “Manual Setup”If your agent doesn’t auto-detect the MCP config, configure the server manually. The command is:
chant serve mcpThe server communicates over stdin/stdout using JSON-RPC 2.0 with line-based framing (one JSON object per line).
Protocol Details
Section titled “Protocol Details”The MCP server implements the Model Context Protocol. It negotiates a revision rather than assuming the client’s, and understands these:
| Version | Role |
|---|---|
2026-07-28 | Preferred. Answered to a client that asks for it, and to one that names no version at all |
2025-11-25 | Answered to a client that asks for it by name. Claude Code 2.1.274 asks for this one |
2025-06-18 | Answered to a client that asks for it by name |
2025-03-26 | Answered to a client that asks for it by name |
2024-11-05 | Answered to a client that asks for it by name |
On the 2026-07-28 revision the requested version and the client identity travel in a _meta envelope on every request, and the server falls back to the top-level protocolVersion and clientInfo fields a prior-revision client (2024-11-05 or any 2025 revision) sends on initialize.
Supported Methods
Section titled “Supported Methods”| Method | Description |
|---|---|
initialize | Handshake — returns the negotiated protocol version, capabilities, and server info: name is chant, and version is the installed chant’s version, the one chant --version prints. At a workspace root with no lexicon of its own it also returns instructions |
server/discover | The initialize result merged with the tool and resource listings, so a client can discover everything in one call |
tools/list | List all available tools (core + plugin-contributed) |
tools/call | Invoke a tool by name with parameters |
resources/list | List all available resources (core + plugin-contributed) |
resources/read | Read a resource by URI |
A message with no id is a notification, notifications/initialized being the one every client sends, and gets no response at all. A tool whose handler returns an object is answered with both a text block and a structuredContent field holding the parsed shape.
Exit Codes
Section titled “Exit Codes”| Code | Meaning |
|---|---|
| 0 | Clean shutdown |
| 1 | Unknown subcommand or startup failure |
See Also
Section titled “See Also”- Agent Integration — Setup guide and
.mcp.jsonconfiguration chant serve lsp— LSP server for editor integration