Skip to content

chant serve mcp

llms.txt lists every page for an agent
chant serve mcp [path]

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.

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.

ParameterTypeRequiredDescription
pathstringYesPath to the infrastructure directory or file
outputstringNoAccepted by the schema and currently ignored; the built output always comes back in the response
format"json" | "yaml"NoOutput 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.

ParameterTypeRequiredDescription
pathstringYesPath to the infrastructure directory or file
fixbooleanNoAuto-fix fixable issues (default: false)

Returns { success, errorCount, warningCount, diagnostics, output }.

Import external templates and convert them to chant TypeScript.

ParameterTypeRequiredDescription
sourcestringYesPath to the template file to import
outputstringNoOutput directory (default: ./infra/)

The response is { success, lexicon, generatedFiles, warnings }. A failed import fails the call.

Summarize the resources in a chant project.

ParameterTypeRequiredDescription
pathstringYesPath to the infrastructure directory
format"markdown" | "json" | "okf"NoOutput 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.

Generate starter files for a chant project.

ParameterTypeRequiredDescription
patternstringYesResource pattern to scaffold (e.g. "s3-bucket", "pipeline")
lexiconstringNoLexicon to use for templates. Auto-detected if omitted
templatestringNoNamed 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 the resource types and the composites of the loaded lexicons.

ParameterTypeRequiredDescription
querystringYesSearch query (case-insensitive substring match)
lexiconstringNoFilter to a specific lexicon
limitnumberNoMaximum 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.

List the composites the loaded lexicons export. “What composites do you have for aws?” is a call with lexicon: "aws".

ParameterTypeRequiredDescription
lexiconstringNoOnly composites from this lexicon
querystringNoKeyword matched against the name, description, bundled resource kinds and parameter names
limitnumberNoMaximum 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.

Capture deployed state for an environment, reading every loaded lexicon that implements describeResources.

ParameterTypeRequiredDescription
environmentstringYesTarget environment
lexiconstringNoRestrict 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.

Compare the current build’s digest against the digest stored in an environment’s last snapshot.

ParameterTypeRequiredDescription
environmentstringYesTarget environment
lexiconstringNoRestrict 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.

List every Op discovered from *.op.ts files with the run state the runtime reports.

ParameterTypeRequiredDescription
runtimestringNoRuntime 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.

Run an Op on a runtime and return its result.

ParameterTypeRequiredDescription
namestringYesOp name (must match an *.op.ts file)
envstringNoEnvironment the run targets, as chant run --env names it; omitted, the runtime uses its own default
runtimestringNoRuntime 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.

Show the state the runtime reports for an Op’s latest run.

ParameterTypeRequiredDescription
namestringYesOp name
runtimestringNoRuntime 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.

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.

ParameterTypeRequiredDescription
namestringYesOp name
gatestringYesGate name (must match the gate’s name in the Op)
notestringNoFree-text context recorded on the resolution
urlstringNoAbsolute http/https URL this resolution happened at
envstringNoEnvironment the woken run runs in, as chant run approve --env names it; omitted, the runtime keeps the gated run’s
runtimestringNoRuntime 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>.

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.

ToolOn a declared Op
op-runRefused. The Op does not start.
op-approveRefused 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-reportAnswer 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.

Return a markdown report for an Op’s latest run.

ParameterTypeRequiredDescription
namestringYesOp name
runtimestringNoRuntime to address: a lexicon name with an opRuntime, or local (the default)

Lexicon plugins can add tools. Plugin tools are namespaced as {pluginName}:{toolName} to avoid conflicts with core 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.

ToolParametersReturns
workspace-lsatthe document of chant workspace ls --json (ls.schema.json)
workspace-statusenv (required), compareTothe document of chant workspace status <env> --json (status.schema.json)
workspace-graphkind (a list), intent, composites, atthe 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-changesrange (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-pointsopen, kind, atthe 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-agentnamethe 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-historyitem (required), kindthe 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-runsunit, decision, by, sincethe 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-recordskind, current, uncommitted, since, id, atthe 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-newrecord (required), kind, prefix, by, sign, dryRunthe result of chant workspace records new (records-new.schema.json)
records-amendid and fields (required), kind, by, sign, dryRunthe result of chant workspace records amend (records-amend.schema.json)
records-reviewid, verdict and by (required), kind, note, session, sign, dryRunthe result of chant workspace records review (records-review.schema.json), whose verdict moves the record’s quorum
records-closeid (required), kind, dryRunthe result of chant workspace records close (records-close.schema.json)
points-answerid, answer, by (all required), note, kind, dryRunthe 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-retractid, by (both required), note, kind, dryRunthe 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-withdrawid, by (both required), note, kind, dryRunthe 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.

Agents can read these resources:

URIMIME TypeDescription
chant://contexttext/markdownLexicon-specific instructions, patterns, and best practices
chant://examples/listapplication/jsonJSON array of available examples ([{ name, description }])
chant://opsapplication/jsonEvery Op discovered from *.op.ts, with overview, phase count, labels and dependencies
chant://ops/{name}/runsapplication/jsonRun history for a named Op, newest first
chant://ops/{name}/runs/latestapplication/jsonLatest run state for a named Op
chant://compositesapplication/jsonEvery composite the loaded lexicons export, the same entries the composites tool returns, unfiltered
chant://knowledgeapplication/jsonThe project’s OKF knowledge bundle as { dir, index, concepts }, empty when the project has authored none
chant://state/{environment}application/jsonEvery lexicon’s snapshot for an environment, keyed by lexicon
chant://state/{environment}/{lexicon}application/jsonOne 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}.

Lexicon plugins can add resources, namespaced as chant://{pluginName}/{path}. Plugin resources with /examples/ in their URI are included in the examples list.

chant init detects your IDE and adds the MCP server to that IDE’s global config file:

IDEConfig 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"]
}
}
}

If your agent doesn’t auto-detect the MCP config, configure the server manually. The command is:

Terminal window
chant serve mcp

The server communicates over stdin/stdout using JSON-RPC 2.0 with line-based framing (one JSON object per line).

The MCP server implements the Model Context Protocol. It negotiates a revision rather than assuming the client’s, and understands these:

VersionRole
2026-07-28Preferred. Answered to a client that asks for it, and to one that names no version at all
2025-11-25Answered to a client that asks for it by name. Claude Code 2.1.274 asks for this one
2025-06-18Answered to a client that asks for it by name
2025-03-26Answered to a client that asks for it by name
2024-11-05Answered 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.

MethodDescription
initializeHandshake — 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/discoverThe initialize result merged with the tool and resource listings, so a client can discover everything in one call
tools/listList all available tools (core + plugin-contributed)
tools/callInvoke a tool by name with parameters
resources/listList all available resources (core + plugin-contributed)
resources/readRead 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.

CodeMeaning
0Clean shutdown
1Unknown subcommand or startup failure

chant