Skip to main content

ai_memory/mcp/
param_names.rs

1// Copyright 2026 AlphaOne LLC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Canonical SSOT for MCP tool-call parameter field names.
5//
6// Closes Fix #5 (literal-sweep v0.7.0 deferred-item): every MCP tool
7// handler that extracts a field from `serde_json::Value` arguments via
8// `.get("X")` / `["X"]` references a JSON key whose canonical truth
9// lived as a scattered string literal across ~100 callsites under
10// `src/mcp/tools/*.rs` + `src/mcp/mod.rs`. This module centralizes
11// every such name as a `pub const FOO: &str = "foo"` so:
12//
13//   1. typos at extraction-site are caught at compile time (a missing
14//      const → "use of undeclared name" rust-error),
15//   2. JSON-Schema fields and the runtime extraction agree by SSOT
16//      reference rather than literal-duplication,
17//   3. a mechanical parity test (`tests/mcp_param_names_invariant.rs`)
18//      asserts every production extraction-site literal corresponds
19//      to a const in this module — new drift fails fast.
20//
21// Adding a new MCP tool parameter:
22//   1. Add `pub const FOO_BAR: &str = "foo_bar";` to this module
23//      (canonical snake_case = JSON key shape per MCP convention).
24//   2. Use `crate::mcp::param_names::FOO_BAR` at the extraction site
25//      in `src/mcp/tools/<your_tool>.rs`.
26//   3. The parity test auto-picks up the new const + literal pairing.
27//
28// The const names mirror the canonical JSON key spelling in UPPER_SNAKE
29// (e.g. `AGENT_ID = "agent_id"`). Future renames touch both this module
30// + the parity-test allowlist in lockstep.
31
32// 100 canonical MCP tool-call parameter field names (v0.7.0 census).
33// Source: grep of every `.get("X")` / `["X"]` literal in
34// `src/mcp/mod.rs` + `src/mcp/tools/*.rs` production code.
35//
36// #1558 wave 4: names that are ALSO crate-wide wire/row field names are
37// defined as aliases of `crate::models::field_names::*` so each string
38// has exactly ONE spelling in the tree. The public surface (const name,
39// value, ALL_PARAM_NAMES census) is unchanged.
40pub const AGENT_FILTER: &str = crate::models::field_names::AGENT_FILTER;
41pub const AGENT_ID: &str = "agent_id";
42pub const AGENT_TYPE: &str = crate::models::field_names::AGENT_TYPE;
43pub const ALIAS: &str = "alias";
44pub const ALIASES: &str = "aliases";
45pub const ALLOWED_AGENTS: &str = "allowed_agents";
46pub const ARGUMENTS: &str = "arguments";
47pub const AS_AGENT: &str = "as_agent";
48pub const BY_SOURCE_URI: &str = "by_source_uri";
49pub const BYTE_ESTIMATE: &str = "byte_estimate";
50pub const CALLER_AGENT_ID: &str = "caller_agent_id";
51pub const CANONICAL_NAME: &str = crate::models::field_names::CANONICAL_NAME;
52pub const CAPABILITIES: &str = crate::models::field_names::CAPABILITIES;
53pub const CITATIONS: &str = "citations";
54pub const CONFIDENCE: &str = crate::models::field_names::CONFIDENCE;
55pub const CONSUMED: &str = "consumed";
56pub const CONTENT: &str = "content";
57pub const CONTEXT: &str = "context";
58pub const CREATED_AT: &str = crate::models::field_names::CREATED_AT;
59pub const DEPTH: &str = "depth";
60pub const DRY_RUN: &str = "dry_run";
61pub const EDIT_SOURCE: &str = "edit_source";
62pub const ENTITY_ID: &str = crate::models::field_names::ENTITY_ID;
63pub const EVENT_TYPES: &str = "event_types";
64pub const EVENTS: &str = "events";
65pub const EXPECTED_VERSION: &str = "expected_version";
66pub const EXPIRES_AT: &str = crate::models::field_names::EXPIRES_AT;
67pub const FAMILY: &str = "family";
68pub const FILTER: &str = "filter";
69pub const FOLDER_PATH: &str = "folder_path";
70pub const FORCE: &str = "force";
71pub const FORCE_RE_ATOMISE: &str = "force_re_atomise";
72pub const FORMAT: &str = "format";
73pub const GOVERNANCE: &str = crate::models::field_names::GOVERNANCE;
74pub const ID: &str = "id";
75pub const ID_A: &str = "id_a";
76pub const ID_B: &str = "id_b";
77pub const IDS: &str = "ids";
78pub const INCLUDE_ARCHIVED: &str = "include_archived";
79pub const INCLUDE_INVALIDATED: &str = "include_invalidated";
80pub const INHERIT: &str = "inherit";
81pub const INLINE_SKILL: &str = "inline_skill";
82pub const INTENT: &str = "intent";
83pub const K: &str = "k";
84pub const KIND: &str = "kind";
85pub const KIND_INNER: &str = "kind_inner";
86pub const LIMIT: &str = "limit";
87pub const LINK_ID: &str = "link_id";
88pub const MAX_ATOM_TOKENS: &str = "max_atom_tokens";
89pub const MAX_DEPTH: &str = "max_depth";
90pub const MAX_RESULTS: &str = "max_results";
91pub const MEMORY_ID: &str = "memory_id";
92pub const METADATA: &str = "metadata";
93pub const NAME: &str = "name";
94pub const NAMESPACE: &str = "namespace";
95pub const NAMESPACE_FILTER: &str = crate::models::field_names::NAMESPACE_FILTER;
96pub const OFFSET: &str = "offset";
97pub const OLDER_THAN_DAYS: &str = crate::models::field_names::OLDER_THAN_DAYS;
98pub const ON_CONFLICT: &str = "on_conflict";
99pub const PARENT: &str = "parent";
100pub const PATTERN: &str = "pattern";
101pub const PAYLOAD: &str = "payload";
102pub const PIPELINE_OVERRIDE: &str = "pipeline_override";
103pub const PRIORITY: &str = "priority";
104pub const QUERY: &str = "query";
105pub const REFLECTION_ID: &str = "reflection_id";
106pub const RELATION: &str = "relation";
107pub const REMEMBER: &str = "remember";
108pub const RESOURCE_PATH: &str = "resource_path";
109pub const SCOPE: &str = "scope";
110pub const SECRET: &str = "secret";
111pub const SIGNATURE: &str = "signature";
112pub const SINCE: &str = "since";
113pub const SKILL_DESCRIPTION: &str = "skill_description";
114pub const SKILL_ID: &str = "skill_id";
115pub const SKILL_NAME: &str = crate::models::field_names::SKILL_NAME;
116pub const SOURCE: &str = "source";
117pub const SOURCE_ID: &str = "source_id";
118pub const SOURCE_IDS: &str = crate::models::field_names::SOURCE_IDS;
119pub const SOURCE_MEMORY_ID: &str = crate::models::field_names::SOURCE_MEMORY_ID;
120pub const SOURCE_SPAN: &str = crate::models::field_names::SOURCE_SPAN;
121pub const SOURCE_URI: &str = crate::models::field_names::SOURCE_URI;
122pub const STATUS: &str = "status";
123pub const SUBSCRIPTION_ID: &str = crate::models::field_names::SUBSCRIPTION_ID;
124pub const SUMMARY: &str = "summary";
125pub const TAGS: &str = "tags";
126pub const TARGET_AGENT_ID: &str = crate::models::field_names::TARGET_AGENT_ID;
127pub const TARGET_FOLDER: &str = crate::models::field_names::TARGET_FOLDER;
128pub const TARGET_ID: &str = "target_id";
129pub const TARGET_TIER: &str = "target_tier";
130pub const THRESHOLD: &str = "threshold";
131pub const TIER: &str = "tier";
132pub const TITLE: &str = "title";
133pub const TO_NAMESPACE: &str = "to_namespace";
134pub const TTL_SECONDS: &str = "ttl_seconds";
135pub const UNREAD_ONLY: &str = "unread_only";
136pub const UNTIL: &str = "until";
137pub const URL: &str = "url";
138pub const VALID_AT: &str = "valid_at";
139pub const VALID_UNTIL: &str = crate::models::field_names::VALID_UNTIL;
140
141/// Every canonical MCP tool-call parameter name, surfaced as a single
142/// allowlist slice for the parity test in
143/// `tests/mcp_param_names_invariant.rs` to assert that every
144/// production `.get("X")` / `["X"]` literal under `src/mcp/` matches.
145///
146/// SSOT pin: this array's length is the v0.7.0 census of unique
147/// param names (100). The parity test compares the grep-extracted
148/// production-literal set with `ALL_PARAM_NAMES` and fails on either
149/// orphan (literal not in allowlist) OR unused-allowlist (allowlist
150/// const that no production code references — surfaces dead consts
151/// for cleanup).
152pub const ALL_PARAM_NAMES: &[&str] = &[
153    AGENT_FILTER,
154    AGENT_ID,
155    AGENT_TYPE,
156    ALIAS,
157    ALIASES,
158    ALLOWED_AGENTS,
159    ARGUMENTS,
160    AS_AGENT,
161    BY_SOURCE_URI,
162    BYTE_ESTIMATE,
163    CALLER_AGENT_ID,
164    CANONICAL_NAME,
165    CAPABILITIES,
166    CITATIONS,
167    CONFIDENCE,
168    CONSUMED,
169    CONTENT,
170    CONTEXT,
171    CREATED_AT,
172    DEPTH,
173    DRY_RUN,
174    EDIT_SOURCE,
175    ENTITY_ID,
176    EVENT_TYPES,
177    EVENTS,
178    EXPECTED_VERSION,
179    EXPIRES_AT,
180    FAMILY,
181    FILTER,
182    FOLDER_PATH,
183    FORCE,
184    FORCE_RE_ATOMISE,
185    FORMAT,
186    GOVERNANCE,
187    ID,
188    ID_A,
189    ID_B,
190    IDS,
191    INCLUDE_ARCHIVED,
192    INCLUDE_INVALIDATED,
193    INHERIT,
194    INLINE_SKILL,
195    INTENT,
196    K,
197    KIND,
198    KIND_INNER,
199    LIMIT,
200    LINK_ID,
201    MAX_ATOM_TOKENS,
202    MAX_DEPTH,
203    MAX_RESULTS,
204    MEMORY_ID,
205    METADATA,
206    NAME,
207    NAMESPACE,
208    NAMESPACE_FILTER,
209    OFFSET,
210    OLDER_THAN_DAYS,
211    ON_CONFLICT,
212    PARENT,
213    PATTERN,
214    PAYLOAD,
215    PIPELINE_OVERRIDE,
216    PRIORITY,
217    QUERY,
218    REFLECTION_ID,
219    RELATION,
220    REMEMBER,
221    RESOURCE_PATH,
222    SCOPE,
223    SECRET,
224    SIGNATURE,
225    SINCE,
226    SKILL_DESCRIPTION,
227    SKILL_ID,
228    SKILL_NAME,
229    SOURCE,
230    SOURCE_ID,
231    SOURCE_IDS,
232    SOURCE_MEMORY_ID,
233    SOURCE_SPAN,
234    SOURCE_URI,
235    STATUS,
236    SUBSCRIPTION_ID,
237    SUMMARY,
238    TAGS,
239    TARGET_AGENT_ID,
240    TARGET_FOLDER,
241    TARGET_ID,
242    TARGET_TIER,
243    THRESHOLD,
244    TIER,
245    TITLE,
246    TO_NAMESPACE,
247    TTL_SECONDS,
248    UNREAD_ONLY,
249    UNTIL,
250    URL,
251    VALID_AT,
252    VALID_UNTIL,
253];
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn all_param_names_length_pins_v070_census() {
261        // SSOT pin. Adjusting this number requires:
262        //   1. Adding the const above
263        //   2. Adding the symbol to ALL_PARAM_NAMES below
264        //   3. Bumping this assertion
265        //   4. Re-running tests/mcp_param_names_invariant.rs to
266        //      confirm no orphan-literal drift.
267        assert_eq!(
268            ALL_PARAM_NAMES.len(),
269            100,
270            "MCP param-name SSOT census drifted from v0.7.0 baseline"
271        );
272    }
273
274    #[test]
275    fn all_param_names_alphabetically_sorted_and_unique() {
276        for i in 1..ALL_PARAM_NAMES.len() {
277            assert!(
278                ALL_PARAM_NAMES[i - 1] < ALL_PARAM_NAMES[i],
279                "ALL_PARAM_NAMES not alphabetically sorted: {} >= {} at index {}",
280                ALL_PARAM_NAMES[i - 1],
281                ALL_PARAM_NAMES[i],
282                i
283            );
284        }
285    }
286
287    #[test]
288    fn all_param_names_match_lowercase_snake_case() {
289        for name in ALL_PARAM_NAMES {
290            for c in name.chars() {
291                assert!(
292                    c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_',
293                    "param name {name:?} contains non-snake-case char {c:?}; MCP \
294                     JSON convention is snake_case ASCII-lowercase + digits + _"
295                );
296            }
297            assert!(
298                !name.starts_with('_') && !name.ends_with('_'),
299                "param name {name:?} has leading/trailing underscore — likely typo"
300            );
301        }
302    }
303}