JSON-RPC 2.0 defines notifications as messages with no id — they require no response, and sending one violates the spec. A server that sends error responses to notifications/initialized (because it tries to echo the id that isn't there) causes clients to receive unexpected extra messages, which can desync the message stream. A server that crashes on an unknown notification type (notifications/cancelled, notifications/roots/list_changed) can be triggered by any client action that emits those events, making stability fragile in real workloads.
Low because notification handling errors cause protocol drift and occasional crashes rather than data exposure, and most SDK-based servers get this right automatically.
In custom message handlers, check for the absence of id to identify notifications and return without responding.
// src/transport/handler.ts
function handleMessage(message: unknown) {
// Notifications: no id field — process but do not respond
if (!('id' in (message as object))) {
const notification = message as { method: string }
if (notification.method === 'notifications/initialized') {
isInitialized = true
}
// Unknown notifications: silently ignore
return
}
// Requests: have an id — must respond
return handleRequest(message as JsonRpcRequest)
}
SDK-based servers handle this automatically — do not override the SDK's default notification handling.
ID: mcp-server.transport-protocol.notifications
Severity: low
What to look for: Count all notification handlers and emitters. Enumerate which MCP notification types are supported (tools/list_changed, resources/list_changed, progress). In JSON-RPC 2.0, notifications are requests with no id field — the server must not send a response. Check that the server correctly identifies notifications (messages without id) and does not respond to them. Common notifications from clients: notifications/initialized, notifications/cancelled, notifications/roots/list_changed. For SDK-based servers, this is handled automatically — check that custom message handling doesn't break this.
Pass criteria: The server processes notifications without sending any response. notifications/initialized is handled to complete the handshake. Unknown notifications are silently ignored (no error response). At least 2 notification types must be handled or emitted.
Fail criteria: The server sends responses to notifications (violating JSON-RPC 2.0), or crashes on unknown notification types, or does not handle notifications/initialized.
Skip (N/A) when: All checks skip when no MCP server is detected.
Cross-reference: For ping support, see ping-support.
Detail on fail: "Custom message handler sends error response for 'notifications/initialized' because it has no 'id' and the handler requires an id for all messages" or "Server crashes when receiving 'notifications/cancelled' — unhandled notification type"
Remediation: Notifications have no id and require no response:
// src/notifications.ts — emit list change notification
server.notification({ method: "notifications/tools/list_changed" })
function handleMessage(message: JsonRpcMessage) {
// Notifications have no id
if (!('id' in message)) {
// Process notification (e.g., update state) but DO NOT respond
if (message.method === 'notifications/initialized') {
isInitialized = true
}
// Unknown notifications: silently ignore
return // no response
}
// ... handle requests (which have an id)
}