From a303ded134eb7ab588f00e3fcb2b2a4899cd0519 Mon Sep 17 00:00:00 2001 From: gastonfeng Date: Mon, 7 Apr 2025 10:42:54 +0800 Subject: [PATCH] feat(mcp): enhance server management with resource handling and transport type support --- packages/ai-mcp/package.json | 2 +- .../src/browser/mcp-command-contribution.ts | 14 ++-- .../ai-mcp/src/common/mcp-server-manager.ts | 6 ++ .../src/node/mcp-server-manager-impl.ts | 16 ++++ packages/ai-mcp/src/node/mcp-server.ts | 82 ++++++++++++++----- 5 files changed, 90 insertions(+), 30 deletions(-) diff --git a/packages/ai-mcp/package.json b/packages/ai-mcp/package.json index 31f4f0b8fce34..d0a2b18baf968 100644 --- a/packages/ai-mcp/package.json +++ b/packages/ai-mcp/package.json @@ -3,7 +3,7 @@ "version": "1.60.0", "description": "Theia - MCP Integration", "dependencies": { - "@modelcontextprotocol/sdk": "1.0.1", + "@modelcontextprotocol/sdk": "^1.0.1" "@theia/ai-core": "1.60.0", "@theia/core": "1.60.0" }, diff --git a/packages/ai-mcp/src/browser/mcp-command-contribution.ts b/packages/ai-mcp/src/browser/mcp-command-contribution.ts index dda602f1db813..354327d226572 100644 --- a/packages/ai-mcp/src/browser/mcp-command-contribution.ts +++ b/packages/ai-mcp/src/browser/mcp-command-contribution.ts @@ -17,7 +17,7 @@ import { AICommandHandlerFactory } from '@theia/ai-core/lib/browser/ai-command-h import { CommandContribution, CommandRegistry, MessageService, nls } from '@theia/core'; import { QuickInputService } from '@theia/core/lib/browser'; import { inject, injectable } from '@theia/core/shared/inversify'; -import { MCPFrontendService, MCPServerStatus } from '../common/mcp-server-manager'; +import { MCPFrontendService, MCPServerStatus } from '../common'; export const StartMCPServer = { id: 'mcp.startserver', @@ -57,7 +57,7 @@ export class MCPCommandContribution implements CommandContribution { try { const startedServers = await this.mcpFrontendService.getStartedServers(); if (!startedServers || startedServers.length === 0) { - this.messageService.error(nls.localize('theia/ai/mcp/error/noRunningServers', 'No MCP servers running.')); + await this.messageService.error(nls.localize('theia/ai/mcp/error/noRunningServers', 'No MCP servers running.')); return; } const selection = await this.getMCPServerSelection(startedServers); @@ -79,9 +79,9 @@ export class MCPCommandContribution implements CommandContribution { const startableServers = servers.filter(server => !startedServers.includes(server)); if (!startableServers || startableServers.length === 0) { if (startedServers && startedServers.length > 0) { - this.messageService.error(nls.localize('theia/ai/mcp/error/allServersRunning', 'All MCP servers are already running.')); + await this.messageService.error(nls.localize('theia/ai/mcp/error/allServersRunning', 'All MCP servers are already running.')); } else { - this.messageService.error(nls.localize('theia/ai/mcp/error/noServersConfigured', 'No MCP servers configured.')); + await this.messageService.error(nls.localize('theia/ai/mcp/error/noServersConfigured', 'No MCP servers configured.')); } return; } @@ -98,7 +98,7 @@ export class MCPCommandContribution implements CommandContribution { if (serverDescription.tools) { toolNames = serverDescription.tools.map(tool => tool.name).join(','); } - this.messageService.info( + await this.messageService.info( nls.localize('theia/ai/mcp/info/serverStarted', 'MCP server "{0}" successfully started. Registered tools: {1}', selection, toolNames || nls.localize('theia/ai/mcp/tool/noTools', 'No tools available.')) ); @@ -108,9 +108,9 @@ export class MCPCommandContribution implements CommandContribution { console.error('Error while starting MCP server:', serverDescription.error); } } - this.messageService.error(nls.localize('theia/ai/mcp/error/startFailed', 'An error occurred while starting the MCP server.')); + await this.messageService.error(nls.localize('theia/ai/mcp/error/startFailed', 'An error occurred while starting the MCP server.')); } catch (error) { - this.messageService.error(nls.localize('theia/ai/mcp/error/startFailed', 'An error occurred while starting the MCP server.')); + await this.messageService.error(nls.localize('theia/ai/mcp/error/startFailed', 'An error occurred while starting the MCP server.')); console.error('Error while starting MCP server:', error); } } diff --git a/packages/ai-mcp/src/common/mcp-server-manager.ts b/packages/ai-mcp/src/common/mcp-server-manager.ts index 325c5a1825bf5..57b4f1ff21ad5 100644 --- a/packages/ai-mcp/src/common/mcp-server-manager.ts +++ b/packages/ai-mcp/src/common/mcp-server-manager.ts @@ -38,6 +38,9 @@ export interface MCPFrontendNotificationService { export interface MCPServer { callTool(toolName: string, arg_string: string): ReturnType; getTools(): ReturnType; + listResources(): ReturnType; + + readResource(resourceId: string): ReturnType; description: MCPServerDescription; } @@ -53,6 +56,9 @@ export interface MCPServerManager { getRunningServers(): Promise; setClient(client: MCPFrontendNotificationService): void; disconnectClient(client: MCPFrontendNotificationService): void; + listResources(serverName: string): ReturnType; + + readResource(serverName: string, resourceId: string): ReturnType; } export interface ToolInformation { diff --git a/packages/ai-mcp/src/node/mcp-server-manager-impl.ts b/packages/ai-mcp/src/node/mcp-server-manager-impl.ts index 12f63cfa96e6e..7150da5378742 100644 --- a/packages/ai-mcp/src/node/mcp-server-manager-impl.ts +++ b/packages/ai-mcp/src/node/mcp-server-manager-impl.ts @@ -131,4 +131,20 @@ export class MCPServerManagerImpl implements MCPServerManager { private notifyClients(): void { this.clients.forEach(client => client.didUpdateMCPServers()); } + + listResources(serverName: string): ReturnType { + const server = this.servers.get(serverName); + if (!server) { + throw new Error(`MCP server "${serverName}" not found.`); + } + return server.listResources(); + } + + readResource(serverName: string, resourceId: string): ReturnType { + const server = this.servers.get(serverName); + if (!server) { + throw new Error(`MCP server "${serverName}" not found.`); + } + return server.readResource(resourceId); + } } diff --git a/packages/ai-mcp/src/node/mcp-server.ts b/packages/ai-mcp/src/node/mcp-server.ts index 801393b883b68..9d5a6f5edfc26 100644 --- a/packages/ai-mcp/src/node/mcp-server.ts +++ b/packages/ai-mcp/src/node/mcp-server.ts @@ -13,10 +13,13 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { MCPServerDescription, MCPServerStatus, ToolInformation } from '../common'; -import { Emitter } from '@theia/core/lib/common/event'; +import {StdioClientTransport} from '@modelcontextprotocol/sdk/client/stdio'; +import {SSEClientTransport} from '@modelcontextprotocol/sdk/client/sse'; +import {Client} from '@modelcontextprotocol/sdk/client/index.js'; +import {MCPServerDescription, MCPServerStatus, ToolInformation} from '../common'; +import {Emitter} from '@theia/core/lib/common/event'; + +export type TransportType = 'stdio' | 'sse'; export class MCPServer { private name: string; @@ -31,6 +34,8 @@ export class MCPServer { // Event emitter for status updates private readonly onDidUpdateStatusEmitter = new Emitter(); readonly onDidUpdateStatus = this.onDidUpdateStatusEmitter.event; + private readonly transportType: TransportType; + private readonly sseUrl?: string; constructor(description: MCPServerDescription) { this.name = description.name; @@ -38,6 +43,12 @@ export class MCPServer { this.args = description.args; this.env = description.env; this.autostart = description.autostart; + if (this.env?.sseUrl) { + this.sseUrl = this.env.sseUrl; + this.transportType = 'sse'; + } else { + this.transportType = 'stdio'; + } console.log(this.autostart); } @@ -84,23 +95,41 @@ export class MCPServer { if (this.isRunnning() && this.status === MCPServerStatus.Starting) { return; } + this.setStatus(MCPServerStatus.Starting); - console.log(`Starting server "${this.name}" with command: ${this.command} and args: ${this.args?.join(' ')} and env: ${JSON.stringify(this.env)}`); - // Filter process.env to exclude undefined values - const sanitizedEnv: Record = Object.fromEntries( - Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined) - ); - - const mergedEnv: Record = { - ...sanitizedEnv, - ...(this.env || {}) - }; - const transport = new StdioClientTransport({ - command: this.command, - args: this.args, - env: mergedEnv, - }); - transport.onerror = error => { + console.log(`Starting server "${this.name}" with transport: ${this.transportType}`); + + let transport; + + if (this.transportType === 'sse') { + if (!this.sseUrl) { + throw new Error(`SSE transport requires a URL, but none was provided for server "${this.name}"`); + } + + console.log(`Using SSE transport with URL: ${this.sseUrl}`); + transport = new SSEClientTransport(new URL(this.sseUrl)); + } else { + // Default to stdio transport + console.log(`Using stdio transport with command: ${this.command} and args: ${this.args?.join(' ')} and env: ${JSON.stringify(this.env)}`); + + // Filter process.env to exclude undefined values + const sanitizedEnv: Record = Object.fromEntries( + Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined) + ); + + const mergedEnv: Record = { + ...sanitizedEnv, + ...(this.env || {}) + }; + + transport = new StdioClientTransport({ + command: this.command, + args: this.args, + env: mergedEnv, + }); + } + + transport.onerror = (error: Error) => { console.error('Error: ' + error); this.error = 'Error: ' + error; this.setStatus(MCPServerStatus.Errored); @@ -112,7 +141,7 @@ export class MCPServer { }, { capabilities: {} }); - this.client.onerror = error => { + this.client.onerror = (error: Error) => { console.error('Error in MCP client: ' + error); this.error = 'Error in MCP client: ' + error; this.setStatus(MCPServerStatus.Errored); @@ -132,7 +161,7 @@ export class MCPServer { let args; try { args = JSON.parse(arg_string); - } catch (error) { + } catch (error: unknown) { console.error( `Failed to parse arguments for calling tool "${toolName}" in MCP server "${this.name}" with command "${this.command}". Invalid JSON: ${arg_string}`, @@ -166,4 +195,13 @@ export class MCPServer { this.client.close(); this.setStatus(MCPServerStatus.NotRunning); } + + listResources(): ReturnType { + return this.client.listResources(); + } + + readResource(resourceId: string): ReturnType { + const params = {uri: resourceId}; + return this.client.readResource(params); + } }