@@ -11,50 +11,84 @@ import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'
1111import { type LoggingLevel , SetLevelRequestSchema } from '@modelcontextprotocol/sdk/types.js'
1212import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
1313import { z } from 'zod'
14-
15- import { asXml , runCode } from './runCode.ts'
14+ import { asXml , getRootDir , runCode } from './runCode.ts'
1615import { Buffer } from 'node:buffer'
16+ import * as path from 'node:path'
1717
1818const VERSION = '0.0.13'
1919
2020export async function main ( ) {
21- const { args } = Deno
22- if ( args . length === 1 && args [ 0 ] === 'stdio' ) {
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 )
31- } else if ( args . length >= 1 && args [ 0 ] === 'sse' ) {
32- const flags = parseArgs ( Deno . args , {
33- string : [ 'port' ] ,
34- default : { port : '3001' } ,
35- } )
36- const port = parseInt ( flags . port )
37- runSse ( port )
38- } else if ( args . length === 1 && args [ 0 ] === 'warmup' ) {
39- await warmup ( )
21+ // Parse global flags once, then branch on subcommand
22+ const flags = parseArgs ( Deno . args , {
23+ string : [ 'port' ] ,
24+ default : { port : '3001' , mount : false } ,
25+ } )
26+ const mode = ( flags . _ [ 0 ] as string | undefined ) ?? ''
27+ const port = parseInt ( flags . port as string )
28+ const mount = flags . mount as string | boolean
29+
30+ if ( mode === 'stdio' ) {
31+ await runStdio ( mount )
32+ } else if ( mode === 'streamable_http' ) {
33+ runStreamableHttp ( port , mount )
34+ } else if ( mode === 'sse' ) {
35+ runSse ( port , mount )
36+ } else if ( mode === 'warmup' ) {
37+ await warmup ( mount )
4038 } else {
4139 console . error (
4240 `\
4341Invalid arguments.
4442
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]
43+ Usage: deno run -N -R=node_modules -W=node_modules --node-modules-dir=auto jsr:@pydantic/mcp-run-python [stdio|streamable_http|sse|warmup] [--port <port>] [--mount [dir]]
4644
4745options:
48- --port <port> Port to run the SSE server on (default: 3001)` ,
46+ --port <port> Port to run the SSE/HTTP server on (default: 3001)
47+ --mount [dir] Relative or absolute directory path or boolean. If omitted: false; if provided without value: true` ,
4948 )
5049 Deno . exit ( 1 )
5150 }
5251}
5352
53+ /*
54+ * Resolve a mountDir cli option to a specific directory
55+ */
56+ export function resolveMountDir ( mountDir : string ) : string {
57+ // Base dir created by emscriptem
58+ // See https://emscripten.org/docs/api_reference/Filesystem-API.html#file-system-api
59+ const baseDir = '/home/web_user'
60+
61+ if ( mountDir . trim ( ) === '' ) {
62+ return path . join ( baseDir , 'persistent' )
63+ }
64+
65+ if ( path . isAbsolute ( mountDir ) ) {
66+ return mountDir
67+ }
68+
69+ // relative path
70+ return path . join ( baseDir , mountDir )
71+ }
72+
73+ /*
74+ * Ensure and cleanup the root directory used by the MCP server
75+ */
76+ function ensureRootDir ( ) {
77+ Deno . mkdirSync ( getRootDir ( ) , { recursive : true } )
78+ }
79+
80+ function cleanupRootDir ( ) {
81+ try {
82+ Deno . removeSync ( getRootDir ( ) , { recursive : true } )
83+ } catch ( err ) {
84+ if ( ! ( err instanceof Deno . errors . NotFound ) ) throw err
85+ }
86+ }
87+
5488/*
5589 * Create an MCP server with the `run_python_code` tool registered.
5690 */
57- function createServer ( ) : McpServer {
91+ function createServer ( mount : string | boolean ) : McpServer {
5892 const server = new McpServer (
5993 {
6094 name : 'MCP Run Python' ,
@@ -68,12 +102,25 @@ function createServer(): McpServer {
68102 } ,
69103 )
70104
105+ let mountDirDescription : string
106+ let mountDir : string | null
107+ if ( mount !== false ) {
108+ // Create temporary directory
109+ ensureRootDir ( )
110+ // Resolve mounted directory
111+ mountDir = resolveMountDir ( typeof mount === 'string' ? mount : '' )
112+ mountDirDescription = `To store files permanently use the directory at: ${ mountDir } \n`
113+ } else {
114+ mountDir = null
115+ mountDirDescription = ''
116+ }
117+
71118 const toolDescription = `Tool to execute Python code and return stdout, stderr, and return value.
72119
73120The code may be async, and the value on the last line will be returned as the return value.
74121
75122The code will be executed with Python 3.12.
76-
123+ ${ mountDirDescription }
77124Dependencies may be defined via PEP 723 script metadata, e.g. to install "pydantic", the script should start
78125with a comment of the form:
79126
@@ -96,15 +143,21 @@ print('python code here')
96143 { python_code : z . string ( ) . describe ( 'Python code to run' ) } ,
97144 async ( { python_code } : { python_code : string } ) => {
98145 const logPromises : Promise < void > [ ] = [ ]
99- const result = await runCode ( [ {
100- name : 'main.py' ,
101- content : python_code ,
102- active : true ,
103- } ] , ( level , data ) => {
104- if ( LogLevels . indexOf ( level ) >= LogLevels . indexOf ( setLogLevel ) ) {
105- logPromises . push ( server . server . sendLoggingMessage ( { level, data } ) )
106- }
107- } )
146+ const result = await runCode (
147+ [
148+ {
149+ name : 'main.py' ,
150+ content : python_code ,
151+ active : true ,
152+ } ,
153+ ] ,
154+ ( level , data ) => {
155+ if ( LogLevels . indexOf ( level ) >= LogLevels . indexOf ( setLogLevel ) ) {
156+ logPromises . push ( server . server . sendLoggingMessage ( { level, data } ) )
157+ }
158+ } ,
159+ mountDir ,
160+ )
108161 await Promise . all ( logPromises )
109162 return {
110163 content : [ { type : 'text' , text : asXml ( result ) } ] ,
@@ -118,10 +171,7 @@ print('python code here')
118171 * Define some QOL functions for both the SSE and Streamable HTTP server implementation
119172 */
120173function httpGetUrl ( req : http . IncomingMessage ) : URL {
121- return new URL (
122- req . url ?? '' ,
123- `http://${ req . headers . host ?? 'unknown' } ` ,
124- )
174+ return new URL ( req . url ?? '' , `http://${ req . headers . host ?? 'unknown' } ` )
125175}
126176
127177function httpGetBody ( req : http . IncomingMessage ) : Promise < JSON > {
@@ -130,41 +180,54 @@ function httpGetBody(req: http.IncomingMessage): Promise<JSON> {
130180 // deno-lint-ignore no-explicit-any
131181 const bodyParts : any [ ] = [ ]
132182 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- } )
183+ req
184+ . on ( 'data' , ( chunk ) => {
185+ bodyParts . push ( chunk )
186+ } )
187+ . on ( 'end' , ( ) => {
188+ body = Buffer . concat ( bodyParts ) . toString ( )
189+ resolve ( JSON . parse ( body ) )
190+ } )
139191 } )
140192}
141193
142- function httpSetTextResponse ( res : http . ServerResponse , status : number , text : string ) {
194+ function httpSetTextResponse (
195+ res : http . ServerResponse ,
196+ status : number ,
197+ text : string ,
198+ ) {
143199 res . setHeader ( 'Content-Type' , 'text/plain' )
144200 res . statusCode = status
145201 res . end ( `${ text } \n` )
146202}
147203
148- function httpSetJsonResponse ( res : http . ServerResponse , status : number , text : string , code : number ) {
204+ function httpSetJsonResponse (
205+ res : http . ServerResponse ,
206+ status : number ,
207+ text : string ,
208+ code : number ,
209+ ) {
149210 res . setHeader ( 'Content-Type' , 'application/json' )
150211 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- } ) )
212+ res . write (
213+ JSON . stringify ( {
214+ jsonrpc : '2.0' ,
215+ error : {
216+ code : code ,
217+ message : text ,
218+ } ,
219+ id : null ,
220+ } ) ,
221+ )
159222 res . end ( )
160223}
161224
162225/*
163226 * Run the MCP server using the Streamable HTTP transport
164227 */
165- function runStreamableHttp ( port : number ) {
228+ function runStreamableHttp ( port : number , mount : string | boolean ) {
166229 // https://github.com/modelcontextprotocol/typescript-sdk?tab=readme-ov-file#with-session-management
167- const mcpServer = createServer ( )
230+ const mcpServer = createServer ( mount )
168231 const transports : { [ sessionId : string ] : StreamableHTTPServerTransport } = { }
169232
170233 const server = http . createServer ( async ( req , res ) => {
@@ -220,7 +283,12 @@ function runStreamableHttp(port: number) {
220283
221284 await mcpServer . connect ( transport )
222285 } else {
223- httpSetJsonResponse ( res , 400 , 'Bad Request: No valid session ID provided' , - 32000 )
286+ httpSetJsonResponse (
287+ res ,
288+ 400 ,
289+ 'Bad Request: No valid session ID provided' ,
290+ - 32000 ,
291+ )
224292 return
225293 }
226294
@@ -239,6 +307,11 @@ function runStreamableHttp(port: number) {
239307 }
240308 } )
241309
310+ // Cleanup root dir on server close
311+ server . on ( 'close' , ( ) => {
312+ cleanupRootDir ( )
313+ } )
314+
242315 server . listen ( port , ( ) => {
243316 console . log (
244317 `Running MCP Run Python version ${ VERSION } with Streamable HTTP transport on port ${ port } ` ,
@@ -249,8 +322,8 @@ function runStreamableHttp(port: number) {
249322/*
250323 * Run the MCP server using the SSE transport, e.g. over HTTP.
251324 */
252- function runSse ( port : number ) {
253- const mcpServer = createServer ( )
325+ function runSse ( port : number , mount : string | boolean ) {
326+ const mcpServer = createServer ( mount )
254327 const transports : { [ sessionId : string ] : SSEServerTransport } = { }
255328
256329 const server = http . createServer ( async ( req , res ) => {
@@ -277,7 +350,11 @@ function runSse(port: number) {
277350 if ( transport ) {
278351 await transport . handlePostMessage ( req , res )
279352 } else {
280- httpSetTextResponse ( res , 400 , `No transport found for sessionId '${ sessionId } '` )
353+ httpSetTextResponse (
354+ res ,
355+ 400 ,
356+ `No transport found for sessionId '${ sessionId } '` ,
357+ )
281358 }
282359 } else if ( pathMatch ) {
283360 httpSetTextResponse ( res , 405 , 'Method not allowed' )
@@ -286,6 +363,11 @@ function runSse(port: number) {
286363 }
287364 } )
288365
366+ // Cleanup root dir on server close
367+ server . on ( 'close' , ( ) => {
368+ cleanupRootDir ( )
369+ } )
370+
289371 server . listen ( port , ( ) => {
290372 console . log (
291373 `Running MCP Run Python version ${ VERSION } with SSE transport on port ${ port } ` ,
@@ -296,32 +378,45 @@ function runSse(port: number) {
296378/*
297379 * Run the MCP server using the Stdio transport.
298380 */
299- async function runStdio ( ) {
300- const mcpServer = createServer ( )
381+ async function runStdio ( mount : string | boolean ) {
382+ const mcpServer = createServer ( mount )
301383 const transport = new StdioServerTransport ( )
384+
385+ // Cleanup root dir on transport close
386+ transport . onclose = ( ) => {
387+ cleanupRootDir ( )
388+ }
389+
302390 await mcpServer . connect ( transport )
303391}
304392
305393/*
306394 * Run pyodide to download packages which can otherwise interrupt the server
307395 */
308- async function warmup ( ) {
396+ async function warmup ( mount ?: string | boolean ) {
309397 console . error (
310- `Running warmup script for MCP Run Python version ${ VERSION } ...` ,
398+ `Running warmup script for MCP Run Python version ${ VERSION } ...` +
399+ ( mount ? ` (mount: ${ typeof mount === 'string' ? mount : 'enabled' } )` : '' ) ,
311400 )
312401 const code = `
313402import numpy
314403a = numpy.array([1, 2, 3])
315404print('numpy array:', a)
316405a
317406`
318- const result = await runCode ( [ {
319- name : 'warmup.py' ,
320- content : code ,
321- active : true ,
322- } ] , ( level , data ) =>
323- // use warn to avoid recursion since console.log is patched in runCode
324- console . error ( `${ level } : ${ data } ` ) )
407+ const result = await runCode (
408+ [
409+ {
410+ name : 'warmup.py' ,
411+ content : code ,
412+ active : true ,
413+ } ,
414+ ] ,
415+ ( level , data ) =>
416+ // use warn to avoid recursion since console.log is patched in runCode
417+ console . error ( `${ level } : ${ data } ` ) ,
418+ null ,
419+ )
325420 console . log ( 'Tool return value:' )
326421 console . log ( asXml ( result ) )
327422 console . log ( '\nwarmup successful 🎉' )
0 commit comments