This commit is contained in:
锦恢 2025-05-19 04:38:36 +08:00
parent 9b45c272b4
commit 23d0ce1d93
10 changed files with 444 additions and 468 deletions

View File

@ -331,6 +331,34 @@
"state": "success",
"serverName": "Huoshan DeepSeek"
}
},
{
"role": "user",
"content": "请将下面的更新日志翻译成 GitHub release 风格的英文说明,请只返回翻译后的结果,不要出现任何多余的前缀: ",
"extraInfo": {
"created": 1747598248965,
"state": "success",
"serverName": "deepseek"
}
},
{
"role": "assistant",
"content": "Heres the translation in GitHub release style:\n\n---\n\n### **New Features**\n- Added support for real-time data synchronization.\n- Introduced a new dashboard for analytics.\n\n### **Improvements**\n- Optimized query performance for large datasets.\n- Enhanced error handling for API requests.\n\n### **Bug Fixes**\n- Fixed an issue where the system crashed on invalid input.\n- Resolved a memory leak in the background service.\n\n### **Breaking Changes**\n- Removed deprecated endpoints (`/v1/old`).\n- Updated the authentication mechanism to use OAuth 2.0.\n\n### **Documentation**\n- Added detailed guides for API integration.\n- Improved inline code comments.",
"extraInfo": {
"created": 1747598260799,
"state": "success",
"serverName": "deepseek",
"usage": {
"prompt_tokens": 7946,
"completion_tokens": 136,
"total_tokens": 8082,
"prompt_tokens_details": {
"cached_tokens": 896
},
"prompt_cache_hit_tokens": 896,
"prompt_cache_miss_tokens": 7050
}
}
}
],
"settings": {

View File

@ -2,7 +2,7 @@
"name": "openmcp",
"displayName": "OpenMCP",
"description": "An all in one MCP Client/TestTool",
"version": "0.0.8",
"version": "0.1.0",
"publisher": "kirigaya",
"author": {
"name": "kirigaya",

View File

@ -19,6 +19,11 @@ interface AddCommandListenerOption {
once: boolean // 只调用一次就销毁
}
export interface ICommandRequestData {
clientId?: string;
[key: string]: any;
}
export class MessageBridge {
private ws: WebSocket | null = null;
private handlers = new Map<string, Set<CommandHandler>>();
@ -206,7 +211,7 @@ export class MessageBridge {
* @param data
* @returns
*/
public commandRequest<T = any>(command: string, data?: any): Promise<RestFulResponse<T>> {
public commandRequest<T = any>(command: string, data?: ICommandRequestData): Promise<RestFulResponse<T>> {
return new Promise<RestFulResponse>((resolve, reject) => {
this.addCommandListener(command, (data) => {
resolve(data as RestFulResponse);

View File

@ -3,6 +3,7 @@ import { pinkLog } from "@/views/setting/util";
import { debugModes, tabs } from "@/components/main-panel/panel";
import { markRaw, ref, nextTick } from "vue";
import { v4 as uuidv4 } from 'uuid';
import type { McpClient } from "@/views/connect/connection-item";
interface SaveTabItem {
name: string;
@ -12,64 +13,55 @@ interface SaveTabItem {
storage: Record<string, any>;
}
interface SaveTab {
export interface SaveTab {
tabs: SaveTabItem[]
currentIndex: number
}
export const panelLoaded = ref(false);
export function loadPanels() {
return new Promise((resolve, reject) => {
const bridge = useMessageBridge();
bridge.addCommandListener('panel/load', data => {
if (data.code !== 200) {
pinkLog('tabs 加载失败');
console.log(data.msg);
} else {
const persistTab = data.msg as SaveTab;
pinkLog('tabs 加载成功');
if (persistTab.tabs.length === 0) {
// 空的,直接返回不需要管
panelLoaded.value = true;
resolve(void 0);
return;
}
tabs.activeIndex = 0;
tabs.content = [];
for (const tab of persistTab.tabs || []) {
const component = tab.componentIndex >= 0? markRaw(debugModes[tab.componentIndex]) : undefined;
tabs.content.push({
id: uuidv4(),
name: tab.name,
icon: tab.icon,
type: tab.type,
componentIndex: tab.componentIndex,
component: component,
storage: tab.storage
});
}
tabs.activeIndex = persistTab.currentIndex;
}
panelLoaded.value = true;
resolve(void 0);
}, { once: true });
bridge.postMessage({
command: 'panel/load'
});
export async function loadPanels(client: McpClient) {
const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest<SaveTab>('panel/load', {
clientId: client.clientId
});
if (code !== 200) {
pinkLog('tabs 加载失败');
console.log(msg);
} else {
const persistTab = msg;
pinkLog('tabs 加载成功');
if (persistTab.tabs.length === 0) {
// 空的,直接返回不需要管
panelLoaded.value = true;
return;
}
tabs.activeIndex = 0;
tabs.content = [];
for (const tab of persistTab.tabs || []) {
const component = tab.componentIndex >= 0? markRaw(debugModes[tab.componentIndex]) : undefined;
tabs.content.push({
id: uuidv4(),
name: tab.name,
icon: tab.icon,
type: tab.type,
componentIndex: tab.componentIndex,
component: component,
storage: tab.storage
});
}
tabs.activeIndex = persistTab.currentIndex;
}
panelLoaded.value = true;
}
let debounceHandler: NodeJS.Timeout;

View File

@ -25,10 +25,10 @@
<span>{{ t('connect-sigature') }}</span>
<span style="width: 310px;">
<el-form :model="connectionArgs" :rules="rules" ref="urlForm">
<el-form-item prop="urlString">
<el-form-item prop="url">
<div class="input-with-label">
<span class="input-label">URL</span>
<el-input v-model="connectionArgs.urlString" placeholder="http://"></el-input>
<el-input v-model="connectionArgs.url" placeholder="http://"></el-input>
</div>
</el-form-item>
<el-form-item prop="oauth">
@ -65,7 +65,7 @@ const rules = reactive<FormRules>({
oauth: [
{ required: false, trigger: 'blur' }
],
urlString: [
url: [
{ required: true, message: 'URL不能为空', trigger: 'blur' }
]
})

View File

@ -1,11 +1,10 @@
import { useMessageBridge } from "@/api/message-bridge";
import { reactive, type Reactive } from "vue";
import type { IConnectionResult, ConnectionTypeOptionItem, IConnectionArgs, IConnectionEnvironment, McpOptions } from "./type";
import { ElMessage } from "element-plus";
import { loadPanels, type SaveTab } from "@/hook/panel";
import { getPlatform } from "@/api/platform";
export type ConnectionType = 'STDIO' | 'SSE' | 'STREAMABLE_HTTP';
export interface ConnectionTypeOptionItem {
value: ConnectionType;
label: string;
}
export const connectionSelectDataViewOption: ConnectionTypeOptionItem[] = [
{
@ -22,37 +21,266 @@ export const connectionSelectDataViewOption: ConnectionTypeOptionItem[] = [
}
]
export interface IConnectionArgs {
type: ConnectionType;
commandString?: string;
cwd?: string;
urlString?: string;
export async function getLaunchSignature(platform: string): Promise<IConnectionArgs[]> {
const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest(platform + '/launch-signature');
if (code !== 200) {
const message = msg.toString();
ElMessage.error(message);
return [];
}
// 判断一下版本,新版本的 msg 应该是数组,老版本是对象
// 返回的数组的第一个为主节点,其余为从节点
if (Array.isArray(msg)) {
return msg;
}
return [msg];
}
export class McpClient {
public clientId?: string;
public name?: string;
public version?: string;
public connectionArgs: Reactive<IConnectionArgs>;
constructor() {
export class McpClient {
public connectionArgs: Reactive<IConnectionArgs>;
public connectionResult: Reactive<IConnectionResult>;
public presetsEnvironment: string[] = ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER'];
public connectionEnvironment: Reactive<IConnectionEnvironment>;
constructor(
public clientVersion: string = '0.0.1',
public clientNamePrefix: string = 'openmcp.connect'
) {
// 连接入参
this.connectionArgs = reactive({
type: 'STDIO',
commandString: '',
cwd: '',
urlString: ''
url: '',
oauth: ''
});
// 连接出参
this.connectionResult = reactive({
success: false,
status: 'disconnected',
clientId: '',
name: '',
version: '',
logString: []
});
// 环境变量
this.connectionEnvironment = reactive({
data: [],
newKey: '',
newValue: ''
});
}
async connect() {
async acquireConnectionSignature(args: IConnectionArgs) {
this.connectionArgs.type = args.type;
this.connectionArgs.commandString = args.commandString || '';
this.connectionArgs.cwd = args.cwd || '';
this.connectionArgs.url = args.url || '';
this.connectionArgs.oauth = args.oauth || '';
}
get clientId() {
return this.connectionResult.clientId;
}
get name() {
return this.connectionResult.name;
}
get version() {
return this.connectionResult.version;
}
get status() {
return this.connectionResult.status;
}
get connected() {
return this.connectionResult.success;
}
get env() {
const env = {} as Record<string, string>;
this.connectionEnvironment.data.forEach(item => {
env[item.key] = item.value;
});
return env;
}
private get commandAndArgs() {
const commandString = this.connectionArgs.commandString;
if (!commandString) {
return { command: '', args: [] };
}
const args = commandString.split(' ');
const command = args.shift() || '';
return { command, args };
}
get connectOption() {
const { command, args } = this.commandAndArgs;
const env = this.env;
const url = this.connectionArgs.url;
const oauth = this.connectionArgs.oauth;
const connectionType = this.connectionArgs.type;
const clientName = this.clientNamePrefix + '.' + this.connectionArgs.type;
const clientVersion = this.clientVersion;
const option: McpOptions = {
connectionType,
command,
args,
url,
oauth,
clientName,
clientVersion,
env,
serverInfo: {
name: this.connectionResult.name,
version: this.connectionResult.version
}
};
return option;
}
public async connect(platform: string) {
const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest<IConnectionResult>('connect', this.connectOption);
this.connectionResult.success = (code === 200);
if (code !== 200) {
const message = msg.toString();
this.connectionResult.logString.push({
type: 'error',
message
});
ElMessage.error(message);
return;
}
this.connectionResult.status = msg.status;
this.connectionResult.clientId = msg.clientId;
this.connectionResult.name = msg.name;
this.connectionResult.version = msg.version;
// 同步成功的连接参数到后端,更新 vscode treeview 中的列表
const deserializeOption = JSON.parse(JSON.stringify(this.connectOption));
bridge.postMessage({
command: platform + '/update-connection-sigature',
data: deserializeOption
});
}
/**
* @description
* -
* -
* @param enabled
*/
public async handleEnvSwitch(enabled: boolean) {
const presetVars = this.presetsEnvironment;
if (enabled) {
const values = await this.lookupEnvVar(presetVars);
if (values) {
// 将 key values 合并进 connectionEnv.data 中
// 若已有相同的 key, 则替换 value
for (let i = 0; i < presetVars.length; i++) {
const key = presetVars[i];
const value = values[i];
const sameNameItems = this.connectionEnvironment.data.filter(item => item.key === key);
if (sameNameItems.length > 0) {
const conflictItem = sameNameItems[0];
conflictItem.value = value;
} else {
this.connectionEnvironment.data.push({
key: key, value: value
});
}
}
}
} else {
// 清空 connectionEnv.data 中所有 key 为 presetVars 的项
const reserveItems = this.connectionEnvironment.data.filter(item => !presetVars.includes(item.key));
this.connectionEnvironment.data = reserveItems;
}
}
/**
* @description
* @param varNames
* @returns
*/
public async lookupEnvVar(varNames: string[]) {
const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest('lookup-env-var', { keys: varNames });
if (code === 200) {
this.connectionResult.logString.push({
type: 'info',
message: '预设环境变量同步完成'
});
return msg;
} else {
this.connectionResult.logString.push({
type: 'error',
message: '预设环境变量同步失败: ' + msg
});
}
}
}
// 用于描述一个连接的数据结构
export interface McpServer {
type: ConnectionType;
clientId: string;
name: string;
class McpClientAdapter {
public clients: McpClient[] = [];
constructor(
public platform: string
) {}
public async launch() {
const launchSignature = await getLaunchSignature(this.platform);
for (const item of launchSignature) {
const client = new McpClient();
// 同步连接参数
await client.acquireConnectionSignature(item);
// 同步环境变量
await client.handleEnvSwitch(true);
// 连接
await client.connect(this.platform);
this.clients.push(client);
}
}
public async loadPanels() {
const masterNode = this.clients[0];
await loadPanels(masterNode);
}
}
const platform = getPlatform();
export const mcpClientAdapter = new McpClientAdapter(platform);

View File

@ -1,386 +0,0 @@
import { useMessageBridge } from '@/api/message-bridge';
import { reactive, ref } from 'vue';
import { pinkLog } from '../setting/util';
import { ElLoading, ElMessage } from 'element-plus';
import { getPlatform, type OpenMcpSupportPlatform } from '@/api/platform';
import { getTour, loadSetting } from '@/hook/setting';
import { loadPanels } from '@/hook/panel';
import type { ConnectionType } from './connection-item';
export const connectionMethods = reactive<{
current: ConnectionType,
data: {
value: ConnectionType,
label: string
}[]
}>({
current: 'STDIO',
data: [
{
value: 'STDIO',
label: 'STDIO'
},
{
value: 'SSE',
label: 'SSE'
},
{
value: 'STREAMABLE_HTTP',
label: 'STREAMABLE_HTTP'
}
]
});
export const connectionSettingRef = ref<any>(null);
export const connectionLogRef = ref<any>(null);
// 主 mcp 服务器的连接参数
export const connectionArgs = reactive({
commandString: '',
cwd: '',
oauth: '',
urlString: ''
});
export interface EnvItem {
key: string
value: string
}
export interface IConnectionEnv {
data: EnvItem[]
newKey: string
newValue: string
}
export interface ConnectionResult {
status: string
clientId: string
name: string
version: string
}
export const connectionEnv = reactive<IConnectionEnv>({
data: [],
newKey: '',
newValue: ''
});
export function makeEnv() {
const env = {} as Record<string, string>;
connectionEnv.data.forEach(item => {
env[item.key] = item.value;
});
return env;
}
// 定义命令行参数接口
export interface McpOptions {
connectionType: ConnectionType;
// STDIO 特定选项
command?: string;
args?: string[];
cwd?: string;
env?: Record<string, string>;
// SSE 特定选项
url?: string;
// 通用客户端选项
clientName?: string;
clientVersion?: string;
}
/**
* @description mcp
* 1.
* 2. mcp
* 3.
* @param option
* @returns
*/
export async function doConnect(
option: {
namespace: OpenMcpSupportPlatform
updateCommandString?: boolean
}
) {
const {
// updateCommandString 为 true 代表是初始化阶段
namespace,
updateCommandString = true
} = option;
// 如果是初始化,则需要请求启动参数
if (updateCommandString) {
pinkLog('请求启动参数');
const connectionItem = await getLaunchSignature(namespace + '/launch-signature');
connectionMethods.current = connectionItem.type;
connectionArgs.commandString = connectionItem.commandString || '';
connectionArgs.cwd = connectionItem.cwd || '';
connectionArgs.oauth = connectionItem.oauth || '';
connectionArgs.urlString = connectionItem.url || '';
}
if (connectionMethods.current === 'STDIO') {
return await launchStdio(namespace);
} else {
return await launchRemote(namespace);
}
}
async function launchStdio(namespace: string) {
const bridge = useMessageBridge();
const env = makeEnv();
const commandComponents = connectionArgs.commandString.split(/\s+/g);
const command = commandComponents[0];
commandComponents.shift();
const connectOption = {
connectionType: 'STDIO',
command: command,
args: commandComponents,
cwd: connectionArgs.cwd,
clientName: 'openmcp.connect.STDIO',
clientVersion: '0.0.1',
env
};
const { code, msg } = await bridge.commandRequest<ConnectionResult>('connect', connectOption);
connectionResult.success = (code === 200);
if (code === 200) {
const message = `connect to ${msg.name} ${msg.version} success, clientId: ${msg.clientId}`;
connectionResult.logString.push({ type: 'info', message });
connectionResult.serverInfo.name = msg.name || '';
connectionResult.serverInfo.version = msg.version || '';
connectionResult.clientId = msg.clientId || '';
// 同步信息到 后端
const commandComponents = connectionArgs.commandString.split(/\s+/g);
const command = commandComponents[0];
commandComponents.shift();
const clientStdioConnectionItem = {
serverInfo: connectionResult.serverInfo,
connectionType: 'STDIO',
name: 'openmcp.connect.STDIO',
command: command,
args: commandComponents,
cwd: connectionArgs.cwd,
env
};
bridge.postMessage({
command: namespace + '/update-connection-sigature',
data: JSON.parse(JSON.stringify(clientStdioConnectionItem))
});
} else {
const messaage = msg.toString();
connectionResult.logString.push({
type: 'error',
message: messaage
});
ElMessage.error(messaage);
}
}
async function launchRemote(namespace: string) {
const bridge = useMessageBridge();
const env = makeEnv();
const connectOption: McpOptions = {
connectionType: connectionMethods.current,
url: connectionArgs.urlString,
clientName: 'openmcp.connect.' + connectionMethods.current,
clientVersion: '0.0.1',
env
};
const { code, msg } = await bridge.commandRequest<ConnectionResult>('connect', connectOption);
connectionResult.success = (code === 200);
if (code === 200) {
const message = `connect to ${msg.name} ${msg.version} success, clientId: ${msg.clientId}`;
connectionResult.logString.push({
type: 'info',
message: message
});
connectionResult.serverInfo.name = msg.name || '';
connectionResult.serverInfo.version = msg.version || '';
connectionResult.clientId = msg.clientId || '';
// 同步信息到 vscode
const clientSseConnectionItem = {
serverInfo: connectionResult.serverInfo,
connectionType: connectionMethods.current,
name: 'openmcp.connect.' + connectionMethods.current,
url: connectionArgs.urlString,
oauth: connectionArgs.oauth,
env: env
};
bridge.postMessage({
command: namespace + '/update-connection-sigature',
data: JSON.parse(JSON.stringify(clientSseConnectionItem))
});
} else {
const message = msg.toString();
connectionResult.logString.push({
type: 'error',
message: message
});
ElMessage.error(message);
}
}
async function getLaunchSignature(signatureName: string) {
const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest(signatureName);
return msg;
}
export function doReconnect() {
// TODO: finish this
console.log();
}
export const connectionResult = reactive<{
success: boolean,
logString: { type: 'info' | 'error' | 'warning', message: string }[],
serverInfo: {
name: string,
version: string
},
clientId: string
}>({
success: false,
logString: [],
serverInfo: {
name: '',
version: ''
},
clientId: ''
});
export const envVarStatus = {
launched: false
};
function lookupEnvVar(varNames: string[]) {
const bridge = useMessageBridge();
return new Promise<string[] | undefined>((resolve, reject) => {
bridge.addCommandListener('lookup-env-var', data => {
const { code, msg } = data;
if (code === 200) {
connectionResult.logString.push({
type: 'info',
message: '预设环境变量同步完成'
});
resolve(msg);
} else {
connectionResult.logString.push({
type: 'error',
message: '预设环境变量同步失败: ' + msg
});
resolve(undefined);
}
}, { once: true });
console.log(varNames);
bridge.postMessage({
command: 'lookup-env-var',
data: {
keys: varNames
}
})
});
}
export async function handleEnvSwitch(enabled: boolean) {
const presetVars = ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER'];
if (enabled) {
const values = await lookupEnvVar(presetVars);
if (values) {
// 将 key values 合并进 connectionEnv.data 中
// 若已有相同的 key, 则替换 value
for (let i = 0; i < presetVars.length; i++) {
const key = presetVars[i];
const value = values[i];
const sameNameItems = connectionEnv.data.filter(item => item.key === key);
if (sameNameItems.length > 0) {
const conflictItem = sameNameItems[0];
conflictItem.value = value;
} else {
connectionEnv.data.push({
key: key, value: value
});
}
}
}
} else {
// 清空 connectionEnv.data 中所有 key 为 presetVars 的项
const reserveItems = connectionEnv.data.filter(item => !presetVars.includes(item.key));
connectionEnv.data = reserveItems;
}
}
export async function loadEnvVar() {
return await handleEnvSwitch(true);
}
export async function initialise() {
pinkLog('准备请求设置');
const loading = ElLoading.service({
fullscreen: true,
lock: true,
text: 'Loading',
background: 'rgba(0, 0, 0, 0.7)'
});
const platform = getPlatform();
// 加载全局设置
loadSetting();
// 设置环境变量
await loadEnvVar();
// 获取引导状态
await getTour();
// 尝试进行初始化连接
await doConnect({
namespace: platform,
updateCommandString: true
});
// loading panels
await loadPanels();
loading.close();
}

View File

@ -44,7 +44,6 @@
<script setup lang="ts">
import { defineComponent, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { connectionEnv, type EnvItem, handleEnvSwitch } from './connection';
defineComponent({ name: 'env-var' });

View File

@ -0,0 +1,30 @@
import { getTour, loadSetting } from "@/hook/setting";
import { ElLoading } from "element-plus";
import { pinkLog } from "../setting/util";
import { mcpClientAdapter } from "./connection-item";
export async function initialise() {
pinkLog('准备请求设置');
const loading = ElLoading.service({
fullscreen: true,
lock: true,
text: 'Loading',
background: 'rgba(0, 0, 0, 0.7)'
});
// 加载全局设置
loadSetting();
// 获取引导状态
await getTour();
// 尝试进行初始化连接
await mcpClientAdapter.launch();
// loading panels
await mcpClientAdapter.loadPanels();
loading.close();
}

View File

@ -0,0 +1,80 @@
export type ConnectionType = 'STDIO' | 'SSE' | 'STREAMABLE_HTTP';
export interface ConnectionTypeOptionItem {
value: ConnectionType;
label: string;
}
export interface IConnectionArgs {
type: ConnectionType;
commandString?: string;
cwd?: string;
url?: string;
oauth?: string;
}
export interface IConnectionResult {
success: boolean;
status: string
clientId: string
name: string
version: string
logString: {
type: 'info' | 'error' | 'warning',
message: string
}[]
}
export interface McpOptions {
connectionType: ConnectionType;
command?: string;
// STDIO 特定选项
args?: string[];
cwd?: string;
env?: Record<string, string>;
// SSE 特定选项
url?: string;
oauth?: any;
// 通用客户端选项
clientName?: string;
clientVersion?: string;
serverInfo: {
name: string
version: string
}
}
export interface EnvItem {
key: string
value: string
}
export interface IConnectionEnvironment {
data: EnvItem[]
newKey: string
newValue: string
}
export interface IConnectionArgs {
type: ConnectionType;
commandString?: string;
cwd?: string;
url?: string;
oauth?: string;
}
export interface ConnectionResult {
status: string
clientId: string
name: string
version: string
}