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"
}
]
}
| Property | Required | Notes |
|---|---|---|
key | yes | Token. Must match the filename. |
name | yes | Server name the agent sees. |
description | no | |
tools | yes | At least one. |
module | no | Owning module, when shipped by one. |
Tool properties
| Property | Required | Notes |
|---|---|---|
key | yes | Token. The tool name the agent calls. |
name | yes | Tool title. |
description | no | Read by the model — say when to use the tool, not just what it is. |
sourceType | yes | query or automation. |
inputSchema | no | Flat 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. |
automationKey | no | automation 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 automation —
automationKey, 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 ownentityKeyis 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_permissionsrow: the row's required role versus the caller's access level for the app the row names.Publicis 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
nullresult 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:
- The tool's own
inputSchema, when it declares one. Always wins, so no existing config changes what it advertises. Flat: one level, five types,objectbecomesadditionalProperties: true, no enums, nothing enforced beyond the field types. sourceType: "automation"— the entry action type'sinput. When the automation's first action's action type declares aninputzod object, the schema is derived from it, and the runparses the request body against that same object before any action executes. Nesting, enums, optionality and.strict()all survive; an invalid call is refused with422Invalid input for action "<actionTypeKey>": …and runs nothing — never a half-run. See Action Types → Declaring an input contract.sourceType: "query"— the generated model. Thestack9-modelsinput model for the query, every field optional.- Nothing to derive → an empty schema (no arguments advertised).
The contract belongs to the automation's entry action — actions[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.
automationKey existsBefore 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.
Related
- Automations — trigger types, including
webhook. - Action Types — what an automation's actions receive,
including
requestId/correlationId. - Automation Schema Reference