Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions build/build.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
#!/usr/bin/env python3
"""Build script to inline ./prepare_env.py into mcp_run_python/deno/prepareEnvCode.ts"""
"""Build script to inline ./prepare_env.py and tool_injection.py into mcp_run_python/deno/prepareEnvCode.ts"""

from pathlib import Path

this_dir = Path(__file__).parent
root_dir = this_dir.parent

# Define source and destination paths
src = this_dir / 'prepare_env.py'
dst = root_dir / 'mcp_run_python' / 'deno' / 'src' / 'prepareEnvCode.ts'
src_dir = root_dir / 'mcp_run_python' / 'deno' / 'src'
prepare_env_src = this_dir / 'prepare_env.py'
tool_injection_src = src_dir / 'tool_injection.py'
dst = src_dir / 'prepareEnvCode.ts'

python_code = src.read_text()
prepare_env_code = prepare_env_src.read_text()
tool_injection_code = tool_injection_src.read_text()

# Escape backslashes for JavaScript string literal
python_code = python_code.replace('\\', '\\\\')
prepare_env_code = prepare_env_code.replace('\\', '\\\\')
tool_injection_code = tool_injection_code.replace('\\', '\\\\')

# Create the JavaScript/TypeScript code
ts_code = f"""// DO NOT EDIT THIS FILE DIRECTLY, INSTEAD RUN "make build"
export const preparePythonCode = `{python_code}`
export const preparePythonCode = `{prepare_env_code}`

export const toolInjectionCode = `{tool_injection_code}`
"""

dst.write_text(ts_code)
Expand Down
167 changes: 167 additions & 0 deletions mcp_run_python/deno/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,126 @@ import { Buffer } from 'node:buffer'

const VERSION = '0.0.13'

// Tool injection support
const EMPTY_TOOLS_RESULT = { toolNames: [], toolSchemas: {} }

const ELICIT_RESPONSE_SCHEMA = z.object({
action: z.enum(['accept', 'decline', 'cancel']),
content: z.optional(z.record(z.string(), z.unknown())),
})

// Schema for tool discovery responses
const TOOL_DISCOVERY_RESPONSE_SCHEMA = {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['accept', 'decline', 'cancel'],
description: 'Action to take with the tool discovery request',
},
content: {
type: 'object',
properties: {
data: {
type: 'string',
description: 'JSON string containing tool_names and tool_schemas',
},
},
description: 'Tool discovery result data',
},
},
required: ['action'],
}

// Schema for tool execution responses
const TOOL_EXECUTION_RESPONSE_SCHEMA = {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['accept', 'decline', 'cancel'],
description: 'Action to take with the tool execution request',
},
content: {
type: 'object',
properties: {
result: {
type: 'string',
description: 'JSON string containing tool execution result',
},
error: {
type: 'string',
description: 'Error message if execution failed',
},
retry: {
type: 'string',
description: 'JSON string containing retry information if tool needs retry',
},
},
description: 'Tool execution result or error information',
},
},
required: ['action'],
}

function makeElicitationRequest(
server: McpServer,
message: string,
requestedSchema: Record<string, unknown>,
): Promise<z.infer<typeof ELICIT_RESPONSE_SCHEMA>> {
return server.server.request(
{
method: 'elicitation/create',
params: { message, requestedSchema },
},
ELICIT_RESPONSE_SCHEMA,
)
}

async function discoverAvailableTools(
server: McpServer,
): Promise<{ toolNames: string[]; toolSchemas: Record<string, Record<string, unknown>> }> {
try {
const result = await makeElicitationRequest(
server,
JSON.stringify({ action: 'discover_tools' }),
TOOL_DISCOVERY_RESPONSE_SCHEMA,
)

if (result.action === 'accept' && result.content?.data) {
const data = JSON.parse(result.content.data as string)
return {
toolNames: data.tool_names || [],
toolSchemas: data.tool_schemas || {},
}
}
} catch (error) {
console.warn('Tool discovery failed:', error)
}

return EMPTY_TOOLS_RESULT
}

function createToolExecutionCallback(
server: McpServer,
): (message: string) => Promise<z.infer<typeof ELICIT_RESPONSE_SCHEMA>> {
return async (message: string) => {
try {
return await makeElicitationRequest(
server,
message,
TOOL_EXECUTION_RESPONSE_SCHEMA,
)
} catch (error) {
console.error('Tool execution failed:', error)
return {
action: 'decline',
content: { error: `Tool execution failed: ${error}` },
}
}
}
}

export async function main() {
const { args } = Deno
const flags = parseArgs(Deno.args, {
Expand Down Expand Up @@ -86,6 +206,38 @@ The code will be executed with Python 3.13.
return {}
})

server.registerTool(
'discover_available_tools',
{
title: 'Discover available tools',
description:
'Discover what tools are available for injection into Python code execution environment. Call this before writing Python code to know which tools you can use.',
inputSchema: {},
},
async () => {
// Discover available tools via elicitation
const { toolNames: availableTools, toolSchemas } = await discoverAvailableTools(server)

return {
content: [
{
type: 'text',
text: JSON.stringify(
{
available_tools: availableTools,
tool_schemas: toolSchemas,
usage_note:
"These tools will be available as Python functions when you run Python code. Tool names with hyphens become underscores (e.g., 'web-search' -> 'web_search').",
},
null,
2,
),
},
],
}
},
)

server.registerTool(
'run_python_code',
{
Expand All @@ -100,6 +252,20 @@ The code will be executed with Python 3.13.
},
async ({ python_code, global_variables }: { python_code: string; global_variables: Record<string, any> }) => {
const logPromises: Promise<void>[] = []

// Discover available tools via elicitation
const { toolNames: availableTools, toolSchemas } = await discoverAvailableTools(server)

// Create elicitation callback for tool execution if tools are available
let toolInjectionOptions
if (availableTools.length > 0) {
toolInjectionOptions = {
elicitationCallback: createToolExecutionCallback(server),
availableTools: availableTools,
toolSchemas: toolSchemas,
}
}

const result = await runCode.run(
deps,
(level, data) => {
Expand All @@ -110,6 +276,7 @@ The code will be executed with Python 3.13.
{ name: 'main.py', content: python_code },
global_variables,
returnMode !== 'xml',
toolInjectionOptions,
)
await Promise.all(logPromises)
return {
Expand Down
58 changes: 56 additions & 2 deletions mcp_run_python/deno/src/runCode.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
// deno-lint-ignore-file no-explicit-any
import { loadPyodide, type PyodideInterface } from 'pyodide'
import { preparePythonCode } from './prepareEnvCode.ts'
import { preparePythonCode, toolInjectionCode } from './prepareEnvCode.ts'
import type { LoggingLevel } from '@modelcontextprotocol/sdk/types.js'

export interface CodeFile {
name: string
content: string
}

interface ElicitationResponse {
action: 'accept' | 'decline' | 'cancel'
content?: Record<string, unknown>
}

export interface ToolInjectionOptions {
elicitationCallback?: (message: string) => Promise<ElicitationResponse>
availableTools?: string[]
toolSchemas?: Record<string, Record<string, unknown>>
}

interface PrepResult {
pyodide: PyodideInterface
preparePyEnv: PreparePyEnv
Expand All @@ -27,6 +38,7 @@ export class RunCode {
file?: CodeFile,
globals?: Record<string, any>,
alwaysReturnJson: boolean = false,
toolInjectionOptions?: ToolInjectionOptions,
): Promise<RunSuccess | RunError> {
let pyodide: PyodideInterface
let sys: any
Expand Down Expand Up @@ -56,8 +68,50 @@ export class RunCode {
}
} else if (file) {
try {
// Create globals for python execution, starting with user's global_variables
const globalVars = { ...(globals || {}), __name__: '__main__' }
const globalsProxy = pyodide.toPy(globalVars)

// Setup tool injection if elicitation callback is provided
if (toolInjectionOptions?.elicitationCallback && toolInjectionOptions?.availableTools?.length) {
const dirPath = '/tmp/mcp_run_python'
const pathlib = pyodide.pyimport('pathlib')
const toolModuleName = '_tool_injection'
pathlib
.Path(`${dirPath}/${toolModuleName}.py`)
.write_text(toolInjectionCode)

const toolInjectionModule = pyodide.pyimport(toolModuleName)

// Create Javascript callback wrapper that handles promises
const jsElicitationCallback = async (message: string): Promise<ElicitationResponse> => {
try {
const result = await toolInjectionOptions.elicitationCallback!(message)
return result
} catch (error) {
log('error', `Elicitation callback error: ${error}`)

return {
action: 'decline',
content: { error: `Elicitation failed: ${error}` },
}
}
}

// Convert to Python and inject tools
const pyCallback = pyodide.toPy(jsElicitationCallback)
const pyTools = pyodide.toPy(toolInjectionOptions.availableTools)
const pyToolSchemas = pyodide.toPy(toolInjectionOptions.toolSchemas || {})
toolInjectionModule.inject_tool_functions(globalsProxy, pyTools, pyCallback, pyToolSchemas)

log(
'info',
`Tool injection enabled for: ${toolInjectionOptions.availableTools.join(', ')}`,
)
}

const rawValue = await pyodide.runPythonAsync(file.content, {
globals: pyodide.toPy({ ...(globals || {}), __name__: '__main__' }),
globals: globalsProxy,
filename: file.name,
})
return {
Expand Down
Loading
Loading