mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 16:49:07 +08:00
feat: implement Python MCP server using existing Pyodide infrastructure (#7506)
This commit is contained in:
parent
a94847faeb
commit
04e6f2c1ad
@ -69,6 +69,9 @@ export enum IpcChannel {
|
||||
Mcp_ServersUpdated = 'mcp:servers-updated',
|
||||
Mcp_CheckConnectivity = 'mcp:check-connectivity',
|
||||
|
||||
// Python
|
||||
Python_Execute = 'python:execute',
|
||||
|
||||
//copilot
|
||||
Copilot_GetAuthMessage = 'copilot:get-auth-message',
|
||||
Copilot_GetCopilotToken = 'copilot:get-copilot-token',
|
||||
|
||||
@ -25,6 +25,7 @@ import NotificationService from './services/NotificationService'
|
||||
import * as NutstoreService from './services/NutstoreService'
|
||||
import ObsidianVaultService from './services/ObsidianVaultService'
|
||||
import { ProxyConfig, proxyManager } from './services/ProxyManager'
|
||||
import { pythonService } from './services/PythonService'
|
||||
import { searchService } from './services/SearchService'
|
||||
import { SelectionService } from './services/SelectionService'
|
||||
import { registerShortcuts, unregisterAllShortcuts } from './services/ShortcutService'
|
||||
@ -48,6 +49,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
const appUpdater = new AppUpdater(mainWindow)
|
||||
const notificationService = new NotificationService(mainWindow)
|
||||
|
||||
// Initialize Python service with main window
|
||||
pythonService.setMainWindow(mainWindow)
|
||||
|
||||
ipcMain.handle(IpcChannel.App_Info, () => ({
|
||||
version: app.getVersion(),
|
||||
isPackaged: app.isPackaged,
|
||||
@ -431,6 +435,14 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
||||
ipcMain.handle(IpcChannel.Mcp_GetInstallInfo, mcpService.getInstallInfo)
|
||||
ipcMain.handle(IpcChannel.Mcp_CheckConnectivity, mcpService.checkMcpConnectivity)
|
||||
|
||||
// Register Python execution handler
|
||||
ipcMain.handle(
|
||||
IpcChannel.Python_Execute,
|
||||
async (_, script: string, context?: Record<string, any>, timeout?: number) => {
|
||||
return await pythonService.executeScript(script, context, timeout)
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle(IpcChannel.App_IsBinaryExist, (_, name: string) => isBinaryExists(name))
|
||||
ipcMain.handle(IpcChannel.App_GetBinaryPath, (_, name: string) => getBinaryPath(name))
|
||||
ipcMain.handle(IpcChannel.App_InstallUvBinary, () => runInstallScript('install-uv.js'))
|
||||
|
||||
@ -6,6 +6,7 @@ import DifyKnowledgeServer from './dify-knowledge'
|
||||
import FetchServer from './fetch'
|
||||
import FileSystemServer from './filesystem'
|
||||
import MemoryServer from './memory'
|
||||
import PythonServer from './python'
|
||||
import ThinkingServer from './sequentialthinking'
|
||||
|
||||
export function createInMemoryMCPServer(name: string, args: string[] = [], envs: Record<string, string> = {}): Server {
|
||||
@ -31,6 +32,9 @@ export function createInMemoryMCPServer(name: string, args: string[] = [], envs:
|
||||
const difyKey = envs.DIFY_KEY
|
||||
return new DifyKnowledgeServer(difyKey, args).server
|
||||
}
|
||||
case '@cherry/python': {
|
||||
return new PythonServer().server
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown in-memory MCP server: ${name}`)
|
||||
}
|
||||
|
||||
113
src/main/mcpServers/python.ts
Normal file
113
src/main/mcpServers/python.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import { pythonService } from '@main/services/PythonService'
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from '@modelcontextprotocol/sdk/types.js'
|
||||
import Logger from 'electron-log'
|
||||
|
||||
/**
|
||||
* Python MCP Server for executing Python code using Pyodide
|
||||
*/
|
||||
class PythonServer {
|
||||
public server: Server
|
||||
|
||||
constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'python-server',
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
this.setupRequestHandlers()
|
||||
}
|
||||
|
||||
private setupRequestHandlers() {
|
||||
// List available tools
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: 'python_execute',
|
||||
description: `Execute Python code using Pyodide in a sandboxed environment. Supports most Python standard library and scientific packages.
|
||||
The code will be executed with Python 3.12.
|
||||
Dependencies may be defined via PEP 723 script metadata, e.g. to install "pydantic", the script should start
|
||||
with a comment of the form:
|
||||
# /// script
|
||||
# dependencies = ['pydantic']
|
||||
# ///
|
||||
print('python code here')`,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
code: {
|
||||
type: 'string',
|
||||
description: 'The Python code to execute'
|
||||
},
|
||||
context: {
|
||||
type: 'object',
|
||||
description: 'Optional context variables to pass to the Python execution environment',
|
||||
additionalProperties: true
|
||||
},
|
||||
timeout: {
|
||||
type: 'number',
|
||||
description: 'Timeout in milliseconds (default: 60000)',
|
||||
default: 60000
|
||||
}
|
||||
},
|
||||
required: ['code']
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// Handle tool calls
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params
|
||||
|
||||
if (name !== 'python_execute') {
|
||||
throw new McpError(ErrorCode.MethodNotFound, `Tool ${name} not found`)
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
code,
|
||||
context = {},
|
||||
timeout = 60000
|
||||
} = args as {
|
||||
code: string
|
||||
context?: Record<string, any>
|
||||
timeout?: number
|
||||
}
|
||||
|
||||
if (!code || typeof code !== 'string') {
|
||||
throw new McpError(ErrorCode.InvalidParams, 'Code parameter is required and must be a string')
|
||||
}
|
||||
|
||||
Logger.info('Executing Python code via Pyodide')
|
||||
|
||||
const result = await pythonService.executeScript(code, context, timeout)
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: result
|
||||
}
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
Logger.error('Python execution error:', errorMessage)
|
||||
|
||||
throw new McpError(ErrorCode.InternalError, `Python execution failed: ${errorMessage}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default PythonServer
|
||||
102
src/main/services/PythonService.ts
Normal file
102
src/main/services/PythonService.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
|
||||
interface PythonExecutionRequest {
|
||||
id: string
|
||||
script: string
|
||||
context: Record<string, any>
|
||||
timeout: number
|
||||
}
|
||||
|
||||
interface PythonExecutionResponse {
|
||||
id: string
|
||||
result?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for executing Python code by communicating with the PyodideService in the renderer process
|
||||
*/
|
||||
export class PythonService {
|
||||
private static instance: PythonService | null = null
|
||||
private mainWindow: BrowserWindow | null = null
|
||||
private pendingRequests = new Map<string, { resolve: (value: string) => void; reject: (error: Error) => void }>()
|
||||
|
||||
private constructor() {
|
||||
// Private constructor for singleton pattern
|
||||
this.setupIpcHandlers()
|
||||
}
|
||||
|
||||
public static getInstance(): PythonService {
|
||||
if (!PythonService.instance) {
|
||||
PythonService.instance = new PythonService()
|
||||
}
|
||||
return PythonService.instance
|
||||
}
|
||||
|
||||
private setupIpcHandlers() {
|
||||
// Handle responses from renderer
|
||||
ipcMain.on('python-execution-response', (_, response: PythonExecutionResponse) => {
|
||||
const request = this.pendingRequests.get(response.id)
|
||||
if (request) {
|
||||
this.pendingRequests.delete(response.id)
|
||||
if (response.error) {
|
||||
request.reject(new Error(response.error))
|
||||
} else {
|
||||
request.resolve(response.result || '')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public setMainWindow(mainWindow: BrowserWindow) {
|
||||
this.mainWindow = mainWindow
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute Python code by sending request to renderer PyodideService
|
||||
*/
|
||||
public async executeScript(
|
||||
script: string,
|
||||
context: Record<string, any> = {},
|
||||
timeout: number = 60000
|
||||
): Promise<string> {
|
||||
if (!this.mainWindow) {
|
||||
throw new Error('Main window not set in PythonService')
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestId = randomUUID()
|
||||
|
||||
// Store the request
|
||||
this.pendingRequests.set(requestId, { resolve, reject })
|
||||
|
||||
// Set up timeout
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.pendingRequests.delete(requestId)
|
||||
reject(new Error('Python execution timed out'))
|
||||
}, timeout + 5000) // Add 5s buffer for IPC communication
|
||||
|
||||
// Update resolve/reject to clear timeout
|
||||
const originalResolve = resolve
|
||||
const originalReject = reject
|
||||
this.pendingRequests.set(requestId, {
|
||||
resolve: (value: string) => {
|
||||
clearTimeout(timeoutId)
|
||||
originalResolve(value)
|
||||
},
|
||||
reject: (error: Error) => {
|
||||
clearTimeout(timeoutId)
|
||||
originalReject(error)
|
||||
}
|
||||
})
|
||||
|
||||
// Send request to renderer
|
||||
const request: PythonExecutionRequest = { id: requestId, script, context, timeout }
|
||||
this.mainWindow?.webContents.send('python-execution-request', request)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const pythonService = PythonService.getInstance()
|
||||
@ -182,6 +182,10 @@ const api = {
|
||||
getInstallInfo: () => ipcRenderer.invoke(IpcChannel.Mcp_GetInstallInfo),
|
||||
checkMcpConnectivity: (server: any) => ipcRenderer.invoke(IpcChannel.Mcp_CheckConnectivity, server)
|
||||
},
|
||||
python: {
|
||||
execute: (script: string, context?: Record<string, any>, timeout?: number) =>
|
||||
ipcRenderer.invoke(IpcChannel.Python_Execute, script, context, timeout)
|
||||
},
|
||||
shell: {
|
||||
openExternal: (url: string, options?: Electron.OpenExternalOptions) => shell.openExternal(url, options)
|
||||
},
|
||||
|
||||
@ -229,3 +229,36 @@ class PyodideService {
|
||||
|
||||
// 创建并导出单例实例
|
||||
export const pyodideService = PyodideService.getInstance()
|
||||
|
||||
// Set up IPC handler for main process requests
|
||||
if (typeof window !== 'undefined' && window.electron?.ipcRenderer) {
|
||||
interface PythonExecutionRequest {
|
||||
id: string
|
||||
script: string
|
||||
context: Record<string, any>
|
||||
timeout: number
|
||||
}
|
||||
|
||||
interface PythonExecutionResponse {
|
||||
id: string
|
||||
result?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
window.electron.ipcRenderer.on('python-execution-request', async (_, request: PythonExecutionRequest) => {
|
||||
try {
|
||||
const result = await pyodideService.runScript(request.script, request.context, request.timeout)
|
||||
const response: PythonExecutionResponse = {
|
||||
id: request.id,
|
||||
result
|
||||
}
|
||||
window.electron.ipcRenderer.send('python-execution-response', response)
|
||||
} catch (error) {
|
||||
const response: PythonExecutionResponse = {
|
||||
id: request.id,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
window.electron.ipcRenderer.send('python-execution-response', response)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -137,6 +137,14 @@ export const builtinMCPServers: MCPServer[] = [
|
||||
DIFY_KEY: 'YOUR_DIFY_KEY'
|
||||
},
|
||||
provider: 'CherryAI'
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
name: '@cherry/python',
|
||||
type: 'inMemory',
|
||||
description: '在安全的沙盒环境中执行 Python 代码。使用 Pyodide 运行 Python,支持大多数标准库和科学计算包',
|
||||
isActive: false,
|
||||
provider: 'CherryAI'
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user