@theia/ai-mcp-server 1.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +175 -0
- package/lib/browser/index.d.ts +3 -0
- package/lib/browser/index.d.ts.map +1 -0
- package/lib/browser/index.js +21 -0
- package/lib/browser/index.js.map +1 -0
- package/lib/browser/mcp-frontend-bootstrap.d.ts +17 -0
- package/lib/browser/mcp-frontend-bootstrap.d.ts.map +1 -0
- package/lib/browser/mcp-frontend-bootstrap.js +47 -0
- package/lib/browser/mcp-frontend-bootstrap.js.map +1 -0
- package/lib/browser/mcp-frontend-contribution.d.ts +44 -0
- package/lib/browser/mcp-frontend-contribution.d.ts.map +1 -0
- package/lib/browser/mcp-frontend-contribution.js +21 -0
- package/lib/browser/mcp-frontend-contribution.js.map +1 -0
- package/lib/browser/mcp-frontend-module.d.ts +4 -0
- package/lib/browser/mcp-frontend-module.d.ts.map +1 -0
- package/lib/browser/mcp-frontend-module.js +37 -0
- package/lib/browser/mcp-frontend-module.js.map +1 -0
- package/lib/browser/mcp-tool-delegate-client.d.ts +27 -0
- package/lib/browser/mcp-tool-delegate-client.d.ts.map +1 -0
- package/lib/browser/mcp-tool-delegate-client.js +128 -0
- package/lib/browser/mcp-tool-delegate-client.js.map +1 -0
- package/lib/common/index.d.ts +2 -0
- package/lib/common/index.d.ts.map +1 -0
- package/lib/common/index.js +20 -0
- package/lib/common/index.js.map +1 -0
- package/lib/common/mcp-tool-delegate.d.ts +25 -0
- package/lib/common/mcp-tool-delegate.d.ts.map +1 -0
- package/lib/common/mcp-tool-delegate.js +22 -0
- package/lib/common/mcp-tool-delegate.js.map +1 -0
- package/lib/node/index.d.ts +5 -0
- package/lib/node/index.d.ts.map +1 -0
- package/lib/node/index.js +23 -0
- package/lib/node/index.js.map +1 -0
- package/lib/node/mcp-backend-contribution-manager.d.ts +16 -0
- package/lib/node/mcp-backend-contribution-manager.d.ts.map +1 -0
- package/lib/node/mcp-backend-contribution-manager.js +61 -0
- package/lib/node/mcp-backend-contribution-manager.js.map +1 -0
- package/lib/node/mcp-backend-module.d.ts +4 -0
- package/lib/node/mcp-backend-module.d.ts.map +1 -0
- package/lib/node/mcp-backend-module.js +56 -0
- package/lib/node/mcp-backend-module.js.map +1 -0
- package/lib/node/mcp-frontend-contribution-manager.d.ts +55 -0
- package/lib/node/mcp-frontend-contribution-manager.d.ts.map +1 -0
- package/lib/node/mcp-frontend-contribution-manager.js +242 -0
- package/lib/node/mcp-frontend-contribution-manager.js.map +1 -0
- package/lib/node/mcp-theia-server-impl.d.ts +32 -0
- package/lib/node/mcp-theia-server-impl.d.ts.map +1 -0
- package/lib/node/mcp-theia-server-impl.js +220 -0
- package/lib/node/mcp-theia-server-impl.js.map +1 -0
- package/lib/node/mcp-theia-server.d.ts +43 -0
- package/lib/node/mcp-theia-server.d.ts.map +1 -0
- package/lib/node/mcp-theia-server.js +22 -0
- package/lib/node/mcp-theia-server.js.map +1 -0
- package/lib/node/mcp-tool-frontend-delegate.d.ts +13 -0
- package/lib/node/mcp-tool-frontend-delegate.d.ts.map +1 -0
- package/lib/node/mcp-tool-frontend-delegate.js +66 -0
- package/lib/node/mcp-tool-frontend-delegate.js.map +1 -0
- package/lib/package.spec.d.ts +1 -0
- package/lib/package.spec.d.ts.map +1 -0
- package/lib/package.spec.js +26 -0
- package/lib/package.spec.js.map +1 -0
- package/package.json +52 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/mcp-frontend-bootstrap.ts +43 -0
- package/src/browser/mcp-frontend-contribution.ts +67 -0
- package/src/browser/mcp-frontend-module.ts +42 -0
- package/src/browser/mcp-tool-delegate-client.ts +133 -0
- package/src/common/index.ts +17 -0
- package/src/common/mcp-tool-delegate.ts +43 -0
- package/src/node/index.ts +20 -0
- package/src/node/mcp-backend-contribution-manager.ts +56 -0
- package/src/node/mcp-backend-module.ts +75 -0
- package/src/node/mcp-frontend-contribution-manager.ts +268 -0
- package/src/node/mcp-theia-server-impl.ts +241 -0
- package/src/node/mcp-theia-server.ts +66 -0
- package/src/node/mcp-tool-frontend-delegate.ts +71 -0
- package/src/package.spec.ts +28 -0
|
@@ -0,0 +1,42 @@
|
|
|
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 { ContainerModule } from '@theia/core/shared/inversify';
|
|
18
|
+
import { bindContributionProvider } from '@theia/core';
|
|
19
|
+
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
|
20
|
+
import {
|
|
21
|
+
RemoteConnectionProvider,
|
|
22
|
+
ServiceConnectionProvider,
|
|
23
|
+
} from '@theia/core/lib/browser/messaging/service-connection-provider';
|
|
24
|
+
import { MCPToolFrontendDelegate, MCPToolDelegateClient, mcpToolDelegatePath } from '../common/mcp-tool-delegate';
|
|
25
|
+
import { MCPFrontendBootstrap } from './mcp-frontend-bootstrap';
|
|
26
|
+
import { MCPFrontendContribution } from './mcp-frontend-contribution';
|
|
27
|
+
import { MCPToolDelegateClientImpl } from './mcp-tool-delegate-client';
|
|
28
|
+
|
|
29
|
+
export default new ContainerModule(bind => {
|
|
30
|
+
bind(MCPFrontendBootstrap).toSelf().inSingletonScope();
|
|
31
|
+
bind(FrontendApplicationContribution).toService(MCPFrontendBootstrap);
|
|
32
|
+
|
|
33
|
+
bind(MCPToolDelegateClient).to(MCPToolDelegateClientImpl).inSingletonScope();
|
|
34
|
+
|
|
35
|
+
bind(MCPToolFrontendDelegate).toDynamicValue(ctx => {
|
|
36
|
+
const connection = ctx.container.get<ServiceConnectionProvider>(RemoteConnectionProvider);
|
|
37
|
+
const client = ctx.container.get<MCPToolDelegateClient>(MCPToolDelegateClient);
|
|
38
|
+
return connection.createProxy<MCPToolFrontendDelegate>(mcpToolDelegatePath, client);
|
|
39
|
+
}).inSingletonScope();
|
|
40
|
+
|
|
41
|
+
bindContributionProvider(bind, MCPFrontendContribution);
|
|
42
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
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 { inject, injectable, named } from '@theia/core/shared/inversify';
|
|
18
|
+
import { ContributionProvider } from '@theia/core';
|
|
19
|
+
import { ILogger } from '@theia/core/lib/common/logger';
|
|
20
|
+
import { Tool, Resource, ResourceContents, Prompt, PromptMessage } from '@modelcontextprotocol/sdk/types';
|
|
21
|
+
import { MCPToolDelegateClient } from '../common/mcp-tool-delegate';
|
|
22
|
+
import { MCPFrontendContribution } from './mcp-frontend-contribution';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Frontend client implementation that handles MCP tool delegation requests from the backend.
|
|
26
|
+
*
|
|
27
|
+
* This class acts as a bridge between the backend MCP server and frontend contributions,
|
|
28
|
+
* forwarding backend requests (tool calls, resource access, prompts) to registered
|
|
29
|
+
* MCPFrontendContribution instances and aggregating their responses.
|
|
30
|
+
*
|
|
31
|
+
* Called by the backend via the MCPToolDelegateClient interface to access frontend-provided
|
|
32
|
+
* MCP tools, resources, and prompts.
|
|
33
|
+
*/
|
|
34
|
+
@injectable()
|
|
35
|
+
export class MCPToolDelegateClientImpl implements MCPToolDelegateClient {
|
|
36
|
+
|
|
37
|
+
@inject(ContributionProvider)
|
|
38
|
+
@named(MCPFrontendContribution)
|
|
39
|
+
protected readonly contributions: ContributionProvider<MCPFrontendContribution>;
|
|
40
|
+
|
|
41
|
+
@inject(ILogger)
|
|
42
|
+
protected readonly logger: ILogger;
|
|
43
|
+
|
|
44
|
+
private getFrontendContributions(): MCPFrontendContribution[] {
|
|
45
|
+
return this.contributions.getContributions();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async callTool(serverId: string, toolName: string, args: unknown): Promise<unknown> {
|
|
49
|
+
const contributions = this.getFrontendContributions();
|
|
50
|
+
|
|
51
|
+
for (const contribution of contributions) {
|
|
52
|
+
if (contribution.getTool) {
|
|
53
|
+
const tool = await contribution.getTool(toolName);
|
|
54
|
+
if (tool) {
|
|
55
|
+
return await tool.handler(JSON.stringify(args));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw new Error(`Tool ${toolName} not found in server ${serverId}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async listTools(serverId: string): Promise<Tool[]> {
|
|
63
|
+
const contributions = this.getFrontendContributions();
|
|
64
|
+
const allTools: Tool[] = [];
|
|
65
|
+
|
|
66
|
+
for (const contribution of contributions) {
|
|
67
|
+
if (contribution.getTools) {
|
|
68
|
+
const tools = await contribution.getTools();
|
|
69
|
+
allTools.push(...tools);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return allTools;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async listResources(serverId: string): Promise<Resource[]> {
|
|
76
|
+
const contributions = this.getFrontendContributions();
|
|
77
|
+
const allResources: Resource[] = [];
|
|
78
|
+
|
|
79
|
+
for (const contribution of contributions) {
|
|
80
|
+
if (contribution.getResources) {
|
|
81
|
+
const resources = await contribution.getResources();
|
|
82
|
+
allResources.push(...resources);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return allResources;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async readResource(serverId: string, uri: string): Promise<ResourceContents> {
|
|
89
|
+
const contributions = this.getFrontendContributions();
|
|
90
|
+
|
|
91
|
+
for (const contribution of contributions) {
|
|
92
|
+
if (contribution.readResource) {
|
|
93
|
+
try {
|
|
94
|
+
const result = await contribution.readResource(uri);
|
|
95
|
+
return result as ResourceContents;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// Continue to next contribution
|
|
98
|
+
this.logger.debug(`Error getting resource ${uri}:`, error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
throw new Error(`Resource ${uri} not found in server ${serverId}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async listPrompts(serverId: string): Promise<Prompt[]> {
|
|
106
|
+
const contributions = this.getFrontendContributions();
|
|
107
|
+
const allPrompts: Prompt[] = [];
|
|
108
|
+
|
|
109
|
+
for (const contribution of contributions) {
|
|
110
|
+
if (contribution.getPrompts) {
|
|
111
|
+
const prompts = await contribution.getPrompts();
|
|
112
|
+
allPrompts.push(...prompts);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return allPrompts;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async getPrompt(serverId: string, name: string, args: unknown): Promise<PromptMessage[]> {
|
|
119
|
+
const contributions = this.getFrontendContributions();
|
|
120
|
+
|
|
121
|
+
for (const contribution of contributions) {
|
|
122
|
+
if (contribution.getPrompt) {
|
|
123
|
+
try {
|
|
124
|
+
return await contribution.getPrompt(name, args);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
// Continue to next contribution
|
|
127
|
+
this.logger.debug(`Error getting prompt ${name}:`, error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw new Error(`Prompt ${name} not found in server ${serverId}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
export * from './mcp-tool-delegate';
|
|
@@ -0,0 +1,43 @@
|
|
|
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 { Tool, Resource, ResourceContents, Prompt, PromptMessage } from '@modelcontextprotocol/sdk/types';
|
|
18
|
+
|
|
19
|
+
export const MCPToolDelegateClient = Symbol('MCPToolDelegateClient');
|
|
20
|
+
/**
|
|
21
|
+
* Client interface for MCP tool operations.
|
|
22
|
+
* This interface is implemented by the frontend and called by the backend.
|
|
23
|
+
*/
|
|
24
|
+
export interface MCPToolDelegateClient {
|
|
25
|
+
callTool(serverId: string, toolName: string, args: unknown): Promise<unknown>;
|
|
26
|
+
listTools(serverId: string): Promise<Tool[]>;
|
|
27
|
+
listResources(serverId: string): Promise<Resource[]>;
|
|
28
|
+
readResource(serverId: string, uri: string): Promise<ResourceContents>;
|
|
29
|
+
listPrompts(serverId: string): Promise<Prompt[]>;
|
|
30
|
+
getPrompt(serverId: string, name: string, args: unknown): Promise<PromptMessage[]>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const MCPToolFrontendDelegate = Symbol('MCPToolFrontendDelegate');
|
|
34
|
+
/**
|
|
35
|
+
* Backend delegate interface for MCP tool operations.
|
|
36
|
+
* This interface extends MCPToolDelegateClient with RPC client setup capability.
|
|
37
|
+
* It is implemented by the backend and acts as a proxy to forward calls to the frontend.
|
|
38
|
+
*/
|
|
39
|
+
export interface MCPToolFrontendDelegate extends MCPToolDelegateClient {
|
|
40
|
+
setClient(client: MCPToolDelegateClient): void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const mcpToolDelegatePath = '/services/mcpToolDelegate';
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
export * from './mcp-theia-server';
|
|
18
|
+
export * from './mcp-theia-server-impl';
|
|
19
|
+
export * from './mcp-backend-contribution-manager';
|
|
20
|
+
export * from './mcp-frontend-contribution-manager';
|
|
@@ -0,0 +1,56 @@
|
|
|
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 { inject, injectable, named } from '@theia/core/shared/inversify';
|
|
18
|
+
import { ILogger } from '@theia/core/lib/common/logger';
|
|
19
|
+
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
|
|
20
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp';
|
|
21
|
+
import { MCPBackendContribution } from './mcp-theia-server';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Manages the registration of backend MCP contributions
|
|
25
|
+
*/
|
|
26
|
+
@injectable()
|
|
27
|
+
export class MCPBackendContributionManager {
|
|
28
|
+
|
|
29
|
+
@inject(ILogger)
|
|
30
|
+
protected readonly logger: ILogger;
|
|
31
|
+
|
|
32
|
+
@inject(ContributionProvider)
|
|
33
|
+
@named(MCPBackendContribution)
|
|
34
|
+
protected readonly contributions: ContributionProvider<MCPBackendContribution>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Register all backend contributions with the MCP server
|
|
38
|
+
*/
|
|
39
|
+
async registerBackendContributions(server: McpServer): Promise<void> {
|
|
40
|
+
const contributions = this.contributions.getContributions();
|
|
41
|
+
this.logger.debug(`Found ${contributions.length} backend MCP contributions to register`);
|
|
42
|
+
|
|
43
|
+
for (const contribution of contributions) {
|
|
44
|
+
try {
|
|
45
|
+
this.logger.debug(`Configuring backend MCP contribution: ${contribution.constructor.name}`);
|
|
46
|
+
await contribution.configure(server);
|
|
47
|
+
this.logger.debug(`Successfully registered backend MCP contribution: ${contribution.constructor.name}`);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
this.logger.error(`Failed to register backend MCP contribution ${contribution.constructor.name}:`, error);
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.logger.debug(`Finished registering all ${contributions.length} backend MCP contributions`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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 { ContainerModule } from '@theia/core/shared/inversify';
|
|
18
|
+
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
|
|
19
|
+
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
|
|
20
|
+
import { ConnectionHandler, RpcConnectionHandler, bindContributionProvider, generateUuid } from '@theia/core';
|
|
21
|
+
import {
|
|
22
|
+
MCPTheiaServer,
|
|
23
|
+
MCPBackendContribution
|
|
24
|
+
} from './mcp-theia-server';
|
|
25
|
+
import { MCPToolFrontendDelegate, MCPToolDelegateClient, mcpToolDelegatePath } from '../common/mcp-tool-delegate';
|
|
26
|
+
import { MCPTheiaServerImpl } from './mcp-theia-server-impl';
|
|
27
|
+
import { MCPBackendContributionManager } from './mcp-backend-contribution-manager';
|
|
28
|
+
import { MCPFrontendContributionManager } from './mcp-frontend-contribution-manager';
|
|
29
|
+
import { MCPToolFrontendDelegateImpl } from './mcp-tool-frontend-delegate';
|
|
30
|
+
|
|
31
|
+
const mcpConnectionModule = ConnectionContainerModule.create(({ bind }) => {
|
|
32
|
+
bind(MCPToolFrontendDelegateImpl).toSelf().inSingletonScope();
|
|
33
|
+
bind(MCPToolFrontendDelegate).toService(MCPToolFrontendDelegateImpl);
|
|
34
|
+
|
|
35
|
+
bind(ConnectionHandler)
|
|
36
|
+
.toDynamicValue(
|
|
37
|
+
({ container }) =>
|
|
38
|
+
new RpcConnectionHandler<MCPToolDelegateClient>(
|
|
39
|
+
mcpToolDelegatePath,
|
|
40
|
+
client => {
|
|
41
|
+
const service = container.get<MCPToolFrontendDelegateImpl>(MCPToolFrontendDelegateImpl);
|
|
42
|
+
const contributionManager = container.get<MCPFrontendContributionManager>(MCPFrontendContributionManager);
|
|
43
|
+
|
|
44
|
+
service.setClient(client);
|
|
45
|
+
|
|
46
|
+
// Generate unique delegate ID and register with contribution manager
|
|
47
|
+
const delegateId = generateUuid();
|
|
48
|
+
contributionManager.addFrontendDelegate(delegateId, service);
|
|
49
|
+
|
|
50
|
+
// Setup cleanup when connection closes
|
|
51
|
+
client.onDidCloseConnection(() => {
|
|
52
|
+
contributionManager.removeFrontendDelegate(delegateId);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return service;
|
|
56
|
+
}
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
.inSingletonScope();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export default new ContainerModule(bind => {
|
|
63
|
+
|
|
64
|
+
bind(MCPTheiaServerImpl).toSelf().inSingletonScope();
|
|
65
|
+
bind(MCPTheiaServer).toService(MCPTheiaServerImpl);
|
|
66
|
+
bind(BackendApplicationContribution).toService(MCPTheiaServerImpl);
|
|
67
|
+
|
|
68
|
+
bind(MCPBackendContributionManager).toSelf().inSingletonScope();
|
|
69
|
+
|
|
70
|
+
bind(MCPFrontendContributionManager).toSelf().inSingletonScope();
|
|
71
|
+
|
|
72
|
+
bindContributionProvider(bind, MCPBackendContribution);
|
|
73
|
+
|
|
74
|
+
bind(ConnectionContainerModule).toConstantValue(mcpConnectionModule);
|
|
75
|
+
});
|
|
@@ -0,0 +1,268 @@
|
|
|
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 { inject, injectable } from '@theia/core/shared/inversify';
|
|
18
|
+
import { ILogger } from '@theia/core/lib/common/logger';
|
|
19
|
+
import { McpServer, RegisteredTool, RegisteredPrompt, RegisteredResource } from '@modelcontextprotocol/sdk/server/mcp';
|
|
20
|
+
import { ReadResourceResult } from '@modelcontextprotocol/sdk/types';
|
|
21
|
+
import { MCPToolFrontendDelegate } from '../common/mcp-tool-delegate';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Manages the registration and delegation of frontend MCP contributions
|
|
25
|
+
*/
|
|
26
|
+
@injectable()
|
|
27
|
+
export class MCPFrontendContributionManager {
|
|
28
|
+
|
|
29
|
+
@inject(ILogger)
|
|
30
|
+
protected readonly logger: ILogger;
|
|
31
|
+
|
|
32
|
+
// Frontend delegates are set dynamically when connections are established
|
|
33
|
+
private frontendDelegates = new Map<string, MCPToolFrontendDelegate>();
|
|
34
|
+
private mcpServer?: McpServer;
|
|
35
|
+
private serverId?: string;
|
|
36
|
+
|
|
37
|
+
private registeredElements: Map<string, (RegisteredTool | RegisteredPrompt | RegisteredResource)[]> = new Map();
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set the MCP server instance and setup frontend delegate notifications
|
|
41
|
+
*/
|
|
42
|
+
async setMCPServer(server: McpServer, serverId: string): Promise<void> {
|
|
43
|
+
this.mcpServer = server;
|
|
44
|
+
this.serverId = serverId;
|
|
45
|
+
this.registerExistingFrontendContributions();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Add a frontend delegate (called when a frontend connection is established)
|
|
50
|
+
*/
|
|
51
|
+
addFrontendDelegate(delegateId: string, delegate: MCPToolFrontendDelegate): void {
|
|
52
|
+
this.frontendDelegates.set(delegateId, delegate);
|
|
53
|
+
|
|
54
|
+
if (this.mcpServer && this.serverId) {
|
|
55
|
+
this.registerFrontendContributionsFromDelegate(delegateId, delegate).catch(error => {
|
|
56
|
+
this.logger.warn(`Failed to register frontend contributions from delegate ${delegateId}:`, error);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Remove a frontend delegate (called when a frontend connection is closed)
|
|
63
|
+
*/
|
|
64
|
+
removeFrontendDelegate(delegateId: string): void {
|
|
65
|
+
this.frontendDelegates.delete(delegateId);
|
|
66
|
+
this.unregisterFrontendContributionsFromDelegate(delegateId);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Unregister frontend contributions from a specific delegate
|
|
71
|
+
*/
|
|
72
|
+
private unregisterFrontendContributionsFromDelegate(delegateId: string): void {
|
|
73
|
+
if (!this.mcpServer) {
|
|
74
|
+
this.logger.warn('MCP server not set, cannot unregister frontend contributions');
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const elements = this.registeredElements.get(delegateId);
|
|
79
|
+
if (elements) {
|
|
80
|
+
for (const element of elements) {
|
|
81
|
+
try {
|
|
82
|
+
element.remove();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.logger.warn(`Failed to unregister element from delegate ${delegateId}: ${error}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
this.registeredElements.delete(delegateId);
|
|
88
|
+
|
|
89
|
+
// Notify that lists have changed
|
|
90
|
+
this.mcpServer.sendToolListChanged();
|
|
91
|
+
this.mcpServer.sendResourceListChanged();
|
|
92
|
+
this.mcpServer.sendPromptListChanged();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Register frontend contributions from existing delegates
|
|
98
|
+
*/
|
|
99
|
+
private async registerExistingFrontendContributions(): Promise<void> {
|
|
100
|
+
for (const [delegateId, delegate] of this.frontendDelegates) {
|
|
101
|
+
try {
|
|
102
|
+
await this.registerFrontendContributionsFromDelegate(delegateId, delegate);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
this.logger.warn(`Failed to register frontend contributions from delegate ${delegateId}:`, error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Register frontend contributions from a specific delegate
|
|
111
|
+
*/
|
|
112
|
+
private async registerFrontendContributionsFromDelegate(delegateId: string, delegate: MCPToolFrontendDelegate): Promise<void> {
|
|
113
|
+
if (!this.mcpServer || !this.serverId) {
|
|
114
|
+
this.logger.warn('MCP server not set, cannot register frontend contributions');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await this.registerFrontendToolsFromDelegate(delegate, delegateId);
|
|
120
|
+
this.mcpServer.sendToolListChanged();
|
|
121
|
+
// Register resources from frontend
|
|
122
|
+
await this.registerFrontendResourcesFromDelegate(delegate, delegateId);
|
|
123
|
+
this.mcpServer.sendResourceListChanged();
|
|
124
|
+
|
|
125
|
+
// Register prompts from frontend
|
|
126
|
+
await this.registerFrontendPromptsFromDelegate(delegate, delegateId);
|
|
127
|
+
this.mcpServer.sendPromptListChanged();
|
|
128
|
+
|
|
129
|
+
} catch (error) {
|
|
130
|
+
this.logger.warn(`Failed to register frontend MCP contributions from delegate ${delegateId}: ${error}`);
|
|
131
|
+
// Don't re-throw to prevent server startup failure
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Unregister frontend contributions for a server
|
|
137
|
+
* @param serverId Unique identifier for the server instance
|
|
138
|
+
*/
|
|
139
|
+
async unregisterFrontendContributions(serverId: string): Promise<void> {
|
|
140
|
+
for (const [delegateId] of this.frontendDelegates) {
|
|
141
|
+
try {
|
|
142
|
+
this.unregisterFrontendContributionsFromDelegate(delegateId);
|
|
143
|
+
// Backend delegates don't need lifecycle notifications
|
|
144
|
+
} catch (error) {
|
|
145
|
+
this.logger.warn(`Error unregistering server from frontend delegate ${delegateId}:`, error);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Register tools from frontend contributions
|
|
152
|
+
*/
|
|
153
|
+
protected async registerFrontendToolsFromDelegate(delegate: MCPToolFrontendDelegate, delegateId: string): Promise<void> {
|
|
154
|
+
if (!this.mcpServer || !this.serverId) {
|
|
155
|
+
throw new Error('MCP server not set');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const tools = await delegate.listTools(this.serverId);
|
|
160
|
+
for (const tool of tools) {
|
|
161
|
+
const registeredTool = this.mcpServer.tool(
|
|
162
|
+
`${tool.name}_${delegateId}`,
|
|
163
|
+
tool.description ?? '',
|
|
164
|
+
tool.inputSchema,
|
|
165
|
+
async args => {
|
|
166
|
+
try {
|
|
167
|
+
const result = await delegate.callTool(
|
|
168
|
+
this.serverId!,
|
|
169
|
+
tool.name,
|
|
170
|
+
args
|
|
171
|
+
);
|
|
172
|
+
return {
|
|
173
|
+
content: [{
|
|
174
|
+
type: 'text',
|
|
175
|
+
text: typeof result === 'string' ? result : JSON.stringify(result)
|
|
176
|
+
}]
|
|
177
|
+
};
|
|
178
|
+
} catch (error) {
|
|
179
|
+
this.logger.error(`Error calling frontend tool ${tool.name}:`, error);
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
const registeredElements = this.registeredElements.get(delegateId) ?? [];
|
|
185
|
+
registeredElements.push(registeredTool);
|
|
186
|
+
this.registeredElements.set(delegateId, registeredElements);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.logger.warn(`Failed to register frontend tools from delegate ${delegateId}: ${error}`);
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Register resources from frontend contributions
|
|
197
|
+
*/
|
|
198
|
+
protected async registerFrontendResourcesFromDelegate(delegate: MCPToolFrontendDelegate, delegateId: string): Promise<void> {
|
|
199
|
+
if (!this.mcpServer || !this.serverId) {
|
|
200
|
+
throw new Error('MCP server not set');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const resources = await delegate.listResources(this.serverId);
|
|
205
|
+
|
|
206
|
+
for (const resource of resources) {
|
|
207
|
+
const registeredResource = this.mcpServer.resource(
|
|
208
|
+
`${resource.name}_${delegateId}`,
|
|
209
|
+
resource.uri,
|
|
210
|
+
async uri => {
|
|
211
|
+
try {
|
|
212
|
+
const result = await delegate.readResource(this.serverId!, uri.href);
|
|
213
|
+
return result as unknown as ReadResourceResult;
|
|
214
|
+
} catch (error) {
|
|
215
|
+
this.logger.error(`Error reading frontend resource ${resource.name}:`, error);
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
const registeredElements = this.registeredElements.get(delegateId) ?? [];
|
|
221
|
+
registeredElements.push(registeredResource);
|
|
222
|
+
this.registeredElements.set(delegateId, registeredElements);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
} catch (error) {
|
|
226
|
+
this.logger.warn(`Failed to register frontend resources from delegate ${delegateId}: ${error}`);
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Register prompts from frontend contributions
|
|
233
|
+
*/
|
|
234
|
+
protected async registerFrontendPromptsFromDelegate(delegate: MCPToolFrontendDelegate, delegateId: string): Promise<void> {
|
|
235
|
+
if (!this.mcpServer || !this.serverId) {
|
|
236
|
+
throw new Error('MCP server not set');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
try {
|
|
240
|
+
const prompts = await delegate.listPrompts(this.serverId);
|
|
241
|
+
|
|
242
|
+
for (const prompt of prompts) {
|
|
243
|
+
const registeredPrompt = this.mcpServer.prompt(
|
|
244
|
+
`${prompt.name}_${delegateId}`,
|
|
245
|
+
prompt.description ?? '',
|
|
246
|
+
prompt.arguments ?? {},
|
|
247
|
+
async args => {
|
|
248
|
+
try {
|
|
249
|
+
const messages = await delegate.getPrompt(this.serverId!, prompt.name, args);
|
|
250
|
+
return {
|
|
251
|
+
messages
|
|
252
|
+
};
|
|
253
|
+
} catch (error) {
|
|
254
|
+
this.logger.error(`Error getting frontend prompt ${prompt.name}:`, error);
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
const registeredElements = this.registeredElements.get(delegateId) ?? [];
|
|
260
|
+
registeredElements.push(registeredPrompt);
|
|
261
|
+
this.registeredElements.set(delegateId, registeredElements);
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
this.logger.warn(`Failed to register frontend prompts from delegate ${delegateId}: ${error}`);
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|