11// deno-lint-ignore-file no-explicit-any
2- import { loadPyodide } from 'pyodide'
2+ import { loadPyodide , type PyodideInterface } from 'pyodide'
33import { preparePythonCode } from './prepareEnvCode.ts'
44import type { LoggingLevel } from '@modelcontextprotocol/sdk/types.js'
55
@@ -8,90 +8,141 @@ export interface CodeFile {
88 content : string
99}
1010
11- export async function runCode (
12- dependencies : string [ ] ,
13- file : CodeFile | undefined ,
14- log : ( level : LoggingLevel , data : string ) => void ,
15- ) : Promise < RunSuccess | RunError > {
16- // remove once we can upgrade to pyodide 0.27.7 and console.log is no longer used.
17- const realConsoleLog = console . log
18- console . log = ( ...args : any [ ] ) => log ( 'debug' , args . join ( ' ' ) )
19-
20- const output : string [ ] = [ ]
21- const pyodide = await loadPyodide ( {
22- stdout : ( msg ) => {
23- log ( 'info' , msg )
24- output . push ( msg )
25- } ,
26- stderr : ( msg ) => {
27- log ( 'warning' , msg )
28- output . push ( msg )
29- } ,
30- } )
31-
32- // see https://github.com/pyodide/pyodide/discussions/5512
33- const origLoadPackage = pyodide . loadPackage
34- pyodide . loadPackage = ( pkgs , options ) =>
35- origLoadPackage ( pkgs , {
36- // stop pyodide printing to stdout/stderr
37- messageCallback : ( msg : string ) => log ( 'debug' , `loadPackage: ${ msg } ` ) ,
38- errorCallback : ( msg : string ) => {
39- log ( 'error' , `loadPackage: ${ msg } ` )
40- output . push ( `install error: ${ msg } ` )
41- } ,
42- ...options ,
43- } )
44-
45- await pyodide . loadPackage ( [ 'micropip' , 'pydantic' ] )
46- const sys = pyodide . pyimport ( 'sys' )
47-
48- const dirPath = '/tmp/mcp_run_python'
49- sys . path . append ( dirPath )
50- const pathlib = pyodide . pyimport ( 'pathlib' )
51- pathlib . Path ( dirPath ) . mkdir ( )
52- const moduleName = '_prepare_env'
53-
54- pathlib . Path ( `${ dirPath } /${ moduleName } .py` ) . write_text ( preparePythonCode )
55-
56- const preparePyEnv : PreparePyEnv = pyodide . pyimport ( moduleName )
11+ interface PrepResult {
12+ pyodide : PyodideInterface
13+ preparePyEnv : PreparePyEnv
14+ sys : any
15+ prepareStatus : PrepareSuccess | PrepareError
16+ }
5717
58- const prepareStatus = await preparePyEnv . prepare_env ( pyodide . toPy ( dependencies ) )
59- let runResult : RunSuccess | RunError
60- if ( prepareStatus . kind == 'error' ) {
61- runResult = {
62- status : 'install-error' ,
63- output,
64- error : prepareStatus . message ,
18+ export class RunCode {
19+ private output : string [ ] = [ ]
20+ private pyodide ?: PyodideInterface
21+ private preparePyEnv ?: PreparePyEnv
22+ private prepPromise ?: Promise < PrepResult >
23+
24+ async run (
25+ dependencies : string [ ] ,
26+ file : CodeFile | undefined ,
27+ log : ( level : LoggingLevel , data : string ) => void ,
28+ ) : Promise < RunSuccess | RunError > {
29+ // remove once we can upgrade to pyodide 0.27.7 and console.log is no longer used.
30+ const realConsoleLog = console . log
31+ console . log = ( ...args : any [ ] ) => log ( 'debug' , args . join ( ' ' ) )
32+
33+ let pyodide : PyodideInterface
34+ let sys : any
35+ let prepareStatus : PrepareSuccess | PrepareError | undefined
36+ let preparePyEnv : PreparePyEnv
37+ if ( this . pyodide && this . preparePyEnv ) {
38+ pyodide = this . pyodide
39+ preparePyEnv = this . preparePyEnv
40+ sys = pyodide . pyimport ( 'sys' )
41+ } else {
42+ if ( ! this . prepPromise ) {
43+ this . prepPromise = this . prepEnv ( dependencies , log )
44+ }
45+ // TODO is this safe if the promise has already been accessed? it seems to work fine
46+ const prep = await this . prepPromise
47+ pyodide = prep . pyodide
48+ preparePyEnv = prep . preparePyEnv
49+ sys = prep . sys
50+ prepareStatus = prep . prepareStatus
6551 }
66- } else if ( file ) {
67- try {
68- const rawValue = await pyodide . runPythonAsync ( file . content , {
69- globals : pyodide . toPy ( { __name__ : '__main__' } ) ,
70- filename : file . name ,
71- } )
52+
53+ let runResult : RunSuccess | RunError
54+ if ( prepareStatus && prepareStatus . kind == 'error' ) {
7255 runResult = {
73- status : 'success ' ,
74- output,
75- returnValueJson : preparePyEnv . dump_json ( rawValue ) ,
56+ status : 'install-error ' ,
57+ output : this . takeOutput ( sys ) ,
58+ error : prepareStatus . message ,
7659 }
77- } catch ( err ) {
60+ } else if ( file ) {
61+ try {
62+ const rawValue = await pyodide . runPythonAsync ( file . content , {
63+ globals : pyodide . toPy ( { __name__ : '__main__' } ) ,
64+ filename : file . name ,
65+ } )
66+ runResult = {
67+ status : 'success' ,
68+ output : this . takeOutput ( sys ) ,
69+ returnValueJson : preparePyEnv . dump_json ( rawValue ) ,
70+ }
71+ } catch ( err ) {
72+ runResult = {
73+ status : 'run-error' ,
74+ output : this . takeOutput ( sys ) ,
75+ error : formatError ( err ) ,
76+ }
77+ }
78+ } else {
7879 runResult = {
79- status : 'run-error ' ,
80- output,
81- error : formatError ( err ) ,
80+ status : 'success ' ,
81+ output : this . takeOutput ( sys ) ,
82+ returnValueJson : null ,
8283 }
8384 }
84- } else {
85- runResult = {
86- status : 'success' ,
87- output,
88- returnValueJson : null ,
85+ console . log = realConsoleLog
86+ return runResult
87+ }
88+
89+ async prepEnv (
90+ dependencies : string [ ] ,
91+ log : ( level : LoggingLevel , data : string ) => void ,
92+ ) : Promise < PrepResult > {
93+ const pyodide = await loadPyodide ( {
94+ stdout : ( msg ) => {
95+ log ( 'info' , msg )
96+ this . output . push ( msg )
97+ } ,
98+ stderr : ( msg ) => {
99+ log ( 'warning' , msg )
100+ this . output . push ( msg )
101+ } ,
102+ } )
103+
104+ // see https://github.com/pyodide/pyodide/discussions/5512
105+ const origLoadPackage = pyodide . loadPackage
106+ pyodide . loadPackage = ( pkgs , options ) =>
107+ origLoadPackage ( pkgs , {
108+ // stop pyodide printing to stdout/stderr
109+ messageCallback : ( msg : string ) => log ( 'debug' , `loadPackage: ${ msg } ` ) ,
110+ errorCallback : ( msg : string ) => {
111+ log ( 'error' , `loadPackage: ${ msg } ` )
112+ this . output . push ( `install error: ${ msg } ` )
113+ } ,
114+ ...options ,
115+ } )
116+
117+ await pyodide . loadPackage ( [ 'micropip' , 'pydantic' ] )
118+ const sys = pyodide . pyimport ( 'sys' )
119+
120+ const dirPath = '/tmp/mcp_run_python'
121+ sys . path . append ( dirPath )
122+ const pathlib = pyodide . pyimport ( 'pathlib' )
123+ pathlib . Path ( dirPath ) . mkdir ( )
124+ const moduleName = '_prepare_env'
125+
126+ pathlib . Path ( `${ dirPath } /${ moduleName } .py` ) . write_text ( preparePythonCode )
127+
128+ const preparePyEnv : PreparePyEnv = pyodide . pyimport ( moduleName )
129+
130+ const prepareStatus = await preparePyEnv . prepare_env ( pyodide . toPy ( dependencies ) )
131+ return {
132+ pyodide,
133+ preparePyEnv,
134+ sys,
135+ prepareStatus,
89136 }
90137 }
91- sys . stdout . flush ( )
92- sys . stderr . flush ( )
93- console . log = realConsoleLog
94- return runResult
138+
139+ private takeOutput ( sys : any ) : string [ ] {
140+ sys . stdout . flush ( )
141+ sys . stderr . flush ( )
142+ const output = this . output
143+ this . output = [ ]
144+ return output
145+ }
95146}
96147
97148interface RunSuccess {
0 commit comments