//! 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 },
],
})]
}