Security Practices for MCP Using JSON-RPC
Model Context Protocol (MCP) is a framework for structured communication between clients and servers using JSON-RPC 2.0. It enables fine-grained message exchange in distributed or modular systems, including those involving AI models, modular agents, or service orchestration. When using lightweight protocols like JSON-RPC 2.0 for MCP communication, developers face a number of security challenges. This article outlines the critical issues, best solutions, and practical developer tools to ensure robust and secure MCP systems.
This guide provides a visual and practical overview of how to secure MCP systems, including message signing, session management, and real-world architecture patterns. It is intended for developers and architects building modular, agent-based, or distributed AI systems.
Message Signing and Verification Flow
sequenceDiagram
participant Client
participant Server
Client->>Client: Serialize method + params
Client->>Client: Sign payload with private key
Client->>Server: Send JSON-RPC + signature + public key
Server->>Server: Canonicalize method + params
Server->>Server: Verify signature with public key
Server-->>Client: Process or reject
Secure Session Bootstrapping
sequenceDiagram
participant Coordinator
participant Agent A
participant Agent B
Agent A->>Coordinator: Request session start
Coordinator->>Agent A: Send session UUID + token
Agent A->>Agent B: Share context with token
Agent B->>Coordinator: Verify session + join
Note over Coordinator: Coordinator tracks roles, rounds, and state
MCP Context Routing
graph TD
A[Client] -->|JSON-RPC| B[Coordinator]
B -->|Session Routing| C1[Agent A]
B -->|Session Routing| C2[Agent B]
C1 -->|Context Message| C2
C2 -->|Response| C1
B -->|Observability| D[Signed Logstore]
Area | Critical Issue | Why It Matters in MCP | Solution |
---|---|---|---|
🗭 Coordination | No native multi-party routing | MCP often involves orchestrating multiple services or agents | Implement a coordinator service to manage routing, roles, and message relays |
🔐 Identity | No identity/auth/session layer | You must authenticate and verify message origin | Use DIDs, mTLS, or signature-based identity layers with session tokens |
⚠️ Errors | Poor observability and error semantics | Hard to trace failures in contextual model flows | Add signed logs, structured error codes, and trace dashboards |
📦 Data Types | No binary/complex data support | Model parameters and responses may not fit JSON easily | Use base64 encoding, CBOR, or attach structured payloads outside core JSON-RPC fields |
🧪 Testing | No simulation or step-wise tooling | Need for protocol replay and debugging | Build deterministic test harnesses with protocol replay and agent mocking |
🧱 Persistence | No session/memory across calls | Context continuity across model calls needs tracked state and roles | Use Redis or in-memory session store to persist role, round, and message state |
💨 Malicious Agents | Any actor can inject harmful logic or context | Rogue agents can derail or poison model execution | Use sandboxing (e.g., VM2/WASM), behavior validation, and allowlists for context flow |
🦮 Session Drift | Agents can desync context/round data | Leads to race conditions, hallucinations, or context overwrite | Use signed session snapshots, round counters, and coordinator replay tools |
🔄 Replay Attacks | Reuse of valid messages across contexts | Undetected, it can manipulate agent behavior or output | Timestamped tokens, nonce validation, or hash chains to ensure freshness |
🛨️ Malicious Agent Mitigation | Compromised agents acting in a valid session | Threatens system integrity | Enforce behavior rules, use secure sandboxes, and validate input/output patterns |
🌐 Decentralized Identity (DIDs) | Lack of trusted global identity resolution | Needed for secure trust delegation across agents | Adopt W3C DIDs and resolve keys via DID Documents or key registries |
📡 Secure Session Management | Sessions may be hijacked, lost, or inconsistent | Critical for model state, context, and replay protection | Track sessions via coordinator, assign unique IDs, use per-session keys |
🔄 Replay and Race Protection | Duplicate or reordered requests can cause errors | Agents may misbehave or reprocess old data | Use nonces, timestamps, and round counters in every message |
Capability | Controlled By | Direction | Side Effects | Approval Needed | Typical Use Cases |
---|---|---|---|---|---|
Tools | Model (LLM) | Client → Server | Yes (potentially) | Yes | Actions, API calls, data manipulation |
Resources | Application | Client → Server | No (read-only) | Typically no | Data retrieval, context gathering |
Prompts | User | Server → Client | No | No | Guided workflows, specialized templates |
Sampling | Server | Server → Client → Server | Indirectly | Yes | Multi-step tasks, agentic behaviors |
- Frontend: Nuxt 3 (with Nuxt Content & Tailwind)
- Backend: Node.js / Fastify / Express
- MCP Coordinator: Custom routing + context logic (stateless + session-aware)
- Agents: Independent microservices or WASM-based modules
- Transport: JSON-RPC 2.0 over HTTPS (or mTLS)
- Security: Signature validation, DIDs, and per-session encryption keys
- Logs: Signed append-only logs + observability dashboard (Grafana / OpenTelemetry)
graph LR
FE[Nuxt3 Client] -->|JSON-RPC| COORD[MCP Coordinator]
COORD --> A1[Agent A]
COORD --> A2[Agent B]
COORD --> REDIS[(Session Store)]
COORD --> LOG[Signed Logs]
A1 --> RES1[(External API)]
A2 --> RES2[(LLM or Tool)]
- ✅ Signed JSON-RPC Messages: Create canonical JSON, sign using EdDSA/ECDSA, send with public key or DID.
- ✅ Session Snapshots: Store JSON snapshots per round with signatures.
- ✅ Replay Detection: Use nonce + timestamps + hash chaining.
- ✅ Testing Harness: Build MCP replay tools with mocked agents.
- ✅ Observability: Use OpenTelemetry or log aggregators with structured events.
- ✅ Signature Validation: Client + agent libs should enforce validation before processing.
- ✅ DID Resolver Service: Resolve public keys tied to agent DIDs.
import { sign, verify } from 'crypto';
const method = 'agent.perform';
const params = { input: 'Run test' };
const payload = JSON.stringify({ method, params });
const signature = sign('sha256', Buffer.from(payload), privateKey);
// Send: { method, params, signature, pubKey }
A secure MCP system built with JSON-RPC requires layers of protection: identity, message integrity, context safety, and clear protocol rules. Combining DIDs, signatures, session control, and malicious agent defenses ensures your modular system remains safe, observable, and extensible.