Skip to content

Commit cf885fe

Browse files
committed
fix(transcript-server): normalize Accept header for lenient MCP compatibility
- Patch rawHeaders (not just req.headers) for @hono/node-server compatibility - The SDK reads from rawHeaders which is normally immutable - Add HTTP logging middleware for debugging - Use app.post() with explicit 405 for GET/DELETE (per SDK examples)
1 parent 8c3b1da commit cf885fe

1 file changed

Lines changed: 93 additions & 2 deletions

File tree

examples/transcript-server/main.ts

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,81 @@ import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js
1313
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1414
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
1515
import cors from "cors";
16-
import type { Request, Response } from "express";
16+
import type { Request, Response, NextFunction } from "express";
1717
import { createServer } from "./server.js";
1818

19+
/**
20+
* Normalize Accept header for lenient MCP compatibility.
21+
* The SDK requires 'application/json, text/event-stream' but some clients send '*\/*'.
22+
* We must patch rawHeaders because @hono/node-server reads from there, not req.headers.
23+
*/
24+
function normalizeAcceptHeader(req: Request, _res: Response, next: NextFunction): void {
25+
const accept = req.headers.accept;
26+
if (!accept || accept === "*/*") {
27+
const normalized = "application/json, text/event-stream";
28+
req.headers.accept = normalized;
29+
30+
// Patch rawHeaders for @hono/node-server compatibility
31+
const nodeReq = req as unknown as { rawHeaders: string[] };
32+
const newRawHeaders: string[] = [];
33+
let found = false;
34+
for (let i = 0; i < nodeReq.rawHeaders.length; i += 2) {
35+
if (nodeReq.rawHeaders[i].toLowerCase() === "accept") {
36+
newRawHeaders.push(nodeReq.rawHeaders[i], normalized);
37+
found = true;
38+
} else {
39+
newRawHeaders.push(nodeReq.rawHeaders[i], nodeReq.rawHeaders[i + 1]);
40+
}
41+
}
42+
if (!found) {
43+
newRawHeaders.push("Accept", normalized);
44+
}
45+
Object.defineProperty(nodeReq, "rawHeaders", { value: newRawHeaders });
46+
}
47+
next();
48+
}
49+
50+
/**
51+
* HTTP logging middleware - logs full request and response details.
52+
*/
53+
function httpLogger(req: Request, res: Response, next: NextFunction): void {
54+
const startTime = Date.now();
55+
const reqId = Math.random().toString(36).slice(2, 8);
56+
57+
// Log request
58+
console.log(`\n[${reqId}] ← ${req.method} ${req.url}`);
59+
console.log(`[${reqId}] Headers:`, JSON.stringify(req.headers, null, 2));
60+
if (req.body && Object.keys(req.body).length > 0) {
61+
console.log(`[${reqId}] Body:`, JSON.stringify(req.body, null, 2));
62+
}
63+
64+
// Capture response
65+
const originalWrite = res.write.bind(res);
66+
const originalEnd = res.end.bind(res);
67+
const chunks: Buffer[] = [];
68+
69+
res.write = function (chunk: any, ...args: any[]): boolean {
70+
if (chunk) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
71+
return originalWrite(chunk, ...args);
72+
};
73+
74+
res.end = function (chunk?: any, ...args: any[]): Response {
75+
if (chunk) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
76+
const duration = Date.now() - startTime;
77+
const body = Buffer.concat(chunks).toString("utf8");
78+
79+
console.log(`[${reqId}] → ${res.statusCode} (${duration}ms)`);
80+
console.log(`[${reqId}] Headers:`, JSON.stringify(res.getHeaders(), null, 2));
81+
if (body) {
82+
console.log(`[${reqId}] Body:`, body.length > 2000 ? body.slice(0, 2000) + "..." : body);
83+
}
84+
85+
return originalEnd(chunk, ...args);
86+
};
87+
88+
next();
89+
}
90+
1991
export interface ServerOptions {
2092
port: number;
2193
name?: string;
@@ -32,8 +104,10 @@ export async function startServer(
32104

33105
const app = createMcpExpressApp({ host: "0.0.0.0" });
34106
app.use(cors());
107+
app.use(normalizeAcceptHeader);
108+
app.use(httpLogger);
35109

36-
app.all("/mcp", async (req: Request, res: Response) => {
110+
app.post("/mcp", async (req: Request, res: Response) => {
37111
const server = createServer();
38112
const transport = new StreamableHTTPServerTransport({
39113
sessionIdGenerator: undefined,
@@ -59,6 +133,23 @@ export async function startServer(
59133
}
60134
});
61135

136+
// GET and DELETE not supported in stateless mode
137+
app.get("/mcp", (_req: Request, res: Response) => {
138+
res.status(405).json({
139+
jsonrpc: "2.0",
140+
error: { code: -32000, message: "Method not allowed in stateless mode" },
141+
id: null,
142+
});
143+
});
144+
145+
app.delete("/mcp", (_req: Request, res: Response) => {
146+
res.status(405).json({
147+
jsonrpc: "2.0",
148+
error: { code: -32000, message: "Method not allowed in stateless mode" },
149+
id: null,
150+
});
151+
});
152+
62153
const httpServer = app.listen(port, (err) => {
63154
if (err) {
64155
console.error("Failed to start server:", err);

0 commit comments

Comments
 (0)