Skip to main content

harn_vm/
mcp_protocol.rs

1//! Harn policy layered on the official MCP Rust SDK protocol model.
2//!
3//! `rmcp` owns protocol versions, standard error codes, typed messages, and
4//! lifecycle semantics. This module retains only Harn policy that is not part
5//! of MCP itself: pagination limits, cache defaults, and conversion helpers for
6//! the VM's JSON-valued interface.
7
8use serde_json::{json, Value as JsonValue};
9
10/// Stable MCP version used by Harn's discover/per-request lifecycle.
11///
12/// Keep this literal mechanically checked against `rmcp` below because public
13/// Harn projections require a `&'static str` while the SDK exposes a typed
14/// [`rmcp::model::ProtocolVersion`].
15pub const PROTOCOL_VERSION: &str = "2026-07-28";
16pub const METHOD_SERVER_DISCOVER: &str = "server/discover";
17pub const METHOD_TASKS_GET: &str = "tasks/get";
18pub const METHOD_TASKS_UPDATE: &str = "tasks/update";
19pub const METHOD_TASKS_CANCEL: &str = "tasks/cancel";
20pub const METHOD_COMPLETION_COMPLETE: &str = "completion/complete";
21pub const METHOD_SAMPLING_CREATE_MESSAGE: &str = "sampling/createMessage";
22pub const METHOD_ELICITATION_CREATE: &str = "elicitation/create";
23pub const TASKS_EXTENSION_ID: &str = rmcp::model::TASKS_EXTENSION_ID;
24pub const METHOD_ROOTS_LIST: &str = "roots/list";
25pub const METHOD_ROOTS_LIST_CHANGED_NOTIFICATION: &str = "notifications/roots/list_changed";
26
27/// Stable per-request metadata keys carried inside `params._meta`.
28pub const MCP_META_KEY_PROTOCOL_VERSION: &str = "io.modelcontextprotocol/protocolVersion";
29pub const MCP_META_KEY_CLIENT_INFO: &str = "io.modelcontextprotocol/clientInfo";
30pub const MCP_META_KEY_CLIENT_CAPABILITIES: &str = "io.modelcontextprotocol/clientCapabilities";
31
32/// Standard HTTP routing headers for the stable protocol.
33pub const MCP_HEADER_PROTOCOL_VERSION: &str =
34    rmcp::transport::common::http_header::HEADER_MCP_PROTOCOL_VERSION;
35pub const MCP_HEADER_METHOD: &str = rmcp::transport::common::http_header::HEADER_MCP_METHOD;
36pub const MCP_HEADER_NAME: &str = rmcp::transport::common::http_header::HEADER_MCP_NAME;
37
38/// `resultType` discriminants exposed to 2026 clients on every response.
39pub const RESULT_TYPE_COMPLETE: &str = "complete";
40pub const RESULT_TYPE_INPUT_REQUIRED: &str = "input_required";
41
42/// Stable MCP protocol errors, projected from the SDK's typed registry.
43pub const UNSUPPORTED_PROTOCOL_VERSION_CODE: i64 =
44    rmcp::model::ErrorCode::UNSUPPORTED_PROTOCOL_VERSION.0 as i64;
45pub const MISSING_REQUIRED_CLIENT_CAPABILITY_CODE: i64 =
46    rmcp::model::ErrorCode::MISSING_REQUIRED_CLIENT_CAPABILITY.0 as i64;
47pub const HEADER_MISMATCH_CODE: i64 = rmcp::model::ErrorCode::HEADER_MISMATCH.0 as i64;
48
49pub const DEFAULT_TASK_POLL_INTERVAL_MS: u64 = 250;
50pub const DEFAULT_MCP_LIST_PAGE_SIZE: usize = 100;
51pub const MCP_LIST_PAGE_SIZE_ENV: &str = "HARN_MCP_LIST_PAGE_SIZE";
52
53/// Conservative cache hints emitted with list/read results when a stable
54/// client asked for them. Both surfaces fall back to these defaults so
55/// implementations can opt out per handler if a tighter or looser bound
56/// is appropriate.
57pub const DEFAULT_LIST_CACHE_TTL_MS: u64 = 5_000;
58pub const DEFAULT_LIST_CACHE_SCOPE: &str = "private";
59pub const DEFAULT_READ_CACHE_TTL_MS: u64 = 1_000;
60pub const DEFAULT_READ_CACHE_SCOPE: &str = "private";
61
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct McpListPage {
64    pub start: usize,
65    pub end: usize,
66    pub next_cursor: Option<String>,
67}
68
69pub const MCP_COMPLETION_MAX_VALUES: usize = 100;
70
71pub use rmcp::model::TaskStatus as McpTaskStatus;
72
73/// Return the SDK task status's canonical wire spelling.
74pub fn mcp_task_status_wire_name(status: McpTaskStatus) -> String {
75    serde_json::to_value(status)
76        .expect("SDK task statuses serialize")
77        .as_str()
78        .expect("SDK task statuses serialize as strings")
79        .to_string()
80}
81
82/// Returns every protocol version known to the official SDK.
83pub fn sdk_protocol_versions() -> Vec<&'static str> {
84    rmcp::model::ProtocolVersion::KNOWN_VERSIONS
85        .iter()
86        .map(rmcp::model::ProtocolVersion::as_str)
87        .collect()
88}
89
90pub fn is_sdk_protocol_version(version: &str) -> bool {
91    rmcp::model::ProtocolVersion::KNOWN_VERSIONS
92        .iter()
93        .any(|supported| supported.as_str() == version)
94}
95
96/// Returns the SDK-known versions that use metadata on each request.
97pub fn request_metadata_protocol_versions() -> Vec<&'static str> {
98    rmcp::model::ProtocolVersion::KNOWN_VERSIONS
99        .iter()
100        .filter(|version| *version >= &rmcp::model::ProtocolVersion::STANDARD_HEADERS)
101        .map(rmcp::model::ProtocolVersion::as_str)
102        .collect()
103}
104
105pub fn is_request_metadata_protocol_version(version: &str) -> bool {
106    rmcp::model::ProtocolVersion::KNOWN_VERSIONS
107        .iter()
108        .filter(|supported| *supported >= &rmcp::model::ProtocolVersion::STANDARD_HEADERS)
109        .any(|supported| supported.as_str() == version)
110}
111
112fn is_initialize_protocol_version(version: &rmcp::model::ProtocolVersion) -> bool {
113    version < &rmcp::model::ProtocolVersion::STANDARD_HEADERS
114        && rmcp::model::ProtocolVersion::KNOWN_VERSIONS.contains(version)
115}
116
117/// Result of the SDK-typed `initialize` negotiation.
118#[derive(Clone, Debug, PartialEq)]
119struct McpInitializeOutcome {
120    client_identity: String,
121    protocol_version: rmcp::model::ProtocolVersion,
122    result: JsonValue,
123}
124
125/// Negotiate the released MCP initialize lifecycle with official SDK types.
126///
127/// MCP 2026-07-28 uses `server/discover` and per-request metadata, but released
128/// clients still open stdio servers with `initialize`. The SDK supports both
129/// lifecycles; this helper keeps Harn's custom dispatch cores aligned without
130/// recreating a second protocol model or version registry.
131fn negotiate_initialize(
132    params: &JsonValue,
133    capabilities: JsonValue,
134    server_info: JsonValue,
135    instructions: Option<&str>,
136) -> Result<McpInitializeOutcome, String> {
137    let request: rmcp::model::InitializeRequestParams = serde_json::from_value(params.clone())
138        .map_err(|error| format!("invalid MCP initialize params: {error}"))?;
139    let protocol_version = if is_initialize_protocol_version(&request.protocol_version) {
140        request.protocol_version.clone()
141    } else {
142        rmcp::model::ProtocolVersion::LATEST
143    };
144    let capabilities: rmcp::model::ServerCapabilities = serde_json::from_value(capabilities)
145        .map_err(|error| format!("invalid MCP server capabilities: {error}"))?;
146    let server_info: rmcp::model::Implementation = serde_json::from_value(server_info)
147        .map_err(|error| format!("invalid MCP server info: {error}"))?;
148    let mut result = rmcp::model::InitializeResult::new(capabilities)
149        .with_protocol_version(protocol_version.clone())
150        .with_server_info(server_info);
151    if let Some(instructions) = instructions {
152        result = result.with_instructions(instructions);
153    }
154    let result = serde_json::to_value(result)
155        .map_err(|error| format!("failed to encode MCP initialize result: {error}"))?;
156    Ok(McpInitializeOutcome {
157        client_identity: format!(
158            "{}/{}",
159            request.client_info.name, request.client_info.version
160        ),
161        protocol_version,
162        result,
163    })
164}
165
166/// The protocol profile for one accepted server request.
167#[derive(Clone, Debug, PartialEq, Eq)]
168pub struct McpRequestProfile {
169    protocol_version: rmcp::model::ProtocolVersion,
170}
171
172impl McpRequestProfile {
173    pub fn uses_result_envelope(&self) -> bool {
174        self.protocol_version >= rmcp::model::ProtocolVersion::V_2026_07_28
175    }
176}
177
178/// Connection state shared by Harn's generic and orchestrator MCP servers.
179///
180/// The state stores only facts negotiated for the connection. Version support,
181/// request metadata, and initialize payloads remain owned by official SDK
182/// types.
183#[derive(Clone, Debug)]
184pub struct McpServerSession {
185    client_identity: String,
186    initialized_protocol_version: Option<rmcp::model::ProtocolVersion>,
187}
188
189impl Default for McpServerSession {
190    fn default() -> Self {
191        Self {
192            client_identity: "unknown".to_string(),
193            initialized_protocol_version: None,
194        }
195    }
196}
197
198impl McpServerSession {
199    pub fn client_identity(&self) -> &str {
200        &self.client_identity
201    }
202
203    pub fn initialize(
204        &mut self,
205        params: &JsonValue,
206        capabilities: JsonValue,
207        server_info: JsonValue,
208        instructions: Option<&str>,
209    ) -> Result<JsonValue, String> {
210        let outcome = negotiate_initialize(params, capabilities, server_info, instructions)?;
211        self.client_identity = outcome.client_identity;
212        self.initialized_protocol_version = Some(outcome.protocol_version);
213        Ok(outcome.result)
214    }
215
216    /// Validate one request and return the response profile it negotiated.
217    pub fn accept_request(
218        &mut self,
219        id: &JsonValue,
220        method: &str,
221        params: &JsonValue,
222    ) -> Result<McpRequestProfile, JsonValue> {
223        let uses_inline_lifecycle = method == METHOD_SERVER_DISCOVER
224            || (self.initialized_protocol_version.is_none() && params.get("_meta").is_some());
225        if uses_inline_lifecycle {
226            let metadata = parse_request_metadata(params);
227            enforce_request_protocol_version(id, &metadata)?;
228            if let Some(info) = metadata.client_info() {
229                self.client_identity = format!("{}/{}", info.name, info.version);
230            }
231            return Ok(McpRequestProfile {
232                protocol_version: rmcp::model::ProtocolVersion::V_2026_07_28,
233            });
234        }
235
236        if let Some(protocol_version) = &self.initialized_protocol_version {
237            return Ok(McpRequestProfile {
238                protocol_version: protocol_version.clone(),
239            });
240        }
241
242        if method == "ping" {
243            return Ok(McpRequestProfile {
244                protocol_version: rmcp::model::ProtocolVersion::LATEST,
245            });
246        }
247
248        Err(crate::jsonrpc::error_response(
249            id.clone(),
250            -32002,
251            "server not initialized",
252        ))
253    }
254}
255
256/// The official SDK's typed stable per-request metadata map.
257pub use rmcp::model::RequestMetaObject as McpRequestMetadata;
258
259/// Extract stable metadata from a request's `params._meta` block.
260pub fn parse_request_metadata(params: &JsonValue) -> McpRequestMetadata {
261    let Some(meta) = params.get("_meta") else {
262        return McpRequestMetadata::default();
263    };
264    serde_json::from_value(meta.clone()).unwrap_or_default()
265}
266
267/// Validate that a request's metadata targets a supported version.
268/// Returns an `Err(error_response)` payload ready to ship back to the
269/// client when the version is recognized but unsupported, leaving the
270/// caller to send the response.
271pub fn enforce_request_protocol_version(
272    id: &JsonValue,
273    metadata: &McpRequestMetadata,
274) -> Result<(), JsonValue> {
275    let Some(version) = metadata.protocol_version() else {
276        return Err(crate::jsonrpc::error_response(
277            id.clone(),
278            -32602,
279            "request _meta is missing or has malformed required fields: io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientInfo, io.modelcontextprotocol/clientCapabilities",
280        ));
281    };
282    if version != rmcp::model::ProtocolVersion::V_2026_07_28 {
283        return Err(unsupported_protocol_version_response(
284            id.clone(),
285            version.as_str(),
286        ));
287    }
288    let missing = metadata.missing_required_keys(&rmcp::model::ProtocolVersion::V_2026_07_28);
289    if !missing.is_empty() {
290        return Err(crate::jsonrpc::error_response(
291            id.clone(),
292            -32602,
293            &format!(
294                "request _meta is missing or has malformed required fields: {}",
295                missing.join(", ")
296            ),
297        ));
298    }
299    Ok(())
300}
301
302/// Build the SDK-defined unsupported-protocol-version JSON-RPC error.
303pub fn unsupported_protocol_version_response(
304    id: impl Into<JsonValue>,
305    requested: &str,
306) -> JsonValue {
307    crate::jsonrpc::error_response_with_data(
308        id,
309        UNSUPPORTED_PROTOCOL_VERSION_CODE,
310        "Unsupported protocol version",
311        json!({
312            "supported": request_metadata_protocol_versions(),
313            "requested": requested,
314        }),
315    )
316}
317
318/// HTTP-header validation outcome. Errors carry a JSON-RPC body so the
319/// HTTP layer can ship either an HTTP 400 with the body or a 200 with
320/// the JSON-RPC error — both paths exist in the stable spec.
321#[derive(Clone, Debug)]
322pub struct McpHttpHeaderOutcome {
323    pub protocol_version: Option<String>,
324}
325
326/// Inspect standard streamable HTTP routing headers. The function is pure: it returns the
327/// negotiated mode and the version pinned by the client, or a JSON-RPC
328/// error body when the headers contradict the request body or name a
329/// version the server does not support.
330///
331/// `body_method` and `body_name` are the values pulled from the JSON-RPC
332/// body so the helper can detect a header/body mismatch.
333pub fn negotiate_http_request<'a, F>(
334    headers: F,
335    body_method: Option<&str>,
336    body_name: Option<&str>,
337    request_id: &JsonValue,
338) -> Result<McpHttpHeaderOutcome, JsonValue>
339where
340    F: Fn(&str) -> Option<&'a str>,
341{
342    let mut outcome = McpHttpHeaderOutcome {
343        protocol_version: None,
344    };
345
346    if let Some(value) = headers(MCP_HEADER_PROTOCOL_VERSION) {
347        if value != PROTOCOL_VERSION {
348            return Err(unsupported_protocol_version_response(
349                request_id.clone(),
350                value,
351            ));
352        }
353        outcome.protocol_version = Some(value.to_string());
354    }
355
356    if let Some(method_header) = headers(MCP_HEADER_METHOD) {
357        if let Some(body_method) = body_method {
358            if method_header != body_method {
359                return Err(crate::jsonrpc::error_response_with_data(
360                    request_id.clone(),
361                    HEADER_MISMATCH_CODE,
362                    "Mcp-Method header does not match request body",
363                    json!({
364                        "headerValue": method_header,
365                        "bodyMethod": body_method,
366                    }),
367                ));
368            }
369        }
370    }
371
372    if let Some(name_header) = headers(MCP_HEADER_NAME) {
373        let expected = body_name.unwrap_or_default();
374        if !expected.is_empty() && name_header != expected {
375            return Err(crate::jsonrpc::error_response_with_data(
376                request_id.clone(),
377                HEADER_MISMATCH_CODE,
378                "Mcp-Name header does not match request body",
379                json!({
380                    "headerValue": name_header,
381                    "bodyName": expected,
382                }),
383            ));
384        }
385    }
386
387    Ok(outcome)
388}
389
390/// Pulls the standard `Mcp-Name` header value for a request body. Stable
391/// servers cross-check this against the header sent on the wire; stable
392/// clients use the same helper when authoring outbound requests.
393pub fn standard_name_header_value(method: &str, params: &JsonValue) -> Option<String> {
394    match method {
395        "tools/call" | "prompts/get" => params
396            .get("name")
397            .and_then(JsonValue::as_str)
398            .map(str::to_string),
399        "resources/read" => params
400            .get("uri")
401            .and_then(JsonValue::as_str)
402            .map(str::to_string),
403        _ => None,
404    }
405}
406
407/// Modify a JSON-RPC result body in place to include the stable protocol's
408/// per-result discriminants.
409pub fn apply_result_envelope(result: &mut JsonValue, cache: Option<&McpCacheHint>) {
410    let Some(object) = result.as_object_mut() else {
411        return;
412    };
413    object
414        .entry("resultType")
415        .or_insert_with(|| JsonValue::String(RESULT_TYPE_COMPLETE.to_string()));
416    if let Some(hint) = cache {
417        if let Some(ttl) = hint.ttl_ms {
418            object.insert("ttlMs".to_string(), json!(ttl));
419        }
420        if let Some(scope) = hint.scope {
421            object.insert("cacheScope".to_string(), JsonValue::String(scope.into()));
422        }
423    }
424}
425
426/// Conservative cache hint surfaced on stable results. Servers can override
427/// the defaults per handler when they have a better answer; clients fall
428/// back to the constants in [`DEFAULT_LIST_CACHE_TTL_MS`].
429#[derive(Clone, Copy, Debug, PartialEq, Eq)]
430pub struct McpCacheHint {
431    pub ttl_ms: Option<u64>,
432    pub scope: Option<&'static str>,
433}
434
435impl McpCacheHint {
436    pub const fn list_default() -> Self {
437        Self {
438            ttl_ms: Some(DEFAULT_LIST_CACHE_TTL_MS),
439            scope: Some(DEFAULT_LIST_CACHE_SCOPE),
440        }
441    }
442
443    pub const fn read_default() -> Self {
444        Self {
445            ttl_ms: Some(DEFAULT_READ_CACHE_TTL_MS),
446            scope: Some(DEFAULT_READ_CACHE_SCOPE),
447        }
448    }
449
450    pub const fn none() -> Self {
451        Self {
452            ttl_ms: None,
453            scope: None,
454        }
455    }
456
457    /// Extract a cache hint from a stable result body. Returns `None` when
458    /// neither `ttlMs` nor a recognized `cacheScope` is present; unknown
459    /// scopes are silently dropped so we stay forward-compatible.
460    pub fn from_result(result: &JsonValue) -> Option<Self> {
461        let ttl_ms = result.get("ttlMs").and_then(JsonValue::as_u64);
462        let scope = result
463            .get("cacheScope")
464            .and_then(JsonValue::as_str)
465            .and_then(Self::canonical_scope);
466        if ttl_ms.is_none() && scope.is_none() {
467            return None;
468        }
469        Some(Self { ttl_ms, scope })
470    }
471
472    fn canonical_scope(value: &str) -> Option<&'static str> {
473        match value {
474            "public" => Some("public"),
475            "private" => Some("private"),
476            _ => None,
477        }
478    }
479
480    pub fn to_json_object(&self) -> serde_json::Map<String, JsonValue> {
481        let mut entry = serde_json::Map::new();
482        if let Some(ttl_ms) = self.ttl_ms {
483            entry.insert("ttlMs".to_string(), json!(ttl_ms));
484        }
485        if let Some(scope) = self.scope {
486            entry.insert("cacheScope".to_string(), JsonValue::String(scope.into()));
487        }
488        entry
489    }
490}
491
492/// Build a JSON object mapping method names to their recorded MCP cache
493/// hints. Empty input yields an empty object.
494pub fn cache_hints_to_json<'a, I>(hints: I) -> JsonValue
495where
496    I: IntoIterator<Item = (&'a String, &'a McpCacheHint)>,
497{
498    let mut object = serde_json::Map::new();
499    for (method, hint) in hints {
500        object.insert(method.clone(), JsonValue::Object(hint.to_json_object()));
501    }
502    JsonValue::Object(object)
503}
504
505/// Build the canonical `server/discover` result both server surfaces
506/// share. Callers supply their advertised capabilities and serverInfo;
507/// the helper handles `resultType`, `supportedVersions`, instructions,
508/// and any stable envelope fields.
509pub fn server_discover_result(
510    capabilities: JsonValue,
511    server_info: JsonValue,
512    instructions: Option<&str>,
513) -> JsonValue {
514    let mut result = json!({
515        "resultType": RESULT_TYPE_COMPLETE,
516        "supportedVersions": request_metadata_protocol_versions(),
517        "capabilities": capabilities,
518        "ttlMs": 0,
519        "cacheScope": "private",
520        "_meta": {
521            "io.modelcontextprotocol/serverInfo": server_info,
522        },
523    });
524    if let Some(instructions) = instructions {
525        result["instructions"] = JsonValue::String(instructions.to_string());
526    }
527    result
528}
529
530pub fn explicit_unsupported_method_response(
531    id: impl Into<JsonValue>,
532    method: &str,
533) -> Option<JsonValue> {
534    let (feature, role, reason) = match method {
535        METHOD_SAMPLING_CREATE_MESSAGE => (
536            "sampling",
537            "client",
538            "MCP sampling is an embedded input request in stable multi-round-trip results; it cannot be called as a top-level request on an MCP server endpoint.",
539        ),
540        METHOD_ELICITATION_CREATE => (
541            "elicitation",
542            "client",
543            "MCP elicitation is an embedded input request in stable multi-round-trip results; it cannot be called as a top-level request on an MCP server endpoint.",
544        ),
545        "subscriptions/listen" => (
546            "subscriptions",
547            "server",
548            "Harn does not advertise or implement the request-scoped notification stream.",
549        ),
550        _ => return None,
551    };
552    Some(crate::jsonrpc::error_response_with_data(
553        id,
554        -32601,
555        &format!("Unsupported MCP client-bound method: {method}"),
556        json!({
557            "type": "mcp.unsupportedFeature",
558            "protocolVersion": PROTOCOL_VERSION,
559            "method": method,
560            "feature": feature,
561            "role": role,
562            "status": "unsupported",
563            "reason": reason,
564        }),
565    ))
566}
567
568pub fn client_supports_tasks(params: &JsonValue) -> bool {
569    params
570        .pointer("/_meta/io.modelcontextprotocol~1clientCapabilities/extensions/io.modelcontextprotocol~1tasks")
571        .is_some()
572}
573
574pub fn tasks_capability() -> JsonValue {
575    json!({
576        TASKS_EXTENSION_ID: {}
577    })
578}
579
580pub fn completions_capability() -> JsonValue {
581    json!({})
582}
583
584pub fn completion_result(
585    id: impl Into<JsonValue>,
586    candidates: Vec<String>,
587    value: &str,
588) -> JsonValue {
589    crate::jsonrpc::response(
590        id,
591        json!({ "completion": completion_payload(candidates, value) }),
592    )
593}
594
595pub fn completion_payload(candidates: Vec<String>, value: &str) -> JsonValue {
596    let needle = value.to_ascii_lowercase();
597    let mut seen = std::collections::BTreeSet::new();
598    let mut ranked = candidates
599        .into_iter()
600        .filter_map(|candidate| {
601            let candidate = candidate.trim().to_string();
602            if candidate.is_empty() || !seen.insert(candidate.clone()) {
603                return None;
604            }
605            let haystack = candidate.to_ascii_lowercase();
606            if !needle.is_empty() && !haystack.contains(&needle) {
607                return None;
608            }
609            let rank = i32::from(!(needle.is_empty() || haystack.starts_with(&needle)));
610            Some((rank, haystack, candidate))
611        })
612        .collect::<Vec<_>>();
613    ranked.sort_by(|left, right| left.0.cmp(&right.0).then_with(|| left.1.cmp(&right.1)));
614
615    let total = ranked.len();
616    let values = ranked
617        .into_iter()
618        .take(MCP_COMPLETION_MAX_VALUES)
619        .map(|(_, _, candidate)| candidate)
620        .collect::<Vec<_>>();
621    json!({
622        "values": values,
623        "total": total,
624        "hasMore": total > MCP_COMPLETION_MAX_VALUES,
625    })
626}
627
628pub fn mcp_list_page_size() -> usize {
629    mcp_list_page_size_from_env(std::env::var(MCP_LIST_PAGE_SIZE_ENV).ok().as_deref())
630}
631
632fn mcp_list_page_size_from_env(raw: Option<&str>) -> usize {
633    raw.and_then(|value| value.parse::<usize>().ok())
634        .filter(|size| *size > 0)
635        .unwrap_or(DEFAULT_MCP_LIST_PAGE_SIZE)
636}
637
638pub fn encode_mcp_list_cursor(offset: usize) -> String {
639    use base64::Engine;
640    base64::engine::general_purpose::STANDARD.encode(offset.to_string().as_bytes())
641}
642
643pub fn mcp_list_page(
644    params: &JsonValue,
645    total_len: usize,
646    method: &str,
647) -> Result<McpListPage, String> {
648    let offset = parse_mcp_list_cursor(params, method)?;
649    let page_size = mcp_list_page_size();
650    let start = offset.min(total_len);
651    let end = start.saturating_add(page_size).min(total_len);
652    let next_cursor = (end < total_len).then(|| encode_mcp_list_cursor(end));
653    Ok(McpListPage {
654        start,
655        end,
656        next_cursor,
657    })
658}
659
660fn parse_mcp_list_cursor(params: &JsonValue, method: &str) -> Result<usize, String> {
661    let Some(cursor) = params.get("cursor") else {
662        return Ok(0);
663    };
664    let Some(cursor) = cursor.as_str() else {
665        return Err(format!("invalid {method} cursor"));
666    };
667    use base64::Engine;
668    let bytes = base64::engine::general_purpose::STANDARD
669        .decode(cursor)
670        .map_err(|_| format!("invalid {method} cursor"))?;
671    let decoded = String::from_utf8(bytes).map_err(|_| format!("invalid {method} cursor"))?;
672    decoded
673        .parse::<usize>()
674        .map_err(|_| format!("invalid {method} cursor"))
675}
676
677#[cfg(test)]
678mod tests {
679    use super::*;
680
681    #[test]
682    fn protocol_registry_matches_official_sdk() {
683        assert_eq!(
684            PROTOCOL_VERSION,
685            rmcp::model::ProtocolVersion::V_2026_07_28.as_str()
686        );
687        assert_eq!(
688            sdk_protocol_versions(),
689            rmcp::model::ProtocolVersion::KNOWN_VERSIONS
690                .iter()
691                .map(rmcp::model::ProtocolVersion::as_str)
692                .collect::<Vec<_>>()
693        );
694        assert_eq!(
695            request_metadata_protocol_versions(),
696            rmcp::model::ProtocolVersion::KNOWN_VERSIONS
697                .iter()
698                .filter(|version| *version >= &rmcp::model::ProtocolVersion::STANDARD_HEADERS)
699                .map(rmcp::model::ProtocolVersion::as_str)
700                .collect::<Vec<_>>()
701        );
702        assert_eq!(UNSUPPORTED_PROTOCOL_VERSION_CODE, -32022);
703        assert_eq!(MISSING_REQUIRED_CLIENT_CAPABILITY_CODE, -32021);
704        assert_eq!(HEADER_MISMATCH_CODE, -32020);
705    }
706
707    #[test]
708    fn initialize_negotiates_every_sdk_released_version() {
709        for protocol_version in rmcp::model::ProtocolVersion::KNOWN_VERSIONS
710            .iter()
711            .filter(|version| *version < &rmcp::model::ProtocolVersion::STANDARD_HEADERS)
712        {
713            let outcome = negotiate_initialize(
714                &json!({
715                    "protocolVersion": protocol_version.as_str(),
716                    "capabilities": {},
717                    "clientInfo": {"name": "codex-mcp-client", "version": "test"},
718                }),
719                json!({"tools": {}}),
720                json!({"name": "harn", "version": "test"}),
721                Some("test server"),
722            )
723            .expect("SDK-supported initialize version should negotiate");
724            assert_eq!(&outcome.protocol_version, protocol_version);
725            assert_eq!(outcome.client_identity, "codex-mcp-client/test");
726            assert_eq!(
727                outcome.result["protocolVersion"],
728                json!(protocol_version.as_str())
729            );
730            assert_eq!(outcome.result["capabilities"]["tools"], json!({}));
731            assert_eq!(outcome.result["serverInfo"]["name"], json!("harn"));
732            assert_eq!(outcome.result["instructions"], json!("test server"));
733
734            let typed: rmcp::model::InitializeResult = serde_json::from_value(outcome.result)
735                .expect("initialize response must remain SDK-typed");
736            assert_eq!(&typed.protocol_version, protocol_version);
737        }
738
739        let modern_initialize = negotiate_initialize(
740            &json!({
741                "protocolVersion": rmcp::model::ProtocolVersion::STANDARD_HEADERS.as_str(),
742                "capabilities": {},
743                "clientInfo": {"name": "codex-mcp-client", "version": "test"},
744            }),
745            json!({"tools": {}}),
746            json!({"name": "harn", "version": "test"}),
747            None,
748        )
749        .expect("modern initialize request should negotiate a released fallback");
750        assert_eq!(
751            modern_initialize.protocol_version,
752            rmcp::model::ProtocolVersion::LATEST
753        );
754    }
755
756    #[test]
757    fn initialized_session_keeps_its_version_when_request_meta_has_a_progress_token() {
758        let mut session = McpServerSession::default();
759        session
760            .initialize(
761                &json!({
762                    "protocolVersion": "2025-11-25",
763                    "capabilities": {},
764                    "clientInfo": {"name": "codex-mcp-client", "version": "test"},
765                }),
766                json!({"tools": {}}),
767                json!({"name": "harn", "version": "test"}),
768                None,
769            )
770            .expect("initialize should negotiate");
771
772        let profile = session
773            .accept_request(
774                &json!(2),
775                "tools/call",
776                &json!({"_meta": {"progressToken": "codex-proof"}}),
777            )
778            .expect("released request metadata should use the initialized version");
779        assert!(!profile.uses_result_envelope());
780    }
781
782    #[test]
783    fn completion_payload_dedupes_and_ranks_prefix_matches() {
784        let response = completion_result(
785            json!(1),
786            vec![
787                "typescript".to_string(),
788                "rust".to_string(),
789                "ruby".to_string(),
790                "rust".to_string(),
791            ],
792            "ru",
793        );
794        assert_eq!(
795            response["result"]["completion"]["values"],
796            json!(["ruby", "rust"])
797        );
798        assert_eq!(response["result"]["completion"]["total"], json!(2));
799        assert_eq!(response["result"]["completion"]["hasMore"], json!(false));
800    }
801
802    #[test]
803    fn task_capability_uses_the_stable_extension_map() {
804        assert!(client_supports_tasks(&json!({
805            "_meta": {
806                MCP_META_KEY_CLIENT_CAPABILITIES: {
807                    "extensions": {TASKS_EXTENSION_ID: {}}
808                }
809            }
810        })));
811        assert!(!client_supports_tasks(&json!({})));
812    }
813
814    #[test]
815    fn task_protocol_shapes_match_latest_spec_names() {
816        assert_eq!(mcp_task_status_wire_name(McpTaskStatus::Working), "working");
817        assert_eq!(
818            mcp_task_status_wire_name(McpTaskStatus::InputRequired),
819            "input_required"
820        );
821        assert!(McpTaskStatus::Completed.is_terminal());
822        assert_eq!(tasks_capability()[TASKS_EXTENSION_ID], json!({}));
823    }
824
825    #[test]
826    fn mcp_list_page_uses_default_size_and_next_cursor() {
827        let page = mcp_list_page(&json!({}), 105, "tools/list").unwrap();
828        assert_eq!(page.start, 0);
829        assert_eq!(page.end, DEFAULT_MCP_LIST_PAGE_SIZE);
830        assert_eq!(
831            page.next_cursor,
832            Some(encode_mcp_list_cursor(DEFAULT_MCP_LIST_PAGE_SIZE))
833        );
834
835        let next = mcp_list_page(
836            &json!({"cursor": page.next_cursor.unwrap()}),
837            105,
838            "tools/list",
839        )
840        .unwrap();
841        assert_eq!(next.start, DEFAULT_MCP_LIST_PAGE_SIZE);
842        assert_eq!(next.end, 105);
843        assert_eq!(next.next_cursor, None);
844    }
845
846    #[test]
847    fn mcp_list_page_size_parses_positive_env_override() {
848        assert_eq!(mcp_list_page_size_from_env(Some("2")), 2);
849        assert_eq!(
850            mcp_list_page_size_from_env(Some("0")),
851            DEFAULT_MCP_LIST_PAGE_SIZE
852        );
853        assert_eq!(
854            mcp_list_page_size_from_env(Some("nope")),
855            DEFAULT_MCP_LIST_PAGE_SIZE
856        );
857        assert_eq!(
858            mcp_list_page_size_from_env(None),
859            DEFAULT_MCP_LIST_PAGE_SIZE
860        );
861    }
862
863    #[test]
864    fn mcp_list_page_rejects_malformed_cursor() {
865        let err = mcp_list_page(&json!({"cursor": "not-base64"}), 5, "resources/list")
866            .expect_err("malformed cursor should fail");
867        assert_eq!(err, "invalid resources/list cursor");
868    }
869
870    #[test]
871    fn stable_metadata_round_trips_through_meta_block() {
872        let params = json!({
873            "_meta": {
874                MCP_META_KEY_PROTOCOL_VERSION: PROTOCOL_VERSION,
875                MCP_META_KEY_CLIENT_INFO: {"name": "harn", "version": "x"},
876                MCP_META_KEY_CLIENT_CAPABILITIES: {"roots": {}},
877            }
878        });
879        let meta = parse_request_metadata(&params);
880        assert_eq!(
881            meta.protocol_version()
882                .as_ref()
883                .map(|version| version.as_str()),
884            Some(PROTOCOL_VERSION)
885        );
886        assert_eq!(
887            serde_json::to_value(meta.client_info()).unwrap(),
888            json!({"name": "harn", "version": "x"})
889        );
890        assert_eq!(
891            serde_json::to_value(meta.client_capabilities()).unwrap(),
892            json!({"roots": {}})
893        );
894        enforce_request_protocol_version(&json!(1), &meta).unwrap();
895    }
896
897    #[test]
898    fn stable_metadata_is_required() {
899        let meta = parse_request_metadata(&json!({}));
900        assert_eq!(meta, McpRequestMetadata::default());
901        let error = enforce_request_protocol_version(&json!(1), &meta).unwrap_err();
902        assert_eq!(error["error"]["code"], json!(-32602));
903    }
904
905    #[test]
906    fn enforce_request_protocol_version_rejects_unknown_version() {
907        let meta = parse_request_metadata(&json!({
908            "_meta": {MCP_META_KEY_PROTOCOL_VERSION: "2099-01-01"}
909        }));
910        let id = json!(7);
911        let err =
912            enforce_request_protocol_version(&id, &meta).expect_err("unknown version should error");
913        assert_eq!(err["id"], id);
914        assert_eq!(
915            err["error"]["code"],
916            json!(UNSUPPORTED_PROTOCOL_VERSION_CODE)
917        );
918        assert_eq!(err["error"]["data"]["requested"], json!("2099-01-01"));
919        let supported = err["error"]["data"]["supported"].as_array().unwrap();
920        assert!(supported.contains(&json!(PROTOCOL_VERSION)));
921    }
922
923    #[test]
924    fn enforce_request_protocol_version_accepts_stable_metadata() {
925        let meta = parse_request_metadata(&json!({
926            "_meta": {
927                MCP_META_KEY_PROTOCOL_VERSION: PROTOCOL_VERSION,
928                MCP_META_KEY_CLIENT_INFO: {"name": "harn", "version": "x"},
929                MCP_META_KEY_CLIENT_CAPABILITIES: {},
930            }
931        }));
932        enforce_request_protocol_version(&json!(1), &meta).unwrap();
933    }
934
935    #[test]
936    fn enforce_request_protocol_version_uses_sdk_required_metadata_validation() {
937        let meta = parse_request_metadata(&json!({
938            "_meta": {MCP_META_KEY_PROTOCOL_VERSION: PROTOCOL_VERSION}
939        }));
940        let error = enforce_request_protocol_version(&json!(1), &meta)
941            .expect_err("stable requests require typed client capabilities");
942        assert_eq!(error["error"]["code"], json!(-32602));
943        assert!(error["error"]["message"]
944            .as_str()
945            .is_some_and(|message| message.contains(MCP_META_KEY_CLIENT_CAPABILITIES)));
946    }
947
948    #[test]
949    fn enforce_request_protocol_version_rejects_old_versions() {
950        let meta = parse_request_metadata(&json!({
951            "_meta": {
952                MCP_META_KEY_PROTOCOL_VERSION: "2025-06-18",
953                MCP_META_KEY_CLIENT_INFO: {"name": "old", "version": "1"},
954                MCP_META_KEY_CLIENT_CAPABILITIES: {},
955            }
956        }));
957        let error = enforce_request_protocol_version(&json!(1), &meta).unwrap_err();
958        assert_eq!(
959            error["error"]["code"],
960            json!(UNSUPPORTED_PROTOCOL_VERSION_CODE)
961        );
962    }
963
964    #[test]
965    fn negotiate_standard_http_headers_detects_stable_protocol_header() {
966        let headers = std::collections::HashMap::from([(
967            MCP_HEADER_PROTOCOL_VERSION.to_string(),
968            PROTOCOL_VERSION.to_string(),
969        )]);
970        let outcome = negotiate_http_request(
971            |key| headers.get(key).map(String::as_str),
972            Some("tools/list"),
973            None,
974            &json!(1),
975        )
976        .unwrap();
977        assert_eq!(outcome.protocol_version.as_deref(), Some(PROTOCOL_VERSION));
978    }
979
980    #[test]
981    fn negotiate_standard_http_headers_rejects_method_body_mismatch() {
982        let headers = std::collections::HashMap::from([(
983            MCP_HEADER_METHOD.to_string(),
984            "tools/list".to_string(),
985        )]);
986        let err = negotiate_http_request(
987            |key| headers.get(key).map(String::as_str),
988            Some("tools/call"),
989            None,
990            &json!(2),
991        )
992        .expect_err("header/body mismatch must error");
993        assert_eq!(err["error"]["code"], json!(HEADER_MISMATCH_CODE));
994        assert_eq!(err["error"]["data"]["headerValue"], json!("tools/list"));
995        assert_eq!(err["error"]["data"]["bodyMethod"], json!("tools/call"));
996    }
997
998    #[test]
999    fn negotiate_standard_http_headers_rejects_name_body_mismatch() {
1000        let headers = std::collections::HashMap::from([
1001            (MCP_HEADER_METHOD.to_string(), "tools/call".to_string()),
1002            (MCP_HEADER_NAME.to_string(), "wrong".to_string()),
1003        ]);
1004        let err = negotiate_http_request(
1005            |key| headers.get(key).map(String::as_str),
1006            Some("tools/call"),
1007            Some("right"),
1008            &json!(3),
1009        )
1010        .expect_err("name mismatch must error");
1011        assert_eq!(err["error"]["code"], json!(HEADER_MISMATCH_CODE));
1012        assert_eq!(err["error"]["data"]["bodyName"], json!("right"));
1013    }
1014
1015    #[test]
1016    fn standard_name_header_value_extracts_method_subject() {
1017        assert_eq!(
1018            standard_name_header_value("tools/call", &json!({"name": "demo"})),
1019            Some("demo".to_string())
1020        );
1021        assert_eq!(
1022            standard_name_header_value("prompts/get", &json!({"name": "p"})),
1023            Some("p".to_string())
1024        );
1025        assert_eq!(
1026            standard_name_header_value("resources/read", &json!({"uri": "harn://x"})),
1027            Some("harn://x".to_string())
1028        );
1029        assert_eq!(standard_name_header_value("tools/list", &json!({})), None);
1030    }
1031
1032    #[test]
1033    fn apply_result_envelope_adds_result_type_and_cache() {
1034        let mut stable = json!({"tools": []});
1035        apply_result_envelope(&mut stable, Some(&McpCacheHint::list_default()));
1036        assert_eq!(stable["resultType"], json!(RESULT_TYPE_COMPLETE));
1037        assert_eq!(stable["ttlMs"], json!(DEFAULT_LIST_CACHE_TTL_MS));
1038        assert_eq!(stable["cacheScope"], json!(DEFAULT_LIST_CACHE_SCOPE));
1039    }
1040
1041    #[test]
1042    fn apply_result_envelope_preserves_caller_provided_result_type() {
1043        let mut result = json!({"resultType": RESULT_TYPE_INPUT_REQUIRED});
1044        apply_result_envelope(&mut result, None);
1045        assert_eq!(result["resultType"], json!(RESULT_TYPE_INPUT_REQUIRED));
1046    }
1047
1048    #[test]
1049    fn server_discover_result_advertises_request_metadata_versions() {
1050        let discover = server_discover_result(
1051            json!({"tools": {}}),
1052            json!({"name": "harn", "version": "x"}),
1053            Some("hello"),
1054        );
1055        assert_eq!(discover["resultType"], json!(RESULT_TYPE_COMPLETE));
1056        assert_eq!(discover["ttlMs"], json!(0));
1057        assert_eq!(discover["cacheScope"], json!("private"));
1058        assert_eq!(
1059            discover["_meta"]["io.modelcontextprotocol/serverInfo"]["name"],
1060            json!("harn")
1061        );
1062        let supported = discover["supportedVersions"].as_array().unwrap();
1063        assert_eq!(supported, &[json!(PROTOCOL_VERSION)]);
1064        assert_eq!(discover["instructions"], json!("hello"));
1065
1066        let typed: rmcp::model::DiscoverResult = serde_json::from_value(discover)
1067            .expect("Harn discovery result must match the official SDK type");
1068        assert_eq!(typed.result_type, rmcp::model::ResultType::COMPLETE);
1069        assert_eq!(typed.ttl_ms, 0);
1070        assert_eq!(
1071            typed.server_info().map(|info| info.name),
1072            Some("harn".to_string())
1073        );
1074    }
1075
1076    #[test]
1077    fn subscriptions_listen_is_explicitly_unsupported() {
1078        let response = explicit_unsupported_method_response(json!(7), "subscriptions/listen")
1079            .expect("known unsupported stable method");
1080        assert_eq!(response["error"]["code"], json!(-32601));
1081        assert_eq!(
1082            response["error"]["data"]["type"],
1083            json!("mcp.unsupportedFeature")
1084        );
1085        assert_eq!(
1086            response["error"]["data"]["protocolVersion"],
1087            json!(PROTOCOL_VERSION)
1088        );
1089    }
1090
1091    #[test]
1092    fn stable_input_methods_are_rejected_as_top_level_server_calls() {
1093        for method in [METHOD_SAMPLING_CREATE_MESSAGE, METHOD_ELICITATION_CREATE] {
1094            let response = explicit_unsupported_method_response(json!(7), method)
1095                .expect("input method has an explicit boundary error");
1096            assert_eq!(response["error"]["code"], json!(-32601));
1097            assert_eq!(response["error"]["data"]["method"], json!(method));
1098        }
1099    }
1100}