From 4e5f65dc63c0b7eb95095838d9de6f882cbb9f8b Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Mon, 9 Dec 2024 00:49:23 +0100 Subject: [PATCH 01/14] Add initial support for MCP fixed #14523 Signed-off-by: Jonas Helming --- examples/browser/package.json | 1 + examples/browser/tsconfig.json | 3 + .../browser/prompttemplate-contribution.ts | 13 +- packages/ai-core/src/common/language-model.ts | 1 + .../src/common/tool-invocation-registry.ts | 46 +++++++ packages/ai-mcp/.eslintrc.js | 10 ++ packages/ai-mcp/README.md | 91 +++++++++++++ packages/ai-mcp/package.json | 49 +++++++ .../src/browser/mcp-command-contribution.ts | 121 ++++++++++++++++++ .../mcp-frontend-application-contribution.ts | 104 +++++++++++++++ .../ai-mcp/src/browser/mcp-frontend-module.ts | 33 +++++ .../ai-mcp/src/browser/mcp-preferences.ts | 65 ++++++++++ packages/ai-mcp/src/common/index.ts | 16 +++ .../ai-mcp/src/common/mcp-server-manager.ts | 49 +++++++ .../ai-mcp/src/node/mcp-backend-module.ts | 31 +++++ .../src/node/mcp-server-manager-impl.ts | 90 +++++++++++++ packages/ai-mcp/src/node/mcp-server.ts | 96 ++++++++++++++ packages/ai-mcp/src/package.spec.ts | 28 ++++ packages/ai-mcp/tsconfig.json | 19 +++ tsconfig.json | 3 + yarn.lock | 21 ++- 21 files changed, 882 insertions(+), 8 deletions(-) create mode 100644 packages/ai-mcp/.eslintrc.js create mode 100644 packages/ai-mcp/README.md create mode 100644 packages/ai-mcp/package.json create mode 100644 packages/ai-mcp/src/browser/mcp-command-contribution.ts create mode 100644 packages/ai-mcp/src/browser/mcp-frontend-application-contribution.ts create mode 100644 packages/ai-mcp/src/browser/mcp-frontend-module.ts create mode 100644 packages/ai-mcp/src/browser/mcp-preferences.ts create mode 100644 packages/ai-mcp/src/common/index.ts create mode 100644 packages/ai-mcp/src/common/mcp-server-manager.ts create mode 100644 packages/ai-mcp/src/node/mcp-backend-module.ts create mode 100644 packages/ai-mcp/src/node/mcp-server-manager-impl.ts create mode 100644 packages/ai-mcp/src/node/mcp-server.ts create mode 100644 packages/ai-mcp/src/package.spec.ts create mode 100644 packages/ai-mcp/tsconfig.json diff --git a/examples/browser/package.json b/examples/browser/package.json index 2fdea15ac3e78..1c5b1ba3bfad9 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -29,6 +29,7 @@ "@theia/ai-history": "1.56.0", "@theia/ai-huggingface": "1.56.0", "@theia/ai-llamafile": "1.56.0", + "@theia/ai-mcp": "1.56.0", "@theia/ai-ollama": "1.56.0", "@theia/ai-openai": "1.56.0", "@theia/ai-terminal": "1.56.0", diff --git a/examples/browser/tsconfig.json b/examples/browser/tsconfig.json index 5c7e5e2b56ab6..ec73ed6072340 100644 --- a/examples/browser/tsconfig.json +++ b/examples/browser/tsconfig.json @@ -29,6 +29,9 @@ { "path": "../../packages/ai-llamafile" }, + { + "path": "../../packages/ai-mcp" + }, { "path": "../../packages/ai-ollama" }, diff --git a/packages/ai-core/src/browser/prompttemplate-contribution.ts b/packages/ai-core/src/browser/prompttemplate-contribution.ts index c8735daa2402b..9465ced70b5ef 100644 --- a/packages/ai-core/src/browser/prompttemplate-contribution.ts +++ b/packages/ai-core/src/browser/prompttemplate-contribution.ts @@ -14,15 +14,15 @@ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { inject, injectable, named } from '@theia/core/shared/inversify'; +import { inject, injectable } from '@theia/core/shared/inversify'; import { GrammarDefinition, GrammarDefinitionProvider, LanguageGrammarDefinitionContribution, TextmateRegistry } from '@theia/monaco/lib/browser/textmate'; import * as monaco from '@theia/monaco-editor-core'; -import { Command, CommandContribution, CommandRegistry, ContributionProvider, MessageService } from '@theia/core'; +import { Command, CommandContribution, CommandRegistry, MessageService } from '@theia/core'; import { TabBarToolbarContribution, TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; import { codicon, Widget } from '@theia/core/lib/browser'; import { EditorWidget, ReplaceOperation } from '@theia/editor/lib/browser'; -import { PromptCustomizationService, PromptService, ToolProvider } from '../common'; +import { PromptCustomizationService, PromptService, ToolInvocationRegistry } from '../common'; import { ProviderResult } from '@theia/monaco-editor-core/esm/vs/editor/common/languages'; const PROMPT_TEMPLATE_LANGUAGE_ID = 'theia-ai-prompt-template'; @@ -56,9 +56,8 @@ export class PromptTemplateContribution implements LanguageGrammarDefinitionCont @inject(PromptCustomizationService) protected readonly customizationService: PromptCustomizationService; - @inject(ContributionProvider) - @named(ToolProvider) - private toolProviders: ContributionProvider; + @inject(ToolInvocationRegistry) + protected readonly toolInvocationRegistry: ToolInvocationRegistry; readonly config: monaco.languages.LanguageConfiguration = { @@ -115,7 +114,7 @@ export class PromptTemplateContribution implements LanguageGrammarDefinitionCont model, position, '~{', - this.toolProviders.getContributions().map(provider => provider.getTool()), + this.toolInvocationRegistry.getAllFunctions(), monaco.languages.CompletionItemKind.Function, tool => tool.id, tool => tool.name, diff --git a/packages/ai-core/src/common/language-model.ts b/packages/ai-core/src/common/language-model.ts index 4c507b74bdfcc..ab0c3e104595c 100644 --- a/packages/ai-core/src/common/language-model.ts +++ b/packages/ai-core/src/common/language-model.ts @@ -38,6 +38,7 @@ export interface ToolRequest { parameters?: { type?: 'object', properties: Record }; description?: string; handler: (arg_string: string) => Promise; + providerName?: string; } export interface LanguageModelRequest { messages: LanguageModelRequestMessage[], diff --git a/packages/ai-core/src/common/tool-invocation-registry.ts b/packages/ai-core/src/common/tool-invocation-registry.ts index 2ebde1921103a..baeac82f725b4 100644 --- a/packages/ai-core/src/common/tool-invocation-registry.ts +++ b/packages/ai-core/src/common/tool-invocation-registry.ts @@ -24,11 +24,44 @@ export const ToolInvocationRegistry = Symbol('ToolInvocationRegistry'); * Registry for all the function calls available to Agents. */ export interface ToolInvocationRegistry { + /** + * Registers a tool into the registry. + * + * @param tool - The `ToolRequest` object representing the tool to be registered. + */ registerTool(tool: ToolRequest): void; + /** + * Retrieves a specific `ToolRequest` from the registry. + * + * @param toolId - The unique identifier of the tool to retrieve. + * @returns The `ToolRequest` object corresponding to the provided tool ID, + * or `undefined` if the tool is not found in the registry. + */ getFunction(toolId: string): ToolRequest | undefined; + /** + * Retrieves multiple `ToolRequest`s configurations from the registry. + * + * @param toolIds - A list of tool IDs to retrieve. + * @returns An array of `ToolRequest` objects for the specified tool IDs. + * If a tool ID is not found, it is skipped in the returned array. + */ getFunctions(...toolIds: string[]): ToolRequest[]; + + /** + * Retrieves all `ToolRequest`s currently registered in the registry. + * + * @returns An array of all `ToolRequest` objects in the registry. + */ + getAllFunctions(): ToolRequest[]; + + /** + * Unregisters all tools provided by a specific tool provider. + * + * @param providerName - The name of the tool provider whose tools should be removed (as specificed in the `ToolRequest`). + */ + unregisterAllTools(providerName: string): void; } export const ToolProvider = Symbol('ToolProvider'); @@ -52,6 +85,19 @@ export class ToolInvocationRegistryImpl implements ToolInvocationRegistry { }); } + unregisterAllTools(providerName: string): void { + const toolsToRemove: string[] = []; + for (const [id, tool] of this.tools.entries()) { + if (tool.providerName === providerName) { + toolsToRemove.push(id); + } + } + toolsToRemove.forEach(id => this.tools.delete(id)); + } + getAllFunctions(): ToolRequest[] { + return Array.from(this.tools.values()); + } + registerTool(tool: ToolRequest): void { if (this.tools.has(tool.id)) { console.warn(`Function with id ${tool.id} is already registered.`); diff --git a/packages/ai-mcp/.eslintrc.js b/packages/ai-mcp/.eslintrc.js new file mode 100644 index 0000000000000..13089943582b6 --- /dev/null +++ b/packages/ai-mcp/.eslintrc.js @@ -0,0 +1,10 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: [ + '../../configs/build.eslintrc.json' + ], + parserOptions: { + tsconfigRootDir: __dirname, + project: 'tsconfig.json' + } +}; diff --git a/packages/ai-mcp/README.md b/packages/ai-mcp/README.md new file mode 100644 index 0000000000000..271efd4a45050 --- /dev/null +++ b/packages/ai-mcp/README.md @@ -0,0 +1,91 @@ +# Model Context Server (MCP) Integration + +The AI MCP package provides an integration that allows users to start and use MCP Servers to provide additional tool functions to LLMs, e.g. search or file access (outside of the workspace). + +## Features +- Add MCP Servers via settings.json +- Start and stop MCP servers. +- Use tool functions provided by MCP servers in prompt templates + +## Commands + +### Start MCP Server + +- **Command ID:** `mcp.startserver` +- **Label:** `MCP: Start MCP Server` +- **Functionality:** Allows you to start a MCP server by selecting from a list of configured servers. + +### Stop MCP Server + +- **Command ID:** `mcp.stopserver` +- **Label:** `MCP: Stop MCP Server` +- **Functionality:** Allows you to stop a running MCP server by selecting from a list of currently running servers. + +## Usage + +1. **Starting a Llamafile Language Server:** + + - Use the command palette to invoke `MCP: Start MCP Server`. + - A quick pick menu will appear with a list of configured MCP Servers. + - Select a server to start. + +2. **Stopping a Llamafile Language Server:** + - Use the command palette to invoke `MCP: Stop MCP Server`. + - A quick pick menu will display a list of currently running MCP Servers. + - Select a server to stop. + +3. **Using provided tool functions** + - Only functions of started MCP servers can be used + - Open a prompt template and add the added tool functions + - Type '~{' to open the auto completion + +## Configuration + +Make sure to configure your MCP Servers properly within the preference settings. + +Example Configuration: + +```json +{ + "ai-features.mcp.mcpServers": { + "memory": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-memory" + ] + }, + "brave-search": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-brave-search" + ], + "env": { + "BRAVE_API_KEY": "YOUR_API_KEY" + } + }, + "filesystem": { + "command": "npx", + "args": [ + "-y", + "@modelcontextprotocol/server-filesystem", + "ABSOLUTE_PATH_TO_ALLOWED_DIRECTORY", + ] + }, + } +} +``` + +Example prompt (for search) +```md +~{mcp_brave-search_brave_web_search} +``` + +Example User query +```md +Search the internet for XYZ +``` + +## More Information +[List of available MCP servers](https://github.com/modelcontextprotocol/servers) diff --git a/packages/ai-mcp/package.json b/packages/ai-mcp/package.json new file mode 100644 index 0000000000000..e430ae3aba109 --- /dev/null +++ b/packages/ai-mcp/package.json @@ -0,0 +1,49 @@ +{ + "name": "@theia/ai-mcp", + "version": "1.56.0", + "description": "Theia - MCP Integration", + "dependencies": { + "@theia/core": "1.56.0", + "@theia/ai-core": "1.56.0", + "@modelcontextprotocol/sdk": "1.0.1" + }, + "publishConfig": { + "access": "public" + }, + "theiaExtensions": [ + { + "frontend": "lib/browser/ai-mcp-frontend-module", + "backend": "lib/node/ai-mcp-backend-module" + } + ], + "keywords": [ + "theia-extension" + ], + "license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0", + "repository": { + "type": "git", + "url": "https://github.com/eclipse-theia/theia.git" + }, + "bugs": { + "url": "https://github.com/eclipse-theia/theia/issues" + }, + "homepage": "https://github.com/eclipse-theia/theia", + "files": [ + "lib", + "src" + ], + "scripts": { + "build": "theiaext build", + "clean": "theiaext clean", + "compile": "theiaext compile", + "lint": "theiaext lint", + "test": "theiaext test", + "watch": "theiaext watch" + }, + "devDependencies": { + "@theia/ext-scripts": "1.56.0" + }, + "nyc": { + "extends": "../../configs/nyc.json" + } +} diff --git a/packages/ai-mcp/src/browser/mcp-command-contribution.ts b/packages/ai-mcp/src/browser/mcp-command-contribution.ts new file mode 100644 index 0000000000000..0eb6aa333e1d5 --- /dev/null +++ b/packages/ai-mcp/src/browser/mcp-command-contribution.ts @@ -0,0 +1,121 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +import { AICommandHandlerFactory } from '@theia/ai-core/lib/browser/ai-command-handler-factory'; +import { CommandContribution, CommandRegistry, MessageService } from '@theia/core'; +import { QuickInputService } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { MCPServerManager } from '../common/mcp-server-manager'; +import { ToolInvocationRegistry, ToolRequest } from '@theia/ai-core'; + +export const StartMCPServer = { + id: 'mcp.startserver', + label: 'MCP: Start MCP Server', +}; +export const StopMCPServer = { + id: 'mcp.stopserver', + label: 'MCP: Stop MCP Server', +}; + +@injectable() +export class MCPCommandContribution implements CommandContribution { + @inject(AICommandHandlerFactory) + protected readonly commandHandlerFactory: AICommandHandlerFactory; + + @inject(QuickInputService) + protected readonly quickInputService: QuickInputService; + + @inject(MessageService) + protected messageService: MessageService; + + @inject(MCPServerManager) + protected readonly mcpServerManager: MCPServerManager; + + @inject(ToolInvocationRegistry) + protected readonly toolInvocationRegistry: ToolInvocationRegistry; + + private async getMCPServerSelection(serverNames: String[]): Promise { + if (!serverNames || serverNames.length === 0) { + this.messageService.error('No MCP Servers configured.'); + return undefined; + } + const options = serverNames.map(mcpServerName => ({ label: mcpServerName as string })); + const result = await this.quickInputService.showQuickPick(options); + if (!result) { + return undefined; + } + return result.label; + } + + registerCommands(commandRegistry: CommandRegistry): void { + commandRegistry.registerCommand(StopMCPServer, this.commandHandlerFactory({ + execute: async () => { + try { + const selection = await this.getMCPServerSelection(await this.mcpServerManager.getStartedServers()); + if (!selection) { + return; + } + this.toolInvocationRegistry.unregisterAllTools(`mcp_${selection}`); + this.mcpServerManager.stopServer(selection); + } catch (error) { + console.error('Error while stopping MCP server:', error); + } + } + })); + + commandRegistry.registerCommand(StartMCPServer, this.commandHandlerFactory({ + execute: async () => { + try { + const selection = await this.getMCPServerSelection(await this.mcpServerManager.getServerNames()); + if (!selection) { + return; + } + this.mcpServerManager.startServer(selection); + const { tools } = await this.mcpServerManager.getTools(selection); + const toolRequests: ToolRequest[] = tools.map((tool: any) => this.convertToToolRequest(tool, selection)); + + for (const toolRequest of toolRequests) { + this.toolInvocationRegistry.registerTool(toolRequest); + } + } catch (error) { + console.error('Error while starting MCP server:', error); + } + } + })); + } + + convertToToolRequest(tool: any, serverName: string): ToolRequest { + const id = `mcp_${serverName}_${tool.name}`; + return { + id: id, + name: id, + providerName: `mcp_${serverName}`, + parameters: tool.inputSchema ? { + type: tool.inputSchema.type, + properties: tool.inputSchema.properties, + } : undefined, + description: tool.description, + handler: async (arg_string: string) => { + try { + return await this.mcpServerManager.callTool(serverName, tool.name, arg_string); + } catch (error) { + console.error(`Error in tool handler for ${tool.name} on server ${serverName}:`, error); + throw error; + } + }, + }; + } + +} diff --git a/packages/ai-mcp/src/browser/mcp-frontend-application-contribution.ts b/packages/ai-mcp/src/browser/mcp-frontend-application-contribution.ts new file mode 100644 index 0000000000000..a082947b11721 --- /dev/null +++ b/packages/ai-mcp/src/browser/mcp-frontend-application-contribution.ts @@ -0,0 +1,104 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +import { FrontendApplicationContribution, PreferenceService } from '@theia/core/lib/browser'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { MCPServerDescription, MCPServerManager } from '../common'; +import { MCP_SERVERS_PREF } from './mcp-preferences'; // Preference constant for MCP servers + +type MCPServerEntry = { + [name: string]: { + command: string; + args: string[]; + env?: { [key: string]: string }; + }; +}; + +@injectable() +export class McpFrontendApplicationContribution implements FrontendApplicationContribution { + + @inject(PreferenceService) + protected preferenceService: PreferenceService; + + @inject(MCPServerManager) + protected manager: MCPServerManager; + + protected prevServers: Map = new Map(); + + onStart(): void { + this.preferenceService.ready.then(() => { + const servers = this.preferenceService.get( + MCP_SERVERS_PREF, + {} + ); + this.syncServers(servers); + this.prevServers = this.convertToMap(servers); + + this.preferenceService.onPreferenceChanged(event => { + if (event.preferenceName === MCP_SERVERS_PREF) { + this.handleServerChanges(event.newValue as MCPServerEntry); + } + }); + }); + } + + protected handleServerChanges(newServers: MCPServerEntry): void { + const oldServers = this.prevServers; + const updatedServers = this.convertToMap(newServers); + + for (const [name] of oldServers) { + if (!updatedServers.has(name)) { + this.manager.removeServer(name); + } + } + + for (const [name, description] of updatedServers) { + const oldDescription = oldServers.get(name); + if (!oldDescription || JSON.stringify(oldDescription) !== JSON.stringify(description)) { + this.manager.addOrUpdateServer(description); + } + } + + this.prevServers = updatedServers; + } + + protected syncServers(servers: MCPServerEntry): void { + const updatedServers = this.convertToMap(servers); + + for (const [, description] of updatedServers) { + this.manager.addOrUpdateServer(description); + } + + for (const [name] of this.prevServers) { + if (!updatedServers.has(name)) { + this.manager.removeServer(name); + } + } + + this.prevServers = updatedServers; + } + + protected convertToMap(servers: MCPServerEntry): Map { + const map = new Map(); + Object.entries(servers).forEach(([name, description]) => { + map.set(name, { + name, + ...description, + env: description.env || undefined + }); + }); + return map; + } +} diff --git a/packages/ai-mcp/src/browser/mcp-frontend-module.ts b/packages/ai-mcp/src/browser/mcp-frontend-module.ts new file mode 100644 index 0000000000000..d040de0844580 --- /dev/null +++ b/packages/ai-mcp/src/browser/mcp-frontend-module.ts @@ -0,0 +1,33 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { CommandContribution } from '@theia/core'; +import { ContainerModule } from '@theia/core/shared/inversify'; +import { MCPCommandContribution } from './mcp-command-contribution'; +import { FrontendApplicationContribution, PreferenceContribution, RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser'; +import { MCPServerManager, MCPServerManagerPath } from '../common/mcp-server-manager'; +import { McpServersPreferenceSchema } from './mcp-preferences'; +import { McpFrontendApplicationContribution } from './mcp-frontend-application-contribution'; + +export default new ContainerModule(bind => { + bind(PreferenceContribution).toConstantValue({ schema: McpServersPreferenceSchema }); + bind(FrontendApplicationContribution).to(McpFrontendApplicationContribution).inSingletonScope(); + bind(CommandContribution).to(MCPCommandContribution); + bind(MCPServerManager).toDynamicValue(ctx => { + const connection = ctx.container.get(RemoteConnectionProvider); + return connection.createProxy(MCPServerManagerPath); + }).inSingletonScope(); +}); diff --git a/packages/ai-mcp/src/browser/mcp-preferences.ts b/packages/ai-mcp/src/browser/mcp-preferences.ts new file mode 100644 index 0000000000000..ef708e7db24e7 --- /dev/null +++ b/packages/ai-mcp/src/browser/mcp-preferences.ts @@ -0,0 +1,65 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { PreferenceSchema } from '@theia/core/lib/browser/preferences/preference-contribution'; + +export const MCP_SERVERS_PREF = 'ai-features.mcp.mcpServers'; + +export const McpServersPreferenceSchema: PreferenceSchema = { + type: 'object', + properties: { + [MCP_SERVERS_PREF]: { + type: 'object', + title: 'MCP Servers Configuration', + markdownDescription: 'Configure MCP servers with command and arguments. Each server is identified by a unique key, such as "sqlite" or "filesystem".\ + To start a server, use the "MCP: Start MCP Server" command, which enables you to select the desired server.\ + To stop a server, use the "MCP: Stop MCP Server" command.\ + \n\ + Example configuration:\n\ + ```\ + {\n\ + "sqlite": {\n\ + "command": "uvx",\n\ + "args": ["mcp-server-sqlite", "--db-path", "/Users/YOUR_USERNAME/test.db"]\n\ + },\n\ + "filesystem": {\n\ + "command": "npx",\n\ + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/YOUR_USERNAME/Desktop"]\n\ + }\n\ + }\ + ```', + additionalProperties: { + type: 'object', + properties: { + command: { + type: 'string', + title: 'Command to execute the MCP server', + markdownDescription: 'The command used to start the MCP server, e.g., "uvx" or "npx".' + }, + args: { + type: 'array', + title: 'Arguments for the command', + markdownDescription: 'An array of arguments to pass to the command.', + items: { + type: 'string' + } + } + }, + required: ['command', 'args'] + } + } + } +}; diff --git a/packages/ai-mcp/src/common/index.ts b/packages/ai-mcp/src/common/index.ts new file mode 100644 index 0000000000000..a282b9a44c96f --- /dev/null +++ b/packages/ai-mcp/src/common/index.ts @@ -0,0 +1,16 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +export * from './mcp-server-manager'; diff --git a/packages/ai-mcp/src/common/mcp-server-manager.ts b/packages/ai-mcp/src/common/mcp-server-manager.ts new file mode 100644 index 0000000000000..0cd2abc02296d --- /dev/null +++ b/packages/ai-mcp/src/common/mcp-server-manager.ts @@ -0,0 +1,49 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +export interface MCPServerManager { + callTool(serverName: string, toolName: any, arg_string: string): unknown; + removeServer(name: string): unknown; + addOrUpdateServer(description: MCPServerDescription): unknown; + getTools(serverName: string): Promise; + getServerNames(): Promise; + startServer(serverName: string): Promise; + stopServer(serverName: string): Promise; + getStartedServers(): Promise; +} +export interface MCPServerDescription { + /** + * The unique name of the MCP server. + */ + name: string; + + /** + * The command to execute the MCP server. + */ + command: string; + + /** + * An array of arguments to pass to the command. + */ + args: string[]; + /** + * Optional environment variables to set when starting the server. + */ + env?: { [key: string]: string }; +} + +export const MCPServerManager = Symbol('MCPServerManager'); + +export const MCPServerManagerPath = '/services/mcpservermanager'; diff --git a/packages/ai-mcp/src/node/mcp-backend-module.ts b/packages/ai-mcp/src/node/mcp-backend-module.ts new file mode 100644 index 0000000000000..5ea32d4fc240c --- /dev/null +++ b/packages/ai-mcp/src/node/mcp-backend-module.ts @@ -0,0 +1,31 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { ContainerModule } from '@theia/core/shared/inversify'; +import { ConnectionHandler, RpcConnectionHandler } from '@theia/core'; +import { MCPServerManagerImpl } from './mcp-server-manager-impl'; +import { MCPServerManager, MCPServerManagerPath } from '../common/mcp-server-manager'; + +export default new ContainerModule(bind => { + bind(MCPServerManager).to(MCPServerManagerImpl).inSingletonScope(); + bind(ConnectionHandler).toDynamicValue(ctx => new RpcConnectionHandler( + MCPServerManagerPath, + () => { + const service = ctx.container.get(MCPServerManager); + return service; + } + )).inSingletonScope(); +}); diff --git a/packages/ai-mcp/src/node/mcp-server-manager-impl.ts b/packages/ai-mcp/src/node/mcp-server-manager-impl.ts new file mode 100644 index 0000000000000..a7798c2aa7f10 --- /dev/null +++ b/packages/ai-mcp/src/node/mcp-server-manager-impl.ts @@ -0,0 +1,90 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** +import { injectable } from '@theia/core/shared/inversify'; +import { MCPServerDescription, MCPServerManager } from '../common/mcp-server-manager'; +import { MCPServer } from './mcp-server'; + +@injectable() +export class MCPServerManagerImpl implements MCPServerManager { + async stopServer(serverName: string): Promise { + const server = this.servers.get(serverName); + if (!server) { + throw new Error(`Server "${serverName}" not found.`); + } + server.stop(); + console.log(`Server "${serverName}" stopped.`); + } + + async getStartedServers(): Promise { + const startedServers: string[] = []; + for (const [name, server] of this.servers.entries()) { + if (server.isStarted()) { + startedServers.push(name); + } + } + return startedServers; + } + + callTool(serverName: string, toolName: string, arg_string: string): unknown { + const server = this.servers.get(serverName); + if (!server) { + throw new Error(`Server "${toolName}" not found.`); + } + return server.callTool(toolName, arg_string); + } + + async startServer(serverName: string): Promise { + const server = this.servers.get(serverName); + if (!server) { + throw new Error(`Server "${serverName}" not found.`); + } + await server.start(); + } + async getServerNames(): Promise { + return Array.from(this.servers.keys()); + } + + public async getTools(serverName: string): Promise { + const server = this.servers.get(serverName); + if (!server) { + throw new Error(`Server "${serverName}" not found.`); + } + return server.getTools(); + + } + private servers: Map = new Map(); + addOrUpdateServer(description: MCPServerDescription): void { + const { name, command, args, env } = description; + const existingServer = this.servers.get(name); + + if (existingServer) { + existingServer.update(command, args, env); + } else { + const newServer = new MCPServer(name, command, args, env); + this.servers.set(name, newServer); + } + } + + removeServer(name: string): void { + const server = this.servers.get(name); + if (server) { + server.stop(); + this.servers.delete(name); + } else { + console.warn(`Server "${name}" not found.`); + } + } +} diff --git a/packages/ai-mcp/src/node/mcp-server.ts b/packages/ai-mcp/src/node/mcp-server.ts new file mode 100644 index 0000000000000..f77a9ca2f39b2 --- /dev/null +++ b/packages/ai-mcp/src/node/mcp-server.ts @@ -0,0 +1,96 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// 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'; +export class MCPServer { + private name: string; + private command: string; + private args: string[]; + private client: Client; + private env?: { [key: string]: string }; + private started: boolean = false; + + constructor(name: string, command: string, args: string[], env?: Record) { + this.name = name; + this.command = command; + this.args = args; + this.env = env; + } + + isStarted(): boolean { + return this.started; + } + + async start(): Promise { + 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(([_, value]) => value !== undefined) as [string, string][] + ); + + const mergedEnv: Record = { + ...sanitizedEnv, + ...(this.env || {}) + }; + const transport = new StdioClientTransport({ + command: this.command, + args: this.args, + env: mergedEnv, + }); + transport.onerror = error => { + console.log('Error: ' + error); + }; + + this.client = new Client({ + name: 'example-client', + version: '1.0.0', + }, { + capabilities: {} + }); + this.client.onerror = error => { + console.log('Error Client: ' + error); + }; + + await this.client.connect(transport); + this.started = true; + } + + async callTool(toolName: string, arg_string: string): Promise { + const args = JSON.parse(arg_string); + const params = { + name: toolName, + arguments: args, + }; + return this.client.callTool(params); + } + + async getTools(): Promise { + return this.client.listTools(); + } + + update(command: string, args: string[], env?: { [key: string]: string }): void { + console.log(`Updating server "${this.name}" with new command: ${command} and args: ${args.join(' ')}`); + this.command = command; + this.args = args; + this.env = env; + } + + stop(): void { + console.log(`Stopping server "${this.name}"`); + this.client.close(); + this.started = false; + } +} diff --git a/packages/ai-mcp/src/package.spec.ts b/packages/ai-mcp/src/package.spec.ts new file mode 100644 index 0000000000000..0285e7159b30b --- /dev/null +++ b/packages/ai-mcp/src/package.spec.ts @@ -0,0 +1,28 @@ +// ***************************************************************************** +// Copyright (C) 2024 EclipseSource GmbH and others. +// +// This program and the accompanying materials are made available under the +// terms of the Eclipse Public License v. 2.0 which is available at +// http://www.eclipse.org/legal/epl-2.0. +// +// This Source Code may also be made available under the following Secondary +// Licenses when the conditions for such availability set forth in the Eclipse +// Public License v. 2.0 are satisfied: GNU General Public License, version 2 +// with the GNU Classpath Exception which is available at +// https://www.gnu.org/software/classpath/license.html. +// +// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 +// ***************************************************************************** + +/* note: this bogus test file is required so that + we are able to run mocha unit tests on this + package, without having any actual unit tests in it. + This way a coverage report will be generated, + showing 0% coverage, instead of no report. + This file can be removed once we have real unit + tests in place. */ + +describe('ai-mcp package', () => { + + it('support code coverage statistics', () => true); +}); diff --git a/packages/ai-mcp/tsconfig.json b/packages/ai-mcp/tsconfig.json new file mode 100644 index 0000000000000..420367fccbfb3 --- /dev/null +++ b/packages/ai-mcp/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../configs/base.tsconfig", + "compilerOptions": { + "composite": true, + "rootDir": "src", + "outDir": "lib" + }, + "include": [ + "src" + ], + "references": [ + { + "path": "../ai-core" + }, + { + "path": "../core" + } + ] +} diff --git a/tsconfig.json b/tsconfig.json index 72e54f2e87b78..2afcd04b2d53c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -75,6 +75,9 @@ { "path": "packages/ai-llamafile" }, + { + "path": "packages/ai-mcp" + }, { "path": "packages/ai-ollama" }, diff --git a/yarn.lock b/yarn.lock index 21d2692e9f2aa..386740b9bff39 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1234,6 +1234,15 @@ dependencies: cross-spawn "^7.0.1" +"@modelcontextprotocol/sdk@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.0.1.tgz#810684e7c11b7e056adab895edf3c4a1bc796205" + integrity sha512-slLdFaxQJ9AlRg+hw28iiTtGvShAOgOKXcD0F91nUcRYiOMuS9ZBYjcdNZRXW9G5JQ511GRTdUy1zQVZDpJ+4w== + dependencies: + content-type "^1.0.5" + raw-body "^3.0.0" + zod "^3.23.8" + "@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz#44d752c1a2dc113f15f781b7cc4f53a307e3fa38" @@ -4385,7 +4394,7 @@ content-disposition@0.5.4: dependencies: safe-buffer "5.2.1" -content-type@~1.0.4, content-type@~1.0.5: +content-type@^1.0.5, content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== @@ -10349,6 +10358,16 @@ raw-body@2.5.2, raw-body@^2.3.0: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f" + integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.6.3" + unpipe "1.0.0" + rc@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" From efa6e9abf7dd382f227ee07dca859f22a40da9c0 Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Wed, 11 Dec 2024 19:49:04 +0100 Subject: [PATCH 02/14] Update packages/ai-mcp/src/node/mcp-server-manager-impl.ts Co-authored-by: Stefan Dirix --- packages/ai-mcp/src/node/mcp-server-manager-impl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a7798c2aa7f10..77ef89d608faa 100644 --- a/packages/ai-mcp/src/node/mcp-server-manager-impl.ts +++ b/packages/ai-mcp/src/node/mcp-server-manager-impl.ts @@ -41,7 +41,7 @@ export class MCPServerManagerImpl implements MCPServerManager { callTool(serverName: string, toolName: string, arg_string: string): unknown { const server = this.servers.get(serverName); if (!server) { - throw new Error(`Server "${toolName}" not found.`); + throw new Error(`MCP server "${toolName}" not found.`); } return server.callTool(toolName, arg_string); } From aef00842c3707864cc6b2a024ad11de7f7ad3b5f Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Wed, 11 Dec 2024 19:49:12 +0100 Subject: [PATCH 03/14] Update packages/ai-mcp/src/node/mcp-server-manager-impl.ts Co-authored-by: Stefan Dirix --- packages/ai-mcp/src/node/mcp-server-manager-impl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 77ef89d608faa..0b03aa5e6cbea 100644 --- a/packages/ai-mcp/src/node/mcp-server-manager-impl.ts +++ b/packages/ai-mcp/src/node/mcp-server-manager-impl.ts @@ -49,7 +49,7 @@ export class MCPServerManagerImpl implements MCPServerManager { async startServer(serverName: string): Promise { const server = this.servers.get(serverName); if (!server) { - throw new Error(`Server "${serverName}" not found.`); + throw new Error(`MCP server "${serverName}" not found.`); } await server.start(); } From 327b404cbab1ab4431e1c67f1900327d3c99f8e8 Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Wed, 11 Dec 2024 19:49:32 +0100 Subject: [PATCH 04/14] Update packages/ai-mcp/src/node/mcp-server-manager-impl.ts Co-authored-by: Stefan Dirix --- packages/ai-mcp/src/node/mcp-server-manager-impl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0b03aa5e6cbea..f6b9f56380b9a 100644 --- a/packages/ai-mcp/src/node/mcp-server-manager-impl.ts +++ b/packages/ai-mcp/src/node/mcp-server-manager-impl.ts @@ -60,7 +60,7 @@ export class MCPServerManagerImpl implements MCPServerManager { public async getTools(serverName: string): Promise { const server = this.servers.get(serverName); if (!server) { - throw new Error(`Server "${serverName}" not found.`); + throw new Error(`MCP server "${serverName}" not found.`); } return server.getTools(); From cf3e9710f819831e43dc6c9dfdcfd3011beb0397 Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Wed, 11 Dec 2024 19:49:37 +0100 Subject: [PATCH 05/14] Update packages/ai-mcp/src/node/mcp-server-manager-impl.ts Co-authored-by: Stefan Dirix --- packages/ai-mcp/src/node/mcp-server-manager-impl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f6b9f56380b9a..c8a92a972c689 100644 --- a/packages/ai-mcp/src/node/mcp-server-manager-impl.ts +++ b/packages/ai-mcp/src/node/mcp-server-manager-impl.ts @@ -84,7 +84,7 @@ export class MCPServerManagerImpl implements MCPServerManager { server.stop(); this.servers.delete(name); } else { - console.warn(`Server "${name}" not found.`); + console.warn(`MCP server "${name}" not found.`); } } } From a331f13de0d87a1fb308f0959270d5a4f6c88bca Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Wed, 11 Dec 2024 19:53:30 +0100 Subject: [PATCH 06/14] Update packages/ai-mcp/package.json Co-authored-by: Stefan Dirix --- packages/ai-mcp/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ai-mcp/package.json b/packages/ai-mcp/package.json index e430ae3aba109..d33981e2ef8e5 100644 --- a/packages/ai-mcp/package.json +++ b/packages/ai-mcp/package.json @@ -12,8 +12,8 @@ }, "theiaExtensions": [ { - "frontend": "lib/browser/ai-mcp-frontend-module", - "backend": "lib/node/ai-mcp-backend-module" + "frontend": "lib/browser/mcp-frontend-module", + "backend": "lib/node/mcp-backend-module" } ], "keywords": [ From 8fb5bf4e8265012e6da2f14b9d7f4fe99f4034c8 Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Wed, 11 Dec 2024 19:56:31 +0100 Subject: [PATCH 07/14] Update packages/ai-mcp/src/browser/mcp-command-contribution.ts Co-authored-by: Stefan Dirix --- packages/ai-mcp/src/browser/mcp-command-contribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ai-mcp/src/browser/mcp-command-contribution.ts b/packages/ai-mcp/src/browser/mcp-command-contribution.ts index 0eb6aa333e1d5..5289f2b22751e 100644 --- a/packages/ai-mcp/src/browser/mcp-command-contribution.ts +++ b/packages/ai-mcp/src/browser/mcp-command-contribution.ts @@ -48,7 +48,7 @@ export class MCPCommandContribution implements CommandContribution { private async getMCPServerSelection(serverNames: String[]): Promise { if (!serverNames || serverNames.length === 0) { - this.messageService.error('No MCP Servers configured.'); + this.messageService.error('No MCP servers configured.'); return undefined; } const options = serverNames.map(mcpServerName => ({ label: mcpServerName as string })); From c0e74c553d4b4ce82a2c04b8b8631113a95f0466 Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Thu, 12 Dec 2024 09:06:19 +0100 Subject: [PATCH 08/14] Adressed review comments Signed-off-by: Jonas Helming --- .../src/browser/mcp-command-contribution.ts | 29 ++++++++++++++++--- .../ai-mcp/src/browser/mcp-preferences.ts | 28 ++++++++++++++---- .../src/node/mcp-server-manager-impl.ts | 9 ++++-- packages/ai-mcp/src/node/mcp-server.ts | 26 +++++++++++++---- 4 files changed, 74 insertions(+), 18 deletions(-) diff --git a/packages/ai-mcp/src/browser/mcp-command-contribution.ts b/packages/ai-mcp/src/browser/mcp-command-contribution.ts index 5289f2b22751e..2f5a8aca83b45 100644 --- a/packages/ai-mcp/src/browser/mcp-command-contribution.ts +++ b/packages/ai-mcp/src/browser/mcp-command-contribution.ts @@ -48,7 +48,6 @@ export class MCPCommandContribution implements CommandContribution { private async getMCPServerSelection(serverNames: String[]): Promise { if (!serverNames || serverNames.length === 0) { - this.messageService.error('No MCP servers configured.'); return undefined; } const options = serverNames.map(mcpServerName => ({ label: mcpServerName as string })); @@ -63,7 +62,12 @@ export class MCPCommandContribution implements CommandContribution { commandRegistry.registerCommand(StopMCPServer, this.commandHandlerFactory({ execute: async () => { try { - const selection = await this.getMCPServerSelection(await this.mcpServerManager.getStartedServers()); + const startedServers = await this.mcpServerManager.getStartedServers(); + if (!startedServers || startedServers.length === 0) { + this.messageService.error('No MCP servers running.'); + return; + } + const selection = await this.getMCPServerSelection(startedServers); if (!selection) { return; } @@ -78,7 +82,19 @@ export class MCPCommandContribution implements CommandContribution { commandRegistry.registerCommand(StartMCPServer, this.commandHandlerFactory({ execute: async () => { try { - const selection = await this.getMCPServerSelection(await this.mcpServerManager.getServerNames()); + const servers = await this.mcpServerManager.getServerNames(); + const startedServers = await this.mcpServerManager.getStartedServers(); + const startableServers = servers.filter(server => !startedServers.includes(server)); + if (!startableServers || startableServers.length === 0) { + if (startedServers && startedServers.length > 0) { + this.messageService.error('All MCP servers are already running.'); + } else { + this.messageService.error('No MCP servers configured.'); + } + return; + } + + const selection = await this.getMCPServerSelection(servers); if (!selection) { return; } @@ -89,7 +105,12 @@ export class MCPCommandContribution implements CommandContribution { for (const toolRequest of toolRequests) { this.toolInvocationRegistry.registerTool(toolRequest); } + const toolNames = tools.map((tool: any) => tool.name || 'Unnamed Tool').join(', '); + this.messageService.info( + `MCP server "${selection}" successfully started. Registered tools: ${toolNames || 'No tools available.'}` + ); } catch (error) { + this.messageService.error('An error occurred while starting the MCP server.'); console.error('Error while starting MCP server:', error); } } @@ -111,7 +132,7 @@ export class MCPCommandContribution implements CommandContribution { try { return await this.mcpServerManager.callTool(serverName, tool.name, arg_string); } catch (error) { - console.error(`Error in tool handler for ${tool.name} on server ${serverName}:`, error); + console.error(`Error in tool handler for ${tool.name} on MCP server ${serverName}:`, error); throw error; } }, diff --git a/packages/ai-mcp/src/browser/mcp-preferences.ts b/packages/ai-mcp/src/browser/mcp-preferences.ts index ef708e7db24e7..b3ff702defbf9 100644 --- a/packages/ai-mcp/src/browser/mcp-preferences.ts +++ b/packages/ai-mcp/src/browser/mcp-preferences.ts @@ -24,20 +24,30 @@ export const McpServersPreferenceSchema: PreferenceSchema = { [MCP_SERVERS_PREF]: { type: 'object', title: 'MCP Servers Configuration', - markdownDescription: 'Configure MCP servers with command and arguments. Each server is identified by a unique key, such as "sqlite" or "filesystem".\ + markdownDescription: 'Configure MCP servers with command, arguments and optionally environment variables. Each server is identified by a unique key, such as\ + "brave-search" or "filesystem".\ To start a server, use the "MCP: Start MCP Server" command, which enables you to select the desired server.\ To stop a server, use the "MCP: Stop MCP Server" command.\ \n\ Example configuration:\n\ ```\ {\n\ - "sqlite": {\n\ - "command": "uvx",\n\ - "args": ["mcp-server-sqlite", "--db-path", "/Users/YOUR_USERNAME/test.db"]\n\ + "brave-search": {\n\ + "command": "npx",\n\ + "args": [\n\ + "-y",\n\ + "@modelcontextprotocol/server-brave-search"\n\ + ],\n\ + "env": {\n\ + "BRAVE_API_KEY": "YOUR_API_KEY"\n\ + }\n\ },\n\ "filesystem": {\n\ "command": "npx",\n\ - "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/YOUR_USERNAME/Desktop"]\n\ + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/YOUR_USERNAME/Desktop"],\n\ + "env": {\n\ + "CUSTOM_ENV_VAR": "custom-value"\n\ + }\n\ }\n\ }\ ```', @@ -56,6 +66,14 @@ export const McpServersPreferenceSchema: PreferenceSchema = { items: { type: 'string' } + }, + env: { + type: 'object', + title: 'Environment variables', + markdownDescription: 'Optional environment variables to set for the server, such as an API key.', + additionalProperties: { + type: 'string' + } } }, required: ['command', 'args'] 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 c8a92a972c689..a7bc636353c16 100644 --- a/packages/ai-mcp/src/node/mcp-server-manager-impl.ts +++ b/packages/ai-mcp/src/node/mcp-server-manager-impl.ts @@ -19,13 +19,16 @@ import { MCPServer } from './mcp-server'; @injectable() export class MCPServerManagerImpl implements MCPServerManager { + + protected servers: Map = new Map(); + async stopServer(serverName: string): Promise { const server = this.servers.get(serverName); if (!server) { - throw new Error(`Server "${serverName}" not found.`); + throw new Error(`MCP server "${serverName}" not found.`); } server.stop(); - console.log(`Server "${serverName}" stopped.`); + console.log(`MCP server "${serverName}" stopped.`); } async getStartedServers(): Promise { @@ -65,7 +68,7 @@ export class MCPServerManagerImpl implements MCPServerManager { return server.getTools(); } - private servers: Map = new Map(); + addOrUpdateServer(description: MCPServerDescription): void { const { name, command, args, env } = description; const existingServer = this.servers.get(name); diff --git a/packages/ai-mcp/src/node/mcp-server.ts b/packages/ai-mcp/src/node/mcp-server.ts index f77a9ca2f39b2..48f771b9ce7c4 100644 --- a/packages/ai-mcp/src/node/mcp-server.ts +++ b/packages/ai-mcp/src/node/mcp-server.ts @@ -35,6 +35,9 @@ export class MCPServer { } async start(): Promise { + if (this.started) { + return; + } 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( @@ -51,17 +54,17 @@ export class MCPServer { env: mergedEnv, }); transport.onerror = error => { - console.log('Error: ' + error); + console.error('Error: ' + error); }; this.client = new Client({ - name: 'example-client', + name: 'theia-client', version: '1.0.0', }, { capabilities: {} }); this.client.onerror = error => { - console.log('Error Client: ' + error); + console.error('Error in MCP client: ' + error); }; await this.client.connect(transport); @@ -69,7 +72,16 @@ export class MCPServer { } async callTool(toolName: string, arg_string: string): Promise { - const args = JSON.parse(arg_string); + let args; + try { + args = JSON.parse(arg_string); + } catch (error) { + console.error( + `Failed to parse arguments for calling tool "${toolName}" in MCP server "${this.name}" with command "${this.command}". + Invalid JSON: ${arg_string}`, + error + ); + } const params = { name: toolName, arguments: args, @@ -82,14 +94,16 @@ export class MCPServer { } update(command: string, args: string[], env?: { [key: string]: string }): void { - console.log(`Updating server "${this.name}" with new command: ${command} and args: ${args.join(' ')}`); this.command = command; this.args = args; this.env = env; } stop(): void { - console.log(`Stopping server "${this.name}"`); + if (!this.started || !this.client) { + return; + } + console.log(`Stopping MCP server "${this.name}"`); this.client.close(); this.started = false; } From 1c3e23a3f93efe18adb101cf1086b79ea1a8dcdd Mon Sep 17 00:00:00 2001 From: Stefan Dirix Date: Thu, 12 Dec 2024 12:47:25 +0100 Subject: [PATCH 09/14] fix types, only use valid servers and make args optional --- .../mcp-frontend-application-contribution.ts | 58 ++++++++++++++----- .../ai-mcp/src/common/mcp-server-manager.ts | 21 +++++-- .../src/node/mcp-server-manager-impl.ts | 4 +- packages/ai-mcp/src/node/mcp-server.ts | 15 ++--- 4 files changed, 67 insertions(+), 31 deletions(-) diff --git a/packages/ai-mcp/src/browser/mcp-frontend-application-contribution.ts b/packages/ai-mcp/src/browser/mcp-frontend-application-contribution.ts index a082947b11721..73b6086cf74ce 100644 --- a/packages/ai-mcp/src/browser/mcp-frontend-application-contribution.ts +++ b/packages/ai-mcp/src/browser/mcp-frontend-application-contribution.ts @@ -13,19 +13,44 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** -import { FrontendApplicationContribution, PreferenceService } from '@theia/core/lib/browser'; +import { FrontendApplicationContribution, PreferenceProvider, PreferenceService } from '@theia/core/lib/browser'; import { inject, injectable } from '@theia/core/shared/inversify'; import { MCPServerDescription, MCPServerManager } from '../common'; -import { MCP_SERVERS_PREF } from './mcp-preferences'; // Preference constant for MCP servers - -type MCPServerEntry = { - [name: string]: { - command: string; - args: string[]; - env?: { [key: string]: string }; - }; +import { MCP_SERVERS_PREF } from './mcp-preferences'; +import { JSONObject } from '@theia/core/shared/@phosphor/coreutils'; + +interface MCPServersPreferenceValue { + command: string; + args?: string[]; + env?: { [key: string]: string }; +}; + +interface MCPServersPreference { + [name: string]: MCPServersPreferenceValue }; +namespace MCPServersPreference { + export function isValue(obj: unknown): obj is MCPServersPreferenceValue { + return !!obj && typeof obj === 'object' && + 'command' in obj && typeof obj.command === 'string' && + (!('args' in obj) || Array.isArray(obj.args) && obj.args.every(arg => typeof arg === 'string')) && + (!('env' in obj) || !!obj.env && typeof obj.env === 'object' && Object.values(obj.env).every(value => typeof value === 'string')); + } +} + +function filterValidValues(servers: unknown): MCPServersPreference { + const result: MCPServersPreference = {}; + if (!servers || typeof servers !== 'object') { + return result; + } + for (const [name, value] of Object.entries(servers)) { + if (typeof name === 'string' && MCPServersPreference.isValue(value)) { + result[name] = value; + } + } + return result; +} + @injectable() export class McpFrontendApplicationContribution implements FrontendApplicationContribution { @@ -39,22 +64,22 @@ export class McpFrontendApplicationContribution implements FrontendApplicationCo onStart(): void { this.preferenceService.ready.then(() => { - const servers = this.preferenceService.get( + const servers = filterValidValues(this.preferenceService.get( MCP_SERVERS_PREF, {} - ); + )); this.syncServers(servers); this.prevServers = this.convertToMap(servers); this.preferenceService.onPreferenceChanged(event => { if (event.preferenceName === MCP_SERVERS_PREF) { - this.handleServerChanges(event.newValue as MCPServerEntry); + this.handleServerChanges(filterValidValues(event.newValue)); } }); }); } - protected handleServerChanges(newServers: MCPServerEntry): void { + protected handleServerChanges(newServers: MCPServersPreference): void { const oldServers = this.prevServers; const updatedServers = this.convertToMap(newServers); @@ -66,7 +91,8 @@ export class McpFrontendApplicationContribution implements FrontendApplicationCo for (const [name, description] of updatedServers) { const oldDescription = oldServers.get(name); - if (!oldDescription || JSON.stringify(oldDescription) !== JSON.stringify(description)) { + // We know that that the descriptions are actual JSONObjects as we construct them ourselves + if (!oldDescription || !PreferenceProvider.deepEqual(oldDescription as unknown as JSONObject, description as unknown as JSONObject)) { this.manager.addOrUpdateServer(description); } } @@ -74,7 +100,7 @@ export class McpFrontendApplicationContribution implements FrontendApplicationCo this.prevServers = updatedServers; } - protected syncServers(servers: MCPServerEntry): void { + protected syncServers(servers: MCPServersPreference): void { const updatedServers = this.convertToMap(servers); for (const [, description] of updatedServers) { @@ -90,7 +116,7 @@ export class McpFrontendApplicationContribution implements FrontendApplicationCo this.prevServers = updatedServers; } - protected convertToMap(servers: MCPServerEntry): Map { + protected convertToMap(servers: MCPServersPreference): Map { const map = new Map(); Object.entries(servers).forEach(([name, description]) => { map.set(name, { diff --git a/packages/ai-mcp/src/common/mcp-server-manager.ts b/packages/ai-mcp/src/common/mcp-server-manager.ts index 0cd2abc02296d..6c35e74f9ce9d 100644 --- a/packages/ai-mcp/src/common/mcp-server-manager.ts +++ b/packages/ai-mcp/src/common/mcp-server-manager.ts @@ -13,16 +13,25 @@ // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** + +import type { Client } from '@modelcontextprotocol/sdk/client/index'; + +export interface MCPServer { + callTool(toolName: string, arg_string: string): ReturnType; + getTools(): ReturnType; +} + export interface MCPServerManager { - callTool(serverName: string, toolName: any, arg_string: string): unknown; - removeServer(name: string): unknown; - addOrUpdateServer(description: MCPServerDescription): unknown; - getTools(serverName: string): Promise; + callTool(serverName: string, toolName: string, arg_string: string): ReturnType; + removeServer(name: string): void; + addOrUpdateServer(description: MCPServerDescription): void; + getTools(serverName: string): ReturnType getServerNames(): Promise; startServer(serverName: string): Promise; stopServer(serverName: string): Promise; getStartedServers(): Promise; } + export interface MCPServerDescription { /** * The unique name of the MCP server. @@ -37,7 +46,8 @@ export interface MCPServerDescription { /** * An array of arguments to pass to the command. */ - args: string[]; + args?: string[]; + /** * Optional environment variables to set when starting the server. */ @@ -45,5 +55,4 @@ export interface MCPServerDescription { } export const MCPServerManager = Symbol('MCPServerManager'); - export const MCPServerManagerPath = '/services/mcpservermanager'; 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 a7bc636353c16..06f869f93be73 100644 --- a/packages/ai-mcp/src/node/mcp-server-manager-impl.ts +++ b/packages/ai-mcp/src/node/mcp-server-manager-impl.ts @@ -41,7 +41,7 @@ export class MCPServerManagerImpl implements MCPServerManager { return startedServers; } - callTool(serverName: string, toolName: string, arg_string: string): unknown { + callTool(serverName: string, toolName: string, arg_string: string): ReturnType { const server = this.servers.get(serverName); if (!server) { throw new Error(`MCP server "${toolName}" not found.`); @@ -60,7 +60,7 @@ export class MCPServerManagerImpl implements MCPServerManager { return Array.from(this.servers.keys()); } - public async getTools(serverName: string): Promise { + public async getTools(serverName: string): ReturnType { const server = this.servers.get(serverName); if (!server) { throw new Error(`MCP server "${serverName}" not found.`); diff --git a/packages/ai-mcp/src/node/mcp-server.ts b/packages/ai-mcp/src/node/mcp-server.ts index 48f771b9ce7c4..52a7932762df7 100644 --- a/packages/ai-mcp/src/node/mcp-server.ts +++ b/packages/ai-mcp/src/node/mcp-server.ts @@ -15,15 +15,16 @@ // ***************************************************************************** import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; + export class MCPServer { private name: string; private command: string; - private args: string[]; + private args?: string[]; private client: Client; private env?: { [key: string]: string }; private started: boolean = false; - constructor(name: string, command: string, args: string[], env?: Record) { + constructor(name: string, command: string, args?: string[], env?: Record) { this.name = name; this.command = command; this.args = args; @@ -38,10 +39,10 @@ export class MCPServer { if (this.started) { return; } - console.log(`Starting server "${this.name}" with command: ${this.command} and args: ${this.args.join(' ')} and env: ${JSON.stringify(this.env)}`); + 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(([_, value]) => value !== undefined) as [string, string][] + Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined) ); const mergedEnv: Record = { @@ -71,7 +72,7 @@ export class MCPServer { this.started = true; } - async callTool(toolName: string, arg_string: string): Promise { + async callTool(toolName: string, arg_string: string): ReturnType { let args; try { args = JSON.parse(arg_string); @@ -89,11 +90,11 @@ export class MCPServer { return this.client.callTool(params); } - async getTools(): Promise { + async getTools(): ReturnType { return this.client.listTools(); } - update(command: string, args: string[], env?: { [key: string]: string }): void { + update(command: string, args?: string[], env?: { [key: string]: string }): void { this.command = command; this.args = args; this.env = env; From 1c2f2285f84b4bb024ca62835e2243208946f55d Mon Sep 17 00:00:00 2001 From: Stefan Dirix Date: Thu, 12 Dec 2024 13:09:54 +0100 Subject: [PATCH 10/14] Apply suggestions from code review --- .../ai-core/src/common/tool-invocation-registry.ts | 2 +- packages/ai-mcp/README.md | 14 +++++++------- .../ai-mcp/src/browser/mcp-command-contribution.ts | 9 +++------ 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/ai-core/src/common/tool-invocation-registry.ts b/packages/ai-core/src/common/tool-invocation-registry.ts index baeac82f725b4..0c11938e7d5eb 100644 --- a/packages/ai-core/src/common/tool-invocation-registry.ts +++ b/packages/ai-core/src/common/tool-invocation-registry.ts @@ -41,7 +41,7 @@ export interface ToolInvocationRegistry { getFunction(toolId: string): ToolRequest | undefined; /** - * Retrieves multiple `ToolRequest`s configurations from the registry. + * Retrieves multiple `ToolRequest`s from the registry. * * @param toolIds - A list of tool IDs to retrieve. * @returns An array of `ToolRequest` objects for the specified tool IDs. diff --git a/packages/ai-mcp/README.md b/packages/ai-mcp/README.md index 271efd4a45050..5ba0d1aa63923 100644 --- a/packages/ai-mcp/README.md +++ b/packages/ai-mcp/README.md @@ -1,9 +1,9 @@ # Model Context Server (MCP) Integration -The AI MCP package provides an integration that allows users to start and use MCP Servers to provide additional tool functions to LLMs, e.g. search or file access (outside of the workspace). +The AI MCP package provides an integration that allows users to start and use MCP servers to provide additional tool functions to LLMs, e.g. search or file access (outside of the workspace). ## Features -- Add MCP Servers via settings.json +- Add MCP servers via settings.json - Start and stop MCP servers. - Use tool functions provided by MCP servers in prompt templates @@ -23,15 +23,15 @@ The AI MCP package provides an integration that allows users to start and use MC ## Usage -1. **Starting a Llamafile Language Server:** +1. **Starting a MCP Server:** - Use the command palette to invoke `MCP: Start MCP Server`. - - A quick pick menu will appear with a list of configured MCP Servers. + - A quick pick menu will appear with a list of configured MCP servers. - Select a server to start. -2. **Stopping a Llamafile Language Server:** +2. **Stopping a MCP Server:** - Use the command palette to invoke `MCP: Stop MCP Server`. - - A quick pick menu will display a list of currently running MCP Servers. + - A quick pick menu will display a list of currently running MCP servers. - Select a server to stop. 3. **Using provided tool functions** @@ -41,7 +41,7 @@ The AI MCP package provides an integration that allows users to start and use MC ## Configuration -Make sure to configure your MCP Servers properly within the preference settings. +Make sure to configure your MCP servers properly within the preference settings. Example Configuration: diff --git a/packages/ai-mcp/src/browser/mcp-command-contribution.ts b/packages/ai-mcp/src/browser/mcp-command-contribution.ts index 2f5a8aca83b45..ccaaca1432b25 100644 --- a/packages/ai-mcp/src/browser/mcp-command-contribution.ts +++ b/packages/ai-mcp/src/browser/mcp-command-contribution.ts @@ -46,16 +46,13 @@ export class MCPCommandContribution implements CommandContribution { @inject(ToolInvocationRegistry) protected readonly toolInvocationRegistry: ToolInvocationRegistry; - private async getMCPServerSelection(serverNames: String[]): Promise { + private async getMCPServerSelection(serverNames: string[]): Promise { if (!serverNames || serverNames.length === 0) { return undefined; } - const options = serverNames.map(mcpServerName => ({ label: mcpServerName as string })); + const options = serverNames.map(mcpServerName => ({ label: mcpServerName })); const result = await this.quickInputService.showQuickPick(options); - if (!result) { - return undefined; - } - return result.label; + return result?.label; } registerCommands(commandRegistry: CommandRegistry): void { From 62487853fd08eaede37e20c46f560216c8819e2d Mon Sep 17 00:00:00 2001 From: Stefan Dirix Date: Thu, 12 Dec 2024 14:49:39 +0100 Subject: [PATCH 11/14] replace remaining any types, use string instead of String --- packages/ai-core/src/common/language-model.ts | 21 ++++++++++++++++++- .../src/browser/mcp-command-contribution.ts | 11 ++++++---- .../ai-mcp/src/common/mcp-server-manager.ts | 4 ++-- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/ai-core/src/common/language-model.ts b/packages/ai-core/src/common/language-model.ts index ab0c3e104595c..7e2e5847ad651 100644 --- a/packages/ai-core/src/common/language-model.ts +++ b/packages/ai-core/src/common/language-model.ts @@ -32,14 +32,33 @@ export const isLanguageModelRequestMessage = (obj: unknown): obj is LanguageMode 'query' in obj && typeof (obj as { query: unknown }).query === 'string' ); +export type ToolRequestParametersProperties = Record; +export interface ToolRequestParameters { + type?: 'object'; + properties: ToolRequestParametersProperties +} export interface ToolRequest { id: string; name: string; - parameters?: { type?: 'object', properties: Record }; + parameters?: ToolRequestParameters description?: string; handler: (arg_string: string) => Promise; providerName?: string; } + +export namespace ToolRequest { + export function isToolRequestParametersProperties(obj: unknown): obj is ToolRequestParametersProperties { + return !!obj && typeof obj === 'object' && + 'key' in obj && typeof obj.key === 'string' && + Object.keys(obj).every(key => typeof key === 'string'); + } + export function isToolRequestParameters(obj: unknown): obj is ToolRequestParameters { + return !!obj && typeof obj === 'object' && + (!('type' in obj) || obj.type === 'object') && + 'properties' in obj && isToolRequestParametersProperties(obj.properties); + } +} + export interface LanguageModelRequest { messages: LanguageModelRequestMessage[], tools?: ToolRequest[]; diff --git a/packages/ai-mcp/src/browser/mcp-command-contribution.ts b/packages/ai-mcp/src/browser/mcp-command-contribution.ts index ccaaca1432b25..ed5b418ae158e 100644 --- a/packages/ai-mcp/src/browser/mcp-command-contribution.ts +++ b/packages/ai-mcp/src/browser/mcp-command-contribution.ts @@ -20,6 +20,8 @@ import { inject, injectable } from '@theia/core/shared/inversify'; import { MCPServerManager } from '../common/mcp-server-manager'; import { ToolInvocationRegistry, ToolRequest } from '@theia/ai-core'; +type MCPTool = Awaited>['tools'][number]; + export const StartMCPServer = { id: 'mcp.startserver', label: 'MCP: Start MCP Server', @@ -97,12 +99,12 @@ export class MCPCommandContribution implements CommandContribution { } this.mcpServerManager.startServer(selection); const { tools } = await this.mcpServerManager.getTools(selection); - const toolRequests: ToolRequest[] = tools.map((tool: any) => this.convertToToolRequest(tool, selection)); + const toolRequests: ToolRequest[] = tools.map(tool => this.convertToToolRequest(tool, selection)); for (const toolRequest of toolRequests) { this.toolInvocationRegistry.registerTool(toolRequest); } - const toolNames = tools.map((tool: any) => tool.name || 'Unnamed Tool').join(', '); + const toolNames = tools.map(tool => tool.name || 'Unnamed Tool').join(', '); this.messageService.info( `MCP server "${selection}" successfully started. Registered tools: ${toolNames || 'No tools available.'}` ); @@ -114,13 +116,14 @@ export class MCPCommandContribution implements CommandContribution { })); } - convertToToolRequest(tool: any, serverName: string): ToolRequest { + convertToToolRequest(tool: MCPTool, serverName: string): ToolRequest { const id = `mcp_${serverName}_${tool.name}`; + return { id: id, name: id, providerName: `mcp_${serverName}`, - parameters: tool.inputSchema ? { + parameters: ToolRequest.isToolRequestParameters(tool.inputSchema) ? { type: tool.inputSchema.type, properties: tool.inputSchema.properties, } : undefined, diff --git a/packages/ai-mcp/src/common/mcp-server-manager.ts b/packages/ai-mcp/src/common/mcp-server-manager.ts index 6c35e74f9ce9d..e316a644a27c6 100644 --- a/packages/ai-mcp/src/common/mcp-server-manager.ts +++ b/packages/ai-mcp/src/common/mcp-server-manager.ts @@ -26,10 +26,10 @@ export interface MCPServerManager { removeServer(name: string): void; addOrUpdateServer(description: MCPServerDescription): void; getTools(serverName: string): ReturnType - getServerNames(): Promise; + getServerNames(): Promise; startServer(serverName: string): Promise; stopServer(serverName: string): Promise; - getStartedServers(): Promise; + getStartedServers(): Promise; } export interface MCPServerDescription { From 1641ecde29678d41cdc98788af6fe3ef1dca5a47 Mon Sep 17 00:00:00 2001 From: Stefan Dirix Date: Thu, 12 Dec 2024 15:05:12 +0100 Subject: [PATCH 12/14] typeguard fix --- packages/ai-core/src/common/language-model.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/ai-core/src/common/language-model.ts b/packages/ai-core/src/common/language-model.ts index 7e2e5847ad651..21513946c99ec 100644 --- a/packages/ai-core/src/common/language-model.ts +++ b/packages/ai-core/src/common/language-model.ts @@ -49,8 +49,9 @@ export interface ToolRequest { export namespace ToolRequest { export function isToolRequestParametersProperties(obj: unknown): obj is ToolRequestParametersProperties { return !!obj && typeof obj === 'object' && - 'key' in obj && typeof obj.key === 'string' && - Object.keys(obj).every(key => typeof key === 'string'); + 'type' in obj && typeof obj.type === 'string' && + Object.entries(obj).every(([key, value]) => typeof key === 'string' && + !!value && typeof value === 'object' && 'type' in value && Object.keys(value).every(k => typeof k === 'string')); } export function isToolRequestParameters(obj: unknown): obj is ToolRequestParameters { return !!obj && typeof obj === 'object' && From 2e9236c3e3f5742d72a42a7d7fc192ed1708506c Mon Sep 17 00:00:00 2001 From: Jonas Helming Date: Thu, 12 Dec 2024 22:40:12 +0100 Subject: [PATCH 13/14] Fix two remaining bugs: 1. properties for valid functions are rejected 2. Started servers are not filtered Signed-off-by: Jonas Helming --- packages/ai-core/src/common/language-model.ts | 15 +++++++++++---- .../src/browser/mcp-command-contribution.ts | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/ai-core/src/common/language-model.ts b/packages/ai-core/src/common/language-model.ts index 21513946c99ec..f4c716270be99 100644 --- a/packages/ai-core/src/common/language-model.ts +++ b/packages/ai-core/src/common/language-model.ts @@ -48,10 +48,17 @@ export interface ToolRequest { export namespace ToolRequest { export function isToolRequestParametersProperties(obj: unknown): obj is ToolRequestParametersProperties { - return !!obj && typeof obj === 'object' && - 'type' in obj && typeof obj.type === 'string' && - Object.entries(obj).every(([key, value]) => typeof key === 'string' && - !!value && typeof value === 'object' && 'type' in value && Object.keys(value).every(k => typeof k === 'string')); + if (!obj || typeof obj !== 'object') { return false; }; + + return Object.entries(obj).every(([key, value]) => + typeof key === 'string' && + value && + typeof value === 'object' && + 'type' in value && + // eslint-disable-next-line @typescript-eslint/no-explicit-any + typeof (value as any).type === 'string' && + Object.keys(value).every(k => typeof k === 'string') + ); } export function isToolRequestParameters(obj: unknown): obj is ToolRequestParameters { return !!obj && typeof obj === 'object' && diff --git a/packages/ai-mcp/src/browser/mcp-command-contribution.ts b/packages/ai-mcp/src/browser/mcp-command-contribution.ts index ed5b418ae158e..ae1921d35b4bd 100644 --- a/packages/ai-mcp/src/browser/mcp-command-contribution.ts +++ b/packages/ai-mcp/src/browser/mcp-command-contribution.ts @@ -93,7 +93,7 @@ export class MCPCommandContribution implements CommandContribution { return; } - const selection = await this.getMCPServerSelection(servers); + const selection = await this.getMCPServerSelection(startableServers); if (!selection) { return; } From 059624057678f63311272c3e2e570d8a974364ed Mon Sep 17 00:00:00 2001 From: Stefan Dirix Date: Fri, 13 Dec 2024 10:27:36 +0100 Subject: [PATCH 14/14] remove casting in type check and protect against deepEqual fail --- packages/ai-core/src/common/language-model.ts | 3 +-- .../mcp-frontend-application-contribution.ts | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/ai-core/src/common/language-model.ts b/packages/ai-core/src/common/language-model.ts index f4c716270be99..067e915bba4e4 100644 --- a/packages/ai-core/src/common/language-model.ts +++ b/packages/ai-core/src/common/language-model.ts @@ -55,8 +55,7 @@ export namespace ToolRequest { value && typeof value === 'object' && 'type' in value && - // eslint-disable-next-line @typescript-eslint/no-explicit-any - typeof (value as any).type === 'string' && + typeof value.type === 'string' && Object.keys(value).every(k => typeof k === 'string') ); } diff --git a/packages/ai-mcp/src/browser/mcp-frontend-application-contribution.ts b/packages/ai-mcp/src/browser/mcp-frontend-application-contribution.ts index 73b6086cf74ce..f972ffa7e8f40 100644 --- a/packages/ai-mcp/src/browser/mcp-frontend-application-contribution.ts +++ b/packages/ai-mcp/src/browser/mcp-frontend-application-contribution.ts @@ -91,8 +91,19 @@ export class McpFrontendApplicationContribution implements FrontendApplicationCo for (const [name, description] of updatedServers) { const oldDescription = oldServers.get(name); - // We know that that the descriptions are actual JSONObjects as we construct them ourselves - if (!oldDescription || !PreferenceProvider.deepEqual(oldDescription as unknown as JSONObject, description as unknown as JSONObject)) { + let diff = false; + try { + // We know that that the descriptions are actual JSONObjects as we construct them ourselves + if (!oldDescription || !PreferenceProvider.deepEqual(oldDescription as unknown as JSONObject, description as unknown as JSONObject)) { + diff = true; + } + } catch (e) { + // In some cases the deepEqual function throws an error, so we fall back to assuming that there is a difference + // This seems to happen in cases where the objects are structured differently, e.g. whole sub-objects are missing + console.debug('Failed to compare MCP server descriptions, assuming a difference', e); + diff = true; + } + if (diff) { this.manager.addOrUpdateServer(description); } }