deploy: current vibn theia state
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled

Made-with: Cursor
This commit is contained in:
2026-02-27 12:01:08 -08:00
commit 8bb5110148
3782 changed files with 640947 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: [
'../../configs/build.eslintrc.json'
],
parserOptions: {
tsconfigRootDir: __dirname,
project: 'tsconfig.json'
}
};

View File

@@ -0,0 +1,118 @@
<div align='center'>
<br />
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
<h2>ECLIPSE THEIA - AI MCP EXTENSION</h2>
<hr />
</div>
## Description
The AI MCP package provides an integration that allows users to start and use MCP servers to provide additional tool functions to LLMs, e.g. search or file access (outside of the workspace).
### Features
- Offers the framework to add/remove and start/stop MCP servers
- Use tool functions provided by MCP servers in prompt templates
### Commands
- Include `@theia/ai-mcp-ui` to gain access to the start and stop MCP sever commands.
### Configuration
To configure MCP servers, include `@theia/mcp-ui` or `bind` the included `mcp-preferences`.
Afterwards, open the preferences and add entries to the `MCP Servers Configuration` section. Each server requires a unique identifier (e.g., `"brave-search"` or `"filesystem"`) and configuration details such as the command, arguments, optional environment variables, and autostart (true by default).
`"autostart"` (true by default) will automatically start the respective MCP server whenever you restart your Theia application. In your current session, however, you'll still need to **manually start it** using the `"MCP: Start MCP Server"` command.
Example Configuration:
```json
{
"ai-features.mcp.mcpServers": {
"memory": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-memory"
],
"autostart": false
},
"brave-search": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-brave-search"
],
"env": {
"BRAVE_API_KEY": "YOUR_API_KEY"
}
},
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"ABSOLUTE_PATH_TO_ALLOWED_DIRECTORY",
]
},
"git": {
"command": "uv",
"args": [
"--directory",
"/path/to/repo",
"run",
"mcp-server-git"
]
},
"git2": {
"command": "uvx",
"args": [
"mcp-server-git",
"--repository",
"/path/to/otherrepo"
]
}
}
}
```
Example prompt (for search)
```md
~{mcp_brave-search_brave_web_search}
```
Example User query
```md
Search the internet for XYZ
```
### More Information
[Theia AI MCP UI README](https://github.com/eclipse-theia/theia/tree/master/packages/ai-mcp-ui)
[User documentation on MCP in the Theia IDE](https://theia-ide.org/docs/user_ai/#mcp-integration)
[List of available MCP servers](https://github.com/modelcontextprotocol/servers)
## Additional Information
- [API documentation for `@theia/mcp`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_ai-mcp.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,52 @@
{
"name": "@theia/ai-mcp",
"version": "1.68.0",
"description": "Theia - MCP Integration",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.1",
"@theia/ai-core": "1.68.0",
"@theia/core": "1.68.0",
"@theia/workspace": "1.68.0"
},
"publishConfig": {
"access": "public"
},
"main": "lib/common",
"theiaExtensions": [
{
"frontend": "lib/browser/mcp-frontend-module",
"backend": "lib/node/mcp-backend-module"
}
],
"keywords": [
"theia-extension"
],
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
"repository": {
"type": "git",
"url": "https://github.com/eclipse-theia/theia.git"
},
"bugs": {
"url": "https://github.com/eclipse-theia/theia/issues"
},
"homepage": "https://github.com/eclipse-theia/theia",
"files": [
"lib",
"src"
],
"scripts": {
"build": "theiaext build",
"clean": "theiaext clean",
"compile": "theiaext compile",
"lint": "theiaext lint",
"test": "theiaext test",
"watch": "theiaext watch"
},
"devDependencies": {
"@theia/ext-scripts": "1.68.0"
},
"nyc": {
"extends": "../../configs/nyc.json"
},
"gitHead": "21358137e41342742707f660b8e222f940a27652"
}

View File

@@ -0,0 +1,275 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { inject, injectable } from '@theia/core/shared/inversify';
import { MCPServerDescription, MCPServerManager } from '../common';
import { MCP_SERVERS_PREF } from '../common/mcp-preferences';
import { MCPFrontendService } from '../common/mcp-server-manager';
import { JSONObject } from '@theia/core/shared/@lumino/coreutils';
import { PreferenceService, PreferenceUtils } from '@theia/core';
import { nls } from '@theia/core/lib/common/nls';
import {
WorkspaceTrustService,
WorkspaceRestrictionContribution,
WorkspaceRestriction
} from '@theia/workspace/lib/browser/workspace-trust-service';
interface BaseMCPServerPreferenceValue {
autostart?: boolean;
}
interface LocalMCPServerPreferenceValue extends BaseMCPServerPreferenceValue {
command: string;
args?: string[];
env?: { [key: string]: string };
}
interface RemoteMCPServerPreferenceValue extends BaseMCPServerPreferenceValue {
serverUrl: string;
serverAuthToken?: string;
serverAuthTokenHeader?: string;
headers?: { [key: string]: string };
}
type MCPServersPreferenceValue = LocalMCPServerPreferenceValue | RemoteMCPServerPreferenceValue;
interface MCPServersPreference {
[name: string]: MCPServersPreferenceValue
};
namespace MCPServersPreference {
export function isValue(obj: unknown): obj is MCPServersPreferenceValue {
return !!obj && typeof obj === 'object' &&
('command' in obj || 'serverUrl' in obj) &&
(!('command' in obj) || typeof obj.command === 'string') &&
(!('args' in obj) || Array.isArray(obj.args) && obj.args.every(arg => typeof arg === 'string')) &&
(!('env' in obj) || !!obj.env && typeof obj.env === 'object' && Object.values(obj.env).every(value => typeof value === 'string')) &&
(!('autostart' in obj) || typeof obj.autostart === 'boolean') &&
(!('serverUrl' in obj) || typeof obj.serverUrl === 'string') &&
(!('serverAuthToken' in obj) || typeof obj.serverAuthToken === 'string') &&
(!('serverAuthTokenHeader' in obj) || typeof obj.serverAuthTokenHeader === 'string') &&
(!('headers' in obj) || !!obj.headers && typeof obj.headers === 'object' && Object.values(obj.headers).every(value => typeof value === 'string'));
}
}
function filterValidValues(servers: unknown): MCPServersPreference {
const result: MCPServersPreference = {};
if (!servers || typeof servers !== 'object') {
return result;
}
for (const [name, value] of Object.entries(servers)) {
if (typeof name === 'string' && MCPServersPreference.isValue(value)) {
result[name] = value;
}
}
return result;
}
@injectable()
export class McpFrontendApplicationContribution implements FrontendApplicationContribution, WorkspaceRestrictionContribution {
@inject(PreferenceService)
protected preferenceService: PreferenceService;
@inject(MCPServerManager)
protected manager: MCPServerManager;
@inject(MCPFrontendService)
protected frontendMCPService: MCPFrontendService;
@inject(WorkspaceTrustService)
protected workspaceTrustService: WorkspaceTrustService;
protected prevServers: Map<string, MCPServerDescription> = new Map();
protected blockedUntrustedServers: Set<string> = new Set();
onStart(): void {
this.preferenceService.ready.then(async () => {
const servers = filterValidValues(this.preferenceService.get(
MCP_SERVERS_PREF,
{}
));
this.prevServers = this.convertToMap(servers);
this.syncServers(this.prevServers);
await this.autoStartServers(this.prevServers);
this.preferenceService.onPreferenceChanged(event => {
if (event.preferenceName === MCP_SERVERS_PREF) {
this.handleServerChanges(filterValidValues(this.preferenceService.get(MCP_SERVERS_PREF, {})));
}
});
this.workspaceTrustService.onDidChangeWorkspaceTrust(async trusted => {
try {
if (trusted) {
await this.startPreviouslyBlockedServers();
} else {
await this.stopAllServers();
}
} catch (error) {
console.error('Failed to handle workspace trust change for MCP servers', error);
}
});
});
this.frontendMCPService.registerToolsForAllStartedServers();
}
protected async startPreviouslyBlockedServers(): Promise<void> {
if (this.blockedUntrustedServers.size === 0) {
return;
}
const startedServers = await this.frontendMCPService.getStartedServers();
for (const name of this.blockedUntrustedServers) {
const serverDesc = this.prevServers.get(name);
if (serverDesc && serverDesc.autostart && !startedServers.includes(name)) {
await this.frontendMCPService.startServer(name);
}
}
this.blockedUntrustedServers.clear();
this.updateBlockedServersStatusBar();
}
protected async stopAllServers(): Promise<void> {
const startedServers = await this.frontendMCPService.getStartedServers();
for (const name of startedServers) {
await this.frontendMCPService.stopServer(name);
const serverDesc = this.prevServers.get(name);
if (serverDesc?.autostart) {
this.blockedUntrustedServers.add(name);
}
}
this.updateBlockedServersStatusBar();
}
protected updateBlockedServersStatusBar(): void {
this.workspaceTrustService.refreshRestrictedModeIndicator();
}
getRestrictions(): WorkspaceRestriction[] {
if (this.blockedUntrustedServers.size === 0) {
return [];
}
return [{
label: nls.localize('theia/ai-mcp/blockedServersLabel', 'MCP Servers (autostart blocked)'),
details: Array.from(this.blockedUntrustedServers)
}];
}
protected async autoStartServers(servers: Map<string, MCPServerDescription>): Promise<void> {
const startedServers = await this.frontendMCPService.getStartedServers();
const isTrusted = await this.workspaceTrustService.getWorkspaceTrust();
for (const [name, serverDesc] of servers) {
if (serverDesc && serverDesc.autostart) {
if (!startedServers.includes(name)) {
// Block MCP autostart in untrusted workspaces to prevent interaction with malicious content.
if (!isTrusted) {
this.blockedUntrustedServers.add(name);
continue;
}
await this.frontendMCPService.startServer(name);
}
}
}
this.updateBlockedServersStatusBar();
}
protected handleServerChanges(newServers: MCPServersPreference): void {
const oldServers = this.prevServers;
const updatedServers = this.convertToMap(newServers);
for (const [name] of oldServers) {
if (!updatedServers.has(name)) {
this.manager.removeServer(name);
this.blockedUntrustedServers.delete(name);
}
}
for (const [name, description] of updatedServers) {
const oldDescription = oldServers.get(name);
let diff = false;
try {
// We know that that the descriptions are actual JSONObjects as we construct them ourselves
if (!oldDescription || !PreferenceUtils.deepEqual(oldDescription as unknown as JSONObject, description as unknown as JSONObject)) {
diff = true;
}
} catch (e) {
// In some cases the deepEqual function throws an error, so we fall back to assuming that there is a difference
// This seems to happen in cases where the objects are structured differently, e.g. whole sub-objects are missing
console.debug('Failed to compare MCP server descriptions, assuming a difference', e);
diff = true;
}
if (diff) {
this.manager.addOrUpdateServer(description);
}
}
this.prevServers = updatedServers;
this.autoStartServers(updatedServers).catch(error => {
console.error('Failed to auto-start MCP servers after preference change', error);
});
}
protected syncServers(servers: Map<string, MCPServerDescription>): void {
for (const [, description] of servers) {
this.manager.addOrUpdateServer(description);
}
for (const [name] of this.prevServers) {
if (!servers.has(name)) {
this.manager.removeServer(name);
}
}
}
protected convertToMap(servers: MCPServersPreference): Map<string, MCPServerDescription> {
const map = new Map<string, MCPServerDescription>();
Object.entries(servers).forEach(([name, description]) => {
let filteredDescription: MCPServerDescription;
if ('serverUrl' in description) {
// Create RemoteMCPServerDescription by picking only remote-specific properties
const { serverUrl, serverAuthToken, serverAuthTokenHeader, headers, autostart } = description;
filteredDescription = {
name,
serverUrl,
...(serverAuthToken && { serverAuthToken }),
...(serverAuthTokenHeader && { serverAuthTokenHeader }),
...(headers && { headers }),
autostart: autostart ?? true,
};
} else {
// Create LocalMCPServerDescription by picking only local-specific properties
const { command, args, env, autostart } = description;
filteredDescription = {
name,
command,
...(args && { args }),
...(env && { env }),
autostart: autostart ?? true,
};
}
map.set(name, filteredDescription);
});
return map;
}
}

View File

@@ -0,0 +1,93 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { ContainerModule } from '@theia/core/shared/inversify';
import { FrontendApplicationContribution, RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser';
import {
MCPFrontendService,
MCPServerManager,
MCPServerManagerPath,
MCPFrontendNotificationService
} from '../common/mcp-server-manager';
import { McpFrontendApplicationContribution } from './mcp-frontend-application-contribution';
import { MCPFrontendServiceImpl } from './mcp-frontend-service';
import { MCPFrontendNotificationServiceImpl } from './mcp-frontend-notification-service';
import { MCPServerManagerServerClientImpl } from './mcp-server-manager-server-client';
import { MCPServerManagerServer, MCPServerManagerServerClient, MCPServerManagerServerPath } from '../common/mcp-protocol';
import { WorkspaceRestrictionContribution } from '@theia/workspace/lib/browser/workspace-trust-service';
export default new ContainerModule(bind => {
bind(McpFrontendApplicationContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(McpFrontendApplicationContribution);
bind(WorkspaceRestrictionContribution).toService(McpFrontendApplicationContribution);
bind(MCPFrontendService).to(MCPFrontendServiceImpl).inSingletonScope();
bind(MCPFrontendNotificationService).to(MCPFrontendNotificationServiceImpl).inSingletonScope();
bind(MCPServerManagerServerClient).to(MCPServerManagerServerClientImpl).inSingletonScope();
bind(MCPServerManagerServer).toDynamicValue(ctx => {
const connection = ctx.container.get<ServiceConnectionProvider>(RemoteConnectionProvider);
const client = ctx.container.get<MCPServerManagerServerClient>(MCPServerManagerServerClient);
return connection.createProxy<MCPServerManagerServer>(MCPServerManagerServerPath, client);
}).inSingletonScope();
bind(MCPServerManager).toDynamicValue(ctx => {
const mgrServer = ctx.container.get<MCPServerManagerServer>(MCPServerManagerServer);
const connection = ctx.container.get<ServiceConnectionProvider>(RemoteConnectionProvider);
const client = ctx.container.get<MCPFrontendNotificationService>(MCPFrontendNotificationService);
const serverClient = ctx.container.get<MCPServerManagerServerClient>(MCPServerManagerServerClient);
const backendServerManager = connection.createProxy<MCPServerManager>(MCPServerManagerPath, client);
// Listen to server updates to clean up removed servers
client.onDidUpdateMCPServers(() =>
backendServerManager.getServerNames()
.then(names => serverClient.cleanServers(names))
.catch((error: unknown) => {
console.error('Error cleaning server descriptions:', error);
}));
// We proxy the MCPServerManager to override addOrUpdateServer and getServerDescription
// to handle the resolve functions via the MCPServerManagerServerClient.
return new Proxy(backendServerManager, {
get(target: MCPServerManager, prop: PropertyKey, receiver: unknown): unknown {
// override addOrUpdateServer to store the original description in the MCPServerManagerServerClient
// to be used in resolveServerDescription if a resolve function is provided
if (prop === 'addOrUpdateServer') {
return async function (this: MCPServerManager, ...args: [serverDescription: Parameters<MCPServerManager['addOrUpdateServer']>[0]]): Promise<void> {
const updated = serverClient.addServerDescription(args[0]);
await mgrServer.addOrUpdateServer(updated);
};
}
// override getServerDescription to mix in the resolve function from the client
if (prop === 'getServerDescription') {
return async function (this: MCPServerManager, name: string): ReturnType<MCPServerManager['getServerDescription']> {
const description = await Reflect.apply(target.getServerDescription, target, [name]);
if (description) {
const resolveFunction = serverClient.getResolveFunction(name);
if (resolveFunction) {
return {
...description,
resolve: resolveFunction
};
}
}
return description;
};
}
return Reflect.get(target, prop, receiver);
}
});
}).inSingletonScope();
});

View File

@@ -0,0 +1,29 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { MCPFrontendNotificationService } from '../common';
import { Emitter, Event } from '@theia/core/lib/common/event';
@injectable()
export class MCPFrontendNotificationServiceImpl implements MCPFrontendNotificationService {
protected readonly onDidUpdateMCPServersEmitter = new Emitter<void>();
public readonly onDidUpdateMCPServers: Event<void> = this.onDidUpdateMCPServersEmitter.event;
didUpdateMCPServers(): void {
this.onDidUpdateMCPServersEmitter.fire();
}
}

View File

@@ -0,0 +1,157 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable, inject } from '@theia/core/shared/inversify';
import { MCPFrontendService, MCPServerDescription, MCPServerManager } from '../common/mcp-server-manager';
import { ToolInvocationRegistry, ToolRequest, PromptService, ToolCallContent, ToolCallContentResult } from '@theia/ai-core';
import { ListToolsResult, TextContent } from '@modelcontextprotocol/sdk/types';
@injectable()
export class MCPFrontendServiceImpl implements MCPFrontendService {
@inject(MCPServerManager)
protected readonly mcpServerManager: MCPServerManager;
@inject(ToolInvocationRegistry)
protected readonly toolInvocationRegistry: ToolInvocationRegistry;
@inject(PromptService)
protected readonly promptService: PromptService;
async startServer(serverName: string): Promise<void> {
await this.mcpServerManager.startServer(serverName);
await this.registerTools(serverName);
}
async hasServer(serverName: string): Promise<boolean> {
const serverNames = await this.getServerNames();
return serverNames.includes(serverName);
}
async isServerStarted(serverName: string): Promise<boolean> {
const startedServers = await this.getStartedServers();
return startedServers.includes(serverName);
}
async registerToolsForAllStartedServers(): Promise<void> {
const startedServers = await this.getStartedServers();
for (const serverName of startedServers) {
await this.registerTools(serverName);
}
}
async registerTools(serverName: string): Promise<void> {
const returnedTools = await this.getTools(serverName);
if (returnedTools) {
const toolRequests: ToolRequest[] = returnedTools.tools.map(tool => this.convertToToolRequest(tool, serverName));
toolRequests.forEach(toolRequest =>
this.toolInvocationRegistry.registerTool(toolRequest)
);
this.createPromptTemplate(serverName, toolRequests);
}
}
getPromptTemplateId(serverName: string): string {
return `mcp_${serverName}_tools`;
}
protected createPromptTemplate(serverName: string, toolRequests: ToolRequest[]): void {
const templateId = this.getPromptTemplateId(serverName);
const functionIds = toolRequests.map(tool => `~{${tool.id}}`);
const template = functionIds.join('\n');
this.promptService.addBuiltInPromptFragment({
id: templateId,
template
});
}
async stopServer(serverName: string): Promise<void> {
this.toolInvocationRegistry.unregisterAllTools(`mcp_${serverName}`);
this.promptService.removePromptFragment(this.getPromptTemplateId(serverName));
await this.mcpServerManager.stopServer(serverName);
}
getStartedServers(): Promise<string[]> {
return this.mcpServerManager.getRunningServers();
}
getServerNames(): Promise<string[]> {
return this.mcpServerManager.getServerNames();
}
async getServerDescription(name: string): Promise<MCPServerDescription | undefined> {
return this.mcpServerManager.getServerDescription(name);
}
async getTools(serverName: string): Promise<ListToolsResult | undefined> {
try {
return await this.mcpServerManager.getTools(serverName);
} catch (error) {
console.error('Error while trying to get tools: ' + error);
return undefined;
}
}
async addOrUpdateServer(description: MCPServerDescription): Promise<void> {
return this.mcpServerManager.addOrUpdateServer(description);
}
private convertToToolRequest(tool: Awaited<ReturnType<MCPServerManager['getTools']>>['tools'][number], serverName: string): ToolRequest {
const id = `mcp_${serverName}_${tool.name}`;
return {
id: id,
name: id,
providerName: `mcp_${serverName}`,
parameters: ToolRequest.isToolRequestParameters(tool.inputSchema) ? {
type: tool.inputSchema.type,
properties: tool.inputSchema.properties,
required: tool.inputSchema.required
} : {
type: 'object',
properties: {}
},
description: tool.description,
handler: async (arg_string: string): Promise<ToolCallContent> => {
try {
const result = await this.mcpServerManager.callTool(serverName, tool.name, arg_string);
if (result.isError) {
const textContent = result.content.find(callContent => callContent.type === 'text') as TextContent | undefined;
return { content: [{ type: 'error', data: textContent?.text ?? 'Unknown Error' }] };
}
const content = result.content.map<ToolCallContentResult>(callContent => {
switch (callContent.type) {
case 'image':
return { type: 'image', base64data: callContent.data, mimeType: callContent.mimeType };
case 'text':
return { type: 'text', text: callContent.text };
case 'resource': {
return { type: 'text', text: JSON.stringify(callContent.resource) };
}
default: {
return { type: 'text', text: JSON.stringify(callContent) };
}
}
});
return { content };
} catch (error) {
console.error(`Error in tool handler for ${tool.name} on MCP server ${serverName}:`, error);
throw error;
}
},
};
}
}

View File

@@ -0,0 +1,82 @@
// *****************************************************************************
// Copyright (C) 2025 Dirk Fauth and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { MCPServerDescription } from '../common';
import { generateUuid } from '@theia/core/lib/common/uuid';
import { cleanServerDescription, MCPServerDescriptionRCP, MCPServerManagerServerClient } from '../common/mcp-protocol';
type StoredServerInfo = Pick<MCPServerDescription, 'name' | 'resolve'>;
@injectable()
export class MCPServerManagerServerClientImpl implements MCPServerManagerServerClient {
protected serverDescriptions: Map<string, StoredServerInfo> = new Map();
addServerDescription(description: MCPServerDescription): MCPServerDescriptionRCP {
if (description.resolve) {
const serverDescription: MCPServerDescriptionRCP = {
...description,
resolveId: generateUuid(),
};
// store only the name and resolve function
if (serverDescription.resolveId) {
this.serverDescriptions.set(serverDescription.resolveId, {
name: description.name,
resolve: description.resolve
});
}
return serverDescription;
}
return description;
}
getResolveFunction(name: string): MCPServerDescription['resolve'] {
for (const storedInfo of this.serverDescriptions.values()) {
if (storedInfo.name === name) {
return storedInfo.resolve;
}
}
return undefined;
}
async resolveServerDescription(description: MCPServerDescriptionRCP): Promise<MCPServerDescription> {
const cleanDescription = cleanServerDescription(description);
if (description.resolveId) {
const storedInfo = this.serverDescriptions.get(description.resolveId);
if (storedInfo?.resolve) {
const updated = await storedInfo.resolve(cleanDescription);
if (updated) {
return updated;
}
}
}
return cleanDescription;
}
cleanServers(serverNames: string[]): void {
const currentNamesSet = new Set(serverNames);
// Remove descriptions for servers that no longer exist
for (const [resolveId, storedInfo] of this.serverDescriptions.entries()) {
if (storedInfo.name && !currentNamesSet.has(storedInfo.name)) {
console.debug('Removing a frontend stored resolve function because the corresponding MCP server was removed', storedInfo);
this.serverDescriptions.delete(resolveId);
}
}
}
}

View File

@@ -0,0 +1,16 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
export * from './mcp-server-manager';

View File

@@ -0,0 +1,117 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { nls, PreferenceSchema } from '@theia/core';
export const MCP_SERVERS_PREF = 'ai-features.mcp.mcpServers';
export const McpServersPreferenceSchema: PreferenceSchema = {
properties: {
[MCP_SERVERS_PREF]: {
type: 'object',
title: nls.localize('theia/ai/mcp/servers/title', 'MCP Server Configuration'),
markdownDescription: nls.localize('theia/ai/mcp/servers/mdDescription', 'Configure MCP servers either local with command, \
arguments and optionally environment variables, \
or remote with server URL, authentication token and optionally an authentication header name. Additionally it is possible to configure autostart (true by default). \
Each server is identified by a unique key, such as "brave-search" or "filesystem". \
To start a server, use the "MCP: Start MCP Server" command, which enables you to select the desired server. \
To stop a server, use the "MCP: Stop MCP Server" command. \
Please note that autostart will only take effect after a restart, you need to start a server manually for the first time.\
\n\
Example configuration:\n\
```\
{\n\
"brave-search": {\n\
"command": "npx",\n\
"args": [\n\
"-y",\n\
"@modelcontextprotocol/server-brave-search"\n\
],\n\
"env": {\n\
"BRAVE_API_KEY": "YOUR_API_KEY"\n\
},\n\
},\n\
"filesystem": {\n\
"command": "npx",\n\
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/YOUR_USERNAME/Desktop"],\n\
"env": {\n\
"CUSTOM_ENV_VAR": "custom-value"\n\
},\n\
"autostart": false\n\
},\n\
"jira": {\n\
"serverUrl": "YOUR_JIRA_MCP_SERVER_URL",\n\
"serverAuthToken": "YOUR_JIRA_MCP_SERVER_TOKEN"\n\
}\n\
}\n```'),
additionalProperties: {
type: 'object',
properties: {
command: {
type: 'string',
title: nls.localize('theia/ai/mcp/servers/command/title', 'Command to execute the MCP server'),
markdownDescription: nls.localize('theia/ai/mcp/servers/command/mdDescription', 'The command used to start the MCP server, e.g., "uvx" or "npx".')
},
args: {
type: 'array',
title: nls.localize('theia/ai/mcp/servers/args/title', 'Arguments for the command'),
markdownDescription: nls.localize('theia/ai/mcp/servers/args/mdDescription', 'An array of arguments to pass to the command.'),
},
env: {
type: 'object',
title: nls.localize('theia/ai/mcp/servers/env/title', 'Environment variables'),
markdownDescription: nls.localize('theia/ai/mcp/servers/env/mdDescription', 'Optional environment variables to set for the server, such as an API key.'),
additionalProperties: {
type: 'string'
}
},
autostart: {
type: 'boolean',
title: nls.localize('theia/ai/mcp/servers/autostart/title', 'Autostart'),
markdownDescription: nls.localize('theia/ai/mcp/servers/autostart/mdDescription',
'Automatically start this server when the frontend starts. Newly added servers are not immediately auto started, but on restart'),
default: true
},
serverUrl: {
type: 'string',
title: nls.localize('theia/ai/mcp/servers/serverUrl/title', 'Server URL'),
markdownDescription: nls.localize('theia/ai/mcp/servers/serverUrl/mdDescription',
'The URL of the remote MCP server. If provided, the server will connect to this URL instead of starting a local process.'),
},
serverAuthToken: {
type: 'string',
title: nls.localize('theia/ai/mcp/servers/serverAuthToken/title', 'Authentication Token'),
markdownDescription: nls.localize('theia/ai/mcp/servers/serverAuthToken/mdDescription',
'The authentication token for the server, if required. This is used to authenticate with the remote server.'),
},
serverAuthTokenHeader: {
type: 'string',
title: nls.localize('theia/ai/mcp/servers/serverAuthTokenHeader/title', 'Authentication Header Name'),
markdownDescription: nls.localize('theia/ai/mcp/servers/serverAuthTokenHeader/mdDescription',
'The header name to use for the server authentication token. If not provided, "Authorization" with "Bearer" will be used.'),
},
headers: {
type: 'object',
title: nls.localize('theia/ai/mcp/servers/headers/title', 'Headers'),
markdownDescription: nls.localize('theia/ai/mcp/servers/headers/mdDescription',
'Optional additional headers included with each request to the server.'),
}
},
required: []
}
}
}
};

View File

@@ -0,0 +1,75 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { MCPServerDescription } from './mcp-server-manager';
/**
* MCPServerDescriptionRCP is a version of MCPServerDescription that can be sent over RCP.
* It omits the 'resolve' function and instead includes an optional 'resolveId' to identify
* the resolve function on the client side.
*/
export type MCPServerDescriptionRCP = Omit<MCPServerDescription, 'resolve'> & {
resolveId?: string;
};
export const MCPServerManagerServer = Symbol('MCPServerManagerServer');
export const MCPServerManagerServerPath = '/services/mcpservermanagerserver';
/**
* The MCPServerManagerServer handles the RCP specialties of adding server descriptions from the frontend
*/
export interface MCPServerManagerServer {
addOrUpdateServer(description: MCPServerDescriptionRCP): Promise<void>;
setClient(client: MCPServerManagerServerClient): void
}
export const MCPServerManagerServerClient = Symbol('MCPServerManagerServerClient');
export interface MCPServerManagerServerClient {
/**
* Adds a server description to the client. If the description contains a resolve function,
* a unique resolveId is generated and only the name and resolve function are stored.
* @param description The server description to add.
* @returns The server description with a unique resolveId if a resolve function is provided
* or the given description if no resolve function is provided.
*/
addServerDescription(description: MCPServerDescription): MCPServerDescriptionRCP;
/**
* Retrieves the resolve function for a given server name.
* @param name The name of the server to retrieve the resolve function for.
* @returns The resolve function if found, or undefined if not found.
*/
getResolveFunction(name: string): MCPServerDescription['resolve'];
/**
* Resolves the server description by calling the resolve function if it exists.
* @param description The server description to resolve.
* @returns The resolved server description.
*/
resolveServerDescription(description: MCPServerDescriptionRCP): Promise<MCPServerDescription>;
/**
* Removes server descriptions that are no longer present in the MCPServerManager.
*
* @param serverNames The current list of server names from the MCPServerManager.
*/
cleanServers(serverNames: string[]): void;
}
/**
* Util function to convert a MCPServerDescriptionRCP to a MCPServerDescription by removing the resolveId.
*/
export const cleanServerDescription = (description: MCPServerDescriptionRCP): MCPServerDescription => {
const { resolveId, ...descriptionProperties } = description;
return { ...descriptionProperties } as MCPServerDescription;
};

View File

@@ -0,0 +1,167 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import type { CallToolResult, ListResourcesResult, ListToolsResult, ReadResourceResult } from '@modelcontextprotocol/sdk/types';
import { Event } from '@theia/core/lib/common/event';
export const MCPFrontendService = Symbol('MCPFrontendService');
export interface MCPFrontendService {
startServer(serverName: string): Promise<void>;
hasServer(serverName: string): Promise<boolean>;
isServerStarted(serverName: string): Promise<boolean>;
registerToolsForAllStartedServers(): Promise<void>;
stopServer(serverName: string): Promise<void>;
addOrUpdateServer(description: MCPServerDescription): Promise<void>;
getStartedServers(): Promise<string[]>;
getServerNames(): Promise<string[]>;
getServerDescription(name: string): Promise<MCPServerDescription | undefined>;
getTools(serverName: string): Promise<ListToolsResult | undefined>;
getPromptTemplateId(serverName: string): string;
}
export const MCPFrontendNotificationService = Symbol('MCPFrontendNotificationService');
export interface MCPFrontendNotificationService {
readonly onDidUpdateMCPServers: Event<void>;
didUpdateMCPServers(): void;
}
export interface MCPServer {
callTool(toolName: string, arg_string: string): Promise<CallToolResult>;
getTools(): Promise<ListToolsResult>;
readResource(resourceId: string): Promise<ReadResourceResult>;
getResources(): Promise<ListResourcesResult>;
description: MCPServerDescription;
}
export interface MCPServerManager {
callTool(serverName: string, toolName: string, arg_string: string): Promise<CallToolResult>;
removeServer(name: string): void;
addOrUpdateServer(description: MCPServerDescription): void;
getTools(serverName: string): Promise<ListToolsResult>;
getServerNames(): Promise<string[]>;
getServerDescription(name: string): Promise<MCPServerDescription | undefined>;
startServer(serverName: string): Promise<void>;
stopServer(serverName: string): Promise<void>;
getRunningServers(): Promise<string[]>;
setClient(client: MCPFrontendNotificationService): void;
disconnectClient(client: MCPFrontendNotificationService): void;
readResource(serverName: string, resourceId: string): Promise<ReadResourceResult>;
getResources(serverName: string): Promise<ListResourcesResult>;
}
export interface ToolInformation {
name: string;
description?: string;
}
export enum MCPServerStatus {
NotRunning = 'Not Running',
NotConnected = 'Not Connected',
Starting = 'Starting',
Connecting = 'Connecting',
Running = 'Running',
Connected = 'Connected',
Errored = 'Errored'
}
export interface BaseMCPServerDescription {
/**
* The unique name of the MCP server.
*/
name: string;
/**
* Flag indicating whether the server should automatically start when the application starts.
*/
autostart?: boolean;
/**
* The current status of the server. Optional because only set by the server.
*/
status?: MCPServerStatus;
/**
* Last error message that the server has returned.
*/
error?: string;
/**
* List of available tools for the server. Returns the name and description if available.
*/
tools?: ToolInformation[];
/**
* Optional resolve function that gets called during server definition resolution.
* This function can be used to dynamically modify server configurations,
* resolve environment variables, validate configurations, or perform any
* necessary preprocessing before the server starts.
*
* @param description The current server description
* @returns A promise that resolves to the processed server description
*/
resolve?: (description: MCPServerDescription) => Promise<MCPServerDescription>;
}
export interface LocalMCPServerDescription extends BaseMCPServerDescription {
/**
* The command to execute the MCP server.
*/
command: string;
/**
* An array of arguments to pass to the command.
*/
args?: string[];
/**
* Optional environment variables to set when starting the server.
*/
env?: { [key: string]: string };
}
export interface RemoteMCPServerDescription extends BaseMCPServerDescription {
/**
* The URL of the remote MCP server.
*/
serverUrl: string;
/**
* The authentication token for the server, if required.
*/
serverAuthToken?: string;
/**
* The header name to use for the server authentication token.
*/
serverAuthTokenHeader?: string;
/**
* Optional additional headers to include in requests to the server.
*/
headers?: Record<string, string>;
}
export type MCPServerDescription = LocalMCPServerDescription | RemoteMCPServerDescription;
export function isLocalMCPServerDescription(description: MCPServerDescription): description is LocalMCPServerDescription {
return (description as LocalMCPServerDescription).command !== undefined;
}
export function isRemoteMCPServerDescription(description: MCPServerDescription): description is RemoteMCPServerDescription {
return (description as RemoteMCPServerDescription).serverUrl !== undefined;
}
export const MCPServerManager = Symbol('MCPServerManager');
export const MCPServerManagerPath = '/services/mcpservermanager';

View File

@@ -0,0 +1,54 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { ContainerModule } from '@theia/core/shared/inversify';
import { ConnectionHandler, PreferenceContribution, RpcConnectionHandler } from '@theia/core';
import { MCPServerManagerImpl } from './mcp-server-manager-impl';
import {
MCPFrontendNotificationService,
MCPServerManager,
MCPServerManagerPath
} from '../common/mcp-server-manager';
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
import { McpServersPreferenceSchema } from '../common/mcp-preferences';
import { MCPServerManagerServerImpl } from './mcp-server-manager-server';
import { MCPServerManagerServer, MCPServerManagerServerClient, MCPServerManagerServerPath } from '../common/mcp-protocol';
// We use a connection module to handle AI services separately for each frontend.
const mcpConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService, bindFrontendService }) => {
bind(MCPServerManager).to(MCPServerManagerImpl).inSingletonScope();
bind(ConnectionHandler).toDynamicValue(ctx => new RpcConnectionHandler<MCPFrontendNotificationService>(
MCPServerManagerPath, client => {
const server = ctx.container.get<MCPServerManager>(MCPServerManager);
server.setClient(client);
client.onDidCloseConnection(() => server.disconnectClient(client));
return server;
}
)).inSingletonScope();
bind(MCPServerManagerServer).to(MCPServerManagerServerImpl).inSingletonScope();
bind(ConnectionHandler).toDynamicValue(ctx => new RpcConnectionHandler<MCPServerManagerServerClient>(
MCPServerManagerServerPath, client => {
const server = ctx.container.get<MCPServerManagerServer>(MCPServerManagerServer);
server.setClient(client);
return server;
}
)).inSingletonScope();
});
export default new ContainerModule(bind => {
bind(PreferenceContribution).toConstantValue({ schema: McpServersPreferenceSchema });
bind(ConnectionContainerModule).toConstantValue(mcpConnectionModule);
});

View File

@@ -0,0 +1,162 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { injectable } from '@theia/core/shared/inversify';
import { MCPServerDescription, MCPServerManager, MCPFrontendNotificationService } from '../common/mcp-server-manager';
import { MCPServer } from './mcp-server';
import { Disposable } from '@theia/core/lib/common/disposable';
import { CallToolResult, ListResourcesResult, ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';
@injectable()
export class MCPServerManagerImpl implements MCPServerManager {
protected servers: Map<string, MCPServer> = new Map();
protected clients: Array<MCPFrontendNotificationService> = [];
protected serverListeners: Map<string, Disposable> = new Map();
async stopServer(serverName: string): Promise<void> {
const server = this.servers.get(serverName);
if (!server) {
throw new Error(`MCP server "${serverName}" not found.`);
}
await server.stop();
console.log(`MCP server "${serverName}" stopped.`);
this.notifyClients();
}
async getRunningServers(): Promise<string[]> {
const runningServers: string[] = [];
for (const [name, server] of this.servers.entries()) {
if (server.isRunning()) {
runningServers.push(name);
}
}
return runningServers;
}
callTool(serverName: string, toolName: string, arg_string: string): Promise<CallToolResult> {
const server = this.servers.get(serverName);
if (!server) {
throw new Error(`MCP server "${toolName}" not found.`);
}
return server.callTool(toolName, arg_string);
}
async startServer(serverName: string): Promise<void> {
const server = this.servers.get(serverName);
if (!server) {
throw new Error(`MCP server "${serverName}" not found.`);
}
const description = await server.getDescription();
if (description.resolve) {
const resolved = await description.resolve(description);
const isEqual = JSON.stringify(description) === JSON.stringify(resolved);
if (!isEqual) {
server.update(resolved);
}
}
await server.start();
this.notifyClients();
}
async getServerNames(): Promise<string[]> {
return Array.from(this.servers.keys());
}
async getServerDescription(name: string): Promise<MCPServerDescription | undefined> {
const server = this.servers.get(name);
return server ? await server.getDescription() : undefined;
}
public async getTools(serverName: string): ReturnType<MCPServer['getTools']> {
const server = this.servers.get(serverName);
if (!server) {
throw new Error(`MCP server "${serverName}" not found.`);
}
return await server.getTools();
}
addOrUpdateServer(description: MCPServerDescription): void {
const existingServer = this.servers.get(description.name);
if (existingServer) {
existingServer.update(description);
} else {
const newServer = new MCPServer(description);
this.servers.set(description.name, newServer);
// Subscribe to status updates from the new server
const listener = newServer.onDidUpdateStatus(() => {
this.notifyClients();
});
// Store the listener for later disposal
this.serverListeners.set(description.name, listener);
}
this.notifyClients();
}
removeServer(name: string): void {
const server = this.servers.get(name);
if (server) {
server.stop();
this.servers.delete(name);
// Clean up the status listener
const listener = this.serverListeners.get(name);
if (listener) {
listener.dispose();
this.serverListeners.delete(name);
}
} else {
console.warn(`MCP server "${name}" not found.`);
}
this.notifyClients();
}
setClient(client: MCPFrontendNotificationService): void {
this.clients.push(client);
}
disconnectClient(client: MCPFrontendNotificationService): void {
const index = this.clients.indexOf(client);
if (index !== -1) {
this.clients.splice(index, 1);
}
this.servers.forEach(server => {
server.stop();
});
}
private notifyClients(): void {
this.clients.forEach(client => client.didUpdateMCPServers());
}
readResource(serverName: string, resourceId: string): Promise<ReadResourceResult> {
const server = this.servers.get(serverName);
if (!server) {
throw new Error(`MCP server "${serverName}" not found.`);
}
return server.readResource(resourceId);
}
getResources(serverName: string): Promise<ListResourcesResult> {
const server = this.servers.get(serverName);
if (!server) {
throw new Error(`MCP server "${serverName}" not found.`);
}
return server.getResources();
}
}

View File

@@ -0,0 +1,49 @@
// *****************************************************************************
// Copyright (C) 2025 Dirk Fauth and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { inject, injectable } from '@theia/core/shared/inversify';
import { MCPServerDescription, MCPServerManager } from '../common';
import { cleanServerDescription, MCPServerDescriptionRCP, MCPServerManagerServer, MCPServerManagerServerClient } from '../common/mcp-protocol';
@injectable()
export class MCPServerManagerServerImpl implements MCPServerManagerServer {
@inject(MCPServerManager)
protected readonly mcpServerManager: MCPServerManager;
protected client: MCPServerManagerServerClient;
setClient(client: MCPServerManagerServerClient): void {
this.client = client;
}
async addOrUpdateServer(descriptionRCP: MCPServerDescriptionRCP): Promise<void> {
const description = cleanServerDescription(descriptionRCP);
if (descriptionRCP.resolveId) {
description.resolve = async (desc: MCPServerDescription) => {
if (this.client) {
const descRCP: MCPServerDescriptionRCP = {
...desc,
resolveId: descriptionRCP.resolveId
};
return this.client.resolveServerDescription(descRCP);
}
return desc; // Fallback if no client is set
};
};
this.mcpServerManager.addOrUpdateServer(description);
}
}

View File

@@ -0,0 +1,259 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { isLocalMCPServerDescription, isRemoteMCPServerDescription, MCPServerDescription, MCPServerStatus, ToolInformation } from '../common';
import { Emitter } from '@theia/core/lib/common/event.js';
import { CallToolResult, CallToolResultSchema, ListResourcesResult, ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
export class MCPServer {
private description: MCPServerDescription;
private transport: Transport;
private client: Client;
private error?: string;
private status: MCPServerStatus;
private readonly onDidUpdateStatusEmitter = new Emitter<MCPServerStatus>();
readonly onDidUpdateStatus = this.onDidUpdateStatusEmitter.event;
constructor(description: MCPServerDescription) {
this.update(description);
}
getStatus(): MCPServerStatus {
return this.status;
}
setStatus(status: MCPServerStatus): void {
this.status = status;
this.onDidUpdateStatusEmitter.fire(status);
}
isRunning(): boolean {
return this.status === MCPServerStatus.Running
|| this.status === MCPServerStatus.Connected;
}
isStopped(): boolean {
return this.status === MCPServerStatus.NotRunning
|| this.status === MCPServerStatus.NotConnected;
}
async getDescription(): Promise<MCPServerDescription> {
let toReturnTools: ToolInformation[] | undefined = undefined;
if (this.isRunning()) {
try {
const { tools } = await this.getTools();
toReturnTools = tools.map(tool => ({
name: tool.name,
description: tool.description
}));
} catch (error) {
console.error('Error fetching tools for description:', error);
}
}
return {
...this.description,
status: this.status,
error: this.error,
tools: toReturnTools
};
}
async start(): Promise<void> {
if (this.isRunning()
|| (this.status === MCPServerStatus.Starting || this.status === MCPServerStatus.Connecting)) {
return;
}
let connected = false;
this.client = new Client(
{
name: 'theia-client',
version: '1.0.0',
},
{
capabilities: {}
}
);
this.error = undefined;
if (isLocalMCPServerDescription(this.description)) {
this.setStatus(MCPServerStatus.Starting);
console.log(
`Starting server "${this.description.name}" with command: ${this.description.command} ` +
`and args: ${this.description.args?.join(' ')} and env: ${JSON.stringify(this.description.env)}`
);
// Filter process.env to exclude undefined values
const sanitizedEnv: Record<string, string> = Object.fromEntries(
Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined)
);
const mergedEnv: Record<string, string> = {
...sanitizedEnv,
...(this.description.env || {})
};
this.transport = new StdioClientTransport({
command: this.description.command,
args: this.description.args,
env: mergedEnv,
});
} else if (isRemoteMCPServerDescription(this.description)) {
this.setStatus(MCPServerStatus.Connecting);
console.log(`Connecting to server "${this.description.name}" via MCP Server Communication with URL: ${this.description.serverUrl}`);
let descHeaders;
if (this.description.headers) {
descHeaders = this.description.headers;
}
// create header for auth token
if (this.description.serverAuthToken) {
if (!descHeaders) {
descHeaders = {};
}
if (this.description.serverAuthTokenHeader) {
descHeaders = { ...descHeaders, [this.description.serverAuthTokenHeader]: this.description.serverAuthToken };
} else {
descHeaders = { ...descHeaders, Authorization: `Bearer ${this.description.serverAuthToken}` };
}
}
if (descHeaders) {
this.transport = new StreamableHTTPClientTransport(new URL(this.description.serverUrl), {
requestInit: { headers: descHeaders },
});
} else {
this.transport = new StreamableHTTPClientTransport(new URL(this.description.serverUrl));
}
try {
await this.client.connect(this.transport);
connected = true;
console.log(`MCP Streamable HTTP successful connected: ${this.description.serverUrl}`);
} catch (e) {
console.log(`MCP SSE fallback initiated: ${this.description.serverUrl}`);
await this.client.close();
if (descHeaders) {
this.transport = new SSEClientTransport(new URL(this.description.serverUrl), {
eventSourceInit: {
fetch: (url, init) =>
fetch(url, { ...init, headers: descHeaders }),
},
requestInit: { headers: descHeaders },
});
} else {
this.transport = new SSEClientTransport(new URL(this.description.serverUrl));
}
}
}
this.transport.onerror = error => {
if (this.isStopped()) {
return;
}
console.error('Error: ', error);
this.error = 'Error: ' + error;
this.setStatus(MCPServerStatus.Errored);
};
this.client.onerror = error => {
console.error('Error in MCP client: ', error);
this.error = 'Error in MCP client: ' + error;
this.setStatus(MCPServerStatus.Errored);
};
try {
if (!connected) {
await this.client.connect(this.transport);
}
this.setStatus(isLocalMCPServerDescription(this.description) ? MCPServerStatus.Running : MCPServerStatus.Connected);
} catch (e) {
this.error = 'Error on MCP startup: ' + e;
await this.client.close();
this.setStatus(MCPServerStatus.Errored);
}
}
async callTool(toolName: string, arg_string: string): Promise<CallToolResult> {
let args;
try {
args = JSON.parse(arg_string);
} catch (error) {
console.error(
`Failed to parse arguments for calling tool "${toolName}" in MCP server "${this.description.name}".
Invalid JSON: ${arg_string}`,
error
);
}
const params = {
name: toolName,
arguments: args,
};
// need to cast since other result schemas (second parameter) might be possible
return this.client.callTool(params, CallToolResultSchema) as Promise<CallToolResult>;
}
async getTools(): ReturnType<Client['listTools']> {
if (this.isRunning()) {
return this.client.listTools();
}
return { tools: [] };
}
update(description: MCPServerDescription): void {
this.description = description;
if (isRemoteMCPServerDescription(description)) {
this.status = MCPServerStatus.NotConnected;
} else {
this.status = MCPServerStatus.NotRunning;
}
}
async stop(): Promise<void> {
if (!this.isRunning() || !this.client) {
return;
}
if (isLocalMCPServerDescription(this.description)) {
console.log(`Stopping MCP server "${this.description.name}"`);
this.setStatus(MCPServerStatus.NotRunning);
} else {
console.log(`Disconnecting MCP server "${this.description.name}"`);
if (this.transport instanceof StreamableHTTPClientTransport) {
console.log(`Terminating session for MCP server "${this.description.name}"`);
await (this.transport as StreamableHTTPClientTransport).terminateSession();
}
this.setStatus(MCPServerStatus.NotConnected);
}
await this.client.close();
}
readResource(resourceId: string): Promise<ReadResourceResult> {
const params = { uri: resourceId };
return this.client.readResource(params);
}
getResources(): Promise<ListResourcesResult> {
return this.client.listResources();
}
}

View File

@@ -0,0 +1,28 @@
// *****************************************************************************
// Copyright (C) 2024 EclipseSource GmbH and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
/* note: this bogus test file is required so that
we are able to run mocha unit tests on this
package, without having any actual unit tests in it.
This way a coverage report will be generated,
showing 0% coverage, instead of no report.
This file can be removed once we have real unit
tests in place. */
describe('ai-mcp package', () => {
it('support code coverage statistics', () => true);
});

View File

@@ -0,0 +1,22 @@
{
"extends": "../../configs/base.tsconfig",
"compilerOptions": {
"composite": true,
"rootDir": "src",
"outDir": "lib"
},
"include": [
"src"
],
"references": [
{
"path": "../ai-core"
},
{
"path": "../core"
},
{
"path": "../workspace"
}
]
}