headless_lms_mcp/protocol/
context.rs1use rmcp::model::{
5 ClientCapabilities, Implementation, MetaObject, ProgressToken, ProtocolVersion,
6 RequestMetaObject,
7};
8use serde_json::{Map, Value};
9
10const META_PROGRESS_TOKEN: &str = "progressToken";
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ClientKind {
15 ClaudeCode,
16 Codex,
17 Other,
18}
19
20impl ClientKind {
21 fn of(client_info: Option<&Implementation>) -> Self {
23 let name = client_info
24 .map(|c| c.name.to_lowercase())
25 .unwrap_or_default();
26 if name.contains("claude") {
27 ClientKind::ClaudeCode
28 } else if name.contains("codex") {
29 ClientKind::Codex
30 } else {
31 ClientKind::Other
32 }
33 }
34}
35
36#[derive(Debug, Clone)]
38pub struct ClientContext {
39 pub protocol_version: Option<ProtocolVersion>,
40 pub capabilities: ClientCapabilities,
41 pub progress_token: Option<ProgressToken>,
42 kind: ClientKind,
43}
44
45impl ClientContext {
46 pub fn from_meta(meta: Map<String, Value>) -> Self {
48 let progress_token = meta
49 .get(META_PROGRESS_TOKEN)
50 .and_then(|token| serde_json::from_value(token.clone()).ok());
51 let request_meta = RequestMetaObject(MetaObject(meta));
52 Self {
53 protocol_version: request_meta.protocol_version(),
54 kind: ClientKind::of(request_meta.client_info().as_ref()),
55 capabilities: request_meta.client_capabilities().unwrap_or_default(),
56 progress_token,
57 }
58 }
59
60 pub fn client_kind(&self) -> ClientKind {
62 self.kind
63 }
64
65 pub fn supports_url_elicitation(&self) -> bool {
67 self.capabilities
68 .elicitation
69 .as_ref()
70 .is_some_and(|e| e.url.is_some())
71 }
72
73 pub fn can_use_input_required(&self) -> bool {
76 self.client_kind() != ClientKind::Codex
77 }
78
79 pub fn result_budget_bytes(&self) -> usize {
82 match self.client_kind() {
83 ClientKind::Codex => 8_000,
84 ClientKind::ClaudeCode => 40_000,
85 ClientKind::Other => 16_000,
86 }
87 }
88
89 pub fn read_back_budget_bytes(&self) -> usize {
91 self.result_budget_bytes() / 2
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use rmcp::model::NumberOrString;
98 use serde_json::json;
99
100 use super::*;
101
102 fn context(meta: Value) -> ClientContext {
103 ClientContext::from_meta(meta.as_object().unwrap().clone())
104 }
105
106 #[test]
107 fn reads_client_info_and_capabilities() {
108 let ctx = context(json!({
109 "io.modelcontextprotocol/protocolVersion": "2026-07-28",
110 "io.modelcontextprotocol/clientInfo": {"name": "claude-code", "version": "3.0.0"},
111 "io.modelcontextprotocol/clientCapabilities": {"elicitation": {"url": {}}},
112 "progressToken": 7,
113 "traceparent": "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
114 }));
115 assert_eq!(ctx.client_kind(), ClientKind::ClaudeCode);
116 assert!(ctx.supports_url_elicitation());
117 assert!(ctx.can_use_input_required());
118 assert_eq!(
119 ctx.progress_token,
120 Some(ProgressToken(NumberOrString::Number(7)))
121 );
122 assert_eq!(ctx.protocol_version, Some(ProtocolVersion::V_2026_07_28));
123 }
124
125 #[test]
126 fn codex_gets_fallbacks() {
127 let ctx = context(json!({
128 "io.modelcontextprotocol/clientInfo": {"name": "codex-mcp-client", "version": "0.9"},
129 "io.modelcontextprotocol/clientCapabilities": {"elicitation": {"url": {}}},
130 }));
131 assert!(!ctx.can_use_input_required());
132 assert_eq!(ctx.result_budget_bytes(), 8_000);
133 assert!(!context(json!({})).supports_url_elicitation());
134 }
135}