-
Notifications
You must be signed in to change notification settings - Fork 1
MCP Protocol Implementation
SecondLayer implements the Model Context Protocol (MCP) to expose legal research tools to LLM clients. The implementation strictly follows the JSON-RPC 2.0 specification and supports multiple MCP protocol versions for broad client compatibility.
The server exposes the same tool registry through three independent transport layers, allowing integration with desktop apps, web applications, and remote LLM services.
| Transport | Endpoint | Protocol | Primary Clients |
|---|---|---|---|
| stdio | Process stdin/stdout | MCP stdio | Claude Desktop, local CLI |
| HTTP REST | POST /api/tools/:toolName |
JSON over HTTP | Web frontend, mobile apps |
| Remote MCP SSE |
GET /v1/sse, POST /sse
|
JSON-RPC 2.0 over SSE / Streamable HTTP | ChatGPT, Claude.ai, remote MCP clients |
-
stdio: Used when the server is spawned as a child process by a local MCP client (e.g., Claude Desktop via
mcp.jsonconfiguration). -
HTTP REST: Traditional request-response pattern. Supports optional SSE streaming via
POST /api/tools/:toolName/streamorAccept: text/event-streamheader. -
Remote MCP SSE: Full MCP protocol over the network. Two sub-modes:
-
SSE transport (
GET /v1/sse): Persistent event-stream connection with message routing viaPOST /v1/sse?sessionId=... -
Streamable HTTP (
POST /sse): Single request-response per JSON-RPC message, compatible with OpenAI's MCP integration.
-
SSE transport (
graph TD
subgraph Clients
C1[Claude Desktop]
C2[Web App / Mobile]
C3[ChatGPT / Claude.ai]
end
subgraph Transport
T1[stdio]
T2[HTTP REST]
T3[Remote MCP SSE]
end
subgraph Core
TR[ToolRegistry]
H[BaseToolHandler instances]
RC[RemoteServiceClient]
end
subgraph External
RADA[RADA Server :3001]
OR[OpenReyestr Server :3005]
end
C1 --> T1
C2 --> T2
C3 --> T3
T1 --> TR
T2 --> TR
T3 --> TR
TR --> H
TR --> RC
RC --> RADA
RC --> OR
The server negotiates the protocol version during the initialize handshake:
-
2024-11-05(MCP baseline) -
2025-03-26(OpenAI ChatGPT) 2025-11-052025-11-25
If the client requests an unrecognized version, the server defaults to 2025-11-05.
| Method | Direction | Description |
|---|---|---|
initialize |
Client -> Server | Protocol handshake; server returns capabilities and version |
tools/list |
Client -> Server | Returns available tool definitions with JSON Schema inputs |
tools/call |
Client -> Server | Execute a tool by name with arguments |
prompts/list |
Client -> Server | List available prompts (currently empty) |
resources/list |
Client -> Server | List available resources (currently empty) |
ping |
Client -> Server | Health check; server returns { pong: true }
|
notifications/progress |
Server -> Client | Progress updates during tool execution |
notifications/* |
Client -> Server | Fire-and-forget notifications (acknowledged with 202) |
Returned during initialize:
{
"protocolVersion": "2025-11-05",
"capabilities": {
"tools": {
"listChanged": false
}
},
"serverInfo": {
"name": "SecondLayer Legal MCP Server",
"version": "1.0.0"
}
}Standard JSON-RPC 2.0 error codes are used:
| Code | Meaning |
|---|---|
-32601 |
Method not found |
-32602 |
Invalid params (e.g., missing tool name) |
-32603 |
Internal error |
-32000 |
Application error (tool execution failure, insufficient credits, service unavailable) |
ChatGPT and similar clients use the Streamable HTTP pattern where each JSON-RPC message is a separate POST /sse request:
sequenceDiagram
participant Client
participant Server
Client->>Server: POST /sse {method: "initialize"}
Server-->>Client: JSON-RPC result (capabilities)
Note over Server: Mcp-Session-Id header set
Client->>Server: POST /sse {method: "tools/list"}
Server-->>Client: JSON-RPC result (tool definitions)
Client->>Server: POST /sse {method: "tools/call", params: {name, arguments}}
Server-->>Client: JSON-RPC result (tool output)
In Streamable HTTP mode (POST), the response Content-Type is application/json. Notifications are suppressed -- only the final result (message with id) is returned.
Standard MCP SSE clients use GET /v1/sse to open a persistent stream, then send messages via POST /v1/sse?sessionId=<id>:
sequenceDiagram
participant Client
participant Server
Client->>Server: GET /v1/sse (Bearer auth)
Server-->>Client: event: endpoint (session URL)
Note over Server: SSEServerTransport created
Client->>Server: POST /v1/sse?sessionId=abc {method: "tools/list"}
Server-->>Client: event: message (tool definitions)
Client->>Server: POST /v1/sse?sessionId=abc {method: "tools/call"}
Server-->>Client: event: message (notifications/progress)
Server-->>Client: event: message (tool result)
SSE messages use the format:
event: message
data: {"jsonrpc":"2.0","id":1,"result":{...}}
Keepalive pings (: ping\n\n) are sent every 30 seconds to prevent connection timeout.
The HTTP API also supports SSE streaming for long-running tools:
POST /api/tools/:toolName/stream
Content-Type: application/json
Accept: text/event-stream
-> event: connected (tool name, timestamp)
-> event: progress (intermediate updates)
-> event: complete (final result)
-> event: end (stream closed)
The ToolRegistry class is the central dispatcher. It maintains:
-
Local handlers --
BaseToolHandlersubclasses registered at startup (26 handler classes) - Remote tool routes -- Proxied to RADA and OpenReyestr services via HTTP
Tool definitions follow the MCP standard schema:
interface ToolDefinition {
name: string;
description: string;
inputSchema: {
type: "object";
properties: Record<string, JSONSchema>;
required?: string[];
};
annotations?: {
title?: string;
readOnlyHint?: boolean;
destructiveHint?: boolean;
idempotentHint?: boolean;
openWorldHint?: boolean;
};
}| Category | Prefix | Examples | Source |
|---|---|---|---|
| Court Decisions | -- |
search_court_decisions, edrsr_get_decision_dispositive
|
Local |
| Legislation | -- |
search_legislation, get_legislation_article
|
Local |
| Legal Analysis | -- |
build_legal_decision, analyze_data
|
Local |
| Document Vault | -- |
store_document, semantic_search, list_documents
|
Local |
| Open Data | -- |
search_edrnpa, search_public_spending
|
Local |
| OSINT | osint_ |
osint_search_sanctions, osint_check_domain_reputation
|
Local |
| Parliament | rada_ |
rada_search_parliament_bills, rada_get_deputy_info
|
Remote (RADA) |
| Business Registry | openreyestr_ |
openreyestr_search_entities, openreyestr_get_beneficiaries
|
Remote (OpenReyestr) |
When ENABLE_UNIFIED_GATEWAY=true, the tool registry aggregates tools from all three services behind a single endpoint. Remote tools are prefixed (rada_*, openreyestr_*) and proxied transparently.
GET /mcp returns a public discovery document (rate-limited):
{
"protocolVersion": "2024-11-05",
"serverInfo": {
"name": "SecondLayer Legal MCP Server",
"version": "1.0.0",
"description": "Ukrainian legal research and document analysis platform"
},
"capabilities": {
"tools": { "count": 45, "listChanged": false },
"prompts": {},
"resources": {}
},
"endpoints": {
"sse": "/sse",
"sse-standard": "/v1/sse",
"http": "/api/tools"
},
"tools": [{ "name": "...", "description": "..." }]
}Each tool has a configurable execution timeout. Overrides exist for expensive operations:
| Tool | Timeout |
|---|---|
search_court_decisions |
120s |
build_legal_decision |
120s |
search_public_spending |
120s |
edrsr_court_decisions_by_court |
90s |
analyze_data |
45s |
| OSINT tools | 35s |
| Default (all others) | 60s |
All transports require authentication. The server supports three credential types:
| Method | Format | Use Case |
|---|---|---|
| JWT |
Authorization: Bearer <jwt> (contains .) |
Web sessions, authenticated users |
| OAuth 2.0 | Authorization: Bearer mcp_token_* |
ChatGPT Custom Connectors, Claude.ai |
| API Key | Authorization: Bearer <key> |
Programmatic access, CI/CD |
The server implements full OAuth discovery for MCP clients that support dynamic registration:
| Endpoint | Purpose |
|---|---|
/.well-known/oauth-authorization-server |
RFC 8414 Authorization Server Metadata |
/.well-known/oauth-protected-resource |
RFC 9728 Protected Resource Metadata |
/.well-known/openid-configuration |
OpenID Connect Discovery |
/sse/.well-known/oauth-protected-resource |
Resource metadata scoped to SSE endpoint |
OPTIONS /sse |
Returns MCP-Auth-* headers for OAuth configuration |
OAuth flow:
- Client discovers authorization server via
/.well-known/oauth-protected-resource - Dynamic client registration at
POST /oauth/register - Authorization code flow with PKCE (
S256orplain) - Token exchange at
POST /oauth/token - Access token used as Bearer credential for MCP calls
When authentication fails, the server returns HTTP 401 with a WWW-Authenticate header containing the resource_metadata URL per RFC 9728.
The HTTP transport supports batch tool calls in a single request:
POST /api/tools/batch
Content-Type: application/json
{
"calls": [
{ "name": "search_court_decisions", "arguments": { "query": "..." } },
{ "name": "get_legislation_article", "arguments": { "law": "...", "article": "..." } }
]
}
All calls execute in parallel. Each call is independently tracked and results are returned as an array.
Every tool execution (across all transports) creates a tracking record that captures:
- Tool name and input parameters
- Execution duration (ms)
- Completion status (completed / failed)
- Associated user or API key
This enables per-user usage metering and rate limiting enforcement.
| File | Responsibility |
|---|---|
mcp_backend/src/api/mcp-sse-server.ts |
MCPSSEServer class -- Streamable HTTP handler, session management |
mcp_backend/src/routes/mcp-sse-routes.ts |
Express router -- all /sse, /v1/sse, /mcp, OAuth discovery routes |
mcp_backend/src/routes/tool-execution-routes.ts |
HTTP REST tool execution (/api/tools/*) |
mcp_backend/src/api/tool-registry.ts |
ToolRegistry -- central tool dispatch, remote aggregation |
mcp_backend/src/api/base-tool-handler.ts |
BaseToolHandler abstract class -- tool definition and execution interface |
mcp_backend/src/factories/tool-services.ts |
Handler registration (26 handlers wired at startup) |
mcp_backend/src/index.ts |
stdio transport entry point |