car-mcp 0.49.0

MCP (Model Context Protocol) server library — transport-agnostic dispatch for exposing CAR capabilities. Used by car-mcp-server (stdio binary) and car-server (HTTP-streamable daemon endpoint).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
//! Cached MCP tool + prompt schema lists.
//!
//! Wrapped in `OnceLock` so the JSON values are constructed once
//! per process even though `tools/list` and `prompts/list` get
//! called frequently. This is the *built-in* set, which every
//! transport advertises; the `OnceLock` caches that baseline rather
//! than the whole advertised surface, because a transport may add
//! tools of its own via `Server::register_tool` and the advertised
//! list is therefore per-`Server`.
//!
//! Adding a built-in means editing this file, and adding a tool means
//! *classifying* it: every entry carries all four `annotations` hints,
//! and `every_advertised_tool_carries_all_four_annotations`
//! (server.rs) fails the build if one is missing. `register_tool`
//! enforces the same four on a registered schema, so the seam does not
//! route around this gate.
//!
//! The hints are host UX only. They exist so a host can auto-approve
//! `memory_query` without prompting the way it must for
//! `memory_delete`. They are NOT a security boundary and nothing in
//! CAR's own governance may key off them — the spec requires a client
//! to treat annotations from an untrusted server as hints and never as
//! guarantees, so the gate stays `policy_check` / the policy layer.
//!
//! Two conventions worth knowing before adding an entry:
//!
//! - `readOnlyHint: true` implies `destructiveHint: false`. The spec
//!   says `destructiveHint` is meaningless when `readOnlyHint` is set
//!   and *defaults to true*, so leaving it unset on a read-only tool
//!   is a trap for a host that reads it naively.
//! - "Destructive" means an existing entry is overwritten or retired,
//!   not merely that the call writes. An append-only write is
//!   `destructiveHint: false`.

use serde_json::{json, Value};
use std::sync::OnceLock;

pub fn cached_tool_schemas() -> &'static Vec<Value> {
    static SCHEMAS: OnceLock<Vec<Value>> = OnceLock::new();
    SCHEMAS.get_or_init(tool_schemas)
}

pub fn cached_prompt_schemas() -> &'static Vec<Value> {
    static PROMPTS: OnceLock<Vec<Value>> = OnceLock::new();
    PROMPTS.get_or_init(prompt_schemas)
}

fn tool_schemas() -> Vec<Value> {
    vec![
        json!({
            "name": "memory_add_fact",
            "description": "Ingest a fact into CAR's graph memory. Kind defaults to \"pattern\"; use \"constraint\" for hard rules.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "subject": { "type": "string" },
                    "body": { "type": "string" },
                    "kind": { "type": "string", "enum": ["pattern", "constraint"] },
                },
                "required": ["subject", "body"],
            },
            "annotations": {
                "readOnlyHint": false,
                "destructiveHint": false,
                "idempotentHint": false,
                "openWorldHint": false,
            },
        }),
        json!({
            "name": "memory_query",
            "description": "Query CAR graph memory using spreading activation. Returns top-k nodes with activation scores.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "query": { "type": "string" },
                    "k": { "type": "integer", "minimum": 1, "maximum": 50 },
                },
                "required": ["query"],
            },
            "annotations": {
                "readOnlyHint": true,
                "destructiveHint": false,
                "idempotentHint": true,
                "openWorldHint": false,
            },
        }),
        json!({
            "name": "memory_update_status",
            "description": "Update proactive memory's private progress/risk status. Status is session-local and not exposed through generic context retrieval.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "body": { "type": "string" },
                    "tenant_id": { "type": "string" },
                },
                "required": ["body"],
            },
            // Destructive because the status is a map slot, not an append: the insert
            // at car-memgine engine.rs:766 drops whatever status was already there.
            "annotations": {
                "readOnlyHint": false,
                "destructiveHint": true,
                "idempotentHint": true,
                "openWorldHint": false,
            },
        }),
        json!({
            "name": "memory_save_knowledge",
            "description": "Save durable proactive knowledge such as task requirements, policies, verified environment facts, and constraints.",
            "inputSchema": proactive_save_schema(),
            // Additive despite "save": a repeat with the same `id` mints `<id>-2`
            // rather than replacing it (car-memgine engine.rs:628 dedupes fact ids),
            // so nothing is overwritten and no call is a no-op.
            "annotations": {
                "readOnlyHint": false,
                "destructiveHint": false,
                "idempotentHint": false,
                "openWorldHint": false,
            },
        }),
        json!({
            "name": "memory_save_procedural",
            "description": "Save durable proactive procedural evidence such as failed attempts, successful fixes, diagnostics, and tool gotchas.",
            "inputSchema": proactive_save_schema(),
            // Additive, for the same reason as memory_save_knowledge — both land in
            // `save_proactive_fact` (car-memgine engine.rs:794).
            "annotations": {
                "readOnlyHint": false,
                "destructiveHint": false,
                "idempotentHint": false,
                "openWorldHint": false,
            },
        }),
        json!({
            "name": "memory_delete",
            "description": "Delete a proactive memory entry by fact id, or clear a private status id like proactive-status:global.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "id": { "type": "string" },
                },
                "required": ["id"],
            },
            "annotations": {
                "readOnlyHint": false,
                "destructiveHint": true,
                "idempotentHint": true,
                "openWorldHint": false,
            },
        }),
        json!({
            "name": "memory_intervene",
            "description": "Select at most one targeted proactive memory reminder for the next action, or return an explicit silent decision.",
            "inputSchema": proactive_request_schema(),
            // A write, despite reading like a query: selecting a reminder bumps the
            // chosen fact's `proactive_injections` counter (car-memgine engine.rs:728),
            // so it is neither read-only nor repeatable without effect.
            "annotations": {
                "readOnlyHint": false,
                "destructiveHint": false,
                "idempotentHint": false,
                "openWorldHint": false,
            },
        }),
        json!({
            "name": "memory_evaluate",
            "description": "Evaluate proactive memory on labeled cases against selective, always-inject, passive-retrieval, and no-memory baselines.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "cases": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "id": { "type": "string" },
                                "request": proactive_request_schema(),
                                "relevant_fact_ids": { "type": "array", "items": { "type": "string" } },
                            },
                            "required": ["id", "request"],
                        },
                    },
                },
                "required": ["cases"],
            },
            "annotations": {
                "readOnlyHint": true,
                "destructiveHint": false,
                "idempotentHint": true,
                "openWorldHint": false,
            },
        }),
        json!({
            "name": "verify",
            "description": "Statically verify an ActionProposal: detect dependency cycles, missing tools, and simulate final state. No execution, no side effects. Each issue carries a 'tier' — decision_procedure | heuristic | sampled — naming which kind of check produced it, so a rule of thumb (loop detection) is distinguishable from an exact check (tool registration) without reading the message text.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "proposal": { "type": "object", "description": "A car_ir::ActionProposal JSON object" },
                    "max_actions": { "type": "integer", "minimum": 1, "maximum": 1000 },
                },
                "required": ["proposal"],
            },
            "annotations": {
                "readOnlyHint": true,
                "destructiveHint": false,
                "idempotentHint": true,
                "openWorldHint": false,
            },
        }),
        json!({
            "name": "simulate",
            "description": "Predict the state an executor would leave behind after running an ActionProposal, by applying each action's DECLARED expected_effects. Runs no tools and has no side effects. An action whose preconditions or state dependencies are unsatisfied contributes nothing, and the actions downstream of it drop out with it, so the cascade follows the data dependencies. A declared effect is ASSUMED to land — this predicts what the declarations imply, not what a tool would really do — and failure_behavior is not modelled, so an independent action alongside a blocked one still contributes here even though the executor's default Abort may never reach it. Read the result as the state assuming execution proceeds as far as the dependency graph allows, never as a claim that a blocked action ran.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "proposal": { "type": "object", "description": "A car_ir::ActionProposal JSON object" },
                    "initial_state": { "type": "object", "description": "State the proposal starts from. Defaults to empty." },
                },
                "required": ["proposal"],
            },
            "annotations": {
                "readOnlyHint": true,
                "destructiveHint": false,
                "idempotentHint": true,
                "openWorldHint": false,
            },
        }),
        json!({
            "name": "equivalent",
            "description": "Check whether two ActionProposals leave the same state behind. This SAMPLES: it probes the states in test_states and nothing else — two trivial defaults (empty, and {x:1, y:2}) when you pass none. A false is a witness, some sampled state separates the two proposals. A true means only that none of the sampled states did; it is not a claim that they never diverge, so widen test_states to raise your confidence. The result carries 'tier': 'sampled', 'states_tested' and 'used_default_states' so a client reads how the answer was derived — including whether the true came off the two trivial defaults or off states you chose — without parsing this text.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "proposal_a": { "type": "object", "description": "A car_ir::ActionProposal JSON object" },
                    "proposal_b": { "type": "object", "description": "The car_ir::ActionProposal to compare it against" },
                    "test_states": {
                        "type": "array",
                        "items": { "type": "object" },
                        "minItems": 1,
                        // The advertised cap and the enforced one are the same
                        // constant, so a schema that promises more than
                        // `tool_equivalent` will accept is a compile-time
                        // impossibility rather than a drift waiting to happen.
                        "maxItems": crate::server::MAX_TEST_STATES,
                        "description": "States to probe. Omit it to get the two trivial defaults — supply your own to sample where it matters. Do not send an empty array: zero probes would be a 'true' backed by nothing, so the server treats [] as omitted rather than answering off no evidence.",
                    },
                },
                "required": ["proposal_a", "proposal_b"],
            },
            "annotations": {
                "readOnlyHint": true,
                "destructiveHint": false,
                "idempotentHint": true,
                "openWorldHint": false,
            },
        }),
        json!({
            "name": "optimize",
            "description": "Rewrite an ActionProposal to expose more parallelism: drop every state_dependency naming a key no action in the proposal writes, so the DAG builder can place more actions in one execution level. Action order and everything else are unchanged. This REWRITES, it does not check — a pruned dependency is one `verify` would have flagged as unavailable, so treat the returned proposal as a new input to `verify` rather than reading the rewrite as a repair. 'pruned' lists exactly what was dropped, per action.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "proposal": { "type": "object", "description": "A car_ir::ActionProposal JSON object" },
                },
                "required": ["proposal"],
            },
            "annotations": {
                "readOnlyHint": true,
                "destructiveHint": false,
                "idempotentHint": true,
                "openWorldHint": false,
            },
        }),
        json!({
            "name": "skill_ingest",
            "description": "Ingest a skill into CAR's graph memory. A skill is executable code associated with a trigger (persona + url pattern + task keywords) so skill_find can later retrieve it for matching tasks.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "name": { "type": "string" },
                    "code": { "type": "string", "description": "Skill body — code, recipe, or procedure text" },
                    "platform": { "type": "string" },
                    "persona": { "type": "string" },
                    "url_pattern": { "type": "string" },
                    "description": { "type": "string" },
                    "task_keywords": { "type": "array", "items": { "type": "string" } },
                    "supersedes": { "type": "string", "description": "Name of an older skill this replaces" },
                },
                "required": ["name", "code"],
            },
            // Destructive when `supersedes` names an existing skill: that skill's node
            // is flipped to `SkillDeprecated` (car-memgine engine.rs:1253) and stops
            // matching skill_find. Not idempotent either — every call inserts a node.
            "annotations": {
                "readOnlyHint": false,
                "destructiveHint": true,
                "idempotentHint": false,
                "openWorldHint": false,
            },
        }),
        json!({
            "name": "skill_list",
            "description": "Enumerate all ingested skills. Optional domain filter returns only skills scoped Global or Domain(domain).",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "domain": { "type": "string" },
                },
            },
            "annotations": {
                "readOnlyHint": true,
                "destructiveHint": false,
                "idempotentHint": true,
                "openWorldHint": false,
            },
        }),
        json!({
            "name": "skill_find",
            "description": "Find top-k skills matching a persona/url/task triple, ranked by activation.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "persona": { "type": "string" },
                    "url": { "type": "string" },
                    "task": { "type": "string" },
                    "k": { "type": "integer", "minimum": 1, "maximum": 20 },
                },
                "required": ["task"],
            },
            "annotations": {
                "readOnlyHint": true,
                "destructiveHint": false,
                "idempotentHint": true,
                "openWorldHint": false,
            },
        }),
        json!({
            "name": "policy_check",
            "description": "Evaluate a proposed tool call against CAR's policy layer BEFORE it runs. Built for a host's PreToolUse hook: pass the tool name and its parameters, get back allow/deny with the rule that decided it. Merges the operator's declarative rules from <CAR_HOME>/policies/ and .car/policies/ (under the working directory; neither is walked upward) with CAR's stateless egress guardrail. Read 'basis' as well as 'decision' — 'no_rules_configured' means nothing was loaded and the allow reviewed nothing, which is NOT the same as 'passed_rules'. An unparseable policy file denies ('policy_load_failed') rather than failing open. Findings carry 'source' so an operator-authored rule is distinguishable from a built-in guardrail, and 'severity' so a warn is distinguishable from a deny.",
            "inputSchema": {
                "type": "object",
                "properties": {
                    "tool": { "type": "string", "description": "Name of the tool the calling agent proposes to run." },
                    "params": { "type": "object", "description": "The tool's parameters, as the calling agent would pass them." },
                },
                "required": ["tool"],
            },
            "annotations": {
                "readOnlyHint": true,
                "destructiveHint": false,
                "idempotentHint": true,
                "openWorldHint": false,
            },
        }),
    ]
}

fn proactive_save_schema() -> Value {
    json!({
        "type": "object",
        "properties": {
            "id": { "type": "string" },
            "subject": { "type": "string" },
            "body": { "type": "string" },
            "tags": { "type": "array", "items": { "type": "string" } },
            "confidence": { "type": "string" },
            "tenant_id": { "type": "string" },
            "is_constraint": { "type": "boolean" },
        },
        "required": ["subject", "body"],
    })
}

fn proactive_request_schema() -> Value {
    json!({
        "type": "object",
        "properties": {
            "query": { "type": "string" },
            "recent": { "type": "array", "items": { "type": "string" } },
            "trigger": {
                "type": "object",
                "properties": {
                    "repeated_failures": { "type": "integer", "minimum": 0 },
                    "tool_error": { "type": "boolean" },
                    "explicit_uncertainty": { "type": "boolean" },
                    "high_risk_action": { "type": "boolean" },
                    "context_shift": { "type": "boolean" },
                },
            },
            "force": { "type": "boolean" },
            "max_candidates": { "type": "integer", "minimum": 1, "maximum": 32 },
            "tenant_id": { "type": "string" },
        },
    })
}

fn prompt_schemas() -> Vec<Value> {
    vec![json!({
        "name": "car_context",
        "description": "Assemble CAR's four-layer context (identity → constraints → facts → conversation → environment → known-unknowns) for a query. Returns the context as a single user message the host can prepend to its own prompt.",
        "arguments": [
            { "name": "query", "description": "Task or question the context should be assembled for.", "required": true },
            { "name": "mode", "description": "\"full\" (default) or \"fast\" — fast skips embedding flush, skill lookup, PPR scoring.", "required": false },
        ],
    })]
}