Skip to content

Commit

Permalink
feat: sumi mcp builtin sever
Browse files Browse the repository at this point in the history
  • Loading branch information
life2015 committed Jan 23, 2025
1 parent 83fc2b7 commit bafda53
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 53 deletions.
19 changes: 13 additions & 6 deletions packages/ai-native/src/browser/ai-core.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ import {
AI_CHAT_VIEW_ID,
AI_MENU_BAR_DEBUG_TOOLBAR,
ChatProxyServiceToken,
ISumiMCPServerBackend,
SumiMCPServerProxyServicePath,
} from '../common';
import { MCPServerDescription, MCPServerManager, MCPServerManagerPath } from '../common/mcp-server-manager';
import { ToolInvocationRegistry, ToolInvocationRegistryImpl } from '../common/tool-invocation-registry';
Expand Down Expand Up @@ -216,8 +218,11 @@ export class AINativeBrowserContribution
@Autowired(CodeActionSingleHandler)
private readonly codeActionSingleHandler: CodeActionSingleHandler;

@Autowired(MCPServerManagerPath)
private readonly mcpServerManager: MCPServerManager;
// @Autowired(MCPServerManagerPath)
// private readonly mcpServerManager: MCPServerManager;

@Autowired(SumiMCPServerProxyServicePath)
private readonly sumiMCPServerBackendProxy: ISumiMCPServerBackend;

constructor() {
this.registerFeature();
Expand Down Expand Up @@ -430,7 +435,9 @@ export class AINativeBrowserContribution
{ id: 'ai.native.mcp.start', label: 'MCP: Start MCP Server' },
{
execute: async () => {
this.mcpServerManager.initBuiltinServer();
// this.mcpServerManager.initBuiltinServer();

this.sumiMCPServerBackendProxy.initBuiltinMCPServer();

const description: MCPServerDescription = {
name: 'filesystem',
Expand All @@ -439,10 +446,10 @@ export class AINativeBrowserContribution
env: {},
};

this.mcpServerManager.addOrUpdateServer(description);
// this.mcpServerManager.addOrUpdateServer(description);

await this.mcpServerManager.startServer(description.name);
await this.mcpServerManager.collectTools(description.name);
// await this.mcpServerManager.startServer(description.name);
// await this.mcpServerManager.collectTools(description.name);
},
},
);
Expand Down
10 changes: 9 additions & 1 deletion packages/ai-native/src/browser/mcp/mcp-server-proxy.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Autowired, Injectable } from '@opensumi/di';
import { ILogger } from '@opensumi/ide-core-browser';

import { IMCPServerRegistry, TokenMCPServerRegistry } from '../types';

Expand All @@ -7,18 +8,25 @@ export class MCPServerProxyService {
@Autowired(TokenMCPServerRegistry)
private readonly mcpServerRegistry: IMCPServerRegistry;

@Autowired(ILogger)
private readonly logger: ILogger;

$callMCPTool(name: string, args: any) {
return this.mcpServerRegistry.callMCPTool(name, args);
}

async $getMCPTools() {
return this.mcpServerRegistry.getMCPTools().map((tool) =>
const tools = await this.mcpServerRegistry.getMCPTools().map((tool) =>
// 不要传递 handler
({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema,
}),
);

this.logger.log('SUMI MCP tools', tools);

return tools;
}
}
4 changes: 4 additions & 0 deletions packages/ai-native/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ export const ChatProxyServiceToken = Symbol('ChatProxyServiceToken');
// 暴露给 Node.js 层,使其可以感知 Opensumi 注册的 MCP 能力
export const TokenMCPServerProxyService = Symbol('TokenMCPServerProxyService');

export interface ISumiMCPServerBackend {
initBuiltinMCPServer(): void;
}

export const SumiMCPServerProxyServicePath = 'SumiMCPServerProxyServicePath';

export interface IChatAgentService {
Expand Down
2 changes: 1 addition & 1 deletion packages/ai-native/src/common/mcp-server-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export interface MCPServerManager {
addOrUpdateServer(description: MCPServerDescription): void;
// invoke in node.js only
addOrUpdateServerDirectly(server: any): void;
initBuiltinServer(): void;
initBuiltinServer(builtinMCPServer: any): void;
getTools(serverName: string): ReturnType<Client['listTools']>;
getServerNames(): Promise<string[]>;
startServer(serverName: string): Promise<void>;
Expand Down
29 changes: 13 additions & 16 deletions packages/ai-native/src/node/anthropic/anthropic-language-model.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Injectable, Autowired } from '@opensumi/di';
import { Anthropic } from '@anthropic-ai/sdk';
import { MessageParam, Model, ToolChoiceAuto, MessageStream, Message } from '@anthropic-ai/sdk/resources/messages';
import { AnthropicProvider, createAnthropic } from '@ai-sdk/anthropic';
import { jsonSchema, streamText, tool } from 'ai';

import { Autowired, Injectable } from '@opensumi/di';
import { ChatReadableStream } from '@opensumi/ide-core-node';
import { CancellationToken } from '@opensumi/ide-utils';

import { ToolInvocationRegistry, ToolInvocationRegistryImpl, ToolRequest } from '../../common/tool-invocation-registry';
import { ChatReadableStream } from '@opensumi/ide-core-node';
import { z } from 'zod';
import { generateText, tool, streamText, jsonSchema } from 'ai';
import { anthropic, AnthropicProvider, createAnthropic } from '@ai-sdk/anthropic';


export const AnthropicModelIdentifier = Symbol('AnthropicModelIdentifier');

Expand Down Expand Up @@ -35,13 +35,10 @@ export class AnthropicModel {

private convertToolRequestToAITool(toolRequest: ToolRequest) {
return tool({
// name: toolRequest.name,
description: toolRequest.description || '',
// TODO 这里应该是 z.object 而不是 JSON Schema
parameters: jsonSchema(toolRequest.parameters),
execute: async (args: any) => {
return await toolRequest.handler(JSON.stringify(args));
}
execute: async (args: any) => await toolRequest.handler(JSON.stringify(args)),
});
}

Expand All @@ -50,12 +47,12 @@ export class AnthropicModel {
request: string,
tools: ToolRequest[],
chatReadableStream: ChatReadableStream,
cancellationToken?: CancellationToken
cancellationToken?: CancellationToken,
): Promise<any> {

try {
const aiTools = Object.fromEntries(
tools.map(tool => [tool.name, this.convertToolRequestToAITool(tool)])
tools.map((tool) => [tool.name, this.convertToolRequestToAITool(tool)]),
);

const abortController = new AbortController();
Expand All @@ -79,10 +76,10 @@ export class AnthropicModel {
if (chunk.type === 'text-delta') {
chatReadableStream.emitData({ kind: 'content', content: chunk.textDelta });
} else if (chunk.type === 'tool-call') {
chatReadableStream.emitData({ kind: 'toolCall', content: {
chatReadableStream.emitData({ kind: 'toolCall', content: {
id: chunk.toolCallId || Date.now().toString(),
type: 'function',
function: { name: chunk.toolName, arguments: JSON.stringify(chunk.args) }
function: { name: chunk.toolName, arguments: JSON.stringify(chunk.args) },
}});
}
}
Expand Down
15 changes: 3 additions & 12 deletions packages/ai-native/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ import { AIBackSerivcePath, AIBackSerivceToken } from '@opensumi/ide-core-common
import { NodeModule } from '@opensumi/ide-core-node';
import { BaseAIBackService } from '@opensumi/ide-core-node/lib/ai-native/base-back.service';

import { SumiMCPServerProxyServicePath } from '../common';
import { TokenMCPServerProxyService } from '../common';
import { MCPServerManager, MCPServerManagerPath } from '../common/mcp-server-manager';
import { SumiMCPServerProxyServicePath , TokenMCPServerProxyService } from '../common';
import { MCPServerManager } from '../common/mcp-server-manager';
import { ToolInvocationRegistry, ToolInvocationRegistryImpl } from '../common/tool-invocation-registry';

import { BuiltinMCPServer, SumiMCPServerBackend, TokenBuiltinMCPServer } from './mcp/sumi-mcp-server';
import { SumiMCPServerBackend } from './mcp/sumi-mcp-server';
import { MCPServerManagerImpl } from './mcp-server-manager-impl';


Expand All @@ -31,21 +30,13 @@ export class AINativeModule extends NodeModule {
token: TokenMCPServerProxyService,
useClass: SumiMCPServerBackend,
},
{
token: TokenBuiltinMCPServer,
useClass: BuiltinMCPServer,
},
];

backServices = [
{
servicePath: AIBackSerivcePath,
token: AIBackSerivceToken,
},
{
servicePath: MCPServerManagerPath,
token: MCPServerManager,
},
{
servicePath: SumiMCPServerProxyServicePath,
token: TokenMCPServerProxyService,
Expand Down
11 changes: 4 additions & 7 deletions packages/ai-native/src/node/mcp-server-manager-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,14 @@ import { Autowired, Injectable } from '@opensumi/di';
import { MCPServerDescription, MCPServerManager, MCPTool } from '../common/mcp-server-manager';
import { ToolInvocationRegistry, ToolInvocationRegistryImpl, ToolRequest } from '../common/tool-invocation-registry';

import { BuiltinMCPServer, TokenBuiltinMCPServer } from './mcp/sumi-mcp-server';
import { BuiltinMCPServer } from './mcp/sumi-mcp-server';
import { IMCPServer, MCPServerImpl } from './mcp-server';

@Injectable()
export class MCPServerManagerImpl implements MCPServerManager {
@Autowired(ToolInvocationRegistry)
private readonly toolInvocationRegistry: ToolInvocationRegistryImpl;

@Autowired(TokenBuiltinMCPServer)
private readonly builtinMCPServer: BuiltinMCPServer;

protected servers: Map<string, IMCPServer> = new Map();

async stopServer(serverName: string): Promise<void> {
Expand Down Expand Up @@ -116,9 +113,9 @@ export class MCPServerManagerImpl implements MCPServerManager {
this.servers.set(server.getServerName(), server);
}

initBuiltinServer(): void {
this.addOrUpdateServerDirectly(this.builtinMCPServer);
this.collectTools(this.builtinMCPServer.getServerName());
initBuiltinServer(builtinMCPServer: BuiltinMCPServer): void {
this.addOrUpdateServerDirectly(builtinMCPServer);
this.collectTools(builtinMCPServer.getServerName());
}

removeServer(name: string): void {
Expand Down
41 changes: 31 additions & 10 deletions packages/ai-native/src/node/mcp/sumi-mcp-server.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,41 @@
// 想要通过 MCP 的方式暴露 Opensumi 的 IDE 能力,就需要 Node.js 层打通 MCP 的通信
// 因为大部分 MCP 功能的实现在前端,因此需要再这里做前后端通信

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';

import { Autowired, Injectable } from '@opensumi/di';
import { RPCService } from '@opensumi/ide-connection';

import { TokenMCPServerProxyService } from '../../common';
import { MCPServerManager } from '../../common/mcp-server-manager';
import { IMCPServerProxyService } from '../../common/types';
import { IMCPServer } from '../mcp-server';
import { MCPServerManagerImpl } from '../mcp-server-manager-impl';

@Injectable()
// 每个 BrowserTab 都对应了一个 SumiMCPServerBackend 实例
// SumiMCPServerBackend 需要做的事情:
// 维护 Browser 端工具的注册和调用
// 处理第三方 MCP Server 的注册和调用

@Injectable({ multiple: true })
export class SumiMCPServerBackend extends RPCService<IMCPServerProxyService> {

// 这里需要考虑不同的 BrowserTab 的区分问题,目前的 POC 所有的 Tab 都会注册到 tools 中
// 后续需要区分不同的 Tab 对应的实例
@Autowired(MCPServerManager)
private readonly mcpServerManager: MCPServerManagerImpl;

private server: Server | undefined;

async getMCPTools() {
if (!this.client) {
throw new Error('SUMI MCP RPC Client not initialized');
}
// 获取 MCP 工具
return await this.client.$getMCPTools();
const tools = await this.client.$getMCPTools();
console.log('[Node backend] SUMI MCP tools', tools);
return tools;
}

async callMCPTool(name: string, args: any) {
Expand All @@ -34,7 +49,12 @@ export class SumiMCPServerBackend extends RPCService<IMCPServerProxyService> {
return this.server;
}

async initMCPServer() {
initBuiltinMCPServer() {
const builtinMCPServer = new BuiltinMCPServer(this);
this.mcpServerManager.initBuiltinServer(builtinMCPServer);
}

async initExposedMCPServer() {
// 初始化 MCP Server
this.server = new Server(
{
Expand Down Expand Up @@ -75,11 +95,11 @@ export class SumiMCPServerBackend extends RPCService<IMCPServerProxyService> {

export const TokenBuiltinMCPServer = Symbol('TokenBuiltinMCPServer');

@Injectable()
export class BuiltinMCPServer implements IMCPServer {

@Autowired(TokenMCPServerProxyService)
private readonly sumiMCPServer: SumiMCPServerBackend;
constructor(
private readonly sumiMCPServer: SumiMCPServerBackend,
) {}

private started: boolean = true;

Expand All @@ -88,7 +108,7 @@ export class BuiltinMCPServer implements IMCPServer {
}

getServerName(): string {
return 'opensumi-builtin-mcp-server';
return 'sumi-builtin';
}

async start(): Promise<void> {
Expand Down Expand Up @@ -118,11 +138,12 @@ export class BuiltinMCPServer implements IMCPServer {
return this.sumiMCPServer.callMCPTool(toolName, args);
}

async getTools(): Promise<any> {
async getTools(): ReturnType<Client['listTools']> {
if (!this.started) {
throw new Error('MCP Server not started');
}
return this.sumiMCPServer.getMCPTools();
const tools = await this.sumiMCPServer.getMCPTools();
return { tools };
}

update(_command: string, _args?: string[], _env?: { [key: string]: string }): void {
Expand Down

1 comment on commit bafda53

@opensumi
Copy link
Contributor

@opensumi opensumi bot commented on bafda53 Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 Next publish successful!

3.7.1-next-1737703128.0

Please sign in to comment.