Skip to main content

MCP Schema Reference

An MCP config (mcps/<key>.json) publishes a set of Stack9 capabilities to AI agents at /api/mcp/<key>. This is the canonical reference for the file's shape and, more importantly, for what each sourceType actually runs and what gates it — derived from packages/stack9-sdk/src/schema/validators/v2/McpValidator.ts and apps/core/src/controllers-core/McpEndpointController.ts.

Structure

{
"key": "pages_mcp",
"name": "Pages MCP",
"description": "Pages authoring tools",
"tools": [
{
"key": "getalldevelopers",
"name": "Get all developers",
"description": "Retrieves all developers.",
"sourceType": "query",
"inputSchema": {
"page": { "type": "number" },
"limit": { "type": "number", "optional": true }
}
},
{
"key": "pages_send_invite",
"name": "Send invite",
"sourceType": "automation",
"automationKey": "webhook_send_invite"
}
]
}
PropertyRequiredNotes
keyyesToken. Must match the filename.
nameyesServer name the agent sees.
descriptionno
toolsyesAt least one.
modulenoOwning module, when shipped by one.

Tool properties

PropertyRequiredNotes
keyyesToken. The tool name the agent calls.
nameyesTool title.
descriptionnoRead by the model — say when to use the tool, not just what it is.
sourceTypeyesquery or automation.
inputSchemanoFlat map of { type, description?, optional? }. Fields are required unless marked. Omit it on an automation tool to derive the schema from the action type's input — see below.
automationKeynoautomation tools only. The automation to run. Defaults to key.

Tool sourcing

sourceType: "query"

Runs the query library entry whose key is the tool key. Only entity-backed queries can be exposed: the entity is derived from the query's path, and the call is gated by the caller's read privilege on that entity — the same model the REST entity routes use. A query with no derivable entity (raw SQL, external connectors) is refused rather than run.

sourceType: "automation"

Runs one webhook automation:

  • Which automationautomationKey, or the tool key when the field is absent. That one key is the whole relationship: it selects the automation that runs and the permission row that gates it. The automation's own entityKey is passed to the run as trigger context; it does not select anything, and it does not need to match the tool key.
  • What gates it — the automation's app_automation_permissions row: the row's required role versus the caller's access level for the app the row names. Public is open to any authenticated caller. No row means denied — unlike the webhook route, which iterates candidates and treats a missing row as "not this one", an MCP tool names exactly one automation, so a missing row can only mean nobody granted it. Automation tools are not gated by any screen declaration; a screen never has to mention them.
  • A tool naming an automation that does not exist (or one that is not webhook-triggered) is a configuration error. It is reported at boot, and the call itself fails with that message — never a null result that reads as a successful empty run.

Where a tool's input schema comes from

A tool advertises exactly one schema, and — for the derived forms — the artefact it advertises is the same one the call is validated against. In precedence order:

  1. The tool's own inputSchema, when it declares one. Always wins, so no existing config changes what it advertises. Flat: one level, five types, object becomes additionalProperties: true, no enums, nothing enforced beyond the field types.
  2. sourceType: "automation" — the entry action type's input. When the automation's first action's action type declares an input zod object, the schema is derived from it, and the run parses the request body against that same object before any action executes. Nesting, enums, optionality and .strict() all survive; an invalid call is refused with 422 Invalid input for action "<actionTypeKey>": … and runs nothing — never a half-run. See Action Types → Declaring an input contract.
  3. sourceType: "query" — the generated model. The stack9-models input model for the query, every field optional.
  4. Nothing to derive → an empty schema (no arguments advertised).
Multi-action automations

The contract belongs to the automation's entry actionactions[0]. Later actions receive the previous action's next() output, not the caller's body, so only the first action describes what a caller sends. An automation with only conditionalActions has no entry action (which action runs first depends on the body being validated) and is therefore not validated: give such a tool an inputSchema, or front the automation with an unconditional entry action.

Why automationKey exists

Before it, the tool key had to equal the automation key and the automation's entityKey, because dispatch matched on entityKey while the permission lookup used the tool key. Two webhook automations sharing an entityKey both ran, and the last one's output won. Existing configs are unaffected: an automation tool with no automationKey resolves to its own key, which is exactly the automation those configs already ran.