Skip to content

Commit a5d117f

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 a5d117f

1 file changed

Lines changed: 103 additions & 2 deletions

File tree

examples/transcript-server/main.ts

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,91 @@ 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(
25+
req: Request,
26+
_res: Response,
27+
next: NextFunction,
28+
): void {
29+
const accept = req.headers.accept;
30+
if (!accept || accept === "*/*") {
31+
const normalized = "application/json, text/event-stream";
32+
req.headers.accept = normalized;
33+
34+
// Patch rawHeaders for @hono/node-server compatibility
35+
const nodeReq = req as unknown as { rawHeaders: string[] };
36+
const newRawHeaders: string[] = [];
37+
let found = false;
38+
for (let i = 0; i < nodeReq.rawHeaders.length; i += 2) {
39+
if (nodeReq.rawHeaders[i].toLowerCase() === "accept") {
40+
newRawHeaders.push(nodeReq.rawHeaders[i], normalized);
41+
found = true;
42+
} else {
43+
newRawHeaders.push(nodeReq.rawHeaders[i], nodeReq.rawHeaders[i + 1]);
44+
}
45+
}
46+
if (!found) {
47+
newRawHeaders.push("Accept", normalized);
48+
}
49+
Object.defineProperty(nodeReq, "rawHeaders", { value: newRawHeaders });
50+
}
51+
next();
52+
}
53+
54+
/**
55+
* HTTP logging middleware - logs full request and response details.
56+
*/
57+
function httpLogger(req: Request, res: Response, next: NextFunction): void {
58+
const startTime = Date.now();
59+
const reqId = Math.random().toString(36).slice(2, 8);
60+
61+
// Log request
62+
console.log(`\n[${reqId}] ← ${req.method} ${req.url}`);
63+
console.log(`[${reqId}] Headers:`, JSON.stringify(req.headers, null, 2));
64+
if (req.body && Object.keys(req.body).length > 0) {
65+
console.log(`[${reqId}] Body:`, JSON.stringify(req.body, null, 2));
66+
}
67+
68+
// Capture response
69+
const originalWrite = res.write.bind(res);
70+
const originalEnd = res.end.bind(res);
71+
const chunks: Buffer[] = [];
72+
73+
res.write = function (chunk: any, ...args: any[]): boolean {
74+
if (chunk) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
75+
return originalWrite(chunk, ...args);
76+
};
77+
78+
res.end = function (chunk?: any, ...args: any[]): Response {
79+
if (chunk) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
80+
const duration = Date.now() - startTime;
81+
const body = Buffer.concat(chunks).toString("utf8");
82+
83+
console.log(`[${reqId}] → ${res.statusCode} (${duration}ms)`);
84+
console.log(
85+
`[${reqId}] Headers:`,
86+
JSON.stringify(res.getHeaders(), null, 2),
87+
);
88+
if (body) {
89+
console.log(
90+
`[${reqId}] Body:`,
91+
body.length > 2000 ? body.slice(0, 2000) + "..." : body,
92+
);
93+
}
94+
95+
return originalEnd(chunk, ...args);
96+
};
97+
98+
next();
99+
}
100+
19101
export interface ServerOptions {
20102
port: number;
21103
name?: string;
@@ -32,8 +114,10 @@ export async function startServer(
32114

33115
const app = createMcpExpressApp({ host: "0.0.0.0" });
34116
app.use(cors());
117+
app.use(normalizeAcceptHeader);
118+
app.use(httpLogger);
35119

36-
app.all("/mcp", async (req: Request, res: Response) => {
120+
app.post("/mcp", async (req: Request, res: Response) => {
37121
const server = createServer();
38122
const transport = new StreamableHTTPServerTransport({
39123
sessionIdGenerator: undefined,
@@ -59,6 +143,23 @@ export async function startServer(
59143
}
60144
});
61145

146+
// GET and DELETE not supported in stateless mode
147+
app.get("/mcp", (_req: Request, res: Response) => {
148+
res.status(405).json({
149+
jsonrpc: "2.0",
150+
error: { code: -32000, message: "Method not allowed in stateless mode" },
151+
id: null,
152+
});
153+
});
154+
155+
app.delete("/mcp", (_req: Request, res: Response) => {
156+
res.status(405).json({
157+
jsonrpc: "2.0",
158+
error: { code: -32000, message: "Method not allowed in stateless mode" },
159+
id: null,
160+
});
161+
});
162+
62163
const httpServer = app.listen(port, (err) => {
63164
if (err) {
64165
console.error("Failed to start server:", err);

0 commit comments

Comments
 (0)