mcp.projection
Who answers `initialize` and with which protocol versions; the transports and the sessions attached over each; the methods served, and that prompts and notifications are not; every tool (or only those with one effect) with its capability, effect and the hints its annotations are derived from; the tools that write the repository; the resources; and whether each client the distribution declares has a configuration here that starts this repository's server. Reads the registry and one file per declared client; writes nothing.
Who answers `initialize` and with which protocol versions; the transports and the sessions attached over each; the methods served, and that prompts and notifications are not; every tool (or only those with one effect) with its capability, effect and the hints its annotations are derived from; the tools that write the repository; the resources; and whether each client the distribution declares has a configuration here that starts this repository's server. Reads the registry and one file per declared client; writes nothing.
- query
- experimental
- module mcp
- #mcp
- #introspection
- #projections
Exposure
Where this one definition is reachable. Absence is explicit: a surface not listed does not carry it.
| surface | as |
|---|---|
| MCP | tool majordomus_mcp resource majordomus://mcp |
| HTTP | GET /api/v1/mcp operationId mcp.projection |
| command line | — |
Schemas
The canonical input and output, from the Rust types; the MCP tool schema and the OpenAPI parameters and responses are derived from these.
input · McpProjectionInput
mcp.projection: which tools to list. Everything else in the answer is
about the projection as a whole and is always given.
use majordomus_cli::capability::builtin::mcp::McpProjectionInput;
use majordomus_cli::capability::Effect;
let all: McpProjectionInput = serde_json::from_str("{}").unwrap();
assert!(all.effect.is_none());
let writers: McpProjectionInput =
serde_json::from_str(r#"{"effect":"repository_mutation"}"#).unwrap();
assert_eq!(writers.effect, Some(Effect::RepositoryMutation));| property | type | required | description |
|---|---|---|---|
| effect | one of 2 | no | List only the tools with this effect: read, process_state or
repository_mutation. Absent lists every tool. |
JSON Schema
{
"$defs": {
"ExecutionEffect": {
"description": "What running a capability changes outside the caller.\n\nClassified, never declared: it follows from the kind, which is the field a declaration\nalready carries. A projection reads this to decide whether to ask before running\nsomething — the Cockpit's confirmation is derived from it — instead of naming\ncapabilities it must treat carefully, which is a list that goes stale the day after it\nis written.",
"oneOf": [
{
"const": "read",
"description": "Nothing changes. Every query and every resource of this executable.",
"type": "string"
},
{
"const": "process_state",
"description": "This process's own memory changes, and nothing outside it.",
"type": "string"
},
{
"const": "repository_mutation",
"description": "The repository changes.\n\nFor most of this executable's life nothing classified to this, because no capability\nof any kind wrote to the repository. That day arrived with [ADR 0040]: development\nsemantics are capabilities of this runtime, and the first of them — `plan.transition`\n— stamps a field into an issue's own tracked record. The variant was put on the model\nin advance so that when one did, it would say so *here*, where a projection already\nreads it and a client already asks before running it, rather than in whichever page\nhappens to render its button.\n\nIt is not classified from the kind, because the kind cannot know it: a command that\nannounces a peer and a command that writes a tracked file are the same kind. A\nhandler that writes the repository says so with\n`ExecutionPolicy::writes_repository`, the way one that stops says so with\n`ExecutionPolicy::stoppable`.\n\n[ADR 0040]: https://github.com/korczis/prismatic-majordomus/blob/master/.ai/repo/adrs/0040-development-semantics-are-capabilities-of-one-runtime.md",
"type": "string"
}
]
}
},
"additionalProperties": false,
"description": "The input of `mcp.projection`: which tools to list. Everything else in the answer is\nabout the projection as a whole and is always given.\n\n```\nuse majordomus_cli::capability::builtin::mcp::McpProjectionInput;\nuse majordomus_cli::capability::Effect;\nlet all: McpProjectionInput = serde_json::from_str(\"{}\").unwrap();\nassert!(all.effect.is_none());\nlet writers: McpProjectionInput =\n serde_json::from_str(r#\"{\"effect\":\"repository_mutation\"}\"#).unwrap();\nassert_eq!(writers.effect, Some(Effect::RepositoryMutation));\n```",
"properties": {
"effect": {
"anyOf": [
{
"$ref": "#/$defs/ExecutionEffect"
},
{
"type": "null"
}
],
"description": "List only the tools with this effect: `read`, `process_state` or\n`repository_mutation`. Absent lists every tool."
}
},
"title": "McpProjectionInput",
"type": "object"
}output · McpProjection
use majordomus_cli::capability::builtin::mcp::McpProjection;
let p: McpProjection = serde_json::from_value(serde_json::json!({
"server": { "name": "majordomus", "title": "Majordomus", "version": "0.14.0" },
"protocol_versions": ["2025-06-18"],
"transports": [{ "id": "stdio", "reached_by": "majordomus mcp", "attached": 0 }],
"serving": { "methods": ["initialize"], "tools": true, "resources": true, "prompts": false, "notifications": false },
"tool_count": 2,
"tools": [],
"effects": [{ "effect": "read", "tools": 1 }, { "effect": "repository_mutation", "tools": 1 }],
"writers": ["majordomus_plan_transition"],
"resources": { "builtin": 1, "objects": 0, "template": "majordomus://<kind>/<identity>" },
"launcher_in_repository": false,
"clients": [],
"findings": [],
})).unwrap();
// the count is of every tool, whatever the `effect` filter narrowed `tools` to
assert_eq!(p.tool_count, p.effects.iter().map(|e| e.tools).sum::<usize>());
assert!(p.tools.is_empty() && p.writers.len() == 1);| property | type | required | description |
|---|---|---|---|
| clients | array | yes | The clients the distribution declares a configuration for, and where each stands. |
| effects | array | yes | The tools counted by effect, every effect present. |
| findings | array | yes | What a person should act on. Empty is nothing to do. |
| launcher_in_repository | boolean | yes | Whether this repository carries the launcher at bin/majordomus-mcp. Where it does
not, the installed command of the same name is what a configuration names. |
| protocol_versions | array | yes | The protocol versions accepted, newest first. |
| resources | reference | yes | The resources. |
| server | reference | yes | Who answers initialize. |
| serving | reference | yes | What of the protocol is served. |
| tool_count | integer | yes | How many tools the registry projects, whatever the filter asked for. |
| tools | array | yes | The tools asked for, in registry order. |
| transports | array | yes | How a client reaches the server. |
| writers | array | yes | The tools that write the repository, by name. The initialize instructions name the
same ones. |
JSON Schema
{
"$defs": {
"ExecutionEffect": {
"description": "What running a capability changes outside the caller.\n\nClassified, never declared: it follows from the kind, which is the field a declaration\nalready carries. A projection reads this to decide whether to ask before running\nsomething — the Cockpit's confirmation is derived from it — instead of naming\ncapabilities it must treat carefully, which is a list that goes stale the day after it\nis written.",
"oneOf": [
{
"const": "read",
"description": "Nothing changes. Every query and every resource of this executable.",
"type": "string"
},
{
"const": "process_state",
"description": "This process's own memory changes, and nothing outside it.",
"type": "string"
},
{
"const": "repository_mutation",
"description": "The repository changes.\n\nFor most of this executable's life nothing classified to this, because no capability\nof any kind wrote to the repository. That day arrived with [ADR 0040]: development\nsemantics are capabilities of this runtime, and the first of them — `plan.transition`\n— stamps a field into an issue's own tracked record. The variant was put on the model\nin advance so that when one did, it would say so *here*, where a projection already\nreads it and a client already asks before running it, rather than in whichever page\nhappens to render its button.\n\nIt is not classified from the kind, because the kind cannot know it: a command that\nannounces a peer and a command that writes a tracked file are the same kind. A\nhandler that writes the repository says so with\n`ExecutionPolicy::writes_repository`, the way one that stops says so with\n`ExecutionPolicy::stoppable`.\n\n[ADR 0040]: https://github.com/korczis/prismatic-majordomus/blob/master/.ai/repo/adrs/0040-development-semantics-are-capabilities-of-one-runtime.md",
"type": "string"
}
]
},
"ExecutionHints": {
"description": "What a caller may assume about a call before making it: the four facts MCP names tool\nannotations, held once on the model so that no projection states its own.\n\nClassified by `ExecutionPolicy::hints`, never declared.\n\n```\nuse majordomus_cli::capability::{CapabilityKind, ExecutionPolicy, Hints};\nlet read: Hints = ExecutionPolicy::classify(CapabilityKind::Query).hints();\nassert_eq!(\n serde_json::to_value(read).unwrap(),\n serde_json::json!({ \"read_only\": true, \"destructive\": false, \"idempotent\": true, \"open_world\": false })\n);\n```",
"properties": {
"destructive": {
"description": "The call may overwrite or remove something that outlives this process.",
"type": "boolean"
},
"idempotent": {
"description": "A second identical call changes nothing the first did not.",
"type": "boolean"
},
"open_world": {
"description": "The call reaches beyond this machine. No handler of this executable does: observing\nthe forge and talking to an advisor are commands a person runs, not capabilities.",
"type": "boolean"
},
"read_only": {
"description": "The call changes nothing, anywhere.",
"type": "boolean"
}
},
"required": [
"read_only",
"destructive",
"idempotent",
"open_world"
],
"type": "object"
},
"McpClient": {
"description": "One client the distribution declares a project-scoped configuration for.\n\n```\nuse majordomus_cli::capability::builtin::mcp::{McpClient, McpClientStanding};\nlet mut clients: Vec<McpClient> = serde_json::from_value(serde_json::json!([\n { \"id\": \"gemini\", \"title\": \"Gemini CLI\", \"config\": \".gemini/settings.json\", \"standing\": \"absent\" },\n { \"id\": \"claude-code\", \"title\": \"Claude Code\", \"config\": \".mcp.json\", \"standing\": \"wired\" },\n])).unwrap();\n// ordered by the provider's id, whatever order the distribution declared them in\nmajordomus_cli::order::canonical(&mut clients);\nassert_eq!(clients[0].id, \"claude-code\");\nassert_eq!(clients[0].standing, McpClientStanding::Wired);\n```",
"properties": {
"config": {
"description": "The file it reads, repository-relative.",
"type": "string"
},
"id": {
"description": "The provider id.",
"type": "string"
},
"standing": {
"$ref": "#/$defs/McpClientStanding",
"description": "Where that file stands here."
},
"title": {
"description": "The name a person knows it by.",
"type": "string"
}
},
"required": [
"id",
"title",
"config",
"standing"
],
"type": "object"
},
"McpClientStanding": {
"description": "Where a client's project-scoped configuration stands in this repository.\n\n```\nuse majordomus_cli::capability::builtin::mcp::McpClientStanding;\n// the words a caller reads, as they are written on every surface\nassert_eq!(serde_json::to_value(McpClientStanding::Wired).unwrap(), \"wired\");\nassert_eq!(serde_json::to_value(McpClientStanding::Foreign).unwrap(), \"foreign\");\nassert_eq!(serde_json::to_value(McpClientStanding::Absent).unwrap(), \"absent\");\n```",
"oneOf": [
{
"const": "wired",
"description": "The file exists and names the launcher: this client starts this repository's server.",
"type": "string"
},
{
"const": "foreign",
"description": "The file exists and does not name the launcher: the client reads it and starts\nsomething else, or nothing.",
"type": "string"
},
{
"const": "absent",
"description": "The file does not exist: this client starts no server here until it is written.",
"type": "string"
}
]
},
"McpEffectCount": {
"description": "How many tools have one effect.\n\n```\nuse majordomus_cli::capability::builtin::mcp::McpEffectCount;\nuse majordomus_cli::capability::Effect;\nlet c: McpEffectCount =\n serde_json::from_value(serde_json::json!({ \"effect\": \"read\", \"tools\": 149 })).unwrap();\nassert_eq!((c.effect, c.tools), (Effect::Read, 149));\n```",
"properties": {
"effect": {
"$ref": "#/$defs/ExecutionEffect",
"description": "The effect."
},
"tools": {
"description": "How many tools have it.",
"format": "uint",
"minimum": 0,
"type": "integer"
}
},
"required": [
"effect",
"tools"
],
"type": "object"
},
"McpFinding": {
"description": "Something about the projection a person should act on.\n\n```\nuse majordomus_cli::capability::builtin::mcp::McpFinding;\nlet f: McpFinding = serde_json::from_value(serde_json::json!({\n \"code\": \"mcp_client_config_foreign\",\n \"message\": \".mcp.json exists and does not name majordomus-mcp\",\n \"remedy\": \"add a `majordomus` server to .mcp.json whose command is majordomus-mcp\",\n})).unwrap();\n// a finding always says what to do, not only what is wrong\nassert!(!f.code.is_empty() && !f.remedy.is_empty());\n```",
"properties": {
"code": {
"description": "The stable code.",
"type": "string"
},
"message": {
"description": "What is wrong.",
"type": "string"
},
"remedy": {
"description": "What to do about it.",
"type": "string"
}
},
"required": [
"code",
"message",
"remedy"
],
"type": "object"
},
"McpResourceSummary": {
"description": "The resources a client can read.\n\n```\nuse majordomus_cli::capability::builtin::mcp::McpResourceSummary;\nlet r: McpResourceSummary = serde_json::from_value(serde_json::json!({\n \"builtin\": 54, \"objects\": 1742, \"template\": \"majordomus://<kind>/<identity>\",\n})).unwrap();\n// the two counts together are the length of `resources/list`\nassert_eq!(r.builtin + r.objects, 1796);\n```",
"properties": {
"builtin": {
"description": "Resources a builtin capability answers.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"objects": {
"description": "Objects of this repository's layer, each one more resource.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"template": {
"description": "The URI shape of an object's resource.",
"type": "string"
}
},
"required": [
"builtin",
"objects",
"template"
],
"type": "object"
},
"McpServerIdentity": {
"description": "Who answers `initialize`, as it answers.\n\n```\nuse majordomus_cli::capability::builtin::mcp::McpServerIdentity;\nlet s: McpServerIdentity = serde_json::from_value(serde_json::json!({\n \"name\": \"majordomus\", \"title\": \"Majordomus\", \"version\": \"0.14.0\",\n})).unwrap();\n// the version is the executable's: the protocol adapter has none of its own to drift\nassert_eq!((s.name.as_str(), s.version.as_str()), (\"majordomus\", \"0.14.0\"));\n```",
"properties": {
"name": {
"description": "The server name.",
"type": "string"
},
"title": {
"description": "The name a person reads.",
"type": "string"
},
"version": {
"description": "The version: this executable's, never one of the protocol adapter's own.",
"type": "string"
}
},
"required": [
"name",
"title",
"version"
],
"type": "object"
},
"McpServing": {
"description": "What the server serves of the protocol, and what it does not.\n\n```\nuse majordomus_cli::capability::builtin::mcp::McpServing;\nlet s: McpServing = serde_json::from_value(serde_json::json!({\n \"methods\": [\"initialize\", \"tools/call\"], \"tools\": true, \"resources\": true,\n \"prompts\": false, \"notifications\": false,\n})).unwrap();\n// what is not served is stated, so a client does not learn it from a refusal\nassert!(s.tools && !s.prompts && !s.notifications);\nassert!(!s.methods.iter().any(|m| m.starts_with(\"prompts/\")));\n```",
"properties": {
"methods": {
"description": "The methods a request may name. Anything else is answered `-32601`.",
"items": {
"type": "string"
},
"type": "array"
},
"notifications": {
"description": "Subscriptions and list-change notifications. Not served: this server sends nothing\nunasked.",
"type": "boolean"
},
"prompts": {
"description": "Prompts. Not served: the repository's prompt assets are resources, because a prompt\nserved unrendered would read as finished.",
"type": "boolean"
},
"resources": {
"description": "Resources: every capability that declares one, and every object of the layer.",
"type": "boolean"
},
"tools": {
"description": "Tools: every capability that declares one.",
"type": "boolean"
}
},
"required": [
"methods",
"tools",
"resources",
"prompts",
"notifications"
],
"type": "object"
},
"McpToolEntry": {
"description": "One tool, as the registry projects it.\n\n```\nuse majordomus_cli::capability::builtin::mcp::McpToolEntry;\nuse majordomus_cli::capability::Effect;\nlet t: McpToolEntry = serde_json::from_value(serde_json::json!({\n \"name\": \"majordomus_plan_transition\", \"capability\": \"plan.transition\",\n \"module\": \"plan\", \"title\": \"Move an issue\", \"effect\": \"repository_mutation\",\n \"hints\": { \"read_only\": false, \"destructive\": true, \"idempotent\": false, \"open_world\": false },\n})).unwrap();\nassert_eq!(t.effect, Effect::RepositoryMutation);\n// the hints are the effect's classification, carried beside it rather than restated\nassert!(t.hints.destructive && !t.hints.read_only && !t.hints.idempotent);\n```",
"properties": {
"capability": {
"description": "The capability it projects; `capabilities.describe` answers its schemas.",
"type": "string"
},
"effect": {
"$ref": "#/$defs/ExecutionEffect",
"description": "What a call changes."
},
"hints": {
"$ref": "#/$defs/ExecutionHints",
"description": "What a caller may assume before calling; the tool's annotations are this value."
},
"module": {
"description": "The module that composes the capability.",
"type": "string"
},
"name": {
"description": "The name a client calls.",
"type": "string"
},
"title": {
"description": "The title.",
"type": "string"
}
},
"required": [
"name",
"capability",
"module",
"title",
"effect",
"hints"
],
"type": "object"
},
"McpTransportEntry": {
"description": "One way a client reaches the server.\n\n```\nuse majordomus_cli::capability::builtin::mcp::McpTransportEntry;\nlet t: McpTransportEntry = serde_json::from_value(serde_json::json!({\n \"id\": \"stdio\", \"reached_by\": \"majordomus mcp\", \"attached\": 1,\n})).unwrap();\n// a count of sessions at the moment of asking, not a claim that a server is running\nassert_eq!((t.id.as_str(), t.attached), (\"stdio\", 1));\n```",
"properties": {
"attached": {
"description": "Sessions attached to this process over it right now.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"id": {
"description": "`stdio` or `http`.",
"type": "string"
},
"reached_by": {
"description": "How it is reached: the command a client starts, or the path on the shared server.",
"type": "string"
}
},
"required": [
"id",
"reached_by",
"attached"
],
"type": "object"
}
},
"description": "The MCP projection of this registry in this repository.\n\n```\nuse majordomus_cli::capability::builtin::mcp::McpProjection;\nlet p: McpProjection = serde_json::from_value(serde_json::json!({\n \"server\": { \"name\": \"majordomus\", \"title\": \"Majordomus\", \"version\": \"0.14.0\" },\n \"protocol_versions\": [\"2025-06-18\"],\n \"transports\": [{ \"id\": \"stdio\", \"reached_by\": \"majordomus mcp\", \"attached\": 0 }],\n \"serving\": { \"methods\": [\"initialize\"], \"tools\": true, \"resources\": true, \"prompts\": false, \"notifications\": false },\n \"tool_count\": 2,\n \"tools\": [],\n \"effects\": [{ \"effect\": \"read\", \"tools\": 1 }, { \"effect\": \"repository_mutation\", \"tools\": 1 }],\n \"writers\": [\"majordomus_plan_transition\"],\n \"resources\": { \"builtin\": 1, \"objects\": 0, \"template\": \"majordomus://<kind>/<identity>\" },\n \"launcher_in_repository\": false,\n \"clients\": [],\n \"findings\": [],\n})).unwrap();\n// the count is of every tool, whatever the `effect` filter narrowed `tools` to\nassert_eq!(p.tool_count, p.effects.iter().map(|e| e.tools).sum::<usize>());\nassert!(p.tools.is_empty() && p.writers.len() == 1);\n```",
"properties": {
"clients": {
"description": "The clients the distribution declares a configuration for, and where each stands.",
"items": {
"$ref": "#/$defs/McpClient"
},
"type": "array"
},
"effects": {
"description": "The tools counted by effect, every effect present.",
"items": {
"$ref": "#/$defs/McpEffectCount"
},
"type": "array"
},
"findings": {
"description": "What a person should act on. Empty is nothing to do.",
"items": {
"$ref": "#/$defs/McpFinding"
},
"type": "array"
},
"launcher_in_repository": {
"description": "Whether this repository carries the launcher at `bin/majordomus-mcp`. Where it does\nnot, the installed command of the same name is what a configuration names.",
"type": "boolean"
},
"protocol_versions": {
"description": "The protocol versions accepted, newest first.",
"items": {
"type": "string"
},
"type": "array"
},
"resources": {
"$ref": "#/$defs/McpResourceSummary",
"description": "The resources."
},
"server": {
"$ref": "#/$defs/McpServerIdentity",
"description": "Who answers `initialize`."
},
"serving": {
"$ref": "#/$defs/McpServing",
"description": "What of the protocol is served."
},
"tool_count": {
"description": "How many tools the registry projects, whatever the filter asked for.",
"format": "uint",
"minimum": 0,
"type": "integer"
},
"tools": {
"description": "The tools asked for, in registry order.",
"items": {
"$ref": "#/$defs/McpToolEntry"
},
"type": "array"
},
"transports": {
"description": "How a client reaches the server.",
"items": {
"$ref": "#/$defs/McpTransportEntry"
},
"type": "array"
},
"writers": {
"description": "The tools that write the repository, by name. The `initialize` instructions name the\nsame ones.",
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"server",
"protocol_versions",
"transports",
"serving",
"tool_count",
"tools",
"effects",
"writers",
"resources",
"launcher_in_repository",
"clients",
"findings"
],
"title": "McpProjection",
"type": "object"
}Policies
- benchmark
- required — a target on every transport the exposure declares; the cases are the input type's
- cache
- disabled — every call runs the handler
Benchmark targets
Derived from the registry for this repository: one requirement per transport, and the cases the input type provides. The whole matrix is on the benchmarks page.
| transport | state | cases | targets |
|---|---|---|---|
| direct | covered | 2 | every-tool writers |
| mcp | covered | 2 | every-tool writers |
| http | covered | 2 | every-tool writers |