JSON-RPC 2.0 is the wire protocol underpinning every MCP interaction. A response missing "jsonrpc": "2.0" will be rejected by any compliant client. Responses that don't echo the request id leave the client unable to correlate responses to outstanding requests — causing hangs or misrouted results. Non-standard error formats ({'error': 'message'} instead of {'error': {'code': -32601, 'message': '...'}}) break client error handling. These are CWE-116 (improper encoding) failures that make the server non-interoperable.
Critical because a malformed JSON-RPC envelope breaks the entire protocol layer — clients reject non-conformant responses and the server becomes non-functional regardless of tool correctness.
Use the official MCP SDK transport, which handles JSON-RPC 2.0 formatting automatically. For custom implementations, every response must match this structure exactly.
// src/transport/handler.ts — JSON-RPC 2.0 response
const response = {
jsonrpc: '2.0', // required, must be the string '2.0'
id: request.id, // must match the incoming request id exactly
result: { /* ... */ }
}
// Error responses
const error = {
jsonrpc: '2.0',
id: request.id,
error: {
code: -32601, // integer, from standard codes
message: 'Method not found', // human-readable string
}
}
Never construct raw JSON and write it to stdout — use StdioServerTransport from the SDK.
ID: mcp-server.transport-protocol.jsonrpc-messages
Severity: critical
What to look for: Count all message handlers. Enumerate which follow JSON-RPC 2.0 format (jsonrpc, method, id, params) vs. which deviate. If using an MCP SDK, the SDK handles JSON-RPC formatting. Verify the SDK is used correctly (not bypassed with custom message construction). For custom implementations, check that every outgoing message includes "jsonrpc": "2.0". Check that responses include the id from the request. Check that error responses use the standard error object with code and message fields. Check that notifications (no id) are handled without sending a response.
Pass criteria: All outgoing messages follow JSON-RPC 2.0 format. Responses include the correct id. Error responses use the standard error structure. SDK-based servers use the SDK's transport correctly without bypassing it. 100% of messages must include the "jsonrpc": "2.0" field.
Fail criteria: Custom message construction that omits jsonrpc field, responses without matching id, non-standard error formats, or SDK transport bypassed with direct stdout writes.
Skip (N/A) when: All checks skip when no MCP server is detected.
Cross-reference: For error response format, see structured-errors.
Detail on fail: "Custom response handler writes JSON directly to stdout without 'jsonrpc': '2.0' field — responses will be rejected by clients" or "Error responses use custom format {'error': 'message'} instead of JSON-RPC {'error': {'code': -32600, 'message': '...'}}'"
Remediation: Use the SDK's transport layer. If you must handle messages manually, follow JSON-RPC 2.0 exactly:
// JSON-RPC 2.0 compliant request format in src/transport/handler.ts
{ "jsonrpc": "2.0", "method": "tools/call", "id": 1, "params": { "name": "search", "arguments": {} } }
// Correct — use the SDK transport
const transport = new StdioServerTransport()
await server.connect(transport)
// If custom, every response must follow this format:
const response = {
jsonrpc: '2.0',
id: request.id, // must match the request
result: { /* ... */ }
}
// Errors must use this format:
const errorResponse = {
jsonrpc: '2.0',
id: request.id,
error: {
code: -32601, // standard JSON-RPC error code
message: 'Method not found',
}
}