diff --git a/package-lock.json b/package-lock.json index 098c30019f0aa..58d5d6403556e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31664,6 +31664,7 @@ "version": "1.62.0", "license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0", "dependencies": { + "@theia/ai-mcp": "1.62.0", "@theia/bulk-edit": "1.62.0", "@theia/callhierarchy": "1.62.0", "@theia/console": "1.62.0", diff --git a/packages/plugin-ext/package.json b/packages/plugin-ext/package.json index 32435712eb800..c2023e7daa613 100644 --- a/packages/plugin-ext/package.json +++ b/packages/plugin-ext/package.json @@ -5,6 +5,7 @@ "main": "lib/common/index.js", "typings": "lib/common/index.d.ts", "dependencies": { + "@theia/ai-mcp": "1.62.0", "@theia/bulk-edit": "1.62.0", "@theia/callhierarchy": "1.62.0", "@theia/console": "1.62.0", diff --git a/packages/plugin-ext/src/common/lm-protocol.ts b/packages/plugin-ext/src/common/lm-protocol.ts new file mode 100644 index 0000000000000..2fa44b7da71ab --- /dev/null +++ b/packages/plugin-ext/src/common/lm-protocol.ts @@ -0,0 +1,137 @@ +// ***************************************************************************** +// Copyright (C) 2025 EclipseSource. +// +// 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 { UriComponents } from './uri-components'; + +/** + * Protocol interfaces for MCP server definition providers. + */ + +export interface McpStdioServerDefinitionDto { + /** + * The human-readable name of the server. + */ + readonly label: string; + + /** + * The working directory used to start the server. + */ + cwd?: UriComponents; + + /** + * The command used to start the server. Node.js-based servers may use + * `process.execPath` to use the editor's version of Node.js to run the script. + */ + command: string; + + /** + * Additional command-line arguments passed to the server. + */ + args?: string[]; + + /** + * Optional additional environment information for the server. Variables + * in this environment will overwrite or remove (if null) the default + * environment variables of the editor's extension host. + */ + env?: Record; + + /** + * Optional version identification for the server. If this changes, the + * editor will indicate that tools have changed and prompt to refresh them. + */ + version?: string; + +} + +/** + * McpHttpServerDefinition represents an MCP server available using the + * Streamable HTTP transport. + */ +export interface McpHttpServerDefinitionDto { + /** + * The human-readable name of the server. + */ + readonly label: string; + + /** + * The URI of the server. The editor will make a POST request to this URI + * to begin each session. + */ + uri: UriComponents; + + /** + * Optional additional heads included with each request to the server. + */ + headers?: Record; + + /** + * Optional version identification for the server. If this changes, the + * editor will indicate that tools have changed and prompt to refresh them. + */ + version?: string; +} + +/** + * Definitions that describe different types of Model Context Protocol servers, + * which can be returned from the {@link McpServerDefinitionProvider}. + */ +export type McpServerDefinitionDto = McpStdioServerDefinitionDto | McpHttpServerDefinitionDto; +export const isMcpHttpServerDefinitionDto = (definition: McpServerDefinitionDto): definition is McpHttpServerDefinitionDto => 'uri' in definition; +/** + * Main side of the MCP server definition registry. + */ +export interface McpServerDefinitionRegistryMain { + /** + * Register an MCP server definition provider. + */ + $registerMcpServerDefinitionProvider(handle: number, name: string): void; + + /** + * Unregister an MCP server definition provider. + */ + $unregisterMcpServerDefinitionProvider(handle: number): void; + + /** + * Notify that server definitions have changed. + */ + $onDidChangeMcpServerDefinitions(handle: number): void; + + /** + * Get server definitions from a provider. + */ + $getServerDefinitions(handle: number): Promise; + + /** + * Resolve a server definition. + */ + $resolveServerDefinition(handle: number, server: McpServerDefinitionDto): Promise; +} + +/** + * Extension side of the MCP server definition registry. + */ +export interface McpServerDefinitionRegistryExt { + /** + * Request server definitions from a provider. + */ + $provideServerDefinitions(handle: number): Promise; + + /** + * Resolve a server definition from a provider. + */ + $resolveServerDefinition(handle: number, server: McpServerDefinitionDto): Promise; +} diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index 990aa62e159bf..2a1f7d1490f47 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -127,6 +127,7 @@ import { AccessibilityInformation } from '@theia/core/lib/common/accessibility'; import { TreeDelta } from '@theia/test/lib/common/tree-delta'; import { TestItemDTO, TestOutputDTO, TestRunDTO, TestRunProfileDTO, TestRunRequestDTO, TestStateChangeDTO } from './test-types'; import { ArgumentProcessor } from './commands'; +import { McpServerDefinitionRegistryMain, McpServerDefinitionRegistryExt } from './lm-protocol'; export interface PreferenceData { [scope: number]: any; @@ -2346,7 +2347,8 @@ export const PLUGIN_RPC_CONTEXT = { TELEMETRY_MAIN: createProxyIdentifier('TelemetryMain'), LOCALIZATION_MAIN: createProxyIdentifier('LocalizationMain'), TESTING_MAIN: createProxyIdentifier('TestingMain'), - URI_MAIN: createProxyIdentifier('UriMain') + URI_MAIN: createProxyIdentifier('UriMain'), + MCP_SERVER_DEFINITION_REGISTRY_MAIN: createProxyIdentifier('McpServerDefinitionRegistryMain') }; export const MAIN_RPC_CONTEXT = { @@ -2389,7 +2391,8 @@ export const MAIN_RPC_CONTEXT = { TABS_EXT: createProxyIdentifier('TabsExt'), TELEMETRY_EXT: createProxyIdentifier('TelemetryExt)'), TESTING_EXT: createProxyIdentifier('TestingExt'), - URI_EXT: createProxyIdentifier('UriExt') + URI_EXT: createProxyIdentifier('UriExt'), + MCP_SERVER_DEFINITION_REGISTRY_EXT: createProxyIdentifier('McpServerDefinitionRegistryExt') }; export interface TasksExt { diff --git a/packages/plugin-ext/src/common/plugin-protocol.ts b/packages/plugin-ext/src/common/plugin-protocol.ts index 41dabc1c62f4a..8690e303e65f5 100644 --- a/packages/plugin-ext/src/common/plugin-protocol.ts +++ b/packages/plugin-ext/src/common/plugin-protocol.ts @@ -104,6 +104,7 @@ export interface PluginPackageContribution { notebooks?: PluginPackageNotebook[]; notebookRenderer?: PluginNotebookRendererContribution[]; notebookPreload?: PluginPackageNotebookPreload[]; + mcpServerDefinitionProviders?: PluginPackageMcpServerDefinitionProviderContribution[]; } export interface PluginPackageNotebook { @@ -126,6 +127,12 @@ export interface PluginPackageNotebookPreload { entrypoint: string; } +export interface PluginPackageMcpServerDefinitionProviderContribution { + id: string; + label: string; + description?: string; +} + export interface PluginPackageAuthenticationProvider { id: string; label: string; diff --git a/packages/plugin-ext/src/hosted/node/plugin-host-module.ts b/packages/plugin-ext/src/hosted/node/plugin-host-module.ts index e633f6d403738..5c9288969dbaf 100644 --- a/packages/plugin-ext/src/hosted/node/plugin-host-module.ts +++ b/packages/plugin-ext/src/hosted/node/plugin-host-module.ts @@ -36,6 +36,7 @@ import { WebviewsExtImpl } from '../../plugin/webviews'; import { TerminalServiceExtImpl } from '../../plugin/terminal-ext'; import { InternalSecretsExt, SecretsExtImpl } from '../../plugin/secrets-ext'; import { setupPluginHostLogger } from './plugin-host-logger'; +import { LmExtImpl } from '../../plugin/lm-ext'; export default new ContainerModule(bind => { const channel = new IPCChannel(); @@ -63,6 +64,7 @@ export default new ContainerModule(bind => { bind(SecretsExtImpl).toSelf().inSingletonScope(); bind(PreferenceRegistryExtImpl).toSelf().inSingletonScope(); bind(DebugExtImpl).toSelf().inSingletonScope(); + bind(LmExtImpl).toSelf().inSingletonScope(); bind(EditorsAndDocumentsExtImpl).toSelf().inSingletonScope(); bind(WorkspaceExtImpl).toSelf().inSingletonScope(); bind(MessageRegistryExt).toSelf().inSingletonScope(); diff --git a/packages/plugin-ext/src/main/browser/lm-main.ts b/packages/plugin-ext/src/main/browser/lm-main.ts new file mode 100644 index 0000000000000..6d1ae722937a1 --- /dev/null +++ b/packages/plugin-ext/src/main/browser/lm-main.ts @@ -0,0 +1,136 @@ +// ***************************************************************************** +// Copyright (C) 2025 EclipseSource +// +// 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 { interfaces } from '@theia/core/shared/inversify'; +import { RPCProtocol } from '../../common/rpc-protocol'; +import { + McpServerDefinitionRegistryMain, + McpServerDefinitionRegistryExt, + McpServerDefinitionDto, + isMcpHttpServerDefinitionDto, +} from '../../common/lm-protocol'; +import { MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc'; +import { MCPServerManager, MCPServerDescription } from '@theia/ai-mcp/lib/common'; + +export class McpServerDefinitionRegistryMainImpl implements McpServerDefinitionRegistryMain { + private readonly proxy: McpServerDefinitionRegistryExt; + private readonly providers = new Map(); + private readonly mcpServerManager: MCPServerManager | undefined; + + constructor( + rpc: RPCProtocol, + container: interfaces.Container + ) { + this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.MCP_SERVER_DEFINITION_REGISTRY_EXT); + try { + this.mcpServerManager = container.get(MCPServerManager); + } catch { + // MCP Server Manager is optional + this.mcpServerManager = undefined; + } + } + + $registerMcpServerDefinitionProvider(handle: number, name: string): void { + this.providers.set(handle, name); + this.loadServerDefinitions(handle); + } + + $unregisterMcpServerDefinitionProvider(handle: number): void { + if (!this.mcpServerManager) { + console.warn('MCP Server Manager not available - MCP server definitions will not be loaded'); + return; + } + const provider = this.providers.get(handle); + if (!provider) { + console.warn(`No MCP Server provider found for handle '${handle}' - MCP server definitions will not be loaded`); + return; + } + this.mcpServerManager.removeServer(provider); + this.providers.delete(handle); + } + + $onDidChangeMcpServerDefinitions(handle: number): void { + // Reload server definitions when provider reports changes + this.loadServerDefinitions(handle); + } + + async $getServerDefinitions(handle: number): Promise { + try { + return await this.proxy.$provideServerDefinitions(handle); + } catch (error) { + console.error('Error getting MCP server definitions:', error); + return []; + } + } + + async $resolveServerDefinition(handle: number, server: McpServerDefinitionDto): Promise { + try { + return await this.proxy.$resolveServerDefinition(handle, server); + } catch (error) { + console.error('Error resolving MCP server definition:', error); + return server; + } + } + + private async loadServerDefinitions(handle: number): Promise { + if (!this.mcpServerManager) { + console.warn('MCP Server Manager not available - MCP server definitions will not be loaded'); + return; + } + + try { + const definitions = await this.$getServerDefinitions(handle); + + for (const definition of definitions) { + const resolved = await this.$resolveServerDefinition(handle, definition); + if (resolved) { + const mcpServerDescription = this.convertToMcpServerDescription(resolved); + this.mcpServerManager.addOrUpdateServer(mcpServerDescription); + } + } + } catch (error) { + console.error('Error loading MCP server definitions:', error); + } + } + + private convertToMcpServerDescription(definition: McpServerDefinitionDto): MCPServerDescription { + if (isMcpHttpServerDefinitionDto(definition)) { + // For HTTP servers, we would need to create a bridge or adapter + // For now, we'll create a placeholder stdio server that could proxy to HTTP + console.warn(`HTTP transport not yet supported for MCP server '${definition.label}'. Skipping.`); + throw new Error(`HTTP transport not yet supported for MCP server '${definition.label}'`); + } + + // Convert env values to strings, filtering out null values + let convertedEnv: Record | undefined; + if (definition.env) { + convertedEnv = {}; + for (const [key, value] of Object.entries(definition.env)) { + if (value !== null) { + convertedEnv[key] = String(value); + } + } + } + + return { + name: definition.label, + command: definition.command!, + args: definition.args, + env: convertedEnv, + autostart: false, // Extensions should manage their own server lifecycle + }; + } +} diff --git a/packages/plugin-ext/src/main/browser/main-context.ts b/packages/plugin-ext/src/main/browser/main-context.ts index 9a5e33cc56b0b..20df695070185 100644 --- a/packages/plugin-ext/src/main/browser/main-context.ts +++ b/packages/plugin-ext/src/main/browser/main-context.ts @@ -67,6 +67,7 @@ import { TestingMainImpl } from './test-main'; import { UriMainImpl } from './uri-main'; import { LoggerMainImpl } from './logger-main'; import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider'; +import { McpServerDefinitionRegistryMainImpl } from './lm-main'; export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void { const loggerMain = new LoggerMainImpl(container); @@ -213,4 +214,7 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container const uriMain = new UriMainImpl(rpc, container); rpc.set(PLUGIN_RPC_CONTEXT.URI_MAIN, uriMain); + + const mcpServerDefinitionRegistryMain = new McpServerDefinitionRegistryMainImpl(rpc, container); + rpc.set(PLUGIN_RPC_CONTEXT.MCP_SERVER_DEFINITION_REGISTRY_MAIN, mcpServerDefinitionRegistryMain); } diff --git a/packages/plugin-ext/src/plugin/lm-ext.ts b/packages/plugin-ext/src/plugin/lm-ext.ts new file mode 100644 index 0000000000000..8648811b35c5a --- /dev/null +++ b/packages/plugin-ext/src/plugin/lm-ext.ts @@ -0,0 +1,158 @@ +// ***************************************************************************** +// Copyright (C) 2025 EclipseSource. +// +// 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 type * as theia from '@theia/plugin'; +import { Disposable } from '@theia/core/lib/common/disposable'; +import { RPCProtocol } from '../common/rpc-protocol'; +import { + McpServerDefinitionRegistryExt, + McpServerDefinitionRegistryMain, + McpServerDefinitionDto, + isMcpHttpServerDefinitionDto, +} from '../common/lm-protocol'; +import { PLUGIN_RPC_CONTEXT } from '../common/plugin-api-rpc'; +import { PluginPackageMcpServerDefinitionProviderContribution } from '../common'; +import { PluginLogger } from './logger'; +import { McpHttpServerDefinition, McpServerDefinition, URI } from './types-impl'; + +// Local interfaces that match the proposed MCP API +interface McpServerDefinitionProvider { + readonly onDidChangeMcpServerDefinitions?: theia.Event; + provideMcpServerDefinitions(): theia.ProviderResult; + resolveMcpServerDefinition?(server: McpServerDefinition): theia.ProviderResult; +} + +export class LmExtImpl implements McpServerDefinitionRegistryExt { + + private proxy: McpServerDefinitionRegistryMain; + private logger: PluginLogger; + private readonly providers = new Map(); + private readonly providerChangeListeners = new Map(); + private handleCounter = 0; + private announcedMCPProviders: string[] = []; + + constructor(protected readonly rpc: RPCProtocol) { + this.proxy = this.rpc.getProxy(PLUGIN_RPC_CONTEXT.MCP_SERVER_DEFINITION_REGISTRY_MAIN); + this.logger = new PluginLogger(this.rpc, 'lm'); + } + + registerMcpServerDefinitionProvider(id: string, provider: McpServerDefinitionProvider): theia.Disposable { + if (this.announcedMCPProviders.indexOf(id) === -1) { + this.logger.warn(`An unknown McpProvider tried to register, please check the package.json: ${id}`); + } + const handle = this.handleCounter++; + this.providers.set(handle, provider); + + this.proxy.$registerMcpServerDefinitionProvider(handle, id); + + if (provider.onDidChangeMcpServerDefinitions) { + const changeListener = provider.onDidChangeMcpServerDefinitions(() => { + this.proxy.$onDidChangeMcpServerDefinitions(handle); + }); + this.providerChangeListeners.set(handle, changeListener); + } + + return Disposable.create(() => { + this.providers.delete(handle); + const changeListener = this.providerChangeListeners.get(handle); + if (changeListener) { + changeListener.dispose(); + this.providerChangeListeners.delete(handle); + } + this.proxy.$unregisterMcpServerDefinitionProvider(handle); + }); + } + + async $provideServerDefinitions(handle: number): Promise { + const provider = this.providers.get(handle); + if (!provider) { + return []; + } + + try { + const definitions = await provider.provideMcpServerDefinitions(); + if (!definitions) { + return []; + } + + return definitions.map(def => this.convertToDto(def)); + } catch (error) { + this.logger.error('Error providing MCP server definitions:', error); + return []; + } + } + + async $resolveServerDefinition(handle: number, server: McpServerDefinitionDto): Promise { + const provider = this.providers.get(handle); + if (!provider || !provider.resolveMcpServerDefinition) { + return server; + } + + try { + const definition = this.convertFromDto(server); + const resolved = await provider.resolveMcpServerDefinition(definition); + return resolved ? this.convertToDto(resolved) : undefined; + } catch (error) { + this.logger.error('Error resolving MCP server definition:', error); + return server; + } + } + + private convertToDto(definition: McpServerDefinition): McpServerDefinitionDto { + if (isMcpHttpServerDefinition(definition)) { + return { + label: definition.label, + headers: definition.headers, + uri: definition.uri, + version: definition.version + }; + } + return { + command: definition.command, + args: definition.args, + cwd: definition.cwd, + version: definition.version, + label: definition.label, + env: definition.env + }; + } + + private convertFromDto(dto: McpServerDefinitionDto): McpServerDefinition { + if (isMcpHttpServerDefinitionDto(dto)) { + return { + label: dto.label, + headers: dto.headers, + uri: URI.revive(dto.uri), + version: dto.version + }; + } + return { + command: dto.command, + args: dto.args, + cwd: URI.revive(dto.cwd), + version: dto.version, + label: dto.label, + env: dto.env + }; + } + + registerMcpContributions(mcpContributions: PluginPackageMcpServerDefinitionProviderContribution[]): void { + this.announcedMCPProviders = mcpContributions.map(contribution => contribution.id); + } +} + +const isMcpHttpServerDefinition = (definition: McpServerDefinition): definition is McpHttpServerDefinition => 'uri' in definition; + diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 5c79b8ee0d32a..c65c9ed5a0a63 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -238,7 +238,9 @@ import { DebugVisualization, TerminalShellExecutionCommandLineConfidence, TerminalCompletionItemKind, - TerminalCompletionList + TerminalCompletionList, + McpHttpServerDefinition, + McpStdioServerDefinition } from './types-impl'; import { AuthenticationExtImpl } from './authentication-ext'; import { SymbolKind } from '../common/plugin-api-rpc-model'; @@ -287,6 +289,7 @@ import { NotebookEditorsExtImpl } from './notebook/notebook-editors'; import { TestingExtImpl } from './tests'; import { UriExtImpl } from './uri-ext'; import { PluginLogger } from './logger'; +import { LmExtImpl } from './lm-ext'; export function createAPIObject(rawObject: T): T { return new Proxy(rawObject, { @@ -351,6 +354,7 @@ export function createAPIFactory( const telemetryExt = rpc.set(MAIN_RPC_CONTEXT.TELEMETRY_EXT, new TelemetryExtImpl()); const testingExt = rpc.set(MAIN_RPC_CONTEXT.TESTING_EXT, new TestingExtImpl(rpc, commandRegistry)); const uriExt = rpc.set(MAIN_RPC_CONTEXT.URI_EXT, new UriExtImpl(rpc)); + const lmExt = rpc.set(MAIN_RPC_CONTEXT.MCP_SERVER_DEFINITION_REGISTRY_EXT, new LmExtImpl(rpc)); rpc.set(MAIN_RPC_CONTEXT.DEBUG_EXT, debugExt); const commandLogger = new PluginLogger(rpc, 'commands-plugin'); @@ -1342,6 +1346,9 @@ export function createAPIFactory( } }; + const mcpContributions = plugin.rawModel.contributes && plugin.rawModel.contributes.mcpServerDefinitionProviders || []; + lmExt.registerMcpContributions(mcpContributions); + const lm: typeof theia.lm = { /** @stubbed LanguageModelChat */ selectChatModels(selector?: theia.LanguageModelChatSelector): Thenable { @@ -1358,7 +1365,10 @@ export function createAPIFactory( return Disposable.NULL; }, /** @stubbed LanguageModelTool */ - tools: [] + tools: [], + registerMcpServerDefinitionProvider(id: string, provider: any): theia.Disposable { + return lmExt.registerMcpServerDefinitionProvider(id, provider); + } }; return { @@ -1589,7 +1599,9 @@ export function createAPIFactory( DebugVisualization, TerminalShellExecutionCommandLineConfidence, TerminalCompletionItemKind, - TerminalCompletionList + TerminalCompletionList, + McpHttpServerDefinition, + McpStdioServerDefinition }; }; } diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index e8a674873686c..77358097381bc 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -4135,3 +4135,107 @@ export enum TerminalShellExecutionCommandLineConfidence { } // #endregion + +/** + * McpStdioServerDefinition represents an MCP server available by running + * a local process and operating on its stdin and stdout streams. The process + * will be spawned as a child process of the extension host and by default + * will not run in a shell environment. + */ +export class McpStdioServerDefinition { + /** + * The human-readable name of the server. + */ + readonly label: string; + + /** + * The working directory used to start the server. + */ + cwd?: URI; + + /** + * The command used to start the server. Node.js-based servers may use + * `process.execPath` to use the editor's version of Node.js to run the script. + */ + command: string; + + /** + * Additional command-line arguments passed to the server. + */ + args?: string[]; + + /** + * Optional additional environment information for the server. Variables + * in this environment will overwrite or remove (if null) the default + * environment variables of the editor's extension host. + */ + env?: Record; + + /** + * Optional version identification for the server. If this changes, the + * editor will indicate that tools have changed and prompt to refresh them. + */ + version?: string; + + /** + * @param label The human-readable name of the server. + * @param command The command used to start the server. + * @param args Additional command-line arguments passed to the server. + * @param env Optional additional environment information for the server. + * @param version Optional version identification for the server. + */ + constructor(label: string, command: string, args?: string[], env?: Record, version?: string) { + this.label = label; + this.command = command; + this.args = args; + this.env = env; + this.version = version; + } +} + +/** + * McpHttpServerDefinition represents an MCP server available using the + * Streamable HTTP transport. + */ +export class McpHttpServerDefinition { + /** + * The human-readable name of the server. + */ + readonly label: string; + + /** + * The URI of the server. The editor will make a POST request to this URI + * to begin each session. + */ + uri: URI; + + /** + * Optional additional heads included with each request to the server. + */ + headers?: Record; + + /** + * Optional version identification for the server. If this changes, the + * editor will indicate that tools have changed and prompt to refresh them. + */ + version?: string; + + /** + * @param label The human-readable name of the server. + * @param uri The URI of the server. + * @param headers Optional additional heads included with each request to the server. + */ + constructor(label: string, uri: URI, headers?: Record, version?: string) { + this.label = label; + this.uri = uri; + this.headers = headers; + this.version = version; + }; +} + +/** + * Definitions that describe different types of Model Context Protocol servers, + * which can be returned from the {@link McpServerDefinitionProvider}. + */ +export type McpServerDefinition = McpStdioServerDefinition | McpHttpServerDefinition; + diff --git a/packages/plugin-ext/tsconfig.json b/packages/plugin-ext/tsconfig.json index b43a5295a5ad1..dd3c091e3525f 100644 --- a/packages/plugin-ext/tsconfig.json +++ b/packages/plugin-ext/tsconfig.json @@ -14,6 +14,9 @@ "src" ], "references": [ + { + "path": "../ai-mcp" + }, { "path": "../bulk-edit" }, diff --git a/packages/plugin/.eslintrc.js b/packages/plugin/.eslintrc.js index 13089943582b6..f0c0649764ae3 100644 --- a/packages/plugin/.eslintrc.js +++ b/packages/plugin/.eslintrc.js @@ -6,5 +6,8 @@ module.exports = { parserOptions: { tsconfigRootDir: __dirname, project: 'tsconfig.json' + }, + rules: { + "@typescript-eslint/no-shadow": "off" } }; diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 4da0528db5dd0..75e91dceb3eb8 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -18662,6 +18662,138 @@ export module '@theia/plugin' { toolMode?: LanguageModelChatToolMode; } + /** + * McpStdioServerDefinition represents an MCP server available by running + * a local process and operating on its stdin and stdout streams. The process + * will be spawned as a child process of the extension host and by default + * will not run in a shell environment. + */ + export class McpStdioServerDefinition { + /** + * The human-readable name of the server. + */ + readonly label: string; + + /** + * The working directory used to start the server. + */ + cwd?: Uri; + + /** + * The command used to start the server. Node.js-based servers may use + * `process.execPath` to use the editor's version of Node.js to run the script. + */ + command: string; + + /** + * Additional command-line arguments passed to the server. + */ + args: string[]; + + /** + * Optional additional environment information for the server. Variables + * in this environment will overwrite or remove (if null) the default + * environment variables of the editor's extension host. + */ + env: Record; + + /** + * Optional version identification for the server. If this changes, the + * editor will indicate that tools have changed and prompt to refresh them. + */ + version?: string; + + /** + * @param label The human-readable name of the server. + * @param command The command used to start the server. + * @param args Additional command-line arguments passed to the server. + * @param env Optional additional environment information for the server. + * @param version Optional version identification for the server. + */ + constructor(label: string, command: string, args?: string[], env?: Record, version?: string); + } + + /** + * McpHttpServerDefinition represents an MCP server available using the + * Streamable HTTP transport. + */ + export class McpHttpServerDefinition { + /** + * The human-readable name of the server. + */ + readonly label: string; + + /** + * The URI of the server. The editor will make a POST request to this URI + * to begin each session. + */ + uri: Uri; + + /** + * Optional additional heads included with each request to the server. + */ + headers: Record; + + /** + * Optional version identification for the server. If this changes, the + * editor will indicate that tools have changed and prompt to refresh them. + */ + version?: string; + + /** + * @param label The human-readable name of the server. + * @param uri The URI of the server. + * @param headers Optional additional heads included with each request to the server. + */ + constructor(label: string, uri: Uri, headers?: Record, version?: string); + } + + /** + * Definitions that describe different types of Model Context Protocol servers, + * which can be returned from the {@link McpServerDefinitionProvider}. + */ + export type McpServerDefinition = McpStdioServerDefinition | McpHttpServerDefinition; + + /** + * A type that can provide Model Context Protocol server definitions. This + * should be registered using {@link lm.registerMcpServerDefinitionProvider} + * during extension activation. + */ + export interface McpServerDefinitionProvider { + /** + * Optional event fired to signal that the set of available servers has changed. + */ + readonly onDidChangeMcpServerDefinitions?: Event; + + /** + * Provides available MCP servers. The editor will call this method eagerly + * to ensure the availability of servers for the language model, and so + * extensions should not take actions which would require user + * interaction, such as authentication. + * + * @param token A cancellation token. + * @returns An array of MCP available MCP servers + */ + provideMcpServerDefinitions(token: CancellationToken): ProviderResult; + + /** + * This function will be called when the editor needs to start a MCP server. + * At this point, the extension may take any actions which may require user + * interaction, such as authentication. Any non-`readonly` property of the + * server may be modified, and the extension should return the resolved server. + * + * The extension may return undefined to indicate that the server + * should not be started, or throw an error. If there is a pending tool + * call, the editor will cancel it and return an error message to the + * language model. + * + * @param server The MCP server to resolve + * @param token A cancellation token. + * @returns The resolved server or thenable that resolves to such. This may + * be the given `server` definition with non-readonly properties filled in. + */ + resolveMcpServerDefinition?(server: T, token: CancellationToken): ProviderResult; + } /** * Namespace for language model related functionality. */ @@ -18745,6 +18877,34 @@ export module '@theia/plugin' { * @stubbed */ export function invokeTool(name: string, options: LanguageModelToolInvocationOptions, token?: CancellationToken): Thenable; + + /** + * Registers a provider that publishes Model Context Protocol servers for the editor to + * consume. This allows MCP servers to be dynamically provided to the editor in + * addition to those the user creates in their configuration files. + * + * Before calling this method, extensions must register the `contributes.mcpServerDefinitionProviders` + * extension point with the corresponding {@link id}, for example: + * + * ```js + * "contributes": { + * "mcpServerDefinitionProviders": [ + * { + * "id": "cool-cloud-registry.mcp-servers", + * "label": "Cool Cloud Registry", + * } + * ] + * } + * ``` + * + * When a new McpServerDefinitionProvider is available, the editor will present a 'refresh' + * action to the user to discover new servers. To enable this flow, extensions should + * call `registerMcpServerDefinitionProvider` during activation. + * @param id The ID of the provider, which is unique to the extension. + * @param provider The provider to register + * @returns A disposable that unregisters the provider when disposed. + */ + export function registerMcpServerDefinitionProvider(id: string, provider: McpServerDefinitionProvider): Disposable; } /**