Skip to content

Commit cf1aad3

Browse files
eneufeldplanger
andauthored
Add support for the new vscode.lm.registerMcpServerDefinitionProvider API (#15755)
Co-authored-by: Philip Langer <planger@eclipsesource.com>
1 parent 30de7d6 commit cf1aad3

File tree

14 files changed

+736
-5
lines changed

14 files changed

+736
-5
lines changed

package-lock.json

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/plugin-ext/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"main": "lib/common/index.js",
66
"typings": "lib/common/index.d.ts",
77
"dependencies": {
8+
"@theia/ai-mcp": "1.62.0",
89
"@theia/bulk-edit": "1.62.0",
910
"@theia/callhierarchy": "1.62.0",
1011
"@theia/console": "1.62.0",
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2025 EclipseSource.
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
17+
import { UriComponents } from './uri-components';
18+
19+
/**
20+
* Protocol interfaces for MCP server definition providers.
21+
*/
22+
23+
export interface McpStdioServerDefinitionDto {
24+
/**
25+
* The human-readable name of the server.
26+
*/
27+
readonly label: string;
28+
29+
/**
30+
* The working directory used to start the server.
31+
*/
32+
cwd?: UriComponents;
33+
34+
/**
35+
* The command used to start the server. Node.js-based servers may use
36+
* `process.execPath` to use the editor's version of Node.js to run the script.
37+
*/
38+
command: string;
39+
40+
/**
41+
* Additional command-line arguments passed to the server.
42+
*/
43+
args?: string[];
44+
45+
/**
46+
* Optional additional environment information for the server. Variables
47+
* in this environment will overwrite or remove (if null) the default
48+
* environment variables of the editor's extension host.
49+
*/
50+
env?: Record<string, string | number | null>;
51+
52+
/**
53+
* Optional version identification for the server. If this changes, the
54+
* editor will indicate that tools have changed and prompt to refresh them.
55+
*/
56+
version?: string;
57+
58+
}
59+
60+
/**
61+
* McpHttpServerDefinition represents an MCP server available using the
62+
* Streamable HTTP transport.
63+
*/
64+
export interface McpHttpServerDefinitionDto {
65+
/**
66+
* The human-readable name of the server.
67+
*/
68+
readonly label: string;
69+
70+
/**
71+
* The URI of the server. The editor will make a POST request to this URI
72+
* to begin each session.
73+
*/
74+
uri: UriComponents;
75+
76+
/**
77+
* Optional additional heads included with each request to the server.
78+
*/
79+
headers?: Record<string, string>;
80+
81+
/**
82+
* Optional version identification for the server. If this changes, the
83+
* editor will indicate that tools have changed and prompt to refresh them.
84+
*/
85+
version?: string;
86+
}
87+
88+
/**
89+
* Definitions that describe different types of Model Context Protocol servers,
90+
* which can be returned from the {@link McpServerDefinitionProvider}.
91+
*/
92+
export type McpServerDefinitionDto = McpStdioServerDefinitionDto | McpHttpServerDefinitionDto;
93+
export const isMcpHttpServerDefinitionDto = (definition: McpServerDefinitionDto): definition is McpHttpServerDefinitionDto => 'uri' in definition;
94+
/**
95+
* Main side of the MCP server definition registry.
96+
*/
97+
export interface McpServerDefinitionRegistryMain {
98+
/**
99+
* Register an MCP server definition provider.
100+
*/
101+
$registerMcpServerDefinitionProvider(handle: number, name: string): void;
102+
103+
/**
104+
* Unregister an MCP server definition provider.
105+
*/
106+
$unregisterMcpServerDefinitionProvider(handle: number): void;
107+
108+
/**
109+
* Notify that server definitions have changed.
110+
*/
111+
$onDidChangeMcpServerDefinitions(handle: number): void;
112+
113+
/**
114+
* Get server definitions from a provider.
115+
*/
116+
$getServerDefinitions(handle: number): Promise<McpServerDefinitionDto[]>;
117+
118+
/**
119+
* Resolve a server definition.
120+
*/
121+
$resolveServerDefinition(handle: number, server: McpServerDefinitionDto): Promise<McpServerDefinitionDto | undefined>;
122+
}
123+
124+
/**
125+
* Extension side of the MCP server definition registry.
126+
*/
127+
export interface McpServerDefinitionRegistryExt {
128+
/**
129+
* Request server definitions from a provider.
130+
*/
131+
$provideServerDefinitions(handle: number): Promise<McpServerDefinitionDto[]>;
132+
133+
/**
134+
* Resolve a server definition from a provider.
135+
*/
136+
$resolveServerDefinition(handle: number, server: McpServerDefinitionDto): Promise<McpServerDefinitionDto | undefined>;
137+
}

packages/plugin-ext/src/common/plugin-api-rpc.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ import { AccessibilityInformation } from '@theia/core/lib/common/accessibility';
127127
import { TreeDelta } from '@theia/test/lib/common/tree-delta';
128128
import { TestItemDTO, TestOutputDTO, TestRunDTO, TestRunProfileDTO, TestRunRequestDTO, TestStateChangeDTO } from './test-types';
129129
import { ArgumentProcessor } from './commands';
130+
import { McpServerDefinitionRegistryMain, McpServerDefinitionRegistryExt } from './lm-protocol';
130131

131132
export interface PreferenceData {
132133
[scope: number]: any;
@@ -2346,7 +2347,8 @@ export const PLUGIN_RPC_CONTEXT = {
23462347
TELEMETRY_MAIN: createProxyIdentifier<TelemetryMain>('TelemetryMain'),
23472348
LOCALIZATION_MAIN: createProxyIdentifier<LocalizationMain>('LocalizationMain'),
23482349
TESTING_MAIN: createProxyIdentifier<TestingMain>('TestingMain'),
2349-
URI_MAIN: createProxyIdentifier<UriMain>('UriMain')
2350+
URI_MAIN: createProxyIdentifier<UriMain>('UriMain'),
2351+
MCP_SERVER_DEFINITION_REGISTRY_MAIN: createProxyIdentifier<McpServerDefinitionRegistryMain>('McpServerDefinitionRegistryMain')
23502352
};
23512353

23522354
export const MAIN_RPC_CONTEXT = {
@@ -2389,7 +2391,8 @@ export const MAIN_RPC_CONTEXT = {
23892391
TABS_EXT: createProxyIdentifier<TabsExt>('TabsExt'),
23902392
TELEMETRY_EXT: createProxyIdentifier<TelemetryExt>('TelemetryExt)'),
23912393
TESTING_EXT: createProxyIdentifier<TestingExt>('TestingExt'),
2392-
URI_EXT: createProxyIdentifier<UriExt>('UriExt')
2394+
URI_EXT: createProxyIdentifier<UriExt>('UriExt'),
2395+
MCP_SERVER_DEFINITION_REGISTRY_EXT: createProxyIdentifier<McpServerDefinitionRegistryExt>('McpServerDefinitionRegistryExt')
23932396
};
23942397

23952398
export interface TasksExt {

packages/plugin-ext/src/common/plugin-protocol.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export interface PluginPackageContribution {
104104
notebooks?: PluginPackageNotebook[];
105105
notebookRenderer?: PluginNotebookRendererContribution[];
106106
notebookPreload?: PluginPackageNotebookPreload[];
107+
mcpServerDefinitionProviders?: PluginPackageMcpServerDefinitionProviderContribution[];
107108
}
108109

109110
export interface PluginPackageNotebook {
@@ -126,6 +127,12 @@ export interface PluginPackageNotebookPreload {
126127
entrypoint: string;
127128
}
128129

130+
export interface PluginPackageMcpServerDefinitionProviderContribution {
131+
id: string;
132+
label: string;
133+
description?: string;
134+
}
135+
129136
export interface PluginPackageAuthenticationProvider {
130137
id: string;
131138
label: string;

packages/plugin-ext/src/hosted/node/plugin-host-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { WebviewsExtImpl } from '../../plugin/webviews';
3636
import { TerminalServiceExtImpl } from '../../plugin/terminal-ext';
3737
import { InternalSecretsExt, SecretsExtImpl } from '../../plugin/secrets-ext';
3838
import { setupPluginHostLogger } from './plugin-host-logger';
39+
import { LmExtImpl } from '../../plugin/lm-ext';
3940

4041
export default new ContainerModule(bind => {
4142
const channel = new IPCChannel();
@@ -63,6 +64,7 @@ export default new ContainerModule(bind => {
6364
bind(SecretsExtImpl).toSelf().inSingletonScope();
6465
bind(PreferenceRegistryExtImpl).toSelf().inSingletonScope();
6566
bind(DebugExtImpl).toSelf().inSingletonScope();
67+
bind(LmExtImpl).toSelf().inSingletonScope();
6668
bind(EditorsAndDocumentsExtImpl).toSelf().inSingletonScope();
6769
bind(WorkspaceExtImpl).toSelf().inSingletonScope();
6870
bind(MessageRegistryExt).toSelf().inSingletonScope();
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2025 EclipseSource
3+
//
4+
// This program and the accompanying materials are made available under the
5+
// terms of the Eclipse Public License v. 2.0 which is available at
6+
// http://www.eclipse.org/legal/epl-2.0.
7+
//
8+
// This Source Code may also be made available under the following Secondary
9+
// Licenses when the conditions for such availability set forth in the Eclipse
10+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
// with the GNU Classpath Exception which is available at
12+
// https://www.gnu.org/software/classpath/license.html.
13+
//
14+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15+
// *****************************************************************************
16+
17+
import { interfaces } from '@theia/core/shared/inversify';
18+
import { RPCProtocol } from '../../common/rpc-protocol';
19+
import {
20+
McpServerDefinitionRegistryMain,
21+
McpServerDefinitionRegistryExt,
22+
McpServerDefinitionDto,
23+
isMcpHttpServerDefinitionDto,
24+
} from '../../common/lm-protocol';
25+
import { MAIN_RPC_CONTEXT } from '../../common/plugin-api-rpc';
26+
import { MCPServerManager, MCPServerDescription } from '@theia/ai-mcp/lib/common';
27+
28+
export class McpServerDefinitionRegistryMainImpl implements McpServerDefinitionRegistryMain {
29+
private readonly proxy: McpServerDefinitionRegistryExt;
30+
private readonly providers = new Map<number, string>();
31+
private readonly mcpServerManager: MCPServerManager | undefined;
32+
33+
constructor(
34+
rpc: RPCProtocol,
35+
container: interfaces.Container
36+
) {
37+
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.MCP_SERVER_DEFINITION_REGISTRY_EXT);
38+
try {
39+
this.mcpServerManager = container.get(MCPServerManager);
40+
} catch {
41+
// MCP Server Manager is optional
42+
this.mcpServerManager = undefined;
43+
}
44+
}
45+
46+
$registerMcpServerDefinitionProvider(handle: number, name: string): void {
47+
this.providers.set(handle, name);
48+
this.loadServerDefinitions(handle);
49+
}
50+
51+
$unregisterMcpServerDefinitionProvider(handle: number): void {
52+
if (!this.mcpServerManager) {
53+
console.warn('MCP Server Manager not available - MCP server definitions will not be loaded');
54+
return;
55+
}
56+
const provider = this.providers.get(handle);
57+
if (!provider) {
58+
console.warn(`No MCP Server provider found for handle '${handle}' - MCP server definitions will not be loaded`);
59+
return;
60+
}
61+
this.mcpServerManager.removeServer(provider);
62+
this.providers.delete(handle);
63+
}
64+
65+
$onDidChangeMcpServerDefinitions(handle: number): void {
66+
// Reload server definitions when provider reports changes
67+
this.loadServerDefinitions(handle);
68+
}
69+
70+
async $getServerDefinitions(handle: number): Promise<McpServerDefinitionDto[]> {
71+
try {
72+
return await this.proxy.$provideServerDefinitions(handle);
73+
} catch (error) {
74+
console.error('Error getting MCP server definitions:', error);
75+
return [];
76+
}
77+
}
78+
79+
async $resolveServerDefinition(handle: number, server: McpServerDefinitionDto): Promise<McpServerDefinitionDto | undefined> {
80+
try {
81+
return await this.proxy.$resolveServerDefinition(handle, server);
82+
} catch (error) {
83+
console.error('Error resolving MCP server definition:', error);
84+
return server;
85+
}
86+
}
87+
88+
private async loadServerDefinitions(handle: number): Promise<void> {
89+
if (!this.mcpServerManager) {
90+
console.warn('MCP Server Manager not available - MCP server definitions will not be loaded');
91+
return;
92+
}
93+
94+
try {
95+
const definitions = await this.$getServerDefinitions(handle);
96+
97+
for (const definition of definitions) {
98+
const resolved = await this.$resolveServerDefinition(handle, definition);
99+
if (resolved) {
100+
const mcpServerDescription = this.convertToMcpServerDescription(resolved);
101+
this.mcpServerManager.addOrUpdateServer(mcpServerDescription);
102+
}
103+
}
104+
} catch (error) {
105+
console.error('Error loading MCP server definitions:', error);
106+
}
107+
}
108+
109+
private convertToMcpServerDescription(definition: McpServerDefinitionDto): MCPServerDescription {
110+
if (isMcpHttpServerDefinitionDto(definition)) {
111+
// For HTTP servers, we would need to create a bridge or adapter
112+
// For now, we'll create a placeholder stdio server that could proxy to HTTP
113+
console.warn(`HTTP transport not yet supported for MCP server '${definition.label}'. Skipping.`);
114+
throw new Error(`HTTP transport not yet supported for MCP server '${definition.label}'`);
115+
}
116+
117+
// Convert env values to strings, filtering out null values
118+
let convertedEnv: Record<string, string> | undefined;
119+
if (definition.env) {
120+
convertedEnv = {};
121+
for (const [key, value] of Object.entries(definition.env)) {
122+
if (value !== null) {
123+
convertedEnv[key] = String(value);
124+
}
125+
}
126+
}
127+
128+
return {
129+
name: definition.label,
130+
command: definition.command!,
131+
args: definition.args,
132+
env: convertedEnv,
133+
autostart: false, // Extensions should manage their own server lifecycle
134+
};
135+
}
136+
}

packages/plugin-ext/src/main/browser/main-context.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import { TestingMainImpl } from './test-main';
6767
import { UriMainImpl } from './uri-main';
6868
import { LoggerMainImpl } from './logger-main';
6969
import { MonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider';
70+
import { McpServerDefinitionRegistryMainImpl } from './lm-main';
7071

7172
export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container): void {
7273
const loggerMain = new LoggerMainImpl(container);
@@ -213,4 +214,7 @@ export function setUpPluginApi(rpc: RPCProtocol, container: interfaces.Container
213214

214215
const uriMain = new UriMainImpl(rpc, container);
215216
rpc.set(PLUGIN_RPC_CONTEXT.URI_MAIN, uriMain);
217+
218+
const mcpServerDefinitionRegistryMain = new McpServerDefinitionRegistryMainImpl(rpc, container);
219+
rpc.set(PLUGIN_RPC_CONTEXT.MCP_SERVER_DEFINITION_REGISTRY_MAIN, mcpServerDefinitionRegistryMain);
216220
}

0 commit comments

Comments
 (0)