2025-03-16 17:17:01 +08:00
/**
* @ file python_execute . cpp
*
2025-03-19 18:44:54 +08:00
* This file implements the Python execution tool , using Python . h to directly call the Python interpreter .
2025-03-16 17:17:01 +08:00
*/
2025-03-19 18:44:54 +08:00
# include "mcp_server.h"
# include "mcp_tool.h"
# include "mcp_resource.h"
2025-03-16 17:17:01 +08:00
# include <iostream>
# include <string>
# include <memory>
# include <stdexcept>
2025-03-17 14:07:41 +08:00
# include <mutex>
2025-04-12 21:18:35 +08:00
# include <shared_mutex>
2025-04-08 23:26:53 +08:00
# include <unordered_map>
2025-04-12 21:18:35 +08:00
# include <chrono>
# include <future>
2025-03-16 17:17:01 +08:00
2025-03-17 16:35:11 +08:00
// Check if Python is found
2025-03-16 17:17:01 +08:00
# ifdef PYTHON_FOUND
# include <Python.h>
# endif
/**
* @ class python_interpreter
2025-04-08 23:26:53 +08:00
* @ brief Python interpreter class for executing Python code with session support
2025-03-16 17:17:01 +08:00
*/
class python_interpreter {
2025-03-17 14:07:41 +08:00
private :
2025-04-12 21:18:35 +08:00
mutable std : : shared_mutex py_mutex ;
2025-03-17 14:07:41 +08:00
bool is_initialized ;
2025-04-08 23:26:53 +08:00
// Map to store Python thread states for each session
mutable std : : unordered_map < std : : string , PyThreadState * > session_states ;
2025-04-12 21:18:35 +08:00
// Default timeout (milliseconds)
static constexpr unsigned int DEFAULT_TIMEOUT_MS = 30000 ; // 30 seconds
2025-03-17 14:07:41 +08:00
2025-03-16 17:17:01 +08:00
public :
/**
2025-03-17 16:35:11 +08:00
* @ brief Constructor , initializes Python interpreter
2025-03-16 17:17:01 +08:00
*/
2025-03-17 14:07:41 +08:00
python_interpreter ( ) : is_initialized ( false ) {
2025-03-16 17:17:01 +08:00
# ifdef PYTHON_FOUND
2025-03-17 14:07:41 +08:00
try {
2025-04-12 21:18:35 +08:00
std : : unique_lock < std : : shared_mutex > lock ( py_mutex ) ;
2025-03-17 14:07:41 +08:00
Py_Initialize ( ) ;
if ( Py_IsInitialized ( ) ) {
is_initialized = true ;
2025-04-08 23:26:53 +08:00
// Create main thread state
PyThreadState * main_thread_state = PyThreadState_Get ( ) ;
PyThreadState_Swap ( NULL ) ;
2025-03-17 14:07:41 +08:00
} else {
2025-03-17 16:35:11 +08:00
std : : cerr < < " Failed to initialize Python interpreter " < < std : : endl ;
2025-03-17 14:07:41 +08:00
}
} catch ( const std : : exception & e ) {
2025-03-17 16:35:11 +08:00
std : : cerr < < " Python interpreter initialization exception: " < < e . what ( ) < < std : : endl ;
2025-03-17 14:07:41 +08:00
}
2025-03-16 17:17:01 +08:00
# endif
}
/**
2025-03-17 16:35:11 +08:00
* @ brief Destructor , releases Python interpreter
2025-03-16 17:17:01 +08:00
*/
~ python_interpreter ( ) {
# ifdef PYTHON_FOUND
2025-03-17 14:07:41 +08:00
if ( is_initialized ) {
2025-04-12 21:18:35 +08:00
std : : unique_lock < std : : shared_mutex > lock ( py_mutex ) ;
2025-04-08 23:26:53 +08:00
// Clean up all session states
for ( auto & pair : session_states ) {
PyThreadState_Swap ( pair . second ) ;
PyThreadState_Clear ( pair . second ) ;
PyThreadState_Delete ( pair . second ) ;
}
session_states . clear ( ) ;
2025-03-17 14:07:41 +08:00
Py_Finalize ( ) ;
is_initialized = false ;
}
2025-03-16 17:17:01 +08:00
# endif
}
2025-04-08 23:26:53 +08:00
/**
2025-04-12 21:18:35 +08:00
* @ brief Check if a session exists
2025-04-08 23:26:53 +08:00
* @ param session_id The session identifier
2025-04-12 21:18:35 +08:00
* @ return bool indicating if session exists
2025-04-08 23:26:53 +08:00
*/
# ifdef PYTHON_FOUND
2025-04-12 21:18:35 +08:00
bool has_session ( const std : : string & session_id ) const {
std : : shared_lock < std : : shared_mutex > lock ( py_mutex ) ;
return session_states . find ( session_id ) ! = session_states . end ( ) ;
}
/**
* @ brief Get an existing thread state for a session
* @ param session_id The session identifier
* @ return PyThreadState for the session or nullptr if not found
*/
PyThreadState * get_existing_session_state ( const std : : string & session_id ) const {
std : : shared_lock < std : : shared_mutex > lock ( py_mutex ) ;
2025-04-08 23:26:53 +08:00
auto it = session_states . find ( session_id ) ;
if ( it ! = session_states . end ( ) ) {
return it - > second ;
}
2025-04-12 21:18:35 +08:00
return nullptr ;
}
/**
* @ brief Create a new thread state for a session
* @ param session_id The session identifier
* @ return PyThreadState for the new session
*/
PyThreadState * create_session_state ( const std : : string & session_id ) const {
std : : unique_lock < std : : shared_mutex > lock ( py_mutex ) ;
if ( session_states . count ( session_id ) ) return session_states [ session_id ] ;
2025-04-08 23:26:53 +08:00
PyThreadState * new_state = Py_NewInterpreter ( ) ;
if ( ! new_state ) {
2025-04-12 21:18:35 +08:00
throw std : : runtime_error ( " Failed to create new Python interpreter for session " + session_id ) ;
2025-04-08 23:26:53 +08:00
}
2025-04-12 21:18:35 +08:00
PyEval_SaveThread ( ) ; // Release GIL after creation
2025-04-08 23:26:53 +08:00
session_states [ session_id ] = new_state ;
return new_state ;
}
2025-04-12 21:18:35 +08:00
/**
* @ brief Get or create a thread state for a session
* @ param session_id The session identifier
* @ return PyThreadState for the session
*/
PyThreadState * get_session_state ( const std : : string & session_id ) const {
// Try to get existing session state with read lock
PyThreadState * state = get_existing_session_state ( session_id ) ;
if ( state ) {
return state ;
}
// If it doesn't exist, create a new session (will use write lock)
return create_session_state ( session_id ) ;
}
2025-04-08 23:26:53 +08:00
# endif
2025-03-16 17:17:01 +08:00
/**
2025-04-12 21:18:35 +08:00
* @ brief Execute Python code in the context of a specific session with timeout
2025-03-17 16:35:11 +08:00
* @ param input JSON object containing Python code
2025-04-08 23:26:53 +08:00
* @ param session_id The session identifier
2025-03-17 16:35:11 +08:00
* @ return JSON object with execution results
2025-03-16 17:17:01 +08:00
*/
2025-04-12 21:18:35 +08:00
mcp : : json forward ( const mcp : : json & input , const std : : string & session_id ) {
2025-03-16 17:17:01 +08:00
# ifdef PYTHON_FOUND
2025-04-12 21:18:35 +08:00
if ( ! is_initialized ) {
return mcp : : json { { " error " , " Python interpreter not properly initialized " } } ;
}
unsigned int timeout_ms = DEFAULT_TIMEOUT_MS ;
if ( input . contains ( " timeout_ms " ) & & input [ " timeout_ms " ] . is_number ( ) ) {
timeout_ms = input [ " timeout_ms " ] . get < unsigned int > ( ) ;
}
std : : packaged_task < mcp : : json ( ) > task ( [ this , & input , session_id ] ( ) {
mcp : : json thread_result ;
2025-03-17 14:07:41 +08:00
2025-04-08 23:26:53 +08:00
PyThreadState * tstate = nullptr ;
try {
tstate = get_session_state ( session_id ) ;
} catch ( const std : : exception & e ) {
return mcp : : json { { " error " , e . what ( ) } } ;
}
2025-03-17 14:07:41 +08:00
2025-04-12 21:18:35 +08:00
PyEval_RestoreThread ( tstate ) ;
2025-03-17 14:07:41 +08:00
try {
if ( input . contains ( " code " ) & & input [ " code " ] . is_string ( ) ) {
std : : string code = input [ " code " ] . get < std : : string > ( ) ;
PyObject * main_module = PyImport_AddModule ( " __main__ " ) ;
PyObject * main_dict = PyModule_GetDict ( main_module ) ;
2025-03-16 17:17:01 +08:00
2025-03-17 14:07:41 +08:00
PyObject * io_module = PyImport_ImportModule ( " io " ) ;
PyObject * string_io = PyObject_GetAttrString ( io_module , " StringIO " ) ;
PyObject * sys_stdout = PyObject_CallObject ( string_io , nullptr ) ;
PyObject * sys_stderr = PyObject_CallObject ( string_io , nullptr ) ;
2025-04-12 21:18:35 +08:00
PySys_SetObject ( " stdout " , sys_stdout ) ;
PySys_SetObject ( " stderr " , sys_stderr ) ;
2025-03-17 14:07:41 +08:00
PyObject * result = PyRun_String ( code . c_str ( ) , Py_file_input , main_dict , main_dict ) ;
2025-04-12 21:18:35 +08:00
if ( ! result ) PyErr_Print ( ) ;
2025-03-17 14:07:41 +08:00
Py_XDECREF ( result ) ;
PyObject * out_value = PyObject_CallMethod ( sys_stdout , " getvalue " , nullptr ) ;
PyObject * err_value = PyObject_CallMethod ( sys_stderr , " getvalue " , nullptr ) ;
2025-04-12 21:18:35 +08:00
std : : string output = out_value & & PyUnicode_Check ( out_value ) ? PyUnicode_AsUTF8 ( out_value ) : " " ;
std : : string error = err_value & & PyUnicode_Check ( err_value ) ? PyUnicode_AsUTF8 ( err_value ) : " " ;
2025-03-17 14:07:41 +08:00
Py_XDECREF ( out_value ) ;
Py_XDECREF ( err_value ) ;
Py_DECREF ( sys_stdout ) ;
Py_DECREF ( sys_stderr ) ;
Py_DECREF ( string_io ) ;
Py_DECREF ( io_module ) ;
2025-04-12 21:18:35 +08:00
if ( ! output . empty ( ) ) thread_result [ " output " ] = output ;
if ( ! error . empty ( ) ) thread_result [ " error " ] = error ;
2025-03-17 14:07:41 +08:00
2025-04-12 21:18:35 +08:00
if ( thread_result . empty ( ) ) {
thread_result [ " warning " ] = " No output generated. Consider using print statements. " ;
2025-03-17 14:07:41 +08:00
}
} else {
2025-04-12 21:18:35 +08:00
thread_result [ " error " ] = " Invalid parameters or code not provided " ;
2025-03-17 14:07:41 +08:00
}
} catch ( const std : : exception & e ) {
2025-04-12 21:18:35 +08:00
thread_result [ " error " ] = std : : string ( " Python execution exception: " ) + e . what ( ) ;
2025-03-16 17:17:01 +08:00
}
2025-03-17 14:07:41 +08:00
2025-04-12 21:18:35 +08:00
tstate = PyEval_SaveThread ( ) ; // Save thread state and release GIL
return thread_result ;
} ) ;
auto future = task . get_future ( ) ;
std : : thread execution_thread ( std : : move ( task ) ) ;
if ( future . wait_for ( std : : chrono : : milliseconds ( timeout_ms ) ) = = std : : future_status : : timeout ) {
if ( execution_thread . joinable ( ) ) execution_thread . detach ( ) ; // detach to prevent zombie threads
return mcp : : json { { " error " , " Python execution timed out after " + std : : to_string ( timeout_ms ) + " ms " } } ;
}
if ( execution_thread . joinable ( ) ) execution_thread . join ( ) ; // Join thread if it's still running
return future . get ( ) ;
2025-03-16 17:17:01 +08:00
# else
2025-04-12 21:18:35 +08:00
return mcp : : json { { " error " , " Python interpreter not available " } } ;
2025-04-08 23:26:53 +08:00
# endif
}
2025-04-12 21:18:35 +08:00
2025-04-08 23:26:53 +08:00
/**
* @ brief Clean up a session and remove its thread state
* @ param session_id The session identifier to clean up
*/
void cleanup_session ( const std : : string & session_id ) {
# ifdef PYTHON_FOUND
2025-04-12 21:18:35 +08:00
std : : unique_lock < std : : shared_mutex > lock ( py_mutex ) ;
2025-04-08 23:26:53 +08:00
auto it = session_states . find ( session_id ) ;
if ( it ! = session_states . end ( ) ) {
2025-04-12 21:18:35 +08:00
PyThreadState * state = it - > second ;
PyEval_RestoreThread ( state ) ;
Py_EndInterpreter ( state ) ; // Properly end the child interpreter
2025-04-08 23:26:53 +08:00
session_states . erase ( it ) ;
2025-04-12 21:18:35 +08:00
PyEval_SaveThread ( ) ; // Restore the main thread's GIL release state
2025-04-08 23:26:53 +08:00
}
2025-03-16 17:17:01 +08:00
# endif
}
2025-04-12 21:18:35 +08:00
2025-03-16 17:17:01 +08:00
} ;
2025-03-17 16:35:11 +08:00
// Global Python interpreter instance
2025-03-16 17:17:01 +08:00
static python_interpreter interpreter ;
2025-03-17 16:35:11 +08:00
// Python execution tool handler function
2025-04-08 23:26:53 +08:00
mcp : : json python_execute_handler ( const mcp : : json & args , const std : : string & session_id ) {
2025-03-16 17:17:01 +08:00
if ( ! args . contains ( " code " ) ) {
2025-03-17 16:35:11 +08:00
throw mcp : : mcp_exception ( mcp : : error_code : : invalid_params , " Missing 'code' parameter " ) ;
2025-03-16 17:17:01 +08:00
}
try {
2025-04-12 21:18:35 +08:00
// Use Python interpreter to execute code with session context and timeout
2025-04-08 23:26:53 +08:00
mcp : : json result = interpreter . forward ( args , session_id ) ;
2025-03-16 17:17:01 +08:00
return { {
{ " type " , " text " } ,
2025-03-17 14:07:41 +08:00
{ " text " , result . dump ( 2 ) }
2025-03-16 17:17:01 +08:00
} } ;
} catch ( const std : : exception & e ) {
throw mcp : : mcp_exception ( mcp : : error_code : : internal_error ,
2025-03-17 16:35:11 +08:00
" Failed to execute Python code: " + std : : string ( e . what ( ) ) ) ;
2025-03-16 17:17:01 +08:00
}
}
2025-03-17 14:07:41 +08:00
// Register the PythonExecute tool
2025-03-16 17:17:01 +08:00
void register_python_execute_tool ( mcp : : server & server ) {
2025-03-17 14:07:41 +08:00
mcp : : tool python_tool = mcp : : tool_builder ( " python_execute " )
2025-04-12 21:18:35 +08:00
. with_description ( " Executes Python code string. Note: Only print outputs are visible, function return values are not captured. Use print statements to see results. " )
. with_string_param ( " code " , " The Python code to execute. Note: Use absolute file paths if code will read/write files. " , true )
. with_number_param ( " timeout_ms " , " Timeout in milliseconds for code execution (default: 30000) " , false )
2025-03-16 17:17:01 +08:00
. build ( ) ;
server . register_tool ( python_tool , python_execute_handler ) ;
2025-04-08 23:26:53 +08:00
// Register session cleanup handler
server . register_session_cleanup ( " python_execute " , [ ] ( const std : : string & session_id ) {
interpreter . cleanup_session ( session_id ) ;
} ) ;
2025-03-16 17:17:01 +08:00
}