push 支持基本的 MCP 项目管理 to 90%

This commit is contained in:
锦恢 2025-04-23 15:29:07 +08:00
parent 4473421708
commit 6c982f1800
9 changed files with 234 additions and 52 deletions

View File

@ -133,4 +133,39 @@ export type APIRequest =
| BaseRequest | BaseRequest
| ResourcesReadRequest | ResourcesReadRequest
| PromptsGetRequest | PromptsGetRequest
| ToolCallRequest; | ToolCallRequest;
export interface IStdioConnectionItem {
type: 'stdio';
name: string;
command: string;
args: string[];
cwd?: string;
env?: { [key: string]: string };
filePath?: string;
}
export interface ISSEConnectionItem {
type: 'sse';
name: string;
url: string;
oauth?: string;
env?: { [key: string]: string };
filePath?: string;
}
export interface IStdioLaunchSignature {
type: 'stdio';
commandString: string;
cwd: string;
}
export interface ISSELaunchSignature {
type:'sse';
url: string;
oauth: string;
}
export type IConnectionItem = IStdioConnectionItem | ISSEConnectionItem;
export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature;

View File

@ -2,6 +2,8 @@ import { useMessageBridge } from '@/api/message-bridge';
import { reactive } from 'vue'; import { reactive } from 'vue';
import { pinkLog } from '../setting/util'; import { pinkLog } from '../setting/util';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { ILaunchSigature } from '@/hook/type';
import { url } from 'inspector';
export const connectionMethods = reactive({ export const connectionMethods = reactive({
current: 'STDIO', current: 'STDIO',
@ -19,6 +21,9 @@ export const connectionMethods = reactive({
export const connectionArgs = reactive({ export const connectionArgs = reactive({
commandString: '', commandString: '',
cwd: '',
env: {},
oauth: '',
urlString: '' urlString: ''
}); });
@ -96,7 +101,7 @@ export function doConnect() {
connectionType: 'STDIO', connectionType: 'STDIO',
command: command, command: command,
args: commandComponents, args: commandComponents,
clientName: 'openmcp.connect.stdio.' + command, clientName: 'openmcp.connect.stdio',
clientVersion: '0.0.1' clientVersion: '0.0.1'
} }
@ -134,17 +139,38 @@ export async function launchConnect(option: { updateCommandString?: boolean } =
} = option; } = option;
connectionMethods.current = 'STDIO'; connectionMethods.current = 'STDIO';
const bridge = useMessageBridge();
pinkLog('请求启动参数'); pinkLog('请求启动参数');
const { commandString, cwd } = await getLaunchCommand(); const connectionItem = await getLaunchSignature();
if (updateCommandString) { if (connectionItem.type === 'stdio') {
connectionArgs.commandString = commandString; if (updateCommandString) {
if (connectionArgs.commandString.length === 0) { connectionArgs.commandString = connectionItem.commandString;
return; connectionArgs.cwd = connectionItem.cwd;
connectionArgs.env = {};
if (connectionArgs.commandString.length === 0) {
return;
}
} }
await launchStdio();
} else {
if (updateCommandString) {
connectionArgs.urlString = connectionItem.url;
if (connectionArgs.urlString.length === 0) {
return;
}
}
await launchSSE();
} }
}
async function launchStdio() {
const bridge = useMessageBridge();
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
// 监听 connect // 监听 connect
@ -157,6 +183,27 @@ export async function launchConnect(option: { updateCommandString?: boolean } =
const res = await getServerVersion() as { name: string, version: string }; const res = await getServerVersion() as { name: string, version: string };
connectionResult.serverInfo.name = res.name || ''; connectionResult.serverInfo.name = res.name || '';
connectionResult.serverInfo.version = res.version || ''; connectionResult.serverInfo.version = res.version || '';
// 同步信息到 vscode
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: connectionArgs.env,
};
bridge.postMessage({
command: 'vscode/update-connection-sigature',
data: JSON.parse(JSON.stringify(clientStdioConnectionItem))
});
} else { } else {
ElMessage({ ElMessage({
type: 'error', type: 'error',
@ -176,8 +223,61 @@ export async function launchConnect(option: { updateCommandString?: boolean } =
connectionType: 'STDIO', connectionType: 'STDIO',
command: command, command: command,
args: commandComponents, args: commandComponents,
cwd: cwd, cwd: connectionArgs.cwd,
clientName: 'openmcp.connect.stdio.' + command, clientName: 'openmcp.connect.stdio',
clientVersion: '0.0.1'
};
bridge.postMessage({
command: 'connect',
data: connectOption
});
});
}
async function launchSSE() {
const bridge = useMessageBridge();
return new Promise<void>((resolve, reject) => {
// 监听 connect
bridge.addCommandListener('connect', async data => {
const { code, msg } = data;
connectionResult.success = (code === 200);
connectionResult.logString = msg;
if (code === 200) {
const res = await getServerVersion() as { name: string, version: string };
connectionResult.serverInfo.name = res.name || '';
connectionResult.serverInfo.version = res.version || '';
// 同步信息到 vscode
const clientSseConnectionItem = {
serverInfo: connectionResult.serverInfo,
connectionType: 'SSE',
name: 'openmcp.connect.sse',
url: connectionArgs.urlString,
oauth: connectionArgs.oauth,
env: connectionArgs.env
};
bridge.postMessage({
command: 'vscode/update-connection-sigature',
data: JSON.parse(JSON.stringify(clientSseConnectionItem))
});
} else {
ElMessage({
type: 'error',
message: msg
});
}
resolve(void 0);
}, { once: true });
const connectOption: MCPOptions = {
connectionType: 'SSE',
url: connectionArgs.urlString,
clientName: 'openmcp.connect.sse',
clientVersion: '0.0.1' clientVersion: '0.0.1'
}; };
@ -189,19 +289,20 @@ export async function launchConnect(option: { updateCommandString?: boolean } =
} }
function getLaunchCommand() {
return new Promise<any>((resolve, reject) => { function getLaunchSignature() {
return new Promise<ILaunchSigature>((resolve, reject) => {
// 与 vscode 进行同步 // 与 vscode 进行同步
const bridge = useMessageBridge(); const bridge = useMessageBridge();
bridge.addCommandListener('vscode/launch-command', data => { bridge.addCommandListener('vscode/launch-signature', data => {
pinkLog('收到启动参数'); pinkLog('收到启动参数');
resolve(data.msg); resolve(data.msg);
}, { once: true }); }, { once: true });
bridge.postMessage({ bridge.postMessage({
command: 'vscode/launch-command', command: 'vscode/launch-signature',
data: {} data: {}
}); });
}) })

View File

@ -3,10 +3,9 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { Implementation } from "@modelcontextprotocol/sdk/types"; import { Implementation } from "@modelcontextprotocol/sdk/types";
import { Writable, Stream } from "node:stream";
// 定义连接类型 // 定义连接类型
type ConnectionType = 'STDIO' | 'SSE'; type ConnectionType = 'STDIO' | 'SSE';
type McpTransport = StdioClientTransport | SSEClientTransport; type McpTransport = StdioClientTransport | SSEClientTransport;
export type IServerVersion = Implementation | undefined; export type IServerVersion = Implementation | undefined;

View File

@ -11,7 +11,7 @@ import { spawnSync } from 'node:child_process';
// TODO: 支持更多的 client // TODO: 支持更多的 client
let client: MCPClient | undefined = undefined; export let client: MCPClient | undefined = undefined;
function tryGetRunCommandError(command: string, args: string[] = [], cwd?: string): string | null { function tryGetRunCommandError(command: string, args: string[] = [], cwd?: string): string | null {
try { try {

View File

@ -1,3 +1,5 @@
export { messageController } from './controller'; export { messageController } from './controller';
export { VSCodeWebViewLike } from './adapter'; export { VSCodeWebViewLike } from './adapter';
export { setVscodeWorkspace } from './util'; export { setVscodeWorkspace } from './util';
// TODO: 更加规范
export { client } from './controller';

View File

@ -20,7 +20,7 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push( context.subscriptions.push(
vscode.commands.registerCommand('openmcp.sidebar.workspace-connection.revealWebviewPanel', (view: ConnectionViewItem) => { vscode.commands.registerCommand('openmcp.sidebar.workspace-connection.revealWebviewPanel', (view: ConnectionViewItem) => {
const item = view.item; const item = view.item;
revealOpenMcpWebviewPanel(context, item.name, item); revealOpenMcpWebviewPanel(context, item.filePath || item.name, item);
}) })
); );

View File

@ -10,6 +10,7 @@ export const panels = new Map<FsPath, vscode.WebviewPanel>();
export interface IStdioConnectionItem { export interface IStdioConnectionItem {
type: 'stdio'; type: 'stdio';
name: string; name: string;
version?: string;
command: string; command: string;
args: string[]; args: string[];
cwd?: string; cwd?: string;
@ -20,14 +21,31 @@ export interface IStdioConnectionItem {
export interface ISSEConnectionItem { export interface ISSEConnectionItem {
type: 'sse'; type: 'sse';
name: string; name: string;
version: string;
url: string; url: string;
oauth?: string; oauth?: string;
env?: { [key: string]: string }; env?: { [key: string]: string };
filePath?: string; filePath?: string;
} }
interface IStdioLaunchSignature {
type: 'stdio';
commandString: string;
cwd: string;
}
interface ISSELaunchSignature {
type:'sse';
url: string;
oauth: string;
}
export type IConnectionItem = IStdioConnectionItem | ISSEConnectionItem;
export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature;
export interface IConnectionConfig { export interface IConnectionConfig {
items: (IStdioConnectionItem | ISSEConnectionItem)[]; items: IConnectionItem[];
} }
export const CONNECTION_CONFIG_NAME = 'openmcp_connection.json'; export const CONNECTION_CONFIG_NAME = 'openmcp_connection.json';
@ -52,7 +70,13 @@ export function getConnectionConfig() {
} }
const rawConnectionString = fs.readFileSync(connectionConfig, 'utf-8'); const rawConnectionString = fs.readFileSync(connectionConfig, 'utf-8');
const connection = JSON.parse(rawConnectionString) as IConnectionConfig; let connection;
try {
connection = JSON.parse(rawConnectionString) as IConnectionConfig;
} catch (error) {
connection = { items: [] };
}
_connectionConfig = connection; _connectionConfig = connection;
return connection; return connection;
} }
@ -77,7 +101,13 @@ export function getWorkspaceConnectionConfig() {
} }
const rawConnectionString = fs.readFileSync(connectionConfig, 'utf-8'); const rawConnectionString = fs.readFileSync(connectionConfig, 'utf-8');
const connection = JSON.parse(rawConnectionString) as IConnectionConfig;
let connection;
try {
connection = JSON.parse(rawConnectionString) as IConnectionConfig;
} catch (error) {
connection = { items: [] };
}
const workspacePath = getWorkspacePath(); const workspacePath = getWorkspacePath();
for (const item of connection.items) { for (const item of connection.items) {
@ -119,8 +149,6 @@ export function saveWorkspaceConnectionConfig(workspace: string) {
interface ClientStdioConnectionItem { interface ClientStdioConnectionItem {
command: string; command: string;
args: string[]; args: string[];
clientName: string;
clientVersion: string;
connectionType: 'STDIO'; connectionType: 'STDIO';
cwd: string; cwd: string;
env: { [key: string]: string }; env: { [key: string]: string };
@ -128,14 +156,21 @@ interface ClientStdioConnectionItem {
interface ClientSseConnectionItem { interface ClientSseConnectionItem {
url: string; url: string;
clientName: string;
clientVersion: string;
connectionType: 'SSE'; connectionType: 'SSE';
oauth: string;
env: { [key: string]: string }; env: { [key: string]: string };
} }
export function updateWorkspaceConnectionConfig(absPath: string, data: ClientStdioConnectionItem | ClientSseConnectionItem) { interface ServerInfo {
const connectionItem = getWorkspaceConnectionConfigItemByPath(absPath); name: string;
version: string;
}
export function updateWorkspaceConnectionConfig(
absPath: string,
data: (ClientStdioConnectionItem | ClientSseConnectionItem) & { serverInfo: ServerInfo }
) {
const connectionItem = getWorkspaceConnectionConfigItemByPath(absPath);
const workspaceConnectionConfig = getWorkspaceConnectionConfig(); const workspaceConnectionConfig = getWorkspaceConnectionConfig();
// 如果存在,删除老的 connectionItem // 如果存在,删除老的 connectionItem
@ -148,8 +183,9 @@ export function updateWorkspaceConnectionConfig(absPath: string, data: ClientStd
if (data.connectionType === 'STDIO') { if (data.connectionType === 'STDIO') {
const connectionItem: IStdioConnectionItem = { const connectionItem: IStdioConnectionItem = {
type:'stdio', type: 'stdio',
name: data.clientName, name: data.serverInfo.name,
version: data.serverInfo.version,
command: data.command, command: data.command,
args: data.args, args: data.args,
cwd: data.cwd.replace(/\\/g, '/'), cwd: data.cwd.replace(/\\/g, '/'),
@ -157,6 +193,9 @@ export function updateWorkspaceConnectionConfig(absPath: string, data: ClientStd
filePath: absPath.replace(/\\/g, '/') filePath: absPath.replace(/\\/g, '/')
}; };
console.log('get connectionItem: ', connectionItem);
// 插入到第一个 // 插入到第一个
workspaceConnectionConfig.items.unshift(connectionItem); workspaceConnectionConfig.items.unshift(connectionItem);
const workspacePath = getWorkspacePath(); const workspacePath = getWorkspacePath();
@ -164,11 +203,24 @@ export function updateWorkspaceConnectionConfig(absPath: string, data: ClientStd
vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh'); vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh');
} else { } else {
const connectionItem: ISSEConnectionItem = {
type: 'sse',
name: data.serverInfo.name,
version: data.serverInfo.version,
url: data.url,
oauth: data.oauth,
filePath: absPath.replace(/\\/g, '/')
};
// 插入到第一个
workspaceConnectionConfig.items.unshift(connectionItem);
const workspacePath = getWorkspacePath();
saveWorkspaceConnectionConfig(workspacePath);
vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh');
} }
} }
function normaliseConnectionFilePath(item: IStdioConnectionItem | ISSEConnectionItem, workspace: string) { function normaliseConnectionFilePath(item: IConnectionItem, workspace: string) {
if (item.filePath) { if (item.filePath) {
if (item.filePath.startsWith('{workspace}')) { if (item.filePath.startsWith('{workspace}')) {
return item.filePath.replace('{workspace}', workspace).replace(/\\/g, '/'); return item.filePath.replace('{workspace}', workspace).replace(/\\/g, '/');

View File

@ -1,5 +1,5 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { getConnectionConfig, getWorkspaceConnectionConfig, ISSEConnectionItem, IStdioConnectionItem } from './global'; import { getConnectionConfig, getWorkspaceConnectionConfig, IConnectionItem } from './global';
class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<ConnectionViewItem> { class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<ConnectionViewItem> {
private _onDidChangeTreeData: vscode.EventEmitter<ConnectionViewItem | undefined | null | void> = new vscode.EventEmitter<ConnectionViewItem | undefined | null | void>(); private _onDidChangeTreeData: vscode.EventEmitter<ConnectionViewItem | undefined | null | void> = new vscode.EventEmitter<ConnectionViewItem | undefined | null | void>();
@ -18,7 +18,7 @@ class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<ConnectionV
const connection = getWorkspaceConnectionConfig(); const connection = getWorkspaceConnectionConfig();
const sidebarItems = connection.items.map((item, index) => { const sidebarItems = connection.items.map((item, index) => {
// 连接的名字 // 连接的名字
const itemName = this.displayName(item); const itemName = `${item.name} (${item.type})`
return new ConnectionViewItem(itemName, vscode.TreeItemCollapsibleState.None, item, 'server'); return new ConnectionViewItem(itemName, vscode.TreeItemCollapsibleState.None, item, 'server');
}) })
@ -26,14 +26,6 @@ class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<ConnectionV
return Promise.resolve(sidebarItems); return Promise.resolve(sidebarItems);
} }
public displayName(item: IStdioConnectionItem | ISSEConnectionItem) {
if (item.filePath) {
const filename = item.filePath.split('/').pop();
return `${filename} (${item.type})`;
}
return item.name;
}
// 添加 refresh 方法 // 添加 refresh 方法
public refresh(): void { public refresh(): void {
this._onDidChangeTreeData.fire(); this._onDidChangeTreeData.fire();
@ -121,7 +113,7 @@ export class ConnectionViewItem extends vscode.TreeItem {
constructor( constructor(
public readonly label: string, public readonly label: string,
public readonly collapsibleState: vscode.TreeItemCollapsibleState, public readonly collapsibleState: vscode.TreeItemCollapsibleState,
public readonly item: IStdioConnectionItem | ISSEConnectionItem, public readonly item: IConnectionItem,
public readonly icon?: string public readonly icon?: string
) { ) {
super(label, collapsibleState); super(label, collapsibleState);

View File

@ -1,7 +1,7 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as fs from 'fs'; import * as fs from 'fs';
import * as fspath from 'path'; import * as fspath from 'path';
import { getWorkspaceConnectionConfigItemByPath, ISSEConnectionItem, IStdioConnectionItem, panels, updateWorkspaceConnectionConfig } from './global'; import { IConnectionItem, ILaunchSigature, panels, updateWorkspaceConnectionConfig } from './global';
import * as OpenMCPService from '../resources/service'; import * as OpenMCPService from '../resources/service';
@ -25,11 +25,10 @@ export function getLaunchCWD(context: vscode.ExtensionContext, uri: vscode.Uri)
return workspaceFolder?.uri.fsPath || ''; return workspaceFolder?.uri.fsPath || '';
} }
export function revealOpenMcpWebviewPanel( export function revealOpenMcpWebviewPanel(
context: vscode.ExtensionContext, context: vscode.ExtensionContext,
panelKey: string, panelKey: string,
option: (IStdioConnectionItem | ISSEConnectionItem) = { option: IConnectionItem = {
type: 'stdio', type: 'stdio',
name: 'OpenMCP', name: 'OpenMCP',
command: 'mcp', command: 'mcp',
@ -68,33 +67,35 @@ export function revealOpenMcpWebviewPanel(
// 拦截消息,注入额外信息 // 拦截消息,注入额外信息
switch (command) { switch (command) {
case 'vscode/launch-command': case 'vscode/launch-signature':
const laucnResultMessage = option.type === 'stdio' ? const launchResultMessage: ILaunchSigature = option.type === 'stdio' ?
{ {
type: 'stdio', type: 'stdio',
commandString: option.command + ' ' + option.args.join(' '), commandString: option.command + ' ' + option.args.join(' '),
cwd: option.cwd cwd: option.cwd || ''
} : } :
{ {
type: 'sse', type: 'sse',
url: option.url, url: option.url,
oauth: option.oauth oauth: option.oauth || ''
}; };
const launchResult = { const launchResult = {
code: 200, code: 200,
msg: laucnResultMessage msg: launchResultMessage
}; };
panel.webview.postMessage({ panel.webview.postMessage({
command: 'vscode/launch-command', command: 'vscode/launch-signature',
data: launchResult data: launchResult
}); });
break; break;
case 'connect': case 'vscode/update-connection-sigature':
updateWorkspaceConnectionConfig(panelKey, data); updateWorkspaceConnectionConfig(panelKey, data);
break;
default: default:
OpenMCPService.messageController(command, data, panel.webview); OpenMCPService.messageController(command, data, panel.webview);
break; break;