@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.
Files changed (77) hide show
  1. package/README.md +175 -0
  2. package/lib/browser/index.d.ts +3 -0
  3. package/lib/browser/index.d.ts.map +1 -0
  4. package/lib/browser/index.js +21 -0
  5. package/lib/browser/index.js.map +1 -0
  6. package/lib/browser/mcp-frontend-bootstrap.d.ts +17 -0
  7. package/lib/browser/mcp-frontend-bootstrap.d.ts.map +1 -0
  8. package/lib/browser/mcp-frontend-bootstrap.js +47 -0
  9. package/lib/browser/mcp-frontend-bootstrap.js.map +1 -0
  10. package/lib/browser/mcp-frontend-contribution.d.ts +44 -0
  11. package/lib/browser/mcp-frontend-contribution.d.ts.map +1 -0
  12. package/lib/browser/mcp-frontend-contribution.js +21 -0
  13. package/lib/browser/mcp-frontend-contribution.js.map +1 -0
  14. package/lib/browser/mcp-frontend-module.d.ts +4 -0
  15. package/lib/browser/mcp-frontend-module.d.ts.map +1 -0
  16. package/lib/browser/mcp-frontend-module.js +37 -0
  17. package/lib/browser/mcp-frontend-module.js.map +1 -0
  18. package/lib/browser/mcp-tool-delegate-client.d.ts +27 -0
  19. package/lib/browser/mcp-tool-delegate-client.d.ts.map +1 -0
  20. package/lib/browser/mcp-tool-delegate-client.js +128 -0
  21. package/lib/browser/mcp-tool-delegate-client.js.map +1 -0
  22. package/lib/common/index.d.ts +2 -0
  23. package/lib/common/index.d.ts.map +1 -0
  24. package/lib/common/index.js +20 -0
  25. package/lib/common/index.js.map +1 -0
  26. package/lib/common/mcp-tool-delegate.d.ts +25 -0
  27. package/lib/common/mcp-tool-delegate.d.ts.map +1 -0
  28. package/lib/common/mcp-tool-delegate.js +22 -0
  29. package/lib/common/mcp-tool-delegate.js.map +1 -0
  30. package/lib/node/index.d.ts +5 -0
  31. package/lib/node/index.d.ts.map +1 -0
  32. package/lib/node/index.js +23 -0
  33. package/lib/node/index.js.map +1 -0
  34. package/lib/node/mcp-backend-contribution-manager.d.ts +16 -0
  35. package/lib/node/mcp-backend-contribution-manager.d.ts.map +1 -0
  36. package/lib/node/mcp-backend-contribution-manager.js +61 -0
  37. package/lib/node/mcp-backend-contribution-manager.js.map +1 -0
  38. package/lib/node/mcp-backend-module.d.ts +4 -0
  39. package/lib/node/mcp-backend-module.d.ts.map +1 -0
  40. package/lib/node/mcp-backend-module.js +56 -0
  41. package/lib/node/mcp-backend-module.js.map +1 -0
  42. package/lib/node/mcp-frontend-contribution-manager.d.ts +55 -0
  43. package/lib/node/mcp-frontend-contribution-manager.d.ts.map +1 -0
  44. package/lib/node/mcp-frontend-contribution-manager.js +242 -0
  45. package/lib/node/mcp-frontend-contribution-manager.js.map +1 -0
  46. package/lib/node/mcp-theia-server-impl.d.ts +32 -0
  47. package/lib/node/mcp-theia-server-impl.d.ts.map +1 -0
  48. package/lib/node/mcp-theia-server-impl.js +220 -0
  49. package/lib/node/mcp-theia-server-impl.js.map +1 -0
  50. package/lib/node/mcp-theia-server.d.ts +43 -0
  51. package/lib/node/mcp-theia-server.d.ts.map +1 -0
  52. package/lib/node/mcp-theia-server.js +22 -0
  53. package/lib/node/mcp-theia-server.js.map +1 -0
  54. package/lib/node/mcp-tool-frontend-delegate.d.ts +13 -0
  55. package/lib/node/mcp-tool-frontend-delegate.d.ts.map +1 -0
  56. package/lib/node/mcp-tool-frontend-delegate.js +66 -0
  57. package/lib/node/mcp-tool-frontend-delegate.js.map +1 -0
  58. package/lib/package.spec.d.ts +1 -0
  59. package/lib/package.spec.d.ts.map +1 -0
  60. package/lib/package.spec.js +26 -0
  61. package/lib/package.spec.js.map +1 -0
  62. package/package.json +52 -0
  63. package/src/browser/index.ts +18 -0
  64. package/src/browser/mcp-frontend-bootstrap.ts +43 -0
  65. package/src/browser/mcp-frontend-contribution.ts +67 -0
  66. package/src/browser/mcp-frontend-module.ts +42 -0
  67. package/src/browser/mcp-tool-delegate-client.ts +133 -0
  68. package/src/common/index.ts +17 -0
  69. package/src/common/mcp-tool-delegate.ts +43 -0
  70. package/src/node/index.ts +20 -0
  71. package/src/node/mcp-backend-contribution-manager.ts +56 -0
  72. package/src/node/mcp-backend-module.ts +75 -0
  73. package/src/node/mcp-frontend-contribution-manager.ts +268 -0
  74. package/src/node/mcp-theia-server-impl.ts +241 -0
  75. package/src/node/mcp-theia-server.ts +66 -0
  76. package/src/node/mcp-tool-frontend-delegate.ts +71 -0
  77. 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
+ }