2
2
3
3
import './polyfill.ts'
4
4
import http from 'node:http'
5
+ import { randomUUID } from 'node:crypto'
5
6
import { parseArgs } from '@std/cli/parse-args'
6
7
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'
7
8
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
9
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
10
+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
8
11
import { type LoggingLevel , SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js'
9
12
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
10
13
import { z } from 'zod'
11
14
12
15
import { asXml , runCode } from './runCode.ts'
16
+ import { Buffer } from 'node:buffer'
13
17
14
18
const VERSION = '0.0.13'
15
19
16
20
export async function main ( ) {
17
21
const { args } = Deno
18
22
if ( args . length === 1 && args [ 0 ] === 'stdio' ) {
19
23
await runStdio ( )
24
+ } else if ( args . length >= 1 && args [ 0 ] === 'streamable_http' ) {
25
+ const flags = parseArgs ( Deno . args , {
26
+ string : [ 'port' ] ,
27
+ default : { port : '3001' } ,
28
+ } )
29
+ const port = parseInt ( flags . port )
30
+ runStreamableHttp ( port )
20
31
} else if ( args . length >= 1 && args [ 0 ] === 'sse' ) {
21
32
const flags = parseArgs ( Deno . args , {
22
33
string : [ 'port' ] ,
@@ -31,7 +42,7 @@ export async function main() {
31
42
`\
32
43
Invalid arguments.
33
44
34
- Usage: deno run -N -R=node_modules -W=node_modules --node-modules-dir=auto jsr:@pydantic/mcp-run-python [stdio|sse|warmup]
45
+ Usage: deno run -N -R=node_modules -W=node_modules --node-modules-dir=auto jsr:@pydantic/mcp-run-python [stdio|streamable_http| sse|warmup]
35
46
36
47
options:
37
48
--port <port> Port to run the SSE server on (default: 3001)` ,
@@ -103,6 +114,138 @@ print('python code here')
103
114
return server
104
115
}
105
116
117
+ /*
118
+ * Define some QOL functions for both the SSE and Streamable HTTP server implementation
119
+ */
120
+ function httpGetUrl ( req : http . IncomingMessage ) : URL {
121
+ return new URL (
122
+ req . url ?? '' ,
123
+ `http://${ req . headers . host ?? 'unknown' } ` ,
124
+ )
125
+ }
126
+
127
+ function httpGetBody ( req : http . IncomingMessage ) : Promise < JSON > {
128
+ // https://nodejs.org/en/learn/modules/anatomy-of-an-http-transaction#request-body
129
+ return new Promise ( ( resolve ) => {
130
+ // deno-lint-ignore no-explicit-any
131
+ const bodyParts : any [ ] = [ ]
132
+ let body
133
+ req . on ( 'data' , ( chunk ) => {
134
+ bodyParts . push ( chunk )
135
+ } ) . on ( 'end' , ( ) => {
136
+ body = Buffer . concat ( bodyParts ) . toString ( )
137
+ resolve ( JSON . parse ( body ) )
138
+ } )
139
+ } )
140
+ }
141
+
142
+ function httpSetTextResponse ( res : http . ServerResponse , status : number , text : string ) {
143
+ res . setHeader ( 'Content-Type' , 'text/plain' )
144
+ res . statusCode = status
145
+ res . end ( `${ text } \n` )
146
+ }
147
+
148
+ function httpSetJsonResponse ( res : http . ServerResponse , status : number , text : string , code : number ) {
149
+ res . setHeader ( 'Content-Type' , 'application/json' )
150
+ res . statusCode = status
151
+ res . write ( JSON . stringify ( {
152
+ jsonrpc : '2.0' ,
153
+ error : {
154
+ code : code ,
155
+ message : text ,
156
+ } ,
157
+ id : null ,
158
+ } ) )
159
+ res . end ( )
160
+ }
161
+
162
+ /*
163
+ * Run the MCP server using the Streamable HTTP transport
164
+ */
165
+ function runStreamableHttp ( port : number ) {
166
+ // https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#with-session-management
167
+ const mcpServer = createServer ( )
168
+ const transports : { [ sessionId : string ] : StreamableHTTPServerTransport } = { }
169
+
170
+ const server = http . createServer ( async ( req , res ) => {
171
+ const url = httpGetUrl ( req )
172
+ let pathMatch = false
173
+ function match ( method : string , path : string ) : boolean {
174
+ if ( url . pathname === path ) {
175
+ pathMatch = true
176
+ return req . method === method
177
+ }
178
+ return false
179
+ }
180
+
181
+ // Reusable handler for GET and DELETE requests
182
+ async function handleSessionRequest ( ) {
183
+ const sessionId = req . headers [ 'mcp-session-id' ] as string | undefined
184
+ if ( ! sessionId || ! transports [ sessionId ] ) {
185
+ httpSetTextResponse ( res , 400 , 'Invalid or missing session ID' )
186
+ return
187
+ }
188
+
189
+ const transport = transports [ sessionId ]
190
+ await transport . handleRequest ( req , res )
191
+ }
192
+
193
+ // Handle different request methods and paths
194
+ if ( match ( 'POST' , '/mcp' ) ) {
195
+ // Check for existing session ID
196
+ const sessionId = req . headers [ 'mcp-session-id' ] as string | undefined
197
+ let transport : StreamableHTTPServerTransport
198
+
199
+ const body = await httpGetBody ( req )
200
+
201
+ if ( sessionId && transports [ sessionId ] ) {
202
+ // Reuse existing transport
203
+ transport = transports [ sessionId ]
204
+ } else if ( ! sessionId && isInitializeRequest ( body ) ) {
205
+ // New initialization request
206
+ transport = new StreamableHTTPServerTransport ( {
207
+ sessionIdGenerator : ( ) => randomUUID ( ) ,
208
+ onsessioninitialized : ( sessionId ) => {
209
+ // Store the transport by session ID
210
+ transports [ sessionId ] = transport
211
+ } ,
212
+ } )
213
+
214
+ // Clean up transport when closed
215
+ transport . onclose = ( ) => {
216
+ if ( transport . sessionId ) {
217
+ delete transports [ transport . sessionId ]
218
+ }
219
+ }
220
+
221
+ await mcpServer . connect ( transport )
222
+ } else {
223
+ httpSetJsonResponse ( res , 400 , 'Bad Request: No valid session ID provided' , - 32000 )
224
+ return
225
+ }
226
+
227
+ // Handle the request
228
+ await transport . handleRequest ( req , res , body )
229
+ } else if ( match ( 'GET' , '/mcp' ) ) {
230
+ // Handle server-to-client notifications via SSE
231
+ await handleSessionRequest ( )
232
+ } else if ( match ( 'DELETE' , '/mcp' ) ) {
233
+ // Handle requests for session termination
234
+ await handleSessionRequest ( )
235
+ } else if ( pathMatch ) {
236
+ httpSetTextResponse ( res , 405 , 'Method not allowed' )
237
+ } else {
238
+ httpSetTextResponse ( res , 404 , 'Page not found' )
239
+ }
240
+ } )
241
+
242
+ server . listen ( port , ( ) => {
243
+ console . log (
244
+ `Running MCP Run Python version ${ VERSION } with Streamable HTTP transport on port ${ port } ` ,
245
+ )
246
+ } )
247
+ }
248
+
106
249
/*
107
250
* Run the MCP server using the SSE transport, e.g. over HTTP.
108
251
*/
@@ -111,10 +254,7 @@ function runSse(port: number) {
111
254
const transports : { [ sessionId : string ] : SSEServerTransport } = { }
112
255
113
256
const server = http . createServer ( async ( req , res ) => {
114
- const url = new URL (
115
- req . url ?? '' ,
116
- `http://${ req . headers . host ?? 'unknown' } ` ,
117
- )
257
+ const url = httpGetUrl ( req )
118
258
let pathMatch = false
119
259
function match ( method : string , path : string ) : boolean {
120
260
if ( url . pathname === path ) {
@@ -123,12 +263,6 @@ function runSse(port: number) {
123
263
}
124
264
return false
125
265
}
126
- function textResponse ( status : number , text : string ) {
127
- res . setHeader ( 'Content-Type' , 'text/plain' )
128
- res . statusCode = status
129
- res . end ( `${ text } \n` )
130
- }
131
- // console.log(`${req.method} ${url}`)
132
266
133
267
if ( match ( 'GET' , '/sse' ) ) {
134
268
const transport = new SSEServerTransport ( '/messages' , res )
@@ -143,12 +277,12 @@ function runSse(port: number) {
143
277
if ( transport ) {
144
278
await transport . handlePostMessage ( req , res )
145
279
} else {
146
- textResponse ( 400 , `No transport found for sessionId '${ sessionId } '` )
280
+ httpSetTextResponse ( res , 400 , `No transport found for sessionId '${ sessionId } '` )
147
281
}
148
282
} else if ( pathMatch ) {
149
- textResponse ( 405 , 'Method not allowed' )
283
+ httpSetTextResponse ( res , 405 , 'Method not allowed' )
150
284
} else {
151
- textResponse ( 404 , 'Page not found' )
285
+ httpSetTextResponse ( res , 404 , 'Page not found' )
152
286
}
153
287
} )
154
288
0 commit comments