# 1.2 MCP Interfaces
## Protocol Design
The Model Context Protocol (MCP) endpoint implements a JSON-RPC 2.0 transport over `application/json`. Binary image data is transmitted inline via base64-encoded strings inside the JSON-RPC `params.arguments.images` array. This design obviates the need for multipart form handling on the MCP path while preserving the same asynchronous lifecycle: immediate acknowledgment (200 for sync methods, 202 for `tools/call`) followed by result delivery via Socket.IO.
```mermaid
flowchart TB
subgraph Client["MCP Client"]
C[Send JSON-RPC + Images]
end
subgraph Server["@CKIR.IO/VISIONS"]
Ctrl[JsonRpcController
/api/v1/mcp]
Pipe[JsonRpcValidationPipe]
Svc[JsonRpcService]
AISvc[AnalyzeImageService]
Queue[BullMQ Queue]
end
subgraph Workers["Workers"]
W1[VisionsDescribe/Compare/OCR Processor]
Preproc[ImagePreprocessingService]
end
subgraph Infra["Infrastructure"]
Ollama[Ollama Server]
Socket[Socket.IO]
end
C -->|POST multipart| Ctrl
Ctrl --> Pipe
Pipe --> Svc
Svc --> AISvc --> Queue
Queue --> W1 --> Preproc --> Ollama --> Socket --> C
```
## MCP Method Reference
### `initialize`
**Request:**
```json
{
"jsonrpc": "2.0",
"method": "initialize",
"params": {
"protocolVersion": "2025-11-25",
"capabilities": {},
"clientInfo": { "name": "my-client", "version": "1.0.0" }
},
"id": 0
}
```
**Response:**
```json
{
"jsonrpc": "2.0",
"id": 0,
"result": {
"protocolVersion": "2025-11-25",
"capabilities": { "tools": { "listChanged": false } },
"serverInfo": { "name": "@ckir.io/visions", "version": "1.1.0" }
}
}
```
The `serverInfo.name` and `version` are dynamically resolved from `package.json` at runtime, eliminating version drift between source and protocol handshake.
### `tools/list`
Returns the singular exposed tool `visions.analyze` along with its JSON Schema input definition.
**Response Snippet:**
```json
{
"tools": [{
"title": "Vision Analysis",
"name": "visions.analyze",
"description": "Perform a specific visual analysis on provided images...",
"inputSchema": {
"type": "object",
"additionalProperties": false,
"required": ["images", "prompt", "task"],
"properties": {
"task": { "type": "string", "enum": ["describe", "compare", "ocr"] },
"prompt": { "type": "string" },
"preprocessing": { "type": "object" }
}
}
}]
}
```
### `tools/call`
**Query Parameters:** None. Everything is carried in the JSON body.
**JSON Body:**
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "visions.analyze",
"arguments": {
"requestId": "abc-123",
"model": "llama3.2-vision",
"task": "describe",
"stream": true,
"roomId": "room-abc",
"event": "vision",
"numCtx": 16384,
"prompt": [{"role":"user","content":"What do you see?"}],
"images": [
{ "data": "iVBORw0KGgo...", "mimeType": "image/png", "name": "photo.png" }
],
"preprocessing": {
"enabled": true,
"variants": { "grayscale": true, "clahe": true }
}
}
}
}
```
`images` is an **array of objects** with:
- `data` — base64-encoded image bytes (required)
- `mimeType` — e.g. `image/png` (optional; defaults to `image/png`)
- `name` — optional filename
**Request Example:**
```json
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "visions.analyze",
"arguments": {
"requestId": "abc-123",
"model": "llama3.2-vision",
"task": "describe",
"stream": true,
"roomId": "room-abc",
"event": "vision",
"numCtx": 16384,
"prompt": [{"role": "user", "content": "What do you see?"}],
"images": [
{ "data": "iVBORw0KGgo...", "mimeType": "image/png", "name": "photo.png" }
],
"preprocessing": {
"enabled": true,
"variants": { "grayscale": true, "clahe": true }
}
}
},
"id": 2
}
```
## Controller Dispatch Logic
```typescript
// json-rpc.controller.ts
async rpc(@McpPayload() req: McpGenericType) {
if (req.method === 'initialize')
return this.wrap(req.id, await this.jsonRpcService.initialize());
if (req.method === 'tools/list')
return await this.jsonRpcService.getRequestedTools(req);
if (req.method === 'notifications/initialized') return;
const args = req.params?.arguments;
if (!args?.model)
throw new BadRequestException("Missing 'model' in arguments");
if (!args.images || args.images.length === 0)
throw new BadRequestException("Missing images");
const results = await this.jsonRpcService.toFilePayloadsFromBase64(
args.requestId, args.images
);
const name = req.params?.name;
if (name === 'visions.analyze')
return this.wrap(req.id, await this.jsonRpcService.analyze({
buffers: results.map(r => r.buffer).filter(Boolean),
meta: results.map(r => r.meta).filter(Boolean) as any,
filters: {
vLLM: args.model,
requestId: args.requestId,
roomId: args.roomId,
stream: args.stream ?? false,
numCtx: args.numCtx,
prompt: args.prompt,
task: args.task,
event: args.event ?? this.socketIOConfigService.config.event,
preprocessing: args.preprocessing,
},
}));
}
```
The `JsonRpcValidationPipe` applies schema enforcement over the deserialized JSON-RPC payload using `@nestjs/common` `ValidationPipe` semantics, ensuring `params.arguments.task` conforms to the `VisionTask` enum before service invocation.
## Response Lifecycle
### Immediate MCP Response (202)
```json
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "Processing started. Connect to Socket.IO for real-time results."
}
],
"isError": false,
"realtime": {
"event": "vision",
"roomId": "optional-room-id",
"requestId": "batch-001"
}
}
}
```
### Streaming Socket.IO Payload
Identical to REST streaming format. The MCP client is expected to subscribe to the Socket.IO room specified in `realtime.roomId` (or derive it from `realtime.requestId`).
```json
{
"meta": [...],
"task": "describe",
"message": { "role": "assistant", "content": "..." },
"done": false
}
```
## JSON-RPC Error Contract
| Code | Meaning | Trigger |
|------|---------|---------|
| `-32700` | Parse error | Malformed JSON in multipart payload field |
| `-32600` | Invalid Request | Missing `jsonrpc`, `method`, or `id` |
| `-32601` | Method not found | `tools/call` on non-existent tool name |
| `-32602` | Invalid params | Schema validation failure (`@MultiPartPayload`) |
| `400` | Bad Request | HTTP-level multipart parsing failure |
## MCP Status Interceptor
An `McpStatusInterceptor` wraps all MCP controller methods to guarantee JSON-RPC envelope compliance even on unexpected exceptions. It transforms NestJS exceptions into standardized JSON-RPC error payloads:
```typescript
{
"jsonrpc": "2.0",
"id": requestId,
"error": {
"code": -32603,
"message": "Internal error: ..."
}
}
```
This interceptor prevents raw stack traces from leaking to MCP clients, which typically have limited error display capabilities.
## Transport Characteristics
| Characteristic | Value | Rationale |
|----------------|-------|-----------|
| Transport | `application/json` over HTTP/1.1 | Images are base64-encoded inside JSON; no multipart handling needed |
| Versioning | URI versioning (`/api/v1/mcp`) | Consistent with REST; future-proof for protocol evolution |
| Authentication | None | Assumes trusted local network; `model` inside `arguments` serves as model selector |
| Streaming | Socket.IO (WebSocket fallback) | JSON-RPC batch responses are not used; real-time streaming via separate channel |
## Comparison with REST Transport
| Dimension | REST `POST /api/v1/vision` | MCP `POST /api/v1/mcp` |
|-----------|---------------------------|------------------------|
| **Data format** | Flat form fields + multipart | JSON-RPC 2.0 envelope + base64 images |
| **Metadata** | Direct query/body | Nested inside `params.arguments` |
| **Client type** | Custom scripts, curl, HTTP libraries | Claude Desktop, Copilot, agentic AI frameworks |
| **Tool discovery** | Manual / Swagger docs | Automatic via `tools/list` |
| **Preprocessing** | `pproc_*` query params | `arguments.preprocessing.*` nested object |
| **Error shape** | HTTP status + body | JSON-RPC error object |
| **Real-time** | Socket.IO (same as MCP) | Socket.IO (same as REST) |
## Architectural Consistency
Despite differing ingress formats, both REST and MCP flows converge on the same internal pipeline after controller dispatch:
1. **BullMQ enqueueing** via `AnalyzeImageService.emit()`
2. **Worker processing** via `VisionsDescribeProcessor` / `Compare` / `OCR` (including optional **Image preprocessing** via `ImagePreprocessingService`)
3. **Ollama inference** with streaming callback
4. **Socket.IO emission** via `SocketIOService.emitTo()`
This convergence ensures that routing-layer changes do not bifurcate the testing or operational surface of the queue and worker layers.