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,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);
});