Skip to main content

somatize_mcp/
protocol.rs

1//! MCP JSON-RPC 2.0 protocol types.
2
3use serde::{Deserialize, Serialize};
4
5/// One JSON-RPC 2.0 request, as read line-by-line off stdin.
6#[derive(Debug, Deserialize)]
7pub struct JsonRpcRequest {
8    /// Protocol version marker; `"2.0"` for every client we speak to.
9    pub jsonrpc: String,
10    /// Request id to echo back in the response. JSON-RPC allows a
11    /// string, a number or null here, so it stays an opaque
12    /// [`serde_json::Value`] rather than committing to one shape.
13    pub id: serde_json::Value,
14    /// The method being invoked — `initialize`, `tools/list`,
15    /// `tools/call`, ...
16    pub method: String,
17    /// Method parameters. Defaults to `Value::Null` because clients may
18    /// omit the field entirely for parameterless methods.
19    #[serde(default)]
20    pub params: serde_json::Value,
21}
22
23/// One JSON-RPC 2.0 response, written as a single line to stdout.
24///
25/// Exactly one of `result` and `error` is set; the [`success`] and
26/// [`error`] constructors are the only ways this crate builds one, so
27/// the invariant holds by construction.
28///
29/// [`success`]: JsonRpcResponse::success
30/// [`error`]: JsonRpcResponse::error
31#[derive(Debug, Serialize)]
32pub struct JsonRpcResponse {
33    /// Protocol version marker, always `"2.0"`.
34    pub jsonrpc: String,
35    /// The id of the request this answers, echoed verbatim.
36    pub id: serde_json::Value,
37    /// The successful payload; absent (not null) on failure.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub result: Option<serde_json::Value>,
40    /// The failure; absent on success.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub error: Option<JsonRpcError>,
43}
44
45/// The `error` member of a failed [`JsonRpcResponse`].
46///
47/// Protocol-level failure (unknown method, bad params, panic). A *tool*
48/// that fails still returns a successful response carrying a
49/// [`ToolCallResult`] with `is_error: true` — that distinction is MCP's,
50/// not ours: the model sees tool errors, the client sees protocol ones.
51#[derive(Debug, Serialize)]
52pub struct JsonRpcError {
53    /// Numeric error code; see [`METHOD_NOT_FOUND`], [`INVALID_PARAMS`],
54    /// [`INTERNAL_ERROR`].
55    pub code: i64,
56    /// Human-readable description of what went wrong.
57    pub message: String,
58    /// Optional structured detail; this server never sets it.
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub data: Option<serde_json::Value>,
61}
62
63impl JsonRpcResponse {
64    /// A successful response carrying `result` for request `id`.
65    pub fn success(id: serde_json::Value, result: serde_json::Value) -> Self {
66        Self {
67            jsonrpc: "2.0".into(),
68            id,
69            result: Some(result),
70            error: None,
71        }
72    }
73
74    /// A failed response for request `id` with the given code and
75    /// message; `data` is left unset.
76    pub fn error(id: serde_json::Value, code: i64, message: impl Into<String>) -> Self {
77        Self {
78            jsonrpc: "2.0".into(),
79            id,
80            result: None,
81            error: Some(JsonRpcError {
82                code,
83                message: message.into(),
84                data: None,
85            }),
86        }
87    }
88}
89
90/// The server's half of the MCP `initialize` handshake.
91#[derive(Debug, Serialize)]
92pub struct InitializeResult {
93    /// MCP protocol revision the server speaks (a date string,
94    /// e.g. `"2024-11-05"`).
95    #[serde(rename = "protocolVersion")]
96    pub protocol_version: String,
97    /// What the server can do — see [`ServerCapabilities`].
98    pub capabilities: ServerCapabilities,
99    /// Who is answering — see [`ServerInfo`].
100    #[serde(rename = "serverInfo")]
101    pub server_info: ServerInfo,
102}
103
104/// What this server offers a client.
105///
106/// Tools only: soma-mcp serves no resources and no prompts, so those
107/// capability fields do not exist here — MCP treats an absent field as
108/// "not supported", which is exactly the claim.
109#[derive(Debug, Serialize)]
110pub struct ServerCapabilities {
111    /// The tools capability; the 20 tools are the whole API.
112    pub tools: ToolsCapability,
113}
114
115/// Details of the tools capability advertised in the handshake.
116#[derive(Debug, Serialize)]
117pub struct ToolsCapability {
118    /// Whether the server emits `tools/list_changed` notifications.
119    /// This server's tool list is fixed at compile time, so it
120    /// advertises `false` and a client need never re-fetch the list.
121    #[serde(rename = "listChanged")]
122    pub list_changed: bool,
123}
124
125/// Server identity reported in the `initialize` handshake.
126#[derive(Debug, Serialize)]
127pub struct ServerInfo {
128    /// Server name (`"soma-mcp"`).
129    pub name: String,
130    /// Crate version, taken from `CARGO_PKG_VERSION` so it cannot drift
131    /// from the release.
132    pub version: String,
133}
134
135/// What this server publishes, and what an agent consumes.
136///
137/// The same type either way: [`somatize_core::tool::ToolSpec`]. Soma is both
138/// a tool provider (here) and a tool caller (`soma-llm`), and describing a
139/// tool twice is how the two descriptions drift.
140pub use somatize_core::tool::ToolSpec as ToolDefinition;
141
142/// What a `tools/call` returns: the text a model will read.
143///
144/// Every handler in [`crate::context`] produces one of these, and the
145/// renderers in [`crate::render`] decide what goes in it — the text IS
146/// the API (each experiment-pool result ends with a `next:` line and
147/// carries its `run_dir:`), so this type stays a thin envelope.
148#[derive(Debug, Serialize)]
149pub struct ToolCallResult {
150    /// The content items, concatenated by [`content_text`] when a
151    /// caller wants the single string a model sees.
152    ///
153    /// [`content_text`]: ToolCallResult::content_text
154    pub content: Vec<ContentItem>,
155    /// `Some(true)` when the tool failed. MCP keeps tool failure inside
156    /// a *successful* response — the model reads the error text and can
157    /// react to it, unlike a protocol-level [`JsonRpcError`].
158    #[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
159    pub is_error: Option<bool>,
160}
161
162/// One block of tool-result content.
163///
164/// This server only ever emits text, so the type is not an enum: a
165/// `type` tag plus the text is the whole story.
166#[derive(Debug, Serialize)]
167pub struct ContentItem {
168    /// MCP content discriminator; always `"text"` here.
169    #[serde(rename = "type")]
170    pub content_type: String,
171    /// The text itself.
172    pub text: String,
173}
174
175impl ContentItem {
176    /// A text content block.
177    pub fn text(s: impl Into<String>) -> Self {
178        Self {
179            content_type: "text".into(),
180            text: s.into(),
181        }
182    }
183}
184
185impl ToolCallResult {
186    /// A successful result carrying one text block.
187    pub fn text(s: impl Into<String>) -> Self {
188        Self {
189            content: vec![ContentItem::text(s)],
190            is_error: None,
191        }
192    }
193
194    /// A failed result: the same text block, flagged `isError` so the
195    /// model knows it is reading a failure, not an answer.
196    pub fn error(s: impl Into<String>) -> Self {
197        Self {
198            content: vec![ContentItem::text(s)],
199            is_error: Some(true),
200        }
201    }
202
203    /// The text a model would see — every content item concatenated.
204    /// The protocol carries text, so this is the whole result.
205    pub fn content_text(&self) -> String {
206        self.content
207            .iter()
208            .map(|c| c.text.as_str())
209            .collect::<Vec<_>>()
210            .join("\n")
211    }
212
213    /// Whether this result is an error; an absent flag means success.
214    pub fn is_error(&self) -> bool {
215        self.is_error.unwrap_or(false)
216    }
217}
218
219/// JSON-RPC 2.0 spec code: the requested method does not exist. What
220/// the server answers for any method it does not recognise.
221pub const METHOD_NOT_FOUND: i64 = -32601;
222/// JSON-RPC 2.0 spec code: the method exists but the parameters are
223/// invalid. Currently unsent — a missing tool argument is reported as a
224/// [`ToolCallResult::error`] instead, so the *model* sees it and can
225/// retry with the argument filled in.
226pub const INVALID_PARAMS: i64 = -32602;
227/// JSON-RPC 2.0 spec code: the server itself failed. Currently unsent;
228/// kept beside its siblings so a future handler does not reinvent the
229/// number.
230pub const INTERNAL_ERROR: i64 = -32603;