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

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

View File

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

View File

@@ -0,0 +1,203 @@
<div align='center'>
<br />
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
<h2>ECLIPSE THEIA - AI MCP SERVER EXTENSION</h2>
<hr />
</div>
## Description
This package provides Model Context Protocol (MCP) server functionality for Theia, enabling AI tools to access Theia services and workspace information.
### Features
- **HTTP Transport**: RESTful HTTP API for MCP communication using the StreamableHTTPServerTransport
- **Backend MCP Contributions**: Register backend-only tools, resources, and prompts
- **Frontend MCP Contributions**: Register frontend-only tools, resources, and prompts that can access frontend services
- **Frontend-Backend Delegation**: Allows frontend contributions to be exposed through the backend MCP server
### Development Setup
#### Starting the MCP Server
1. Start Theia application
2. The MCP server will automatically start and be available at `/mcp` e.g. `http://localhost:3000/mcp`
### API Endpoints
- `POST /mcp` - MCP protocol endpoint (for all MCP protocol operations)
### Architecture
The MCP server architecture consists of:
1. **HTTP Transport Layer**: Manages HTTP connections using StreamableHTTPServerTransport
2. **MCP Server**: Core server implementation that handles MCP protocol messages
3. **Backend Contributions**: Extensions that run on the Node.js backend
4. **Frontend Contributions**: Extensions that run in the browser frontend
5. **Frontend-Backend Bridge**: RPC mechanism to connect frontend and backend
### Creating Backend Contributions
Backend contributions run in the Node.js backend and have access to backend services:
```typescript
@injectable()
export class MyBackendContribution implements MCPBackendContribution {
@inject(ILogger)
protected readonly logger: ILogger;
async configure(server: McpServer): Promise<void> {
// Register a tool
server.tool('my-backend-tool', {
type: 'object',
properties: {
input: { type: 'string' }
}
}, async (args) => {
this.logger.info('my-backend-tool called with args:', args);
return {
content: [{ type: 'text', text: 'Result from backend' }]
};
});
// Register a resource
server.resource(
'my-resource',
'theia://resource-uri',
async (uri) => {
return {
content: 'Resource content'
};
}
);
// Register a prompt
server.prompt(
'my-prompt',
'Prompt description',
{}, // Arguments schema
async (args) => {
return {
messages: [{
role: 'user',
content: { type: 'text', text: 'Prompt content' }
}]
};
}
);
}
}
```
Register the contribution in your backend module:
```typescript
bind(MyBackendContribution).toSelf().inSingletonScope();
bind(MCPBackendContribution).toService(MyBackendContribution);
```
### Creating Frontend Contributions
Frontend contributions run in the browser and have access to frontend services:
```typescript
@injectable()
export class MyFrontendContribution implements MCPFrontendContribution {
@inject(WorkspaceService)
protected readonly workspaceService: WorkspaceService;
async getTools(): Promise<Tool[]> {
return [{
name: 'workspace-info',
description: 'Get workspace info',
inputSchema: {
type: 'object',
properties: {}
}
}];
}
async getTool(name: string): Promise<ToolProvider | undefined> {
if (name === 'workspace-info') {
return {
handler: async () => {
const roots = await this.workspaceService.roots;
return {
roots: roots.map(r => r.resource.toString())
};
},
inputSchema: z.object({})
};
}
}
async getResources(): Promise<Resource[]> {
return [{
name: 'Workspace Information',
uri: 'workspace://info',
description: 'Workspace info',
mimeType: 'application/json'
}];
}
async readResource(uri: string): Promise<unknown> {
if (uri === 'workspace://info') {
const roots = await this.workspaceService.roots;
return { roots: roots.map(r => r.resource.toString()) };
}
}
async getPrompts(): Promise<Prompt[]> {
return [{
name: 'workspace-context',
description: 'Generate workspace context',
arguments: []
}];
}
async getPrompt(name: string, args: unknown): Promise<PromptMessage[]> {
if (name === 'workspace-context') {
return [{
role: 'user',
content: { type: 'text', text: 'Workspace context information' }
}];
}
}
}
```
Register the contribution in your frontend module:
```typescript
bind(MyFrontendContribution).toSelf().inSingletonScope();
bind(MCPFrontendContribution).toService(MyFrontendContribution);
```
### Security Considerations
- The MCP server exposes Theia functionality over HTTP
- Only enable the server in trusted environments
- Consider adding authentication and authorization for production use
- Restrict access to sensitive operations in your contributions
## Additional Information
- [API documentation for `@theia/ai-mcp-server`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_ai-mcp-server.html)
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
- [Theia - Website](https://theia-ide.org/)
## License
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
## Trademark
"Theia" is a trademark of the Eclipse Foundation
<https://www.eclipse.org/theia>

View File

@@ -0,0 +1,50 @@
# MCP Server Usage Examples
This document provides practical examples of how to use the Theia MCP Server.
## Testing the MCP Server
### Using WebSocket-based Clients (Recommended)
For testing use: `npx @modelcontextprotocol/inspector` open the app with the token pre-filled.
In the app select Streamable HTTP as the Transport Type and add your endpoint (e.g. `http://localhost:3000/mcp`) in the URL field.
## Available Tools
The MCP server currently has the following tools available:
| Tool Name | Description | Parameters |
|-----------|-------------|------------|
| `test-tool` | A simple test tool for verification | None |
Additional tools are added through backend and frontend contributions.
## Integration with AI Agents
The MCP server is designed to be integrated with AI agents and tools that follow the Model Context Protocol specification. These agents can use the provided tools to interact with the Theia workspace.
## Security Considerations
- The MCP server has full access to Theia's command system and workspace
- Only enable the server in trusted environments
- Consider using authentication/authorization for HTTP transport in production
## Debugging
Enable debug logging by setting the log level:
```bash
THEIA_LOG_LEVEL=debug theia start
```
The server will log MCP operations, session management, and error details.
## Development Notes
When developing against the MCP server, note that it uses:
1. JSON-RPC 2.0 over HTTP
2. Streamable HTTP transport that requires specific headers and protocol handling
3. Session management via the `mcp-session-id` header
For development and testing purposes, examine the server logs when `THEIA_LOG_LEVEL=debug` is enabled to understand the expected protocol interactions.

View File

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

View File

@@ -0,0 +1,18 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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-frontend-bootstrap';
export * from './mcp-frontend-contribution';

View File

@@ -0,0 +1,43 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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 { MaybePromise } from '@theia/core';
import { FrontendApplication, FrontendApplicationContribution } from '@theia/core/lib/browser';
import { ILogger } from '@theia/core/lib/common/logger';
import { MCPToolFrontendDelegate } from '../common/mcp-tool-delegate';
/**
* Bootstraps MCP frontend components during application startup.
*
* This contribution ensures that the MCPToolFrontendDelegate is properly instantiated
* and available in the dependency injection container when the frontend application starts.
* It acts as a lightweight initializer to activate MCP functionality in the browser.
*/
@injectable()
export class MCPFrontendBootstrap implements FrontendApplicationContribution {
@inject(MCPToolFrontendDelegate)
protected readonly frontendDelegate: MCPToolFrontendDelegate;
@inject(ILogger)
protected readonly logger: ILogger;
onStart(_app: FrontendApplication): MaybePromise<void> {
this.logger.debug('MCPFrontendBootstrap initialized');
}
}

View File

@@ -0,0 +1,67 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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 { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
import { Tool, Resource, Prompt, PromptMessage } from '@modelcontextprotocol/sdk/types';
import { z } from 'zod';
export const MCPFrontendContribution = Symbol('MCPFrontendContribution');
/**
* Tool provider interface for frontend contributions
*/
export interface ToolProvider {
handler: (args: unknown) => Promise<unknown>;
inputSchema: z.ZodSchema;
}
/**
* Contribution interface for extending the MCP server with frontend-only tools, resources, and prompts
*/
export interface MCPFrontendContribution {
/**
* Get tools provided by this contribution
*/
getTools?(): Promise<Tool[]> | Tool[];
/**
* Get specific tool by name
*/
getTool?(name: string): Promise<ToolProvider | undefined> | ToolProvider | undefined;
/**
* Get resources provided by this contribution
*/
getResources?(): Promise<Resource[]> | Resource[];
/**
* Read specific resource by URI
*/
readResource?(uri: string): Promise<unknown> | unknown;
/**
* Get prompts provided by this contribution
*/
getPrompts?(): Promise<Prompt[]> | Prompt[];
/**
* Get specific prompt by name with arguments
*/
getPrompt?(name: string, args: unknown): Promise<PromptMessage[]> | PromptMessage[];
}
export const MCPFrontendContributionProvider = Symbol('MCPFrontendContributionProvider');
export interface MCPFrontendContributionProvider extends ContributionProvider<MCPFrontendContribution> { }

View File

@@ -0,0 +1,42 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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 { bindContributionProvider } from '@theia/core';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import {
RemoteConnectionProvider,
ServiceConnectionProvider,
} from '@theia/core/lib/browser/messaging/service-connection-provider';
import { MCPToolFrontendDelegate, MCPToolDelegateClient, mcpToolDelegatePath } from '../common/mcp-tool-delegate';
import { MCPFrontendBootstrap } from './mcp-frontend-bootstrap';
import { MCPFrontendContribution } from './mcp-frontend-contribution';
import { MCPToolDelegateClientImpl } from './mcp-tool-delegate-client';
export default new ContainerModule(bind => {
bind(MCPFrontendBootstrap).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(MCPFrontendBootstrap);
bind(MCPToolDelegateClient).to(MCPToolDelegateClientImpl).inSingletonScope();
bind(MCPToolFrontendDelegate).toDynamicValue(ctx => {
const connection = ctx.container.get<ServiceConnectionProvider>(RemoteConnectionProvider);
const client = ctx.container.get<MCPToolDelegateClient>(MCPToolDelegateClient);
return connection.createProxy<MCPToolFrontendDelegate>(mcpToolDelegatePath, client);
}).inSingletonScope();
bindContributionProvider(bind, MCPFrontendContribution);
});

View File

@@ -0,0 +1,133 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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, named } from '@theia/core/shared/inversify';
import { ContributionProvider } from '@theia/core';
import { ILogger } from '@theia/core/lib/common/logger';
import { Tool, Resource, ResourceContents, Prompt, PromptMessage } from '@modelcontextprotocol/sdk/types';
import { MCPToolDelegateClient } from '../common/mcp-tool-delegate';
import { MCPFrontendContribution } from './mcp-frontend-contribution';
/**
* Frontend client implementation that handles MCP tool delegation requests from the backend.
*
* This class acts as a bridge between the backend MCP server and frontend contributions,
* forwarding backend requests (tool calls, resource access, prompts) to registered
* MCPFrontendContribution instances and aggregating their responses.
*
* Called by the backend via the MCPToolDelegateClient interface to access frontend-provided
* MCP tools, resources, and prompts.
*/
@injectable()
export class MCPToolDelegateClientImpl implements MCPToolDelegateClient {
@inject(ContributionProvider)
@named(MCPFrontendContribution)
protected readonly contributions: ContributionProvider<MCPFrontendContribution>;
@inject(ILogger)
protected readonly logger: ILogger;
private getFrontendContributions(): MCPFrontendContribution[] {
return this.contributions.getContributions();
}
async callTool(serverId: string, toolName: string, args: unknown): Promise<unknown> {
const contributions = this.getFrontendContributions();
for (const contribution of contributions) {
if (contribution.getTool) {
const tool = await contribution.getTool(toolName);
if (tool) {
return await tool.handler(JSON.stringify(args));
}
}
}
throw new Error(`Tool ${toolName} not found in server ${serverId}`);
}
async listTools(serverId: string): Promise<Tool[]> {
const contributions = this.getFrontendContributions();
const allTools: Tool[] = [];
for (const contribution of contributions) {
if (contribution.getTools) {
const tools = await contribution.getTools();
allTools.push(...tools);
}
}
return allTools;
}
async listResources(serverId: string): Promise<Resource[]> {
const contributions = this.getFrontendContributions();
const allResources: Resource[] = [];
for (const contribution of contributions) {
if (contribution.getResources) {
const resources = await contribution.getResources();
allResources.push(...resources);
}
}
return allResources;
}
async readResource(serverId: string, uri: string): Promise<ResourceContents> {
const contributions = this.getFrontendContributions();
for (const contribution of contributions) {
if (contribution.readResource) {
try {
const result = await contribution.readResource(uri);
return result as ResourceContents;
} catch (error) {
// Continue to next contribution
this.logger.debug(`Error getting resource ${uri}:`, error);
}
}
}
throw new Error(`Resource ${uri} not found in server ${serverId}`);
}
async listPrompts(serverId: string): Promise<Prompt[]> {
const contributions = this.getFrontendContributions();
const allPrompts: Prompt[] = [];
for (const contribution of contributions) {
if (contribution.getPrompts) {
const prompts = await contribution.getPrompts();
allPrompts.push(...prompts);
}
}
return allPrompts;
}
async getPrompt(serverId: string, name: string, args: unknown): Promise<PromptMessage[]> {
const contributions = this.getFrontendContributions();
for (const contribution of contributions) {
if (contribution.getPrompt) {
try {
return await contribution.getPrompt(name, args);
} catch (error) {
// Continue to next contribution
this.logger.debug(`Error getting prompt ${name}:`, error);
}
}
}
throw new Error(`Prompt ${name} not found in server ${serverId}`);
}
}

View File

@@ -0,0 +1,17 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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-tool-delegate';

View File

@@ -0,0 +1,43 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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 { Tool, Resource, ResourceContents, Prompt, PromptMessage } from '@modelcontextprotocol/sdk/types';
export const MCPToolDelegateClient = Symbol('MCPToolDelegateClient');
/**
* Client interface for MCP tool operations.
* This interface is implemented by the frontend and called by the backend.
*/
export interface MCPToolDelegateClient {
callTool(serverId: string, toolName: string, args: unknown): Promise<unknown>;
listTools(serverId: string): Promise<Tool[]>;
listResources(serverId: string): Promise<Resource[]>;
readResource(serverId: string, uri: string): Promise<ResourceContents>;
listPrompts(serverId: string): Promise<Prompt[]>;
getPrompt(serverId: string, name: string, args: unknown): Promise<PromptMessage[]>;
}
export const MCPToolFrontendDelegate = Symbol('MCPToolFrontendDelegate');
/**
* Backend delegate interface for MCP tool operations.
* This interface extends MCPToolDelegateClient with RPC client setup capability.
* It is implemented by the backend and acts as a proxy to forward calls to the frontend.
*/
export interface MCPToolFrontendDelegate extends MCPToolDelegateClient {
setClient(client: MCPToolDelegateClient): void;
}
export const mcpToolDelegatePath = '/services/mcpToolDelegate';

View File

@@ -0,0 +1,20 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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-theia-server';
export * from './mcp-theia-server-impl';
export * from './mcp-backend-contribution-manager';
export * from './mcp-frontend-contribution-manager';

View File

@@ -0,0 +1,56 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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, named } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core/lib/common/logger';
import { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { MCPBackendContribution } from './mcp-theia-server';
/**
* Manages the registration of backend MCP contributions
*/
@injectable()
export class MCPBackendContributionManager {
@inject(ILogger)
protected readonly logger: ILogger;
@inject(ContributionProvider)
@named(MCPBackendContribution)
protected readonly contributions: ContributionProvider<MCPBackendContribution>;
/**
* Register all backend contributions with the MCP server
*/
async registerBackendContributions(server: McpServer): Promise<void> {
const contributions = this.contributions.getContributions();
this.logger.debug(`Found ${contributions.length} backend MCP contributions to register`);
for (const contribution of contributions) {
try {
this.logger.debug(`Configuring backend MCP contribution: ${contribution.constructor.name}`);
await contribution.configure(server);
this.logger.debug(`Successfully registered backend MCP contribution: ${contribution.constructor.name}`);
} catch (error) {
this.logger.error(`Failed to register backend MCP contribution ${contribution.constructor.name}:`, error);
throw error;
}
}
this.logger.debug(`Finished registering all ${contributions.length} backend MCP contributions`);
}
}

View File

@@ -0,0 +1,75 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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 { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
import { ConnectionHandler, RpcConnectionHandler, bindContributionProvider, generateUuid } from '@theia/core';
import {
MCPTheiaServer,
MCPBackendContribution
} from './mcp-theia-server';
import { MCPToolFrontendDelegate, MCPToolDelegateClient, mcpToolDelegatePath } from '../common/mcp-tool-delegate';
import { MCPTheiaServerImpl } from './mcp-theia-server-impl';
import { MCPBackendContributionManager } from './mcp-backend-contribution-manager';
import { MCPFrontendContributionManager } from './mcp-frontend-contribution-manager';
import { MCPToolFrontendDelegateImpl } from './mcp-tool-frontend-delegate';
const mcpConnectionModule = ConnectionContainerModule.create(({ bind }) => {
bind(MCPToolFrontendDelegateImpl).toSelf().inSingletonScope();
bind(MCPToolFrontendDelegate).toService(MCPToolFrontendDelegateImpl);
bind(ConnectionHandler)
.toDynamicValue(
({ container }) =>
new RpcConnectionHandler<MCPToolDelegateClient>(
mcpToolDelegatePath,
client => {
const service = container.get<MCPToolFrontendDelegateImpl>(MCPToolFrontendDelegateImpl);
const contributionManager = container.get<MCPFrontendContributionManager>(MCPFrontendContributionManager);
service.setClient(client);
// Generate unique delegate ID and register with contribution manager
const delegateId = generateUuid();
contributionManager.addFrontendDelegate(delegateId, service);
// Setup cleanup when connection closes
client.onDidCloseConnection(() => {
contributionManager.removeFrontendDelegate(delegateId);
});
return service;
}
)
)
.inSingletonScope();
});
export default new ContainerModule(bind => {
bind(MCPTheiaServerImpl).toSelf().inSingletonScope();
bind(MCPTheiaServer).toService(MCPTheiaServerImpl);
bind(BackendApplicationContribution).toService(MCPTheiaServerImpl);
bind(MCPBackendContributionManager).toSelf().inSingletonScope();
bind(MCPFrontendContributionManager).toSelf().inSingletonScope();
bindContributionProvider(bind, MCPBackendContribution);
bind(ConnectionContainerModule).toConstantValue(mcpConnectionModule);
});

View File

@@ -0,0 +1,272 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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 { ILogger } from '@theia/core/lib/common/logger';
import { McpServer, RegisteredTool, RegisteredPrompt, RegisteredResource } from '@modelcontextprotocol/sdk/server/mcp.js';
import type { ReadResourceResult } from '@modelcontextprotocol/sdk/types.js';
import { MCPToolFrontendDelegate } from '../common/mcp-tool-delegate';
import { z } from 'zod';
/**
* Manages the registration and delegation of frontend MCP contributions
*/
@injectable()
export class MCPFrontendContributionManager {
@inject(ILogger)
protected readonly logger: ILogger;
// Frontend delegates are set dynamically when connections are established
private frontendDelegates = new Map<string, MCPToolFrontendDelegate>();
private mcpServer?: McpServer;
private serverId?: string;
private registeredElements: Map<string, (RegisteredTool | RegisteredPrompt | RegisteredResource)[]> = new Map();
/**
* Set the MCP server instance and setup frontend delegate notifications
*/
async setMCPServer(server: McpServer, serverId: string): Promise<void> {
this.mcpServer = server;
this.serverId = serverId;
this.registerExistingFrontendContributions();
}
/**
* Add a frontend delegate (called when a frontend connection is established)
*/
addFrontendDelegate(delegateId: string, delegate: MCPToolFrontendDelegate): void {
this.frontendDelegates.set(delegateId, delegate);
if (this.mcpServer && this.serverId) {
this.registerFrontendContributionsFromDelegate(delegateId, delegate).catch(error => {
this.logger.warn(`Failed to register frontend contributions from delegate ${delegateId}:`, error);
});
}
}
/**
* Remove a frontend delegate (called when a frontend connection is closed)
*/
removeFrontendDelegate(delegateId: string): void {
this.frontendDelegates.delete(delegateId);
this.unregisterFrontendContributionsFromDelegate(delegateId);
}
/**
* Unregister frontend contributions from a specific delegate
*/
private unregisterFrontendContributionsFromDelegate(delegateId: string): void {
if (!this.mcpServer) {
this.logger.warn('MCP server not set, cannot unregister frontend contributions');
return;
}
const elements = this.registeredElements.get(delegateId);
if (elements) {
for (const element of elements) {
try {
element.remove();
} catch (error) {
this.logger.warn(`Failed to unregister element from delegate ${delegateId}: ${error}`);
}
}
this.registeredElements.delete(delegateId);
// Notify that lists have changed
this.mcpServer.sendToolListChanged();
this.mcpServer.sendResourceListChanged();
this.mcpServer.sendPromptListChanged();
}
}
/**
* Register frontend contributions from existing delegates
*/
private async registerExistingFrontendContributions(): Promise<void> {
for (const [delegateId, delegate] of this.frontendDelegates) {
try {
await this.registerFrontendContributionsFromDelegate(delegateId, delegate);
} catch (error) {
this.logger.warn(`Failed to register frontend contributions from delegate ${delegateId}:`, error);
}
}
}
/**
* Register frontend contributions from a specific delegate
*/
private async registerFrontendContributionsFromDelegate(delegateId: string, delegate: MCPToolFrontendDelegate): Promise<void> {
if (!this.mcpServer || !this.serverId) {
this.logger.warn('MCP server not set, cannot register frontend contributions');
return;
}
try {
await this.registerFrontendToolsFromDelegate(delegate, delegateId);
this.mcpServer.sendToolListChanged();
// Register resources from frontend
await this.registerFrontendResourcesFromDelegate(delegate, delegateId);
this.mcpServer.sendResourceListChanged();
// Register prompts from frontend
await this.registerFrontendPromptsFromDelegate(delegate, delegateId);
this.mcpServer.sendPromptListChanged();
} catch (error) {
this.logger.warn(`Failed to register frontend MCP contributions from delegate ${delegateId}: ${error}`);
// Don't re-throw to prevent server startup failure
}
}
/**
* Unregister frontend contributions for a server
* @param serverId Unique identifier for the server instance
*/
async unregisterFrontendContributions(serverId: string): Promise<void> {
for (const [delegateId] of this.frontendDelegates) {
try {
this.unregisterFrontendContributionsFromDelegate(delegateId);
// Backend delegates don't need lifecycle notifications
} catch (error) {
this.logger.warn(`Error unregistering server from frontend delegate ${delegateId}:`, error);
}
}
}
/**
* Register tools from frontend contributions
*/
protected async registerFrontendToolsFromDelegate(delegate: MCPToolFrontendDelegate, delegateId: string): Promise<void> {
if (!this.mcpServer || !this.serverId) {
throw new Error('MCP server not set');
}
try {
const tools = await delegate.listTools(this.serverId);
for (const tool of tools) {
const registeredTool = this.mcpServer.registerTool(
`${tool.name}_${delegateId}`,
{
description: tool.description ?? '',
// Cast needed: SDK's Tool.inputSchema type is looser than what z.fromJSONSchema expects
inputSchema: z.fromJSONSchema(tool.inputSchema as Parameters<typeof z.fromJSONSchema>[0])
},
async args => {
try {
const result = await delegate.callTool(
this.serverId!,
tool.name,
args
);
return {
content: [{
type: 'text',
text: typeof result === 'string' ? result : JSON.stringify(result)
}]
};
} catch (error) {
this.logger.error(`Error calling frontend tool ${tool.name}:`, error);
throw error;
}
}
);
const registeredElements = this.registeredElements.get(delegateId) ?? [];
registeredElements.push(registeredTool);
this.registeredElements.set(delegateId, registeredElements);
}
} catch (error) {
this.logger.warn(`Failed to register frontend tools from delegate ${delegateId}: ${error}`);
throw error;
}
}
/**
* Register resources from frontend contributions
*/
protected async registerFrontendResourcesFromDelegate(delegate: MCPToolFrontendDelegate, delegateId: string): Promise<void> {
if (!this.mcpServer || !this.serverId) {
throw new Error('MCP server not set');
}
try {
const resources = await delegate.listResources(this.serverId);
for (const resource of resources) {
const registeredResource = this.mcpServer.resource(
`${resource.name}_${delegateId}`,
resource.uri,
async uri => {
try {
const result = await delegate.readResource(this.serverId!, uri.href);
return result as unknown as ReadResourceResult;
} catch (error) {
this.logger.error(`Error reading frontend resource ${resource.name}:`, error);
throw error;
}
}
);
const registeredElements = this.registeredElements.get(delegateId) ?? [];
registeredElements.push(registeredResource);
this.registeredElements.set(delegateId, registeredElements);
}
} catch (error) {
this.logger.warn(`Failed to register frontend resources from delegate ${delegateId}: ${error}`);
throw error;
}
}
/**
* Register prompts from frontend contributions
*/
protected async registerFrontendPromptsFromDelegate(delegate: MCPToolFrontendDelegate, delegateId: string): Promise<void> {
if (!this.mcpServer || !this.serverId) {
throw new Error('MCP server not set');
}
try {
const prompts = await delegate.listPrompts(this.serverId);
for (const prompt of prompts) {
const registeredPrompt = this.mcpServer.prompt(
`${prompt.name}_${delegateId}`,
prompt.description ?? '',
prompt.arguments ?? {},
async args => {
try {
const messages = await delegate.getPrompt(this.serverId!, prompt.name, args);
return {
messages
};
} catch (error) {
this.logger.error(`Error getting frontend prompt ${prompt.name}:`, error);
throw error;
}
}
);
const registeredElements = this.registeredElements.get(delegateId) ?? [];
registeredElements.push(registeredPrompt);
this.registeredElements.set(delegateId, registeredElements);
}
} catch (error) {
this.logger.warn(`Failed to register frontend prompts from delegate ${delegateId}: ${error}`);
throw error;
}
}
}

View File

@@ -0,0 +1,241 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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 { ILogger } from '@theia/core/lib/common/logger';
import { BackendApplicationContribution } from '@theia/core/lib/node/backend-application';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { generateUuid } from '@theia/core';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import * as express from '@theia/core/shared/express';
import { randomUUID } from 'crypto';
import { MCPTheiaServer } from './mcp-theia-server';
import { MCPBackendContributionManager } from './mcp-backend-contribution-manager';
import { MCPFrontendContributionManager } from './mcp-frontend-contribution-manager';
@injectable()
export class MCPTheiaServerImpl implements MCPTheiaServer, BackendApplicationContribution {
@inject(ILogger)
protected readonly logger: ILogger;
@inject(MCPBackendContributionManager)
protected readonly backendContributionManager: MCPBackendContributionManager;
@inject(MCPFrontendContributionManager)
protected readonly frontendContributionManager: MCPFrontendContributionManager;
protected server?: McpServer;
protected httpTransports: Map<string, StreamableHTTPServerTransport> = new Map();
protected httpApp?: express.Application;
protected running = false;
protected serverId: string = generateUuid();
async configure?(app: express.Application): Promise<void> {
this.httpApp = app;
try {
await this.start();
} catch (error) {
this.logger.error('Failed to start MCP server during initialization:', error);
}
}
async start(): Promise<void> {
if (this.running) {
throw new Error('MCP server is already running');
}
this.server = new McpServer({
name: 'Theia MCP Server',
version: '1.0.0'
}, {
capabilities: {
tools: {
listChanged: true
},
resources: {
listChanged: true
},
prompts: {
listChanged: true
}
}
});
await this.registerContributions();
await this.setupHttpTransport();
this.running = true;
}
async stop(): Promise<void> {
if (!this.running) {
return;
}
try {
if (this.serverId) {
await this.frontendContributionManager.unregisterFrontendContributions(this.serverId);
}
for (const transport of this.httpTransports.values()) {
transport.close();
}
this.httpTransports.clear();
if (this.server) {
this.server.close();
this.server = undefined;
}
this.running = false;
} catch (error) {
this.logger.error('Error stopping MCP server:', error);
throw error;
}
}
getServer(): McpServer | undefined {
return this.server;
}
isRunning(): boolean {
return this.running;
}
getServerId(): string | undefined {
return this.serverId;
}
protected async setupHttpTransport(): Promise<void> {
if (!this.server) {
throw new Error('Server not initialized');
}
if (!this.httpApp) {
throw new Error('AppServer not initialized');
}
this.setupHttpEndpoints(this.httpApp);
}
protected setupHttpEndpoints(app: express.Application): void {
app.all('/mcp', async (req, res) => {
await this.handleStreamableHttpRequest(req, res);
});
}
protected async handleStreamableHttpRequest(req: express.Request, res: express.Response): Promise<void> {
if (!this.server) {
res.status(503).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'MCP Server not initialized',
},
id: undefined,
});
return;
}
const sessionId = req.headers['mcp-session-id'] as string | undefined;
let transport: StreamableHTTPServerTransport;
try {
if (sessionId && this.httpTransports.has(sessionId)) {
transport = this.httpTransports.get(sessionId)!;
} else {
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: newSessionId => {
this.httpTransports.set(newSessionId, transport);
}
});
transport.onclose = () => {
if (transport.sessionId) {
this.httpTransports.delete(transport.sessionId);
}
};
await this.server.connect(transport);
}
await transport.handleRequest(req, res, req.body);
} catch (error) {
this.logger.error('Error handling MCP Streamable HTTP request:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: `Internal server error: ${error}`,
},
id: req.body?.id || undefined,
});
}
}
}
protected async registerContributions(): Promise<void> {
if (!this.server) {
throw new Error('Server not initialized');
}
await this.backendContributionManager.registerBackendContributions(this.server);
await this.registerFrontendContributions();
}
protected async registerFrontendContributions(): Promise<void> {
if (!this.server) {
throw new Error('Server not initialized');
}
try {
await this.frontendContributionManager.setMCPServer(this.server, this.serverId);
} catch (error) {
this.logger.debug('Frontend contributions registration failed (this is normal if no frontend is connected):', error);
}
}
onStop(): void {
this.logger.debug('MCP Server stopping...');
if (this.running) {
try {
if (this.serverId) {
this.frontendContributionManager.unregisterFrontendContributions(this.serverId)
.catch(error => this.logger.warn('Failed to unregister frontend contributions during shutdown:', error));
}
for (const transport of this.httpTransports.values()) {
transport.close();
}
this.httpTransports.clear();
if (this.server) {
this.server.close();
this.server = undefined;
}
this.running = false;
} catch (error) {
this.logger.error('Error stopping MCP server:', error);
}
}
}
}

View File

@@ -0,0 +1,66 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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 { ContributionProvider } from '@theia/core/lib/common/contribution-provider';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
export const MCPTheiaServer = Symbol('MCPTheiaServer');
/**
* Main interface for the Theia MCP server (backend only)
*/
export interface MCPTheiaServer {
/**
* Start the MCP server with the given configuration
*/
start(): Promise<void>;
/**
* Stop the MCP server
*/
stop(): Promise<void>;
/**
* Get the underlying MCP server instance
*/
getServer(): McpServer | undefined;
/**
* Get the server ID
*/
getServerId(): string | undefined;
/**
* Check if the server is running
*/
isRunning(): boolean;
}
export const MCPBackendContribution = Symbol('MCPBackendContribution');
/**
* Contribution interface for extending the MCP server with backend-only contributions
*/
export interface MCPBackendContribution {
/**
* Configure MCP server (for backend contributions)
* @param server The MCP server instance to configure
*/
configure(server: McpServer): Promise<void> | void;
}
export const MCPBackendContributionProvider = Symbol('MCPBackendContributionProvider');
export interface MCPBackendContributionProvider extends ContributionProvider<MCPBackendContribution> { }

View File

@@ -0,0 +1,71 @@
// *****************************************************************************
// Copyright (C) 2025 EclipseSource.
//
// 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 { Tool, Resource, ResourceContents, Prompt, PromptMessage } from '@modelcontextprotocol/sdk/types.js';
import { MCPToolFrontendDelegate, MCPToolDelegateClient } from '../common/mcp-tool-delegate';
@injectable()
export class MCPToolFrontendDelegateImpl implements MCPToolFrontendDelegate {
private client?: MCPToolDelegateClient;
setClient(client: MCPToolDelegateClient): void {
this.client = client;
}
async callTool(serverId: string, toolName: string, args: unknown): Promise<unknown> {
if (!this.client) {
throw new Error('MCPToolDelegateClient not set');
}
return this.client.callTool(serverId, toolName, args);
}
async listTools(serverId: string): Promise<Tool[]> {
if (!this.client) {
throw new Error('MCPToolDelegateClient not set');
}
return this.client.listTools(serverId);
}
async listResources(serverId: string): Promise<Resource[]> {
if (!this.client) {
throw new Error('MCPToolDelegateClient not set');
}
return this.client.listResources(serverId);
}
async readResource(serverId: string, uri: string): Promise<ResourceContents> {
if (!this.client) {
throw new Error('MCPToolDelegateClient not set');
}
return this.client.readResource(serverId, uri);
}
async listPrompts(serverId: string): Promise<Prompt[]> {
if (!this.client) {
throw new Error('MCPToolDelegateClient not set');
}
return this.client.listPrompts(serverId);
}
async getPrompt(serverId: string, name: string, args: unknown): Promise<PromptMessage[]> {
if (!this.client) {
throw new Error('MCPToolDelegateClient not set');
}
return this.client.getPrompt(serverId, name, args);
}
}

View File

@@ -0,0 +1,28 @@
// *****************************************************************************
// Copyright (C) 2025 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-server package', () => {
it('support code coverage statistics', () => true);
});

View File

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