1use serde_json::{json, Value as JsonValue};
9
10pub 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
27pub 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
32pub 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
38pub const RESULT_TYPE_COMPLETE: &str = "complete";
40pub const RESULT_TYPE_INPUT_REQUIRED: &str = "input_required";
41
42pub 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
53pub 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
73pub 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
82pub 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
96pub 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#[derive(Clone, Debug, PartialEq)]
119struct McpInitializeOutcome {
120 client_identity: String,
121 protocol_version: rmcp::model::ProtocolVersion,
122 result: JsonValue,
123}
124
125fn 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#[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#[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 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
256pub use rmcp::model::RequestMetaObject as McpRequestMetadata;
258
259pub 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
267pub 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
302pub 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#[derive(Clone, Debug)]
322pub struct McpHttpHeaderOutcome {
323 pub protocol_version: Option<String>,
324}
325
326pub 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
390pub 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
407pub 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#[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 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
492pub 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
505pub 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(¶ms);
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}