deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
93
packages/ai-mcp.disabled/src/browser/mcp-frontend-module.ts
Normal file
93
packages/ai-mcp.disabled/src/browser/mcp-frontend-module.ts
Normal 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();
|
||||
});
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
157
packages/ai-mcp.disabled/src/browser/mcp-frontend-service.ts
Normal file
157
packages/ai-mcp.disabled/src/browser/mcp-frontend-service.ts
Normal 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;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
packages/ai-mcp.disabled/src/common/index.ts
Normal file
16
packages/ai-mcp.disabled/src/common/index.ts
Normal 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';
|
||||
117
packages/ai-mcp.disabled/src/common/mcp-preferences.ts
Normal file
117
packages/ai-mcp.disabled/src/common/mcp-preferences.ts
Normal 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: []
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
75
packages/ai-mcp.disabled/src/common/mcp-protocol.ts
Normal file
75
packages/ai-mcp.disabled/src/common/mcp-protocol.ts
Normal 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;
|
||||
};
|
||||
167
packages/ai-mcp.disabled/src/common/mcp-server-manager.ts
Normal file
167
packages/ai-mcp.disabled/src/common/mcp-server-manager.ts
Normal 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';
|
||||
54
packages/ai-mcp.disabled/src/node/mcp-backend-module.ts
Normal file
54
packages/ai-mcp.disabled/src/node/mcp-backend-module.ts
Normal 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);
|
||||
});
|
||||
162
packages/ai-mcp.disabled/src/node/mcp-server-manager-impl.ts
Normal file
162
packages/ai-mcp.disabled/src/node/mcp-server-manager-impl.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
259
packages/ai-mcp.disabled/src/node/mcp-server.ts
Normal file
259
packages/ai-mcp.disabled/src/node/mcp-server.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
28
packages/ai-mcp.disabled/src/package.spec.ts
Normal file
28
packages/ai-mcp.disabled/src/package.spec.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user