mcp-server: accept JSON-RPC notifications (no id field)
JSON-RPC 2.0 notifications are fire-and-forget and omit the `id` field entirely. The server was requiring `id: Value`, so MCP clients sending `notifications/initialized` after the handshake got a `parse error: missing field \`id\`` response back. Claude Code then treated the handshake as broken and never called `tools/list`, so none of our memory/journal/graph/channel tools showed up in the client's tool list. Make `Request.id` an `Option<Value>` with `#[serde(default)]`: - Missing id → notification (None) → no response of any kind. - Explicit null id → still a request, null response id (legal). - Parse errors still respond with id:null per spec. Update respond() / respond_error() to no-op on None so a dispatched handler that tries to reply to a notification silently does nothing instead of violating the spec. Verified with a manual handshake: initialize → notification → tools/list now round-trips cleanly. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
199ce0ecdc
commit
11e2f20820
1 changed files with 16 additions and 4 deletions
|
|
@ -23,7 +23,11 @@ struct Request {
|
||||||
method: String,
|
method: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
params: Value,
|
params: Value,
|
||||||
id: Value,
|
/// Absent for JSON-RPC notifications (fire-and-forget); present for
|
||||||
|
/// requests that expect a response. Note that `Some(Value::Null)` is
|
||||||
|
/// an explicit null id (still a request), distinct from `None`.
|
||||||
|
#[serde(default)]
|
||||||
|
id: Option<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
|
|
@ -50,7 +54,10 @@ struct DaemonResponse {
|
||||||
id: Value,
|
id: Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn respond(id: Value, result: Value) {
|
/// Write a successful response for a request. Silently no-ops for
|
||||||
|
/// notifications (id = None) — they are fire-and-forget.
|
||||||
|
fn respond(id: Option<Value>, result: Value) {
|
||||||
|
let Some(id) = id else { return; };
|
||||||
let resp = Response { jsonrpc: "2.0".into(), result, id };
|
let resp = Response { jsonrpc: "2.0".into(), result, id };
|
||||||
let json = serde_json::to_string(&resp).unwrap();
|
let json = serde_json::to_string(&resp).unwrap();
|
||||||
let mut stdout = io::stdout().lock();
|
let mut stdout = io::stdout().lock();
|
||||||
|
|
@ -58,7 +65,11 @@ fn respond(id: Value, result: Value) {
|
||||||
let _ = stdout.flush();
|
let _ = stdout.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn respond_error(id: Value, code: i64, message: &str) {
|
/// Write an error response for a request. Silently no-ops for
|
||||||
|
/// notifications (id = None) per JSON-RPC 2.0 — a server MUST NOT reply
|
||||||
|
/// to a notification, even with an error.
|
||||||
|
fn respond_error(id: Option<Value>, code: i64, message: &str) {
|
||||||
|
let Some(id) = id else { return; };
|
||||||
let resp = ErrorResponse {
|
let resp = ErrorResponse {
|
||||||
jsonrpc: "2.0".into(),
|
jsonrpc: "2.0".into(),
|
||||||
error: json!({ "code": code, "message": message }),
|
error: json!({ "code": code, "message": message }),
|
||||||
|
|
@ -232,7 +243,8 @@ fn main() {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("consciousness-mcp: bad json-rpc: {e}");
|
eprintln!("consciousness-mcp: bad json-rpc: {e}");
|
||||||
eprintln!("consciousness-mcp: input was: {line}");
|
eprintln!("consciousness-mcp: input was: {line}");
|
||||||
respond_error(Value::Null, -32700, &format!("parse error: {e}"));
|
// Per spec, parse errors get a response with id: null.
|
||||||
|
respond_error(Some(Value::Null), -32700, &format!("parse error: {e}"));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue