Skip to content
Merged
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
},
"dependencies": {
"@anthropic-ai/sdk": "^0.53.0",
"@e2b/code-interpreter": "^1.5.1",
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^4.0.0",
"@google/genai": "^1.5.1",
Expand Down
2 changes: 1 addition & 1 deletion src/main/presenter/configPresenter/mcpConfHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ export class McpConfHelper {
// 设置MCP服务器配置
async setMcpServers(servers: Record<string, MCPServerConfig>): Promise<void> {
this.mcpStore.set('mcpServers', servers)
eventBus.emit(MCP_EVENTS.CONFIG_CHANGED, {
eventBus.send(MCP_EVENTS.CONFIG_CHANGED, SendTarget.ALL_WINDOWS, {
mcpServers: servers,
defaultServers: this.mcpStore.get('defaultServers') || [],
mcpEnabled: this.mcpStore.get('mcpEnabled')
Expand Down
2 changes: 1 addition & 1 deletion src/main/presenter/mcpPresenter/inMemoryServers/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function getInMemoryServer(
case 'imageServer':
return new ImageServer(args[0], args[1])
case 'powerpack':
return new PowerpackServer()
return new PowerpackServer(env)
case 'difyKnowledge':
return new DifyKnowledgeServer(
env as {
Expand Down
203 changes: 172 additions & 31 deletions src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { execFile } from 'child_process'
import { promisify } from 'util'
import { nanoid } from 'nanoid'
import { runCode, type CodeFile } from '../pythonRunner'
import { Sandbox } from '@e2b/code-interpreter'

// Schema 定义
const GetTimeArgsSchema = z.object({
Expand Down Expand Up @@ -51,6 +52,20 @@ const RunPythonCodeArgsSchema = z.object({
.describe('Code execution timeout in milliseconds, default 5 seconds')
})

// E2B 代码执行 Schema
const E2BRunCodeArgsSchema = z.object({
code: z
.string()
.describe(
'Python code to execute in E2B secure sandbox. Supports Jupyter Notebook syntax and has access to common Python libraries.'
),
language: z
.string()
.optional()
.default('python')
.describe('Programming language for code execution, currently supports python')
})

// 限制和安全配置
const CODE_EXECUTION_FORBIDDEN_PATTERNS = [
// 允许os模块用于系统信息读取,但仍然禁止其他危险模块
Expand All @@ -74,8 +89,13 @@ export class PowerpackServer {
private server: Server
private bunRuntimePath: string | null = null
private nodeRuntimePath: string | null = null
private useE2B: boolean = false
private e2bApiKey: string = ''

constructor(env?: Record<string, any>) {
// 从环境变量中获取 E2B 配置
this.parseE2BConfig(env)

constructor() {
// 查找内置的运行时路径
this.setupRuntimes()

Expand All @@ -96,6 +116,20 @@ export class PowerpackServer {
this.setupRequestHandlers()
}

// 解析 E2B 配置
private parseE2BConfig(env?: Record<string, any>): void {
if (env) {
this.useE2B = env.USE_E2B === true || env.USE_E2B === 'true'
this.e2bApiKey = env.E2B_API_KEY || ''

// 如果启用了 E2B 但没有提供 API Key,记录警告
if (this.useE2B && !this.e2bApiKey) {
console.warn('E2B is enabled but no API key provided. E2B functionality will be disabled.')
this.useE2B = false
}
}
}

// 设置运行时路径
private setupRuntimes(): void {
const runtimeBasePath = path
Expand Down Expand Up @@ -130,17 +164,19 @@ export class PowerpackServer {
}
}

if (!this.bunRuntimePath && !this.nodeRuntimePath) {
console.warn('未找到内置运行时(Bun或Node.js),代码执行功能将不可用')
if (!this.bunRuntimePath && !this.nodeRuntimePath && !this.useE2B) {
console.warn('No runtime found (Bun, Node.js, or E2B), code execution will be unavailable')
} else if (this.useE2B) {
console.info('Using E2B for code execution')
} else if (this.bunRuntimePath) {
console.info('使用内置Bun运行时')
console.info('Using built-in Bun runtime')
} else if (this.nodeRuntimePath) {
console.info('使用内置Node.js运行时')
console.info('Using built-in Node.js runtime')
}
}

// 启动服务器
public startServer(transport: Transport): void {
public async startServer(transport: Transport): Promise<void> {
this.server.connect(transport)
}

Expand Down Expand Up @@ -247,6 +283,65 @@ export class PowerpackServer {
}
}

// 使用 E2B 执行代码
private async executeE2BCode(code: string): Promise<string> {
if (!this.useE2B) {
throw new Error('E2B is not enabled')
}

let sandbox: Sandbox | null = null
try {
sandbox = await Sandbox.create()
const result = await sandbox.runCode(code)

Comment on lines +292 to +296
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

API key must be passed to Sandbox.create

Sandbox.create() supports { apiKey }. Omitting it means:

  • The call will 401/403 in production.
  • Users have no way to override via the UI even though you request it.
-      sandbox = await Sandbox.create()
+      sandbox = await Sandbox.create({ apiKey: this.e2bApiKey })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let sandbox: Sandbox | null = null
try {
sandbox = await Sandbox.create()
const result = await sandbox.runCode(code)
let sandbox: Sandbox | null = null
try {
sandbox = await Sandbox.create({ apiKey: this.e2bApiKey })
const result = await sandbox.runCode(code)
🤖 Prompt for AI Agents
In src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts around
lines 292 to 296, the call to Sandbox.create() is missing the required apiKey
parameter. To fix this, modify the call to Sandbox.create() to include the
apiKey argument, passing the appropriate API key value so that the sandbox can
authenticate properly and avoid 401/403 errors in production.

// 格式化结果
const output: string[] = []

// 添加执行结果
if (result.results && result.results.length > 0) {
for (const res of result.results) {
if ((res as any).isError) {
const error = (res as any).error
output.push(`Error: ${error?.name || 'Unknown'}: ${error?.value || 'Unknown error'}`)
if (error?.traceback) {
output.push(error.traceback.join('\n'))
}
} else if (res.text) {
output.push(res.text)
} else if ((res as any).data) {
output.push(JSON.stringify((res as any).data, null, 2))
}
}
}

// 添加日志输出
if (result.logs) {
if (result.logs.stdout.length > 0) {
output.push('STDOUT:')
output.push(...result.logs.stdout)
}
if (result.logs.stderr.length > 0) {
output.push('STDERR:')
output.push(...result.logs.stderr)
}
}

return output.join('\n') || 'Code executed successfully (no output)'
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
throw new Error(`E2B execution failed: ${errorMessage}`)
} finally {
// 清理沙箱
if (sandbox) {
try {
await sandbox.kill()
} catch (error) {
console.error('Failed to close E2B sandbox:', error)
}
}
}
}

// 设置请求处理器
private setupRequestHandlers(): void {
// 设置工具列表处理器
Expand All @@ -271,36 +366,49 @@ export class PowerpackServer {
}
]

// 只有在JavaScript运行时可用时才添加代码执行工具
if (this.bunRuntimePath || this.nodeRuntimePath) {
// 根据配置添加代码执行工具
if (this.useE2B) {
// 使用 E2B 执行代码
tools.push({
name: 'run_code',
description:
'Execute Python code in a secure E2B sandbox environment. Supports Jupyter Notebook syntax and has access to common Python libraries. ' +
'The code will be executed in an isolated environment with full Python ecosystem support. ' +
'This is safer than local execution as it runs in a controlled cloud sandbox. ' +
'Perfect for data analysis, calculations, visualizations, and any Python programming tasks.',
inputSchema: zodToJsonSchema(E2BRunCodeArgsSchema)
})
} else {
// 使用本地运行时执行代码
if (this.bunRuntimePath || this.nodeRuntimePath) {
tools.push({
name: 'run_node_code',
description:
'Execute simple JavaScript/TypeScript code in a secure sandbox environment (Bun or Node.js). Suitable for calculations, data transformations, encryption/decryption, and network operations. ' +
'The code needs to be output to the console, and the output content needs to be formatted as a string. ' +
'For security reasons, the code cannot perform file operations, modify system settings, spawn child processes, or execute external code from network. ' +
'Code execution has a timeout limit, default is 5 seconds, you can adjust it based on the estimated time of the code, generally not recommended to exceed 2 minutes. ' +
'When a problem can be solved by a simple and secure JavaScript/TypeScript code or you have generated a simple code for the user and want to execute it, please use this tool, providing more reliable information to the user.',
inputSchema: zodToJsonSchema(RunNodeCodeArgsSchema)
})
}

tools.push({
name: 'run_node_code',
name: 'run_python_code',
description:
'Execute simple JavaScript/TypeScript code in a secure sandbox environment (Bun or Node.js). Suitable for calculations, data transformations, encryption/decryption, and network operations. ' +
'The code needs to be output to the console, and the output content needs to be formatted as a string. ' +
'For security reasons, the code cannot perform file operations, modify system settings, spawn child processes, or execute external code from network. ' +
'Execute simple Python code in a secure sandbox environment. Suitable for calculations, data analysis, and scientific computing. ' +
'The code needs to be output to the print function, and the output content needs to be formatted as a string. ' +
'The code will be executed with Python 3.12. ' +
'Code execution has a timeout limit, default is 5 seconds, you can adjust it based on the estimated time of the code, generally not recommended to exceed 2 minutes. ' +
'When a problem can be solved by a simple and secure JavaScript/TypeScript code or you have generated a simple code for the user and want to execute it, please use this tool, providing more reliable information to the user.',
inputSchema: zodToJsonSchema(RunNodeCodeArgsSchema)
'Dependencies may be defined via PEP 723 script metadata, e.g. to install "pydantic", the script should startwith a comment of the form:' +
`# /// script\n` +
`# dependencies = ['pydantic']\n ` +
`# ///\n` +
`print('hello world').`,
inputSchema: zodToJsonSchema(RunPythonCodeArgsSchema)
})
}

// 添加Python代码执行工具
tools.push({
name: 'run_python_code',
description:
'Execute simple Python code in a secure sandbox environment. Suitable for calculations, data analysis, and scientific computing. ' +
'The code needs to be output to the print function, and the output content needs to be formatted as a string. ' +
'The code will be executed with Python 3.12. ' +
'Code execution has a timeout limit, default is 5 seconds, you can adjust it based on the estimated time of the code, generally not recommended to exceed 2 minutes. ' +
'Dependencies may be defined via PEP 723 script metadata, e.g. to install "pydantic", the script should startwith a comment of the form:' +
`# /// script\n` +
`# dependencies = ['pydantic']\n ` +
`# ///\n` +
`print('hello world').`,
inputSchema: zodToJsonSchema(RunPythonCodeArgsSchema)
})

return { tools }
})

Expand Down Expand Up @@ -369,8 +477,36 @@ export class PowerpackServer {
}
}

case 'run_code': {
// E2B 代码执行
if (!this.useE2B) {
throw new Error('E2B is not enabled')
}

const parsed = E2BRunCodeArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`无效的代码参数: ${parsed.error}`)
}

const { code } = parsed.data
const result = await this.executeE2BCode(code)

return {
content: [
{
type: 'text',
text: `代码执行结果 (E2B Sandbox):\n\n${result}`
}
]
}
}
Comment on lines +480 to +502
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

run_code handler ignores the language argument & offers no timeout controls

The schema exposes language (default python) yet the handler:

const { code } = parsed.data
const result = await this.executeE2BCode(code)
  • silently discards language – misleading for callers.
  • offers no per-execution timeout; long-running notebooks will block the server.

Consider:

-            const { code } = parsed.data
-            const result = await this.executeE2BCode(code)
+            const { code, language } = parsed.data
+            if (language !== 'python') {
+              throw new Error(`Unsupported language "${language}". Currently only 'python' is allowed.`)
+            }
+            const result = await this.executeE2BCode(code /*, timeout? */)
🤖 Prompt for AI Agents
In src/main/presenter/mcpPresenter/inMemoryServers/powerpackServer.ts lines 480
to 502, the 'run_code' handler parses a 'language' argument but does not use it,
causing confusion, and lacks timeout controls for code execution, risking server
blocking. Update the handler to extract and pass the 'language' parameter to the
executeE2BCode method or equivalent, ensuring the execution respects the
specified language. Additionally, implement a timeout mechanism for the code
execution to prevent long-running tasks from blocking the server, such as using
a timer or cancellation token to abort execution after a set duration.


case 'run_node_code': {
// 再次检查JavaScript运行时是否可用
// 本地 JavaScript 代码执行
if (this.useE2B) {
throw new Error('Local code execution is disabled when E2B is enabled')
}

if (!this.bunRuntimePath && !this.nodeRuntimePath) {
throw new Error('JavaScript runtime is not available, cannot execute code')
}
Expand All @@ -394,6 +530,11 @@ export class PowerpackServer {
}

case 'run_python_code': {
// 本地 Python 代码执行
if (this.useE2B) {
throw new Error('Local code execution is disabled when E2B is enabled')
}

const parsed = RunPythonCodeArgsSchema.safeParse(args)
if (!parsed.success) {
throw new Error(`无效的代码参数: ${parsed.error}`)
Expand Down
2 changes: 1 addition & 1 deletion src/main/presenter/mcpPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ export class McpPresenter implements IMCPPresenter {
} catch (error) {
console.error(`[MCP] Failed to restart server ${serverName}:`, error)
// 即使重启失败,也要确保状态正确,标记为未运行
eventBus.emit(MCP_EVENTS.SERVER_STOPPED, serverName)
eventBus.send(MCP_EVENTS.SERVER_STOPPED, SendTarget.ALL_WINDOWS, serverName)
}
}
}
Expand Down
18 changes: 11 additions & 7 deletions src/main/presenter/mcpPresenter/mcpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
import { type Transport } from '@modelcontextprotocol/sdk/shared/transport.js'
import { eventBus } from '@/eventbus'
import { eventBus, SendTarget } from '@/eventbus'
import { MCP_EVENTS } from '@/events'
import path from 'path'
import { presenter } from '@/presenter'
Expand Down Expand Up @@ -535,10 +535,14 @@ export class McpClient {
console.info(`MCP server ${this.serverName} connected successfully`)

// 触发服务器状态变更事件
eventBus.emit((MCP_EVENTS as MCPEventsType).SERVER_STATUS_CHANGED, {
name: this.serverName,
status: 'running'
})
eventBus.send(
(MCP_EVENTS as MCPEventsType).SERVER_STATUS_CHANGED,
SendTarget.ALL_WINDOWS,
{
name: this.serverName,
status: 'running'
}
)
})
.catch((error) => {
console.error(`Failed to connect to MCP server ${this.serverName}:`, error)
Expand All @@ -560,7 +564,7 @@ export class McpClient {
console.error(`Failed to connect to MCP server ${this.serverName}:`, error)

// 触发服务器状态变更事件
eventBus.emit((MCP_EVENTS as MCPEventsType).SERVER_STATUS_CHANGED, {
eventBus.send((MCP_EVENTS as MCPEventsType).SERVER_STATUS_CHANGED, SendTarget.ALL_WINDOWS, {
name: this.serverName,
status: 'stopped'
})
Expand All @@ -582,7 +586,7 @@ export class McpClient {
console.log(`Disconnected from MCP server: ${this.serverName}`)

// 触发服务器状态变更事件
eventBus.emit((MCP_EVENTS as MCPEventsType).SERVER_STATUS_CHANGED, {
eventBus.send((MCP_EVENTS as MCPEventsType).SERVER_STATUS_CHANGED, SendTarget.ALL_WINDOWS, {
name: this.serverName,
status: 'stopped'
})
Expand Down
Loading