@@ -6,14 +6,26 @@ import { promisify } from 'node:util';
66
77import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' ;
88import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js' ;
9- import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js' ;
109import { z } from 'zod' ;
1110
1211const execFileAsync = promisify ( execFile ) ;
1312
1413const CUSTOM_PREFIX = process . env . CUSTOM_PREFIX || '' ;
1514const 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+
1729const app = express ( ) ;
1830app . use ( express . json ( ) ) ;
1931
@@ -30,11 +42,13 @@ app.use(
3042// In-memory transport map per session
3143const 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
80135app . 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)
116185app . 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
127197app . 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
140212app . get ( '/' , ( _req , res ) => {
141213 res . json ( {
142214 status : 'ok' ,
@@ -147,5 +219,5 @@ app.get('/', (_req, res) => {
147219} ) ;
148220
149221app . 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