22
33import './polyfill.ts'
44import http from 'node:http'
5+ import { randomUUID } from 'node:crypto'
56import { parseArgs } from '@std/cli/parse-args'
67import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'
78import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
9+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
10+ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
811import { type LoggingLevel , SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js'
912import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
1013import { z } from 'zod'
1114
1215import { asXml , runCode } from './runCode.ts'
16+ import { Buffer } from 'node:buffer'
1317
1418const VERSION = '0.0.13'
1519
1620export async function main ( ) {
1721 const { args } = Deno
1822 if ( args . length === 1 && args [ 0 ] === 'stdio' ) {
1923 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 )
2031 } else if ( args . length >= 1 && args [ 0 ] === 'sse' ) {
2132 const flags = parseArgs ( Deno . args , {
2233 string : [ 'port' ] ,
@@ -31,7 +42,7 @@ export async function main() {
3142 `\
3243Invalid arguments.
3344
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]
3546
3647options:
3748 --port <port> Port to run the SSE server on (default: 3001)` ,
@@ -103,6 +114,138 @@ print('python code here')
103114 return server
104115}
105116
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+
106249/*
107250 * Run the MCP server using the SSE transport, e.g. over HTTP.
108251 */
@@ -111,10 +254,7 @@ function runSse(port: number) {
111254 const transports : { [ sessionId : string ] : SSEServerTransport } = { }
112255
113256 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 )
118258 let pathMatch = false
119259 function match ( method : string , path : string ) : boolean {
120260 if ( url . pathname === path ) {
@@ -123,12 +263,6 @@ function runSse(port: number) {
123263 }
124264 return false
125265 }
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}`)
132266
133267 if ( match ( 'GET' , '/sse' ) ) {
134268 const transport = new SSEServerTransport ( '/messages' , res )
@@ -143,12 +277,12 @@ function runSse(port: number) {
143277 if ( transport ) {
144278 await transport . handlePostMessage ( req , res )
145279 } else {
146- textResponse ( 400 , `No transport found for sessionId '${ sessionId } '` )
280+ httpSetTextResponse ( res , 400 , `No transport found for sessionId '${ sessionId } '` )
147281 }
148282 } else if ( pathMatch ) {
149- textResponse ( 405 , 'Method not allowed' )
283+ httpSetTextResponse ( res , 405 , 'Method not allowed' )
150284 } else {
151- textResponse ( 404 , 'Page not found' )
285+ httpSetTextResponse ( res , 404 , 'Page not found' )
152286 }
153287 } )
154288
0 commit comments