Add dev scripts for mitmproxy debugging and fix JSON-RPC error responses

- Add dev-with-mitm.sh for TUI-based traffic inspection
- Add dev-with-mitmweb.sh for web UI-based traffic inspection
- Fix missing 'id' field in JSON-RPC error responses (required by spec)
- Migrate from zod-to-json-schema to Zod v4's native z.toJSONSchema()

The mitmproxy scripts help developers debug MCP traffic by running
the server through a reverse proxy that intercepts all client requests.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Jerome Swannack 2025-12-05 13:35:29 +00:00
commit 07300e8453
No known key found for this signature in database
4 changed files with 83 additions and 13 deletions

31
dev-with-mitm.sh Executable file
View file

@ -0,0 +1,31 @@
#!/bin/bash
# Easy script to run MCP server with mitmproxy intercepting client traffic
echo "🚀 Starting MCP server with mitmproxy..."
echo ""
echo "This will:"
echo " 1. Start your MCP server on port 3232"
echo " 2. Start mitmproxy reverse proxy on port 8080"
echo " 3. Intercept all client → server traffic"
echo ""
echo "📡 Connect your MCP client to: http://localhost:8080/mcp"
echo "🔍 View traffic in mitmproxy TUI"
echo ""
echo "Press Ctrl+C to stop both processes"
echo ""
# Trap Ctrl+C to kill both processes
trap 'kill 0' EXIT
# Start the MCP server in background
npm run dev &
SERVER_PID=$!
# Give server time to start
sleep 3
# Start mitmproxy in reverse proxy mode (foreground so we see the TUI)
mitmproxy --mode reverse:http://localhost:3232 --listen-port 8080
# This line won't be reached until mitmproxy exits
wait

31
dev-with-mitmweb.sh Executable file
View file

@ -0,0 +1,31 @@
#!/bin/bash
# Easy script to run MCP server with mitmproxy web interface
echo "🚀 Starting MCP server with mitmproxy web UI..."
echo ""
echo "This will:"
echo " 1. Start your MCP server on port 3232"
echo " 2. Start mitmproxy with web UI on port 8081"
echo " 3. Proxy client traffic through port 8080"
echo ""
echo "📡 Connect your MCP client to: http://localhost:8080/mcp"
echo "🌐 View traffic in browser at: http://localhost:8081"
echo ""
echo "Press Ctrl+C to stop both processes"
echo ""
# Trap Ctrl+C to kill both processes
trap 'kill 0' EXIT
# Start the MCP server in background
npm run dev &
SERVER_PID=$!
# Give server time to start
sleep 3
# Start mitmweb in reverse proxy mode
mitmweb --mode reverse:http://localhost:3232 --listen-port 8080 --web-port 8081
# This line won't be reached until mitmweb exits
wait

View file

@ -67,6 +67,7 @@ export async function handleStreamableHTTP(req: Request, res: Response) {
}); });
res.status(401).json({ res.status(401).json({
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": null,
"error": { "error": {
"code": -32002, "code": -32002,
"message": "User ID required" "message": "User ID required"
@ -87,6 +88,7 @@ export async function handleStreamableHTTP(req: Request, res: Response) {
}); });
res.status(401).json({ res.status(401).json({
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": null,
"error": { "error": {
"code": -32001, "code": -32001,
"message": "Session not found or access denied" "message": "Session not found or access denied"
@ -148,6 +150,7 @@ export async function handleStreamableHTTP(req: Request, res: Response) {
}); });
res.status(400).json({ res.status(400).json({
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": null,
"error": { "error": {
"code": -32600, "code": -32600,
"message": "Invalid request method for existing session" "message": "Invalid request method for existing session"
@ -167,6 +170,7 @@ export async function handleStreamableHTTP(req: Request, res: Response) {
if (!res.headersSent) { if (!res.headersSent) {
res.status(500).json({ res.status(500).json({
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": null,
"error": { "error": {
"code": -32603, "code": -32603,
"message": "Internal error during request processing" "message": "Internal error during request processing"

View file

@ -19,12 +19,13 @@ import {
UnsubscribeRequestSchema, UnsubscribeRequestSchema,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod"; import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
type ToolInput = { type ToolInput = Tool["inputSchema"];
type: "object";
properties?: Record<string, unknown>; // Helper to convert Zod schema to JSON schema using Zod v4's native support
required?: string[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any
const toJsonSchema = (schema: z.ZodType<any>): ToolInput => {
return z.toJSONSchema(schema) as ToolInput;
}; };
/* Input schemas for tools implemented in this server */ /* Input schemas for tools implemented in this server */
@ -413,40 +414,40 @@ export const createMcpServer = (): McpServerWrapper => {
{ {
name: ToolName.ECHO, name: ToolName.ECHO,
description: "Echoes back the input", description: "Echoes back the input",
inputSchema: zodToJsonSchema(EchoSchema) as ToolInput, inputSchema: toJsonSchema(EchoSchema),
}, },
{ {
name: ToolName.ADD, name: ToolName.ADD,
description: "Adds two numbers", description: "Adds two numbers",
inputSchema: zodToJsonSchema(AddSchema) as ToolInput, inputSchema: toJsonSchema(AddSchema),
}, },
{ {
name: ToolName.LONG_RUNNING_OPERATION, name: ToolName.LONG_RUNNING_OPERATION,
description: description:
"Demonstrates a long running operation with progress updates", "Demonstrates a long running operation with progress updates",
inputSchema: zodToJsonSchema(LongRunningOperationSchema) as ToolInput, inputSchema: toJsonSchema(LongRunningOperationSchema),
}, },
{ {
name: ToolName.SAMPLE_LLM, name: ToolName.SAMPLE_LLM,
description: "Samples from an LLM using MCP's sampling feature", description: "Samples from an LLM using MCP's sampling feature",
inputSchema: zodToJsonSchema(SampleLLMSchema) as ToolInput, inputSchema: toJsonSchema(SampleLLMSchema),
}, },
{ {
name: ToolName.GET_TINY_IMAGE, name: ToolName.GET_TINY_IMAGE,
description: "Returns the MCP_TINY_IMAGE", description: "Returns the MCP_TINY_IMAGE",
inputSchema: zodToJsonSchema(GetTinyImageSchema) as ToolInput, inputSchema: toJsonSchema(GetTinyImageSchema),
}, },
{ {
name: ToolName.ANNOTATED_MESSAGE, name: ToolName.ANNOTATED_MESSAGE,
description: description:
"Demonstrates how annotations can be used to provide metadata about content", "Demonstrates how annotations can be used to provide metadata about content",
inputSchema: zodToJsonSchema(AnnotatedMessageSchema) as ToolInput, inputSchema: toJsonSchema(AnnotatedMessageSchema),
}, },
{ {
name: ToolName.GET_RESOURCE_REFERENCE, name: ToolName.GET_RESOURCE_REFERENCE,
description: description:
"Returns a resource reference that can be used by MCP clients", "Returns a resource reference that can be used by MCP clients",
inputSchema: zodToJsonSchema(GetResourceReferenceSchema) as ToolInput, inputSchema: toJsonSchema(GetResourceReferenceSchema),
}, },
{ {
name: ToolName.ELICIT_INPUTS, name: ToolName.ELICIT_INPUTS,
@ -524,9 +525,12 @@ export const createMcpServer = (): McpServerWrapper => {
ToolName.SAMPLE_LLM, ToolName.SAMPLE_LLM,
maxTokens maxTokens
); );
const contentArray = Array.isArray(result.content) ? result.content : [result.content];
const firstContent = contentArray[0];
const textContent = firstContent && "text" in firstContent ? firstContent.text : JSON.stringify(result.content);
return { return {
content: [ content: [
{ type: "text", text: `LLM sampling result: ${result.content.text}` }, { type: "text", text: `LLM sampling result: ${textContent}` },
], ],
}; };
} }