重构 OpenMCPService,采用新的架构

This commit is contained in:
锦恢 2025-04-27 19:11:24 +08:00
parent 8ef6ddd1ed
commit 31fa5ead4f
30 changed files with 734 additions and 949 deletions

View File

@ -55,7 +55,7 @@ export function makeEnv() {
type ConnectionType = 'STDIO' | 'SSE';
// 定义命令行参数接口
export interface MCPOptions {
export interface McpOptions {
connectionType: ConnectionType;
// STDIO 特定选项
command?: string;
@ -70,7 +70,7 @@ export interface MCPOptions {
}
export function doConnect() {
let connectOption: MCPOptions;
let connectOption: McpOptions;
const bridge = useMessageBridge();
const env = makeEnv();
@ -313,7 +313,7 @@ async function launchSSE() {
resolve(void 0);
}, { once: true });
const connectOption: MCPOptions = {
const connectOption: McpOptions = {
connectionType: 'SSE',
url: connectionArgs.urlString,
clientName: 'openmcp.connect.sse',

View File

@ -0,0 +1,33 @@
import { PostMessageble } from "../hook/adapter";
import { McpClient } from "../mcp/client.service";
export type RequestClientType = McpClient | undefined;
export type RequestHandler<T, R> = (
client: RequestClientType,
data: T,
webview: PostMessageble
) => Promise<R>;
export interface RequestHandlerStore<T, R> {
handler: RequestHandler<T, R>
option?: ControllerOption;
}
export interface MapperDescriptor<T> {
configurable?: boolean;
enumerable?: boolean;
value?: RequestHandler<T, RestfulResponse>;
writable?: boolean;
get?(): any;
set?(v: any): void;
}
export interface RestfulResponse {
code: number;
msg: any;
}
export interface ControllerOption {
[key: string]: any;
}

View File

@ -0,0 +1,17 @@
import { MapperDescriptor, RequestHandler, RequestClientType, RestfulResponse, ControllerOption, RequestHandlerStore } from "./index.dto";
export { MapperDescriptor, RequestHandler, RequestClientType };
export const requestHandlerStorage = new Map<string, RequestHandlerStore<any, RestfulResponse>>();
export function Controller(command: string, option: ControllerOption = {}) {
return function<T>(target: any, propertykey: string, descriptor: MapperDescriptor<T>) {
const handler = descriptor.value;
if (requestHandlerStorage.has(command)) {
throw new Error(`Duplicate request handler for ${command}`);
}
if (handler) {
requestHandlerStorage.set(command, { handler, option });
}
return descriptor;
}
}

View File

@ -0,0 +1,41 @@
import { requestHandlerStorage } from ".";
import { PostMessageble } from "../hook/adapter";
import { LlmController } from "../llm/llm.controller";
import { ClientController } from "../mcp/client.controller";
import { ConnectController } from "../mcp/connect.controller";
import { client } from "../mcp/connect.service";
import { PanelController } from "../panel/panel.controller";
import { SettingController } from "../setting/setting.controller";
export const ModuleControllers = [
ConnectController,
ClientController,
LlmController,
PanelController,
SettingController
];
export async function routeMessage(command: string, data: any, webview: PostMessageble) {
const handlerStore = requestHandlerStorage.get(command);
if (handlerStore) {
const { handler, option = {} } = handlerStore;
try {
// TODO: select client based on something
const res = await handler(client, data, webview);
// res.code = -1 代表当前请求不需要返回发送
if (res.code >= 0) {
webview.postMessage({ command, data: res });
}
} catch (error) {
webview.postMessage({
command, data: {
code: 500,
msg: (error as any).toString()
}
});
}
}
return
}

View File

@ -1,97 +0,0 @@
import { PostMessageble } from '../hook/adapter';
import { lookupEnvVarService } from '../service/env-var';
import {
callToolService,
getPromptService,
getServerVersionService,
listPromptsService,
listResourcesService,
listResourceTemplatesService,
listToolsService,
readResourceService
} from '../service/mcp-server';
import { abortMessageService, chatCompletionService } from '../service/llm';
import { panelLoadService, panelSaveService } from '../service/panel';
import { settingLoadService, settingSaveService } from '../service/setting';
import { pingService } from '../service/util';
import { client, connectService } from '../service/connect';
export function messageController(command: string, data: any, webview: PostMessageble) {
switch (command) {
case 'connect':
connectService(client, data, webview);
break;
case 'server/version':
getServerVersionService(client, data, webview);
break;
case 'prompts/list':
listPromptsService(client, data, webview);
break;
case 'prompts/get':
getPromptService(client, data, webview);
break;
case 'resources/list':
listResourcesService(client, data, webview);
break;
case 'resources/templates/list':
listResourceTemplatesService(client, data, webview);
break;
case 'resources/read':
readResourceService(client, data, webview);
break;
case 'tools/list':
listToolsService(client, data, webview);
break;
case 'tools/call':
callToolService(client, data, webview);
break;
case 'ping':
pingService(client, data, webview);
break;
case 'setting/save':
settingSaveService(client, data, webview);
break;
case 'setting/load':
settingLoadService(client, data, webview);
break;
case 'panel/save':
panelSaveService(client, data, webview);
break;
case 'panel/load':
panelLoadService(client, data, webview);
break;
case 'llm/chat/completions':
chatCompletionService(client, data, webview);
break;
case 'llm/chat/completions/abort':
abortMessageService(client, data, webview);
break;
case 'lookup-env-var':
lookupEnvVarService(client, data, webview);
break;
default:
break;
}
}

View File

@ -1,5 +1,3 @@
import { OpenAI } from 'openai';
export const llms = [
{
id: 'deepseek',
@ -86,57 +84,3 @@ export const llms = [
userModel: 'moonshot-v1-8k'
}
];
type MyMessageType = OpenAI.Chat.ChatCompletionMessageParam & {
extraInfo?: any;
}
type MyToolMessageType = OpenAI.Chat.ChatCompletionToolMessageParam & {
extraInfo?: any;
}
function postProcessToolMessages(message: MyToolMessageType) {
if (typeof message.content === 'string') {
return;
}
for (const content of message.content) {
const contentType = content.type as string;
const rawContent = content as any;
if (contentType === 'image') {
delete rawContent._meta;
rawContent.type = 'text';
// 从缓存中提取图像数据
rawContent.text = '图片已被删除';
}
}
message.content = JSON.stringify(message.content);
}
export function postProcessMessages(messages: MyMessageType[]) {
for (const message of messages) {
// 去除 extraInfo 属性
delete message.extraInfo;
switch (message.role) {
case 'user':
break;
case 'assistant':
break;
case 'system':
break;
case 'tool':
postProcessToolMessages(message);
break;
default:
break;
}
}
}

View File

@ -1,161 +1,5 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { llms } from './llm';
import { IServerVersion } from './client';
export let VSCODE_WORKSPACE = '';
export function setVscodeWorkspace(workspace: string) {
VSCODE_WORKSPACE = workspace;
}
function getConfigurationPath() {
// 如果是 vscode 插件下,则修改为 ~/.openmcp/config.json
if (VSCODE_WORKSPACE) {
// 在 VSCode 插件环境下
const homeDir = os.homedir();
const configDir = path.join(homeDir, '.openmcp');
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
return path.join(configDir, 'setting.json');
}
return 'setting.json';
}
function getTabSavePath(serverInfo: IServerVersion) {
const { name = 'untitle', version = '0.0.0' } = serverInfo || {};
const tabSaveName = `tabs.${name}.json`;
// 如果是 vscode 插件下,则修改为 ~/.vscode/openmcp.json
if (VSCODE_WORKSPACE) {
// 在 VSCode 插件环境下
const configDir = path.join(VSCODE_WORKSPACE, '.vscode');
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
return path.join(configDir, tabSaveName);
}
return tabSaveName;
}
function getDefaultLanguage() {
if (process.env.VSCODE_PID) {
// TODO: 获取 vscode 内部的语言
}
return 'zh';
}
interface IConfig {
MODEL_INDEX: number;
[key: string]: any;
}
const DEFAULT_CONFIG: IConfig = {
MODEL_INDEX: 0,
LLM_INFO: llms,
LANG: getDefaultLanguage()
};
interface SaveTabItem {
name: string;
icon: string;
type: string;
componentIndex: number;
storage: Record<string, any>;
}
interface SaveTab {
tabs: SaveTabItem[]
currentIndex: number
}
const DEFAULT_TABS: SaveTab = {
tabs: [],
currentIndex: -1
}
function createConfig(): IConfig {
const configPath = getConfigurationPath();
const configDir = path.dirname(configPath);
// 确保配置目录存在
if (configDir && !fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
// 写入默认配置
fs.writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2), 'utf-8');
return DEFAULT_CONFIG;
}
function createSaveTabConfig(serverInfo: IServerVersion): SaveTab {
const configPath = getTabSavePath(serverInfo);
const configDir = path.dirname(configPath);
// 确保配置目录存在
if (configDir && !fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
// 写入默认配置
fs.writeFileSync(configPath, JSON.stringify(DEFAULT_TABS, null, 2), 'utf-8');
return DEFAULT_TABS;
}
export function loadConfig(): IConfig {
const configPath = getConfigurationPath();
if (!fs.existsSync(configPath)) {
return createConfig();
}
try {
const configData = fs.readFileSync(configPath, 'utf-8');
return JSON.parse(configData) as IConfig;
} catch (error) {
console.error('Error loading config file, creating new one:', error);
return createConfig();
}
}
export function saveConfig(config: Partial<IConfig>): void {
const configPath = getConfigurationPath();
console.log('save to ' + configPath);
try {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
} catch (error) {
console.error('Error saving config file:', error);
throw error;
}
}
export function loadTabSaveConfig(serverInfo: IServerVersion): SaveTab {
const tabSavePath = getTabSavePath(serverInfo);
if (!fs.existsSync(tabSavePath)) {
return createSaveTabConfig(serverInfo);
}
try {
const configData = fs.readFileSync(tabSavePath, 'utf-8');
return JSON.parse(configData) as SaveTab;
} catch (error) {
console.error('Error loading config file, creating new one:', error);
return createSaveTabConfig(serverInfo);
}
}
export function saveTabSaveConfig(serverInfo: IServerVersion, config: Partial<IConfig>): void {
const tabSavePath = getTabSavePath(serverInfo);
try {
fs.writeFileSync(tabSavePath, JSON.stringify(config, null, 2), 'utf-8');
} catch (error) {
console.error('Error saving config file:', error);
throw error;
}
}

View File

@ -1,5 +1,5 @@
export { messageController } from './controller';
export { routeMessage } from './common/router';
export { VSCodeWebViewLike } from './hook/adapter';
export { setVscodeWorkspace } from './hook/setting';
// TODO: 更加规范
export { client } from './service/connect';
export { client } from './mcp/connect.service';

View File

@ -0,0 +1,29 @@
import { Controller, RequestClientType } from "../common";
import { PostMessageble } from "../hook/adapter";
import { abortMessageService, streamingChatCompletion } from "./llm.service";
export class LlmController {
@Controller('llm/chat/completions')
async chatCompletion(client: RequestClientType, data: any, webview: PostMessageble) {
if (!client) {
return {
code: 501,
msg:'mcp client 尚未连接'
};
}
await streamingChatCompletion(data, webview);
return {
code: -1,
msg: 'terminate'
};
}
@Controller('llm/chat/completions/abort')
async abortChatCompletion(client: RequestClientType, data: any, webview: PostMessageble) {
return abortMessageService(data, webview);
}
}

View File

@ -0,0 +1,9 @@
import { OpenAI } from "openai";
export type MyMessageType = OpenAI.Chat.ChatCompletionMessageParam & {
extraInfo?: any;
}
export type MyToolMessageType = OpenAI.Chat.ChatCompletionToolMessageParam & {
extraInfo?: any;
}

View File

@ -0,0 +1,146 @@
import { PostMessageble } from "../hook/adapter";
import { OpenAI } from "openai";
import { MyMessageType, MyToolMessageType } from "./llm.dto";
import { RestfulResponse } from "../common/index.dto";
export let currentStream: AsyncIterable<any> | null = null;
export async function streamingChatCompletion(
data: any,
webview: PostMessageble
) {
let { baseURL, apiKey, model, messages, temperature, tools = [] } = data;
const client = new OpenAI({
baseURL,
apiKey
});
if (tools.length === 0) {
tools = undefined;
}
postProcessMessages(messages);
const stream = await client.chat.completions.create({
model,
messages,
temperature,
tools,
tool_choice: 'auto',
web_search_options: {},
stream: true
});
// 存储当前的流式传输对象
currentStream = stream;
// 流式传输结果
for await (const chunk of stream) {
if (!currentStream) {
// 如果流被中止,则停止循环
// TODO: 为每一个标签页设置不同的 currentStream 管理器
stream.controller.abort();
// 传输结束
webview.postMessage({
command: 'llm/chat/completions/done',
data: {
code: 200,
msg: {
success: true,
stage: 'abort'
}
}
});
break;
}
if (chunk.choices) {
const chunkResult = {
code: 200,
msg: {
chunk
}
};
webview.postMessage({
command: 'llm/chat/completions/chunk',
data: chunkResult
});
}
}
// 传输结束
webview.postMessage({
command: 'llm/chat/completions/done',
data: {
code: 200,
msg: {
success: true,
stage: 'done'
}
}
});
}
// 处理中止消息的函数
export function abortMessageService(data: any, webview: PostMessageble): RestfulResponse {
if (currentStream) {
// 标记流已中止
currentStream = null;
}
return {
code: 200,
msg: {
success: true
}
}
}
function postProcessToolMessages(message: MyToolMessageType) {
if (typeof message.content === 'string') {
return;
}
for (const content of message.content) {
const contentType = content.type as string;
const rawContent = content as any;
if (contentType === 'image') {
delete rawContent._meta;
rawContent.type = 'text';
// 从缓存中提取图像数据
rawContent.text = '图片已被删除';
}
}
message.content = JSON.stringify(message.content);
}
export function postProcessMessages(messages: MyMessageType[]) {
for (const message of messages) {
// 去除 extraInfo 属性
delete message.extraInfo;
switch (message.role) {
case 'user':
break;
case 'assistant':
break;
case 'system':
break;
case 'tool':
postProcessToolMessages(message);
break;
default:
break;
}
}
}

View File

@ -1,7 +1,7 @@
import WebSocket from 'ws';
import pino from 'pino';
import { messageController } from './controller';
import { routeMessage } from './common/router';
import { VSCodeWebViewLike } from './hook/adapter';
export interface VSCodeMessage {
@ -23,7 +23,7 @@ const logger = pino({
});
export type MessageHandler = (message: VSCodeMessage) => void;
const wss = new WebSocket.Server({ port: 8080 });
const wss = new (WebSocket as any).Server({ port: 8080 });
wss.on('connection', ws => {
@ -44,6 +44,6 @@ wss.on('connection', ws => {
logger.info(`command: [${message.command || 'No Command'}]`);
const { command, data } = message;
messageController(command, data, webview);
routeMessage(command, data, webview);
});
});

View File

@ -0,0 +1,138 @@
import { Controller, RequestClientType } from "../common";
import { PostMessageble } from "../hook/adapter";
export class ClientController {
@Controller('server/version')
async getServerVersion(client: RequestClientType, data: any, webview: PostMessageble) {
if (!client) {
return {
code: 501,
msg:'mcp client 尚未连接'
};
}
const version = client.getServerVersion();
return {
code: 200,
msg: version
};
}
@Controller('prompts/list')
async listPrompts(client: RequestClientType, data: any, webview: PostMessageble) {
if (!client) {
const connectResult = {
code: 501,
msg: 'mcp client 尚未连接'
};
return connectResult;
}
const prompts = await client.listPrompts();
const result = {
code: 200,
msg: prompts
};
return result;
}
@Controller('prompts/get')
async getPrompt(client: RequestClientType, option: any, webview: PostMessageble) {
if (!client) {
return {
code: 501,
msg: 'mcp client 尚未连接'
};
}
const prompt = await client.getPrompt(option.promptId, option.args || {});
return {
code: 200,
msg: prompt
};
}
@Controller('resources/list')
async listResources(client: RequestClientType, data: any, webview: PostMessageble) {
if (!client) {
return {
code: 501,
msg: 'mcp client 尚未连接'
};
}
const resources = await client.listResources();
return {
code: 200,
msg: resources
};
}
@Controller('resources/templates/list')
async listResourceTemplates(client: RequestClientType, data: any, webview: PostMessageble) {
if (!client) {
return {
code: 501,
msg: 'mcp client 尚未连接'
};
}
const resources = await client.listResourceTemplates();
return {
code: 200,
msg: resources
};
}
@Controller('resources/read')
async readResource(client: RequestClientType, option: any, webview: PostMessageble) {
if (!client) {
return {
code: 501,
msg: 'mcp client 尚未连接'
};
}
const resource = await client.readResource(option.resourceUri);
return {
code: 200,
msg: resource
};
}
@Controller('tools/list')
async listTools(client: RequestClientType, data: any, webview: PostMessageble) {
if (!client) {
return {
code: 501,
msg: 'mcp client 尚未连接'
};
}
const tools = await client.listTools();
return {
code: 200,
msg: tools
};
}
@Controller('tools/call')
async callTool(client: RequestClientType, option: any, webview: PostMessageble) {
if (!client) {
return {
code: 501,
msg: 'mcp client 尚未连接'
};
}
const toolResult = await client.callTool({
name: option.toolName,
arguments: option.toolArgs
});
return {
code: 200,
msg: toolResult
};
}
}

View File

@ -0,0 +1,37 @@
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { Implementation } from "@modelcontextprotocol/sdk/types";
export interface GetPromptOption {
promptId: string;
args?: Record<string, any>;
}
export interface ReadResourceOption {
resourceUri: string;
}
export interface CallToolOption {
toolName: string;
toolArgs: Record<string, any>;
}
// 定义连接类型
export type ConnectionType = 'STDIO' | 'SSE';
export type McpTransport = StdioClientTransport | SSEClientTransport;
export type IServerVersion = Implementation | undefined;
// 定义命令行参数接口
export interface McpOptions {
connectionType: ConnectionType;
// STDIO 特定选项
command?: string;
args?: string[];
// SSE 特定选项
url?: string;
cwd?: string;
// 通用客户端选项
clientName?: string;
clientVersion?: string;
}

View File

@ -2,38 +2,18 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { Implementation } from "@modelcontextprotocol/sdk/types";
// 定义连接类型
type ConnectionType = 'STDIO' | 'SSE';
type McpTransport = StdioClientTransport | SSEClientTransport;
export type IServerVersion = Implementation | undefined;
// 定义命令行参数接口
export interface MCPOptions {
connectionType: ConnectionType;
// STDIO 特定选项
command?: string;
args?: string[];
// SSE 特定选项
url?: string;
cwd?: string;
// 通用客户端选项
clientName?: string;
clientVersion?: string;
}
import { McpOptions, McpTransport, IServerVersion } from './client.dto';
// 增强的客户端类
export class MCPClient {
export class McpClient {
private client: Client;
private transport?: McpTransport;
private options: MCPOptions;
private options: McpOptions;
private serverVersion: IServerVersion;
private transportStdErr: string = '';
constructor(options: MCPOptions) {
constructor(options: McpOptions) {
this.options = options;
this.serverVersion = undefined;
@ -144,8 +124,8 @@ export class MCPClient {
}
// Connect 函数实现
export async function connect(options: MCPOptions): Promise<MCPClient> {
const client = new MCPClient(options);
export async function connect(options: McpOptions): Promise<McpClient> {
const client = new McpClient(options);
await client.connect();
return client;
}

View File

@ -0,0 +1,39 @@
import { Controller, RequestClientType } from '../common';
import { PostMessageble } from '../hook/adapter';
import { connectService } from './connect.service';
export class ConnectController {
@Controller('connect')
async connect(client: RequestClientType, data: any, webview: PostMessageble) {
const res = await connectService(client, data);
return res;
}
@Controller('lookup-env-var')
async lookupEnvVar(client: RequestClientType, data: any, webview: PostMessageble) {
const { keys } = data;
const values = keys.map((key: string) => process.env[key] || '');
return {
code: 200,
msg: values
}
}
@Controller('ping')
async ping(client: RequestClientType, data: any, webview: PostMessageble) {
if (!client) {
const connectResult = {
code: 501,
msg:'mcp client 尚未连接'
};
return connectResult;
}
return {
code: 200,
msg: {}
}
}
}

View File

@ -1,12 +1,14 @@
import { PostMessageble } from '../hook/adapter';
import { connect, MCPClient, type MCPOptions } from '../hook/client';
import { spawnSync } from 'node:child_process';
import { RequestClientType } from '../common';
import { connect } from './client.service';
import { RestfulResponse } from '../common/index.dto';
import { McpOptions } from './client.dto';
// TODO: 支持更多的 client
export let client: MCPClient | undefined = undefined;
function tryGetRunCommandError(command: string, args: string[] = [], cwd?: string): string | null {
// TODO: 更多的 client
export let client: RequestClientType = undefined;
export function tryGetRunCommandError(command: string, args: string[] = [], cwd?: string): string | null {
try {
console.log('current command', command);
console.log('current args', args);
@ -30,10 +32,9 @@ function tryGetRunCommandError(command: string, args: string[] = [], cwd?: strin
}
export async function connectService(
_client: MCPClient | undefined,
option: MCPOptions,
webview: PostMessageble
) {
_client: RequestClientType,
option: McpOptions
): Promise<RestfulResponse> {
try {
console.log('ready to connect', option);
@ -42,7 +43,8 @@ export async function connectService(
code: 200,
msg: 'Connect to OpenMCP successfully\nWelcome back, Kirigaya'
};
webview.postMessage({ command: 'connect', data: connectResult });
return connectResult;
} catch (error) {
// TODO: 这边获取到的 error 不够精致,如何才能获取到更加精准的错误
@ -61,6 +63,7 @@ export async function connectService(
code: 500,
msg: errorMsg
};
webview.postMessage({ command: 'connect', data: connectResult });
return connectResult;
}
}

View File

@ -0,0 +1,28 @@
import { Controller, RequestClientType } from "../common";
import { PostMessageble } from "../hook/adapter";
import { loadTabSaveConfig, saveTabSaveConfig } from "./panel.service";
export class PanelController {
@Controller('panel/save')
async savePanel(client: RequestClientType, data: any, webview: PostMessageble) {
const serverInfo = client?.getServerVersion();
saveTabSaveConfig(serverInfo, data);
return {
code: 200,
msg: 'Settings saved successfully'
};
}
@Controller('panel/load')
async loadPanel(client: RequestClientType, data: any, webview: PostMessageble) {
const serverInfo = client?.getServerVersion();
const config = loadTabSaveConfig(serverInfo);
return {
code: 200,
msg: config
};
}
}

View File

@ -0,0 +1,12 @@
export interface SaveTabItem {
name: string;
icon: string;
type: string;
componentIndex: number;
storage: Record<string, any>;
}
export interface SaveTab {
tabs: SaveTabItem[]
currentIndex: number
}

View File

@ -0,0 +1,68 @@
import * as fs from 'fs';
import * as path from 'path';
import { VSCODE_WORKSPACE } from '../hook/setting';
import { IServerVersion } from '../mcp/client.dto';
import { SaveTab } from './panel.dto';
import { IConfig } from '../setting/setting.dto';
const DEFAULT_TABS: SaveTab = {
tabs: [],
currentIndex: -1
}
function getTabSavePath(serverInfo: IServerVersion) {
const { name = 'untitle', version = '0.0.0' } = serverInfo || {};
const tabSaveName = `tabs.${name}.json`;
// 如果是 vscode 插件下,则修改为 ~/.vscode/openmcp.json
if (VSCODE_WORKSPACE) {
// 在 VSCode 插件环境下
const configDir = path.join(VSCODE_WORKSPACE, '.vscode');
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
return path.join(configDir, tabSaveName);
}
return tabSaveName;
}
function createSaveTabConfig(serverInfo: IServerVersion): SaveTab {
const configPath = getTabSavePath(serverInfo);
const configDir = path.dirname(configPath);
// 确保配置目录存在
if (configDir && !fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
// 写入默认配置
fs.writeFileSync(configPath, JSON.stringify(DEFAULT_TABS, null, 2), 'utf-8');
return DEFAULT_TABS;
}
export function loadTabSaveConfig(serverInfo: IServerVersion): SaveTab {
const tabSavePath = getTabSavePath(serverInfo);
if (!fs.existsSync(tabSavePath)) {
return createSaveTabConfig(serverInfo);
}
try {
const configData = fs.readFileSync(tabSavePath, 'utf-8');
return JSON.parse(configData) as SaveTab;
} catch (error) {
console.error('Error loading config file, creating new one:', error);
return createSaveTabConfig(serverInfo);
}
}
export function saveTabSaveConfig(serverInfo: IServerVersion, config: Partial<IConfig>): void {
const tabSavePath = getTabSavePath(serverInfo);
try {
fs.writeFileSync(tabSavePath, JSON.stringify(config, null, 2), 'utf-8');
} catch (error) {
console.error('Error saving config file:', error);
throw error;
}
}

View File

@ -1,27 +0,0 @@
import { PostMessageble } from "../hook/adapter";
import { MCPClient } from "../hook/client";
export async function lookupEnvVarService(client: MCPClient | undefined, data: any, webview: PostMessageble) {
try {
const { keys } = data;
const values = keys.map((key: string) => process.env[key] || '');
webview.postMessage({
command: 'lookup-env-var',
data: {
code: 200,
msg: values
}
});
} catch (error) {
webview.postMessage({
command: 'lookup-env-var',
data: {
code: 500,
msg: `Failed to lookup env vars: ${(error as Error).message}`
}
});
}
}

View File

@ -1,120 +0,0 @@
import { OpenAI } from 'openai';
import { MCPClient } from '../hook/client';
import { PostMessageble } from '../hook/adapter';
import { postProcessMessages } from '../hook/llm';
let currentStream: AsyncIterable<any> | null = null;
export async function chatCompletionService(client: MCPClient | undefined, data: any, webview: PostMessageble) {
if (!client) {
const connectResult = {
code: 501,
msg: 'mcp client 尚未连接'
};
webview.postMessage({ command: 'ping', data: connectResult });
return;
}
let { baseURL, apiKey, model, messages, temperature, tools = [] } = data;
try {
const client = new OpenAI({
baseURL,
apiKey
});
if (tools.length === 0) {
tools = undefined;
}
postProcessMessages(messages);
const stream = await client.chat.completions.create({
model,
messages,
temperature,
tools,
tool_choice: 'auto',
web_search_options: {},
stream: true
});
// 存储当前的流式传输对象
currentStream = stream;
// 流式传输结果
for await (const chunk of stream) {
if (!currentStream) {
// 如果流被中止,则停止循环
// TODO: 为每一个标签页设置不同的 currentStream 管理器
stream.controller.abort();
// 传输结束
webview.postMessage({
command: 'llm/chat/completions/done',
data: {
code: 200,
msg: {
success: true,
stage: 'abort'
}
}
});
break;
}
if (chunk.choices) {
const chunkResult = {
code: 200,
msg: {
chunk
}
};
webview.postMessage({
command: 'llm/chat/completions/chunk',
data: chunkResult
});
}
}
// 传输结束
webview.postMessage({
command: 'llm/chat/completions/done',
data: {
code: 200,
msg: {
success: true,
stage: 'done'
}
}
});
} catch (error) {
webview.postMessage({
command: 'llm/chat/completions/chunk',
data: {
code: 500,
msg: `OpenAI API error: ${(error as Error).message}`
}
});
}
}
// 处理中止消息的函数
export function abortMessageService(client: MCPClient | undefined, data: any, webview: PostMessageble) {
if (currentStream) {
// 标记流已中止
currentStream = null;
}
webview.postMessage({
command: 'llm/chat/completions/abort',
data: {
code: 200,
msg: {
success: true
}
}
});
}

View File

@ -1,289 +0,0 @@
import { PostMessageble } from "../hook/adapter";
import { MCPClient } from "../hook/client";
// ==================== 接口定义 ====================
export interface GetPromptOption {
promptId: string;
args?: Record<string, any>;
}
export interface ReadResourceOption {
resourceUri: string;
}
export interface CallToolOption {
toolName: string;
toolArgs: Record<string, any>;
}
// ==================== 函数实现 ====================
/**
* @description prompts
*/
export async function listPromptsService(
client: MCPClient | undefined,
data: any,
webview: PostMessageble
) {
if (!client) {
const connectResult = {
code: 501,
msg: 'mcp client 尚未连接'
};
webview.postMessage({ command: 'prompts/list', data: connectResult });
return;
}
try {
const prompts = await client.listPrompts();
const result = {
code: 200,
msg: prompts
};
webview.postMessage({ command: 'prompts/list', data: result });
} catch (error) {
const result = {
code: 500,
msg: (error as any).toString()
};
webview.postMessage({ command: 'prompts/list', data: result });
}
}
/**
* @description prompt
*/
export async function getPromptService(
client: MCPClient | undefined,
option: GetPromptOption,
webview: PostMessageble
) {
if (!client) {
const connectResult = {
code: 501,
msg: 'mcp client 尚未连接'
};
webview.postMessage({ command: 'prompts/get', data: connectResult });
return;
}
try {
const prompt = await client.getPrompt(option.promptId, option.args || {});
const result = {
code: 200,
msg: prompt
};
webview.postMessage({ command: 'prompts/get', data: result });
} catch (error) {
const result = {
code: 500,
msg: (error as any).toString()
};
webview.postMessage({ command: 'prompts/get', data: result });
}
}
/**
* @description resources
*/
export async function listResourcesService(
client: MCPClient | undefined,
data: any,
webview: PostMessageble
) {
if (!client) {
const connectResult = {
code: 501,
msg: 'mcp client 尚未连接'
};
webview.postMessage({ command: 'resources/list', data: connectResult });
return;
}
try {
const resources = await client.listResources();
const result = {
code: 200,
msg: resources
};
webview.postMessage({ command: 'resources/list', data: result });
} catch (error) {
const result = {
code: 500,
msg: (error as any).toString()
};
webview.postMessage({ command: 'resources/list', data: result });
}
}
/**
* @description resources
*/
export async function listResourceTemplatesService(
client: MCPClient | undefined,
data: any,
webview: PostMessageble
) {
if (!client) {
const connectResult = {
code: 501,
msg: 'mcp client 尚未连接'
};
webview.postMessage({ command: 'resources/templates/list', data: connectResult });
return;
}
try {
const resources = await client.listResourceTemplates();
const result = {
code: 200,
msg: resources
};
webview.postMessage({ command: 'resources/templates/list', data: result });
} catch (error) {
const result = {
code: 500,
msg: (error as any).toString()
};
webview.postMessage({ command: 'resources/templates/list', data: result });
}
}
/**
* @description resource
*/
export async function readResourceService(
client: MCPClient | undefined,
option: ReadResourceOption,
webview: PostMessageble
) {
if (!client) {
const connectResult = {
code: 501,
msg: 'mcp client 尚未连接'
};
webview.postMessage({ command: 'resources/read', data: connectResult });
return;
}
try {
const resource = await client.readResource(option.resourceUri);
const result = {
code: 200,
msg: resource
};
webview.postMessage({ command: 'resources/read', data: result });
} catch (error) {
const result = {
code: 500,
msg: (error as any).toString()
};
webview.postMessage({ command: 'resources/read', data: result });
}
}
/**
* @description
*/
export async function listToolsService(
client: MCPClient | undefined,
data: any,
webview: PostMessageble
) {
if (!client) {
const connectResult = {
code: 501,
msg: 'mcp client 尚未连接'
};
webview.postMessage({ command: 'tools/list', data: connectResult });
return;
}
try {
const tools = await client.listTools();
const result = {
code: 200,
msg: tools
};
webview.postMessage({ command: 'tools/list', data: result });
} catch (error) {
const result = {
code: 500,
msg: (error as any).toString()
};
webview.postMessage({ command: 'tools/list', data: result });
}
}
/**
* @description
*/
export async function callToolService(
client: MCPClient | undefined,
option: CallToolOption,
webview: PostMessageble
) {
if (!client) {
const connectResult = {
code: 501,
msg: 'mcp client 尚未连接'
};
webview.postMessage({ command: 'tools/call', data: connectResult });
return;
}
try {
const toolResult = await client.callTool({
name: option.toolName,
arguments: option.toolArgs
});
const result = {
code: 200,
msg: toolResult
};
webview.postMessage({ command: 'tools/call', data: result });
} catch (error) {
const result = {
code: 500,
msg: (error as any).toString()
};
webview.postMessage({ command: 'tools/call', data: result });
}
}
export async function getServerVersionService(
client: MCPClient | undefined,
data: any,
webview: PostMessageble
) {
if (!client) {
const connectResult = {
code: 501,
msg:'mcp client 尚未连接'
};
webview.postMessage({ command: 'server/version', data: connectResult });
return;
}
try {
const version = client.getServerVersion();
const result = {
code: 200,
msg: version
};
webview.postMessage({ command:'server/version', data: result });
} catch (error) {
const result = {
code: 500,
msg: (error as any).toString()
};
webview.postMessage({ command:'server/version', data: result });
}
}

View File

@ -1,18 +0,0 @@
import { PostMessageble } from "../hook/adapter";
import { MCPClient } from "../hook/client";
export function ocrService(
client: MCPClient | undefined,
data: any,
webview: PostMessageble
) {
webview.postMessage({
command: 'ping',
data: {
code: 200,
msg: {}
}
});
}

View File

@ -1,55 +0,0 @@
import { PostMessageble } from '../hook/adapter';
import { loadConfig, loadTabSaveConfig, saveConfig, saveTabSaveConfig } from '../hook/setting';
import { MCPClient } from '../hook/client';
export async function panelSaveService(client: MCPClient | undefined, data: any, webview: PostMessageble) {
try {
// 保存配置
const serverInfo = client?.getServerVersion();
saveTabSaveConfig(serverInfo, data);
webview.postMessage({
command: 'panel/save',
data: {
code: 200,
msg: 'Settings saved successfully'
}
});
} catch (error) {
webview.postMessage({
command: 'panel/save',
data: {
code: 500,
msg: `Failed to save settings: ${(error as Error).message}`
}
});
}
}
export async function panelLoadService(
client: MCPClient | undefined,
data: any,
webview: PostMessageble
) {
try {
// 加载配置
const serverInfo = client?.getServerVersion();
const config = loadTabSaveConfig(serverInfo);
webview.postMessage({
command: 'panel/load',
data: {
code: 200,
msg: config // 直接返回配置对象
}
});
} catch (error) {
webview.postMessage({
command: 'panel/load',
data: {
code: 500,
msg: `Failed to load settings: ${(error as Error).message}`
}
});
}
}

View File

@ -1,60 +0,0 @@
import { PostMessageble } from '../hook/adapter';
import { loadConfig, saveConfig } from '../hook/setting';
import { MCPClient } from '../hook/client';
export async function settingSaveService(
client: MCPClient | undefined,
data: any,
webview: PostMessageble
) {
try {
// 保存配置
saveConfig(data);
console.log('Settings saved successfully');
webview.postMessage({
command: 'setting/save',
data: {
code: 200,
msg: 'Settings saved successfully'
}
});
} catch (error) {
console.log('Setting save failed:', error);
webview.postMessage({
command: 'setting/save',
data: {
code: 500,
msg: `Failed to save settings: ${(error as Error).message}`
}
});
}
}
export async function settingLoadService(
client: MCPClient | undefined,
data: any,
webview: PostMessageble
) {
try {
// 加载配置
const config = loadConfig();
webview.postMessage({
command: 'setting/load',
data: {
code: 200,
msg: config // 直接返回配置对象
}
});
} catch (error) {
webview.postMessage({
command: 'setting/load',
data: {
code: 500,
msg: `Failed to load settings: ${(error as Error).message}`
}
});
}
}

View File

@ -1,25 +0,0 @@
import { PostMessageble } from "../hook/adapter";
import { MCPClient } from "../hook/client";
export function pingService(
client: MCPClient | undefined,
data: any,
webview: PostMessageble
) {
if (!client) {
const connectResult = {
code: 501,
msg: 'mcp client 尚未连接'
};
webview.postMessage({ command: 'ping', data: connectResult });
return;
}
webview.postMessage({
command: 'ping',
data: {
code: 200,
msg: {}
}
});
}

View File

@ -0,0 +1,27 @@
import { Controller, RequestClientType } from "../common";
import { PostMessageble } from "../hook/adapter";
import { loadSetting, saveSetting } from "./setting.service";
export class SettingController {
@Controller('setting/save')
async saveSetting(client: RequestClientType, data: any, webview: PostMessageble) {
saveSetting(data);
console.log('Settings saved successfully');
return {
code: 200,
msg: 'Settings saved successfully'
};
}
@Controller('setting/load')
async loadSetting(client: RequestClientType, data: any, webview: PostMessageble) {
const config = loadSetting();
return {
code: 200,
msg: config
}
}
}

View File

@ -0,0 +1,4 @@
export interface IConfig {
MODEL_INDEX: number;
[key: string]: any;
}

View File

@ -0,0 +1,77 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { VSCODE_WORKSPACE } from '../hook/setting';
import { IConfig } from './setting.dto';
import { llms } from '../hook/llm';
function getConfigurationPath() {
// 如果是 vscode 插件下,则修改为 ~/.openmcp/config.json
if (VSCODE_WORKSPACE) {
// 在 VSCode 插件环境下
const homeDir = os.homedir();
const configDir = path.join(homeDir, '.openmcp');
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
return path.join(configDir, 'setting.json');
}
return 'setting.json';
}
function getDefaultLanguage() {
if (process.env.VSCODE_PID) {
// TODO: 获取 vscode 内部的语言
}
return 'zh';
}
const DEFAULT_CONFIG: IConfig = {
MODEL_INDEX: 0,
LLM_INFO: llms,
LANG: getDefaultLanguage()
};
function createConfig(): IConfig {
const configPath = getConfigurationPath();
const configDir = path.dirname(configPath);
// 确保配置目录存在
if (configDir && !fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true });
}
// 写入默认配置
fs.writeFileSync(configPath, JSON.stringify(DEFAULT_CONFIG, null, 2), 'utf-8');
return DEFAULT_CONFIG;
}
export function loadSetting(): IConfig {
const configPath = getConfigurationPath();
if (!fs.existsSync(configPath)) {
return createConfig();
}
try {
const configData = fs.readFileSync(configPath, 'utf-8');
return JSON.parse(configData) as IConfig;
} catch (error) {
console.error('Error loading config file, creating new one:', error);
return createConfig();
}
}
export function saveSetting(config: Partial<IConfig>): void {
const configPath = getConfigurationPath();
console.log('save to ' + configPath);
try {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
} catch (error) {
console.error('Error saving config file:', error);
throw error;
}
}