Skip to content

Commit 94d00dc

Browse files
committed
Enhance logging and session management in MCP server
1 parent 6472127 commit 94d00dc

File tree

1 file changed

+107
-35
lines changed

1 file changed

+107
-35
lines changed

src/server.js

Lines changed: 107 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,26 @@ import { promisify } from 'node:util';
66

77
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
88
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
9-
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
109
import { z } from 'zod';
1110

1211
const execFileAsync = promisify(execFile);
1312

1413
const CUSTOM_PREFIX = process.env.CUSTOM_PREFIX || '';
1514
const INTERNAL_PORT = parseInt(process.env.INTERNAL_PORT || '8080', 10);
1615

16+
// Basic timestamped logging
17+
function log(...args) {
18+
const ts = new Date().toISOString();
19+
console.log(`[${ts}]`, ...args);
20+
}
21+
22+
// Build "CUSTOM_PREFIX/<absolute-url>" while avoiding double slashes
23+
function buildPrefixed(url) {
24+
if (!CUSTOM_PREFIX) return url;
25+
const sep = CUSTOM_PREFIX.endsWith('/') ? '' : '/';
26+
return `${CUSTOM_PREFIX}${sep}${url}`;
27+
}
28+
1729
const app = express();
1830
app.use(express.json());
1931

@@ -30,11 +42,13 @@ app.use(
3042
// In-memory transport map per session
3143
const transports = new Map();
3244

33-
// Build "CUSTOM_PREFIX/<absolute-url>" while avoiding double slashes
34-
function buildPrefixed(url) {
35-
if (!CUSTOM_PREFIX) return url;
36-
const sep = CUSTOM_PREFIX.endsWith('/') ? '' : '/';
37-
return `${CUSTOM_PREFIX}${sep}${url}`;
45+
// Helper: read session id from header or from query (?session=...)
46+
// This improves compatibility with clients that cannot attach custom headers.
47+
function getSessionId(req) {
48+
const h = req.headers['mcp-session-id'];
49+
const fromHeader = Array.isArray(h) ? h[0] : h;
50+
const fromQuery = typeof req.query?.session === 'string' ? req.query.session : undefined;
51+
return fromHeader || fromQuery || null;
3852
}
3953

4054
// Create MCP server and register tools
@@ -53,18 +67,59 @@ function createMcpServer() {
5367
},
5468
async ({ url }) => {
5569
const prefixed = buildPrefixed(url);
70+
71+
// Log the request info
72+
log(`[tool:read_web_url] request`, { originalUrl: url, prefixedUrl: prefixed });
73+
74+
// Use a sentinel marker so we can capture HTTP status without logging/returning the body
75+
const STATUS_MARKER = '<<<MCP_HTTP_STATUS:';
76+
const STATUS_END = '>>>';
77+
5678
try {
5779
const { stdout } = await execFileAsync(
5880
'curl',
59-
['-sL', '--fail', prefixed],
60-
{ maxBuffer: 25 * 1024 * 1024 } // 25 MiB cap
81+
[
82+
'-sL',
83+
'--fail',
84+
// Follow redirects and fetch content
85+
prefixed,
86+
// Append final HTTP status code to stdout after the body
87+
'-w',
88+
`\n${STATUS_MARKER}%{http_code}${STATUS_END}`
89+
],
90+
{
91+
maxBuffer: 25 * 1024 * 1024 // 25 MiB cap
92+
}
6193
);
62-
return { content: [{ type: 'text', text: stdout }] };
94+
95+
// Split body and status using the marker
96+
let body = stdout;
97+
let httpCode = '000';
98+
const idx = stdout.lastIndexOf(STATUS_MARKER);
99+
if (idx !== -1) {
100+
body = stdout.slice(0, idx);
101+
const tail = stdout.slice(idx + STATUS_MARKER.length);
102+
const endIdx = tail.indexOf(STATUS_END);
103+
if (endIdx !== -1) {
104+
httpCode = tail.slice(0, endIdx).trim();
105+
}
106+
}
107+
108+
// Log summary without printing the response body
109+
log(`[tool:read_web_url] response received`, {
110+
prefixedUrl: prefixed,
111+
httpCode,
112+
bytes: body.length
113+
});
114+
115+
return { content: [{ type: 'text', text: body }] };
63116
} catch (err) {
117+
// Log error details but not the response body
64118
const msg =
65119
err && typeof err === 'object' && 'stderr' in err && err.stderr
66120
? String(err.stderr)
67121
: String(err?.message || err);
122+
log(`[tool:read_web_url] error`, { prefixedUrl: prefixed, error: msg });
68123
return {
69124
content: [{ type: 'text', text: `curl error for ${prefixed}: ${msg}` }],
70125
isError: true
@@ -76,67 +131,84 @@ function createMcpServer() {
76131
return server;
77132
}
78133

79-
// Streamable HTTP with session management
134+
// POST: JSON-RPC over Streamable HTTP
80135
app.post('/mcp', async (req, res) => {
81-
const sessionIdHeader = req.headers['mcp-session-id'];
82-
const sessionId = Array.isArray(sessionIdHeader) ? sessionIdHeader[0] : sessionIdHeader;
136+
const body = req.body || {};
137+
const method = body?.method;
138+
let sessionId = getSessionId(req);
83139
let transport = sessionId ? transports.get(sessionId) : undefined;
84140

85-
if (!transport) {
86-
if (!isInitializeRequest(req.body)) {
87-
res
88-
.status(400)
89-
.json({
90-
jsonrpc: '2.0',
91-
error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
92-
id: null
93-
});
94-
return;
95-
}
141+
const isInit = method === 'initialize';
142+
log(`[mcp] POST`, { method, sessionId: sessionId || null, isInit });
96143

144+
// Create a new session if:
145+
// - The request is initialize, or
146+
// - No session was provided (compat mode for clients that don't initialize explicitly)
147+
if (!transport && (isInit || !sessionId)) {
148+
log(`[mcp] creating new session`);
97149
transport = new StreamableHTTPServerTransport({
98150
sessionIdGenerator: () => randomUUID()
99-
// For local deployments consider DNS rebinding protections and allowedHosts/origins if needed
151+
// Optionally: allowedOrigins / allowedHosts for hardened deployments
100152
});
101153

102154
const server = createMcpServer();
103-
104155
transport.onclose = () => {
105156
if (transport.sessionId) transports.delete(transport.sessionId);
106157
server.close();
158+
log(`[mcp] session closed`, { sessionId: transport.sessionId || null });
107159
};
108160

109161
await server.connect(transport);
110162
transports.set(transport.sessionId, transport);
163+
sessionId = transport.sessionId;
164+
res.setHeader('Mcp-Session-Id', sessionId);
165+
log(`[mcp] session created`, { sessionId });
111166
}
112167

113-
await transport.handleRequest(req, res, req.body);
168+
if (!transport) {
169+
log(`[mcp] missing/invalid session`, { method, sessionId: sessionId || null });
170+
return res.status(400).json({
171+
jsonrpc: '2.0',
172+
error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
173+
id: body?.id ?? null
174+
});
175+
}
176+
177+
// Always expose session ID for clients to persist it
178+
res.setHeader('Mcp-Session-Id', sessionId);
179+
180+
// Hand off to the transport
181+
await transport.handleRequest(req, res, body);
114182
});
115183

184+
// GET: SSE stream for notifications (requires a valid session)
116185
app.get('/mcp', async (req, res) => {
117-
const sessionIdHeader = req.headers['mcp-session-id'];
118-
const sessionId = Array.isArray(sessionIdHeader) ? sessionIdHeader[0] : sessionIdHeader;
186+
const sessionId = getSessionId(req);
187+
log(`[mcp] GET (SSE)`, { sessionId: sessionId || null });
119188
const transport = sessionId ? transports.get(sessionId) : undefined;
120189
if (!transport) {
121-
res.status(400).send('Invalid or missing session ID');
122-
return;
190+
log(`[mcp] GET invalid/missing session`, { sessionId: sessionId || null });
191+
return res.status(400).send('Invalid or missing session ID');
123192
}
124193
await transport.handleRequest(req, res);
125194
});
126195

196+
// DELETE: close a session
127197
app.delete('/mcp', async (req, res) => {
128-
const sessionIdHeader = req.headers['mcp-session-id'];
129-
const sessionId = Array.isArray(sessionIdHeader) ? sessionIdHeader[0] : sessionIdHeader;
198+
const sessionId = getSessionId(req);
199+
log(`[mcp] DELETE`, { sessionId: sessionId || null });
130200
const transport = sessionId ? transports.get(sessionId) : undefined;
131201
if (!transport) {
132-
res.status(400).send('Invalid or missing session ID');
133-
return;
202+
log(`[mcp] DELETE invalid/missing session`, { sessionId: sessionId || null });
203+
return res.status(400).send('Invalid or missing session ID');
134204
}
135205
transports.delete(sessionId);
136206
transport.close();
207+
log(`[mcp] session deleted`, { sessionId });
137208
res.status(204).end();
138209
});
139210

211+
// Simple health endpoint
140212
app.get('/', (_req, res) => {
141213
res.json({
142214
status: 'ok',
@@ -147,5 +219,5 @@ app.get('/', (_req, res) => {
147219
});
148220

149221
app.listen(INTERNAL_PORT, () => {
150-
console.log(`MCP Streamable HTTP server listening on ${INTERNAL_PORT}`);
222+
log(`MCP Streamable HTTP server listening on ${INTERNAL_PORT}`);
151223
});

0 commit comments

Comments
 (0)