0.1.0 完成 vscode 插件端的改造

This commit is contained in:
锦恢 2025-05-22 02:56:55 +08:00
parent f37b8babcd
commit df8b4df2c0
19 changed files with 336 additions and 303 deletions

View File

@ -1,5 +1,6 @@
import { pinkLog, redLog } from '@/views/setting/util'; import { pinkLog, redLog } from '@/views/setting/util';
import { acquireVsCodeApi, electronApi, getPlatform } from './platform'; import { acquireVsCodeApi, electronApi, getPlatform } from './platform';
import { isReactive } from 'vue';
export interface VSCodeMessage { export interface VSCodeMessage {
command: string; command: string;
@ -208,6 +209,21 @@ export class MessageBridge {
return () => commandHandlers.delete(wrapperCommandHandler); return () => commandHandlers.delete(wrapperCommandHandler);
} }
private deserializeReactiveData(data: any) {
if (isReactive(data)) {
return JSON.parse(JSON.stringify(data));
}
// 只对第一层进行遍历
for (const key in data) {
if (isReactive(data[key])) {
data[key] = JSON.parse(JSON.stringify(data[key]));
}
}
return data;
}
/** /**
* @description do as axios does * @description do as axios does
* @param command * @param command
@ -215,6 +231,7 @@ export class MessageBridge {
* @returns * @returns
*/ */
public commandRequest<T = any>(command: string, data?: ICommandRequestData): Promise<RestFulResponse<T>> { public commandRequest<T = any>(command: string, data?: ICommandRequestData): Promise<RestFulResponse<T>> {
return new Promise<RestFulResponse>((resolve, reject) => { return new Promise<RestFulResponse>((resolve, reject) => {
this.addCommandListener(command, (data) => { this.addCommandListener(command, (data) => {
resolve(data as RestFulResponse); resolve(data as RestFulResponse);
@ -222,7 +239,7 @@ export class MessageBridge {
this.postMessage({ this.postMessage({
command, command,
data data: this.deserializeReactiveData(data)
}); });
}); });
} }

View File

@ -22,8 +22,7 @@ export function getSystemPrompt(name: string) {
export async function saveSystemPrompts() { export async function saveSystemPrompts() {
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const payload = JSON.parse(JSON.stringify(systemPrompts.value)); const res = await bridge.commandRequest('system-prompts/save', { prompts: systemPrompts.value });
const res = await bridge.commandRequest('system-prompts/save', { prompts: payload });
if (res.code === 200) { if (res.code === 200) {
pinkLog('system prompt 保存成功'); pinkLog('system prompt 保存成功');
} }

View File

@ -84,7 +84,11 @@ const handleKeydown = (event: KeyboardEvent) => {
const copy = async () => { const copy = async () => {
try { try {
if (navigator.clipboard) { if (navigator.clipboard) {
await navigator.clipboard.writeText(userInput.value); await navigator.clipboard.write([
new ClipboardItem({
'text/plain': new Blob([userInput.value], { type: 'text/plain' })
})
]);
} else { } else {
const textarea = document.createElement('textarea'); const textarea = document.createElement('textarea');
textarea.value = userInput.value; textarea.value = userInput.value;

View File

@ -145,7 +145,6 @@ async function connect() {
} }
.connect-action { .connect-action {
margin-top: 20px;
padding: 10px; padding: 10px;
} }
</style> </style>

View File

@ -367,7 +367,9 @@ export class McpClient {
*/ */
public async lookupEnvVar(varNames: string[]) { public async lookupEnvVar(varNames: string[]) {
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest('lookup-env-var', { keys: varNames }); const { code, msg } = await bridge.commandRequest('lookup-env-var', {
keys: varNames
});
if (code === 200) { if (code === 200) {
this.connectionResult.logString.push({ this.connectionResult.logString.push({

View File

@ -20,6 +20,7 @@ export function createTest(call: ToolCall) {
tab.name = t("tools"); tab.name = t("tools");
const storage: ToolStorage = { const storage: ToolStorage = {
activeNames: [0],
currentToolName: call.function.name, currentToolName: call.function.name,
formData: JSON.parse(call.function.arguments) formData: JSON.parse(call.function.arguments)
}; };

View File

@ -2,4 +2,4 @@ export { routeMessage } from './common/router';
export { VSCodeWebViewLike, TaskLoopAdapter } from './hook/adapter'; export { VSCodeWebViewLike, TaskLoopAdapter } from './hook/adapter';
export { setVscodeWorkspace, setRunningCWD } from './hook/setting'; export { setVscodeWorkspace, setRunningCWD } from './hook/setting';
// TODO: 更加规范 // TODO: 更加规范
export { client } from './mcp/connect.service'; export { clientMap } from './mcp/connect.service';

View File

@ -38,7 +38,7 @@ function refreshConnectionOption(envPath: string) {
fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4)); fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4));
return { data: [ defaultOption ] }; return { items: [ defaultOption ] };
} }
function acquireConnectionOption() { function acquireConnectionOption() {
@ -51,16 +51,16 @@ function acquireConnectionOption() {
try { try {
const option = JSON.parse(fs.readFileSync(envPath, 'utf-8')); const option = JSON.parse(fs.readFileSync(envPath, 'utf-8'));
if (!option.data) { if (!option.items) {
return refreshConnectionOption(envPath); return refreshConnectionOption(envPath);
} }
if (option.data && option.data.length === 0) { if (option.items && option.items.length === 0) {
return refreshConnectionOption(envPath); return refreshConnectionOption(envPath);
} }
// 按照前端的规范,整理成 commandString 样式 // 按照前端的规范,整理成 commandString 样式
option.data = option.data.map((item: any) => { option.items = option.items.map((item: any) => {
if (item.connectionType === 'STDIO') { if (item.connectionType === 'STDIO') {
item.commandString = [item.command, ...item.args]?.join(' '); item.commandString = [item.command, ...item.args]?.join(' ');
} else { } else {
@ -80,7 +80,7 @@ function acquireConnectionOption() {
function updateConnectionOption(data: any) { function updateConnectionOption(data: any) {
const envPath = path.join(__dirname, '..', '.env'); const envPath = path.join(__dirname, '..', '.env');
const connection = { data }; const connection = { items: data };
fs.writeFileSync(envPath, JSON.stringify(connection, null, 4)); fs.writeFileSync(envPath, JSON.stringify(connection, null, 4));
} }
@ -117,7 +117,7 @@ wss.on('connection', (ws: any) => {
case 'web/launch-signature': case 'web/launch-signature':
const launchResult = { const launchResult = {
code: 200, code: 200,
msg: option.data msg: option.items
}; };
webview.postMessage({ webview.postMessage({

View File

@ -16,8 +16,22 @@ export class ConnectController {
const { keys } = data; const { keys } = data;
const values = keys.map((key: string) => { const values = keys.map((key: string) => {
// TODO: 在 Windows 上测试 // TODO: 在 Windows 上测试
if (process.platform === 'win32' && key.toLowerCase() === 'path') { console.log(key);
key = 'Path'; // 确保正确匹配环境变量的 ke console.log(process.env);
if (process.platform === 'win32') {
switch (key) {
case 'USER':
return process.env.USERNAME || '';
case 'HOME':
return process.env.USERPROFILE || process.env.HOME;
case 'LOGNAME':
return process.env.USERNAME || '';
case 'SHELL':
return process.env.SHELL || process.env.COMSPEC;
case 'TERM':
return process.env.TERM || '未设置 (Windows 默认终端)';
}
} }
return process.env[key] || ''; return process.env[key] || '';

View File

@ -5,7 +5,7 @@ import { createOcrWorker, saveBase64ImageData } from "./ocr.service";
export class OcrController { export class OcrController {
@Controller('ocr/get-ocr-image') @Controller('ocr/get-ocr-image')
async getOcrImage(client: RequestClientType, data: any, webview: PostMessageble) { async getOcrImage(data: any, webview: PostMessageble) {
const { filename } = data; const { filename } = data;
const buffer = diskStorage.getSync(filename); const buffer = diskStorage.getSync(filename);
const base64String = buffer ? buffer.toString('base64'): undefined; const base64String = buffer ? buffer.toString('base64'): undefined;
@ -18,7 +18,7 @@ export class OcrController {
} }
@Controller('ocr/start-ocr') @Controller('ocr/start-ocr')
async startOcr(client: RequestClientType, data: any, webview: PostMessageble) { async startOcr(data: any, webview: PostMessageble) {
const { base64String, mimeType } = data; const { base64String, mimeType } = data;
const filename = saveBase64ImageData(base64String, mimeType); const filename = saveBase64ImageData(base64String, mimeType);

View File

@ -39,7 +39,7 @@ function refreshConnectionOption(envPath: string) {
fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4)); fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4));
return { data: [defaultOption] }; return { items: [defaultOption] };
} }
function acquireConnectionOption() { function acquireConnectionOption() {
@ -52,16 +52,16 @@ function acquireConnectionOption() {
try { try {
const option = JSON.parse(fs.readFileSync(envPath, 'utf-8')); const option = JSON.parse(fs.readFileSync(envPath, 'utf-8'));
if (!option.data) { if (!option.items) {
return refreshConnectionOption(envPath); return refreshConnectionOption(envPath);
} }
if (option.data && option.data.length === 0) { if (option.items && option.items.length === 0) {
return refreshConnectionOption(envPath); return refreshConnectionOption(envPath);
} }
// 按照前端的规范,整理成 commandString 样式 // 按照前端的规范,整理成 commandString 样式
option.data = option.data.map((item: any) => { option.items = option.items.map((item: any) => {
if (item.connectionType === 'STDIO') { if (item.connectionType === 'STDIO') {
item.commandString = [item.command, ...item.args]?.join(' '); item.commandString = [item.command, ...item.args]?.join(' ');
} else { } else {
@ -88,7 +88,7 @@ const authPassword = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '.env
function updateConnectionOption(data: any) { function updateConnectionOption(data: any) {
const envPath = path.join(__dirname, '..', '.env'); const envPath = path.join(__dirname, '..', '.env');
const connection = { data }; const connection = { items: data };
fs.writeFileSync(envPath, JSON.stringify(connection, null, 4)); fs.writeFileSync(envPath, JSON.stringify(connection, null, 4));
} }
@ -147,7 +147,7 @@ wss.on('connection', (ws: any) => {
case 'web/launch-signature': case 'web/launch-signature':
const launchResult = { const launchResult = {
code: 200, code: 200,
msg: option.data msg: option.items
}; };
webview.postMessage({ webview.postMessage({

View File

@ -6,47 +6,43 @@ import * as fs from 'fs';
export type FsPath = string; export type FsPath = string;
export const panels = new Map<FsPath, vscode.WebviewPanel>(); export const panels = new Map<FsPath, vscode.WebviewPanel>();
export interface IStdioConnectionItem {
type: 'STDIO';
name: string;
version?: string;
command: string;
args: string[];
cwd?: string;
env?: { [key: string]: string };
filePath?: string;
}
export interface ISSEConnectionItem {
type: 'SSE';
name: string;
version: string;
url: string;
oauth?: string;
env?: { [key: string]: 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: IConnectionItem[]; items: (McpOptions[] | McpOptions)[];
} }
export type ConnectionType = 'STDIO' | 'SSE' | 'STREAMABLE_HTTP';
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
}
// vscode 专用
filePath?: string;
name?: string;
version?: string;
type?: ConnectionType;
[key: string]: any;
}
export const CONNECTION_CONFIG_NAME = 'openmcp_connection.json'; export const CONNECTION_CONFIG_NAME = 'openmcp_connection.json';
let _connectionConfig: IConnectionConfig | undefined; let _connectionConfig: IConnectionConfig | undefined;
@ -119,11 +115,14 @@ export function getWorkspaceConnectionConfig() {
} }
const workspacePath = getWorkspacePath(); const workspacePath = getWorkspacePath();
for (const item of connection.items) { for (let item of connection.items) {
item = Array.isArray(item) ? item[0] : item;
const itemType = item.type || item.connectionType;
if (item.filePath && item.filePath.startsWith('{workspace}')) { if (item.filePath && item.filePath.startsWith('{workspace}')) {
item.filePath = item.filePath.replace('{workspace}', workspacePath).replace(/\\/g, '/'); item.filePath = item.filePath.replace('{workspace}', workspacePath).replace(/\\/g, '/');
} }
if (item.type === 'STDIO' && item.cwd && item.cwd.startsWith('{workspace}')) { if (itemType === 'STDIO' && item.cwd && item.cwd.startsWith('{workspace}')) {
item.cwd = item.cwd.replace('{workspace}', workspacePath).replace(/\\/g, '/'); item.cwd = item.cwd.replace('{workspace}', workspacePath).replace(/\\/g, '/');
} }
} }
@ -165,40 +164,26 @@ export function saveWorkspaceConnectionConfig(workspace: string) {
const connectionConfigPath = fspath.join(configDir, CONNECTION_CONFIG_NAME); const connectionConfigPath = fspath.join(configDir, CONNECTION_CONFIG_NAME);
const workspacePath = getWorkspacePath(); const workspacePath = getWorkspacePath();
for (const item of connectionConfig.items) { for (let item of connectionConfig.items) {
item = Array.isArray(item) ? item[0] : item;
const itemType = item.type || item.connectionType;
item.type = undefined;
if (item.filePath && item.filePath.replace(/\\/g, '/').startsWith(workspacePath)) { if (item.filePath && item.filePath.replace(/\\/g, '/').startsWith(workspacePath)) {
item.filePath = item.filePath.replace(workspacePath, '{workspace}').replace(/\\/g, '/'); item.filePath = item.filePath.replace(workspacePath, '{workspace}').replace(/\\/g, '/');
} }
if (item.type ==='STDIO' && item.cwd && item.cwd.replace(/\\/g, '/').startsWith(workspacePath)) { if (item.type === 'STDIO' && item.cwd && item.cwd.replace(/\\/g, '/').startsWith(workspacePath)) {
item.cwd = item.cwd.replace(workspacePath, '{workspace}').replace(/\\/g, '/'); item.cwd = item.cwd.replace(workspacePath, '{workspace}').replace(/\\/g, '/');
} }
} }
fs.writeFileSync(connectionConfigPath, JSON.stringify(connectionConfig, null, 2), 'utf-8'); fs.writeFileSync(connectionConfigPath, JSON.stringify(connectionConfig, null, 2), 'utf-8');
} }
interface ClientStdioConnectionItem {
command: string;
args: string[];
connectionType: 'STDIO';
cwd: string;
env: { [key: string]: string };
}
interface ClientSseConnectionItem {
url: string;
connectionType: 'SSE';
oauth: string;
env: { [key: string]: string };
}
interface ServerInfo {
name: string;
version: string;
}
export function updateWorkspaceConnectionConfig( export function updateWorkspaceConnectionConfig(
absPath: string, absPath: string,
data: (ClientStdioConnectionItem | ClientSseConnectionItem) & { serverInfo: ServerInfo } data: McpOptions[]
) { ) {
const connectionItem = getWorkspaceConnectionConfigItemByPath(absPath); const connectionItem = getWorkspaceConnectionConfigItemByPath(absPath);
const workspaceConnectionConfig = getWorkspaceConnectionConfig(); const workspaceConnectionConfig = getWorkspaceConnectionConfig();
@ -211,48 +196,29 @@ export function updateWorkspaceConnectionConfig(
} }
} }
if (data.connectionType === 'STDIO') { // 对于第一个 item 添加 filePath
const connectionItem: IStdioConnectionItem = { // 对路径进行标准化
type: 'STDIO', data.forEach(item => {
name: data.serverInfo.name, item.filePath = absPath.replace(/\\/g, '/');
version: data.serverInfo.version, item.cwd = item.cwd?.replace(/\\/g, '/');
command: data.command, item.name = item.serverInfo?.name;
args: data.args, item.version = item.serverInfo?.version;
cwd: data.cwd.replace(/\\/g, '/'), item.type = undefined;
env: data.env, });
filePath: absPath.replace(/\\/g, '/')
};
console.log('get connectionItem: ', connectionItem); console.log('get connectionItem: ', data);
// 插入到第一个
workspaceConnectionConfig.items.unshift(data);
const workspacePath = getWorkspacePath();
saveWorkspaceConnectionConfig(workspacePath);
vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh');
// 插入到第一个
workspaceConnectionConfig.items.unshift(connectionItem);
const workspacePath = getWorkspacePath();
saveWorkspaceConnectionConfig(workspacePath);
vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh');
} 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');
}
} }
export function updateInstalledConnectionConfig( export function updateInstalledConnectionConfig(
absPath: string, absPath: string,
data: (ClientStdioConnectionItem | ClientSseConnectionItem) & { serverInfo: ServerInfo } data: McpOptions[]
) { ) {
const connectionItem = getInstalledConnectionConfigItemByPath(absPath); const connectionItem = getInstalledConnectionConfigItemByPath(absPath);
const installedConnectionConfig = getConnectionConfig(); const installedConnectionConfig = getConnectionConfig();
@ -265,45 +231,26 @@ export function updateInstalledConnectionConfig(
} }
} }
if (data.connectionType === 'STDIO') { // 对于第一个 item 添加 filePath
const connectionItem: IStdioConnectionItem = { // 对路径进行标准化
type: 'STDIO', data.forEach(item => {
name: data.serverInfo.name, item.filePath = absPath.replace(/\\/g, '/');
version: data.serverInfo.version, item.cwd = item.cwd?.replace(/\\/g, '/');
command: data.command, item.name = item.serverInfo?.name;
args: data.args, item.version = item.serverInfo?.version;
cwd: data.cwd.replace(/\\/g, '/'), item.type = undefined;
env: data.env, });
filePath: absPath.replace(/\\/g, '/')
};
console.log('get connectionItem: ', connectionItem); console.log('get connectionItem: ', data);
// 插入到第一个
// 插入到第一个 installedConnectionConfig.items.unshift(data);
installedConnectionConfig.items.unshift(connectionItem); saveConnectionConfig();
saveConnectionConfig(); vscode.commands.executeCommand('openmcp.sidebar.installed-connection.refresh');
vscode.commands.executeCommand('openmcp.sidebar.installed-connection.refresh');
} else {
const connectionItem: ISSEConnectionItem = {
type: 'SSE',
name: data.serverInfo.name,
version: data.serverInfo.version,
url: data.url,
oauth: data.oauth,
filePath: absPath.replace(/\\/g, '/')
};
// 插入到第一个
installedConnectionConfig.items.unshift(connectionItem);
saveConnectionConfig();
vscode.commands.executeCommand('openmcp.sidebar.installed-connection.refresh');
}
} }
function normaliseConnectionFilePath(item: IConnectionItem, workspace: string) { function normaliseConnectionFilePath(item: McpOptions, 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, '/');
@ -329,7 +276,9 @@ export function getWorkspaceConnectionConfigItemByPath(absPath: string) {
const workspaceConnectionConfig = getWorkspaceConnectionConfig(); const workspaceConnectionConfig = getWorkspaceConnectionConfig();
const normaliseAbsPath = absPath.replace(/\\/g, '/'); const normaliseAbsPath = absPath.replace(/\\/g, '/');
for (const item of workspaceConnectionConfig.items) { for (let item of workspaceConnectionConfig.items) {
item = Array.isArray(item)? item[0] : item;
const filePath = normaliseConnectionFilePath(item, workspacePath); const filePath = normaliseConnectionFilePath(item, workspacePath);
if (filePath === normaliseAbsPath) { if (filePath === normaliseAbsPath) {
return item; return item;
@ -347,7 +296,9 @@ export function getInstalledConnectionConfigItemByPath(absPath: string) {
const installedConnectionConfig = getConnectionConfig(); const installedConnectionConfig = getConnectionConfig();
const normaliseAbsPath = absPath.replace(/\\/g, '/'); const normaliseAbsPath = absPath.replace(/\\/g, '/');
for (const item of installedConnectionConfig.items) { for (let item of installedConnectionConfig.items) {
item = Array.isArray(item)? item[0] : item;
const filePath = (item.filePath || '').replace(/\\/g, '/'); const filePath = (item.filePath || '').replace(/\\/g, '/');
if (filePath === normaliseAbsPath) { if (filePath === normaliseAbsPath) {
return item; return item;

View File

@ -1,5 +1,5 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import type { IConnectionItem } from '../global'; import { McpOptions } from '../global';
export class SidebarItem extends vscode.TreeItem { export class SidebarItem extends vscode.TreeItem {
constructor( constructor(
@ -18,7 +18,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: IConnectionItem, public readonly item: McpOptions[] | McpOptions,
public readonly icon?: string public readonly icon?: string
) { ) {
super(label, collapsibleState); super(label, collapsibleState);

View File

@ -22,6 +22,7 @@ export class McpInstalledConnectProvider implements vscode.TreeDataProvider<Conn
const connection = getConnectionConfig(); const connection = getConnectionConfig();
const sidebarItems = connection.items.map((item, index) => { const sidebarItems = connection.items.map((item, index) => {
// 连接的名字 // 连接的名字
item = Array.isArray(item)? item[0] : item;
const itemName = `${item.name} (${item.type})` const itemName = `${item.name} (${item.type})`
return new ConnectionViewItem(itemName, vscode.TreeItemCollapsibleState.None, item, 'server'); return new ConnectionViewItem(itemName, vscode.TreeItemCollapsibleState.None, item, 'server');
}) })
@ -33,7 +34,9 @@ export class McpInstalledConnectProvider implements vscode.TreeDataProvider<Conn
@RegisterCommand('revealWebviewPanel') @RegisterCommand('revealWebviewPanel')
public revealWebviewPanel(context: vscode.ExtensionContext, view: ConnectionViewItem) { public revealWebviewPanel(context: vscode.ExtensionContext, view: ConnectionViewItem) {
const item = view.item; const item = view.item;
revealOpenMcpWebviewPanel(context, 'installed', item.filePath || item.name, item); const masterNode = Array.isArray(item)? item[0] : item;
const name = masterNode.filePath || masterNode.name || '';
revealOpenMcpWebviewPanel(context, 'installed', name, item);
} }
@RegisterCommand('refresh') @RegisterCommand('refresh')
@ -45,7 +48,7 @@ export class McpInstalledConnectProvider implements vscode.TreeDataProvider<Conn
public async addConnection(context: vscode.ExtensionContext) { public async addConnection(context: vscode.ExtensionContext) {
const item = await acquireInstalledConnection(); const item = await acquireInstalledConnection();
if (!item) { if (item.length === 0) {
return; return;
} }

View File

@ -1,11 +1,13 @@
import { getConnectionConfig, IConnectionItem, panels, saveConnectionConfig, getFirstValidPathFromCommand } from "../global"; import { getConnectionConfig, panels, saveConnectionConfig, getFirstValidPathFromCommand, McpOptions } from "../global";
import { exec, spawn } from 'node:child_process'; import { exec, spawn } from 'node:child_process';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
export async function deleteInstalledConnection(item: IConnectionItem) { export async function deleteInstalledConnection(item: McpOptions[] | McpOptions) {
// 弹出确认对话框 // 弹出确认对话框
const masterNode = Array.isArray(item) ? item[0] : item;
const name = masterNode.name;
const confirm = await vscode.window.showWarningMessage( const confirm = await vscode.window.showWarningMessage(
`确定要删除连接 "${item.name}" 吗?`, `确定要删除连接 "${name}" 吗?`,
{ modal: true }, { modal: true },
'确定' '确定'
); );
@ -26,12 +28,12 @@ export async function deleteInstalledConnection(item: IConnectionItem) {
// 刷新侧边栏视图 // 刷新侧边栏视图
vscode.commands.executeCommand('openmcp.sidebar.installed-connection.refresh'); vscode.commands.executeCommand('openmcp.sidebar.installed-connection.refresh');
panels.delete(item.name);
// 如果该连接有对应的webview面板则关闭它 // 如果该连接有对应的webview面板则关闭它
if (panels.has(item.filePath || item.name)) { const filePath = masterNode.filePath || '';
const panel = panels.get(item.filePath || item.name); const panel = panels.get(filePath);
panel?.dispose(); panel?.dispose();
} panels.delete(filePath);
} }
} }
@ -50,15 +52,15 @@ export async function validateAndGetCommandPath(commandString: string, cwd?: str
} }
} }
export async function acquireInstalledConnection(): Promise<IConnectionItem | undefined> { export async function acquireInstalledConnection(): Promise<McpOptions[]> {
// 让用户选择连接类型 // 让用户选择连接类型
const connectionType = await vscode.window.showQuickPick(['STDIO', 'SSE'], { const connectionType = await vscode.window.showQuickPick(['STDIO', 'SSE', 'STREAMABLE_HTTP'], {
placeHolder: '请选择连接类型', placeHolder: '请选择连接类型',
canPickMany: false canPickMany: false
}); });
if (!connectionType) { if (!connectionType) {
return; // 用户取消选择 return []; // 用户取消选择
} }
if (connectionType === 'STDIO') { if (connectionType === 'STDIO') {
@ -69,7 +71,7 @@ export async function acquireInstalledConnection(): Promise<IConnectionItem | un
}); });
if (!commandString) { if (!commandString) {
return; // 用户取消输入 return []; // 用户取消输入
} }
// 获取 cwd // 获取 cwd
@ -84,7 +86,7 @@ export async function acquireInstalledConnection(): Promise<IConnectionItem | un
console.log('Command Path:', commandPath); console.log('Command Path:', commandPath);
} catch (error) { } catch (error) {
vscode.window.showErrorMessage(`无效的 command: ${error}`); vscode.window.showErrorMessage(`无效的 command: ${error}`);
return; return [];
} }
const commands = commandString.split(' '); const commands = commandString.split(' ');
@ -96,24 +98,24 @@ export async function acquireInstalledConnection(): Promise<IConnectionItem | un
const filePath = await getFirstValidPathFromCommand(commandString, cwd || ''); const filePath = await getFirstValidPathFromCommand(commandString, cwd || '');
// 保存连接配置 // 保存连接配置
return { return [{
type: 'STDIO', connectionType: 'STDIO',
name: `STDIO-${Date.now()}`, name: `STDIO-${Date.now()}`,
command: command, command: command,
args, args,
cwd: cwd || '', cwd: cwd || '',
filePath: filePath, filePath: filePath,
}; }];
} else if (connectionType === 'SSE') { } else if (connectionType === 'SSE') {
// 获取 url // 获取 url
const url = await vscode.window.showInputBox({ const url = await vscode.window.showInputBox({
prompt: '请输入连接的 URL', prompt: '请输入连接的 URL',
placeHolder: '例如: https://127.0.0.1:8282' placeHolder: '例如: https://127.0.0.1:8282/sse'
}); });
if (!url) { if (!url) {
return; // 用户取消输入 return []; // 用户取消输入
} }
// 获取 oauth // 获取 oauth
@ -123,13 +125,40 @@ export async function acquireInstalledConnection(): Promise<IConnectionItem | un
}); });
// 保存连接配置 // 保存连接配置
return { return [{
type: 'SSE', connectionType: 'SSE',
name: `SSE-${Date.now()}`, name: `SSE-${Date.now()}`,
version: '1.0', // 假设默认版本为 1.0,可根据实际情况修改 version: '1.0', // 假设默认版本为 1.0,可根据实际情况修改
url: url, url: url,
oauth: oauth || '' oauth: oauth || ''
}];
} else if (connectionType === 'STREAMABLE_HTTP') {
// 获取 url
const url = await vscode.window.showInputBox({
prompt: '请输入连接的 URL',
placeHolder: '例如: https://127.0.0.1:8282/stream'
});
if (!url) {
return []; // 用户取消输入
} }
// 获取 oauth
const oauth = await vscode.window.showInputBox({
prompt: '请输入 OAuth 令牌,可选',
placeHolder: '例如: your-oauth-token'
});
// 保存连接配置
return [{
connectionType: 'STREAMABLE_HTTP',
name: `STREAMABLE_HTTP-${Date.now()}`,
version: '1.0', // 假设默认版本为 1.0,可根据实际情况修改
url: url,
oauth: oauth || ''
}];
} }
return [];
} }

View File

@ -22,6 +22,7 @@ export class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<Conn
const connection = getWorkspaceConnectionConfig(); const connection = getWorkspaceConnectionConfig();
const sidebarItems = connection.items.map((item, index) => { const sidebarItems = connection.items.map((item, index) => {
// 连接的名字 // 连接的名字
item = Array.isArray(item) ? item[0] : item;
const itemName = `${item.name} (${item.type})` const itemName = `${item.name} (${item.type})`
return new ConnectionViewItem(itemName, vscode.TreeItemCollapsibleState.None, item, 'server'); return new ConnectionViewItem(itemName, vscode.TreeItemCollapsibleState.None, item, 'server');
}) })
@ -33,7 +34,9 @@ export class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<Conn
@RegisterCommand('revealWebviewPanel') @RegisterCommand('revealWebviewPanel')
public revealWebviewPanel(context: vscode.ExtensionContext, view: ConnectionViewItem) { public revealWebviewPanel(context: vscode.ExtensionContext, view: ConnectionViewItem) {
const item = view.item; const item = view.item;
revealOpenMcpWebviewPanel(context, 'workspace', item.filePath || item.name, item); const masterNode = Array.isArray(item)? item[0] : item;
const name = masterNode.filePath || masterNode.name || '';
revealOpenMcpWebviewPanel(context, 'workspace', name, item);
} }
@RegisterCommand('refresh') @RegisterCommand('refresh')

View File

@ -1,92 +1,12 @@
import { getFirstValidPathFromCommand, getWorkspaceConnectionConfig, getWorkspacePath, IConnectionItem, panels, saveWorkspaceConnectionConfig } from "../global"; import { getFirstValidPathFromCommand, getWorkspaceConnectionConfig, getWorkspacePath, McpOptions, panels, saveWorkspaceConnectionConfig } from "../global";
import * as vscode from 'vscode'; import * as vscode from 'vscode';
export async function deleteUserConnection(item: McpOptions[] | McpOptions) {
export async function acquireUserCustomConnection(): Promise<IConnectionItem | undefined> {
// 让用户选择连接类型
const connectionType = await vscode.window.showQuickPick(['STDIO', 'SSE'], {
placeHolder: '请选择连接类型'
});
if (!connectionType) {
return; // 用户取消选择
}
if (connectionType === 'STDIO') {
// 获取 command
const commandString = await vscode.window.showInputBox({
prompt: '请输入连接的 command',
placeHolder: '例如: mcp run main.py'
});
if (!commandString) {
return; // 用户取消输入
}
// 获取 cwd
const cwd = await vscode.window.showInputBox({
prompt: '请输入工作目录 (cwd),可选',
placeHolder: '例如: /path/to/project'
});
// 校验 command + cwd 是否有效
try {
const commandPath = await validateAndGetCommandPath(commandString, cwd);
console.log('Command Path:', commandPath);
} catch (error) {
vscode.window.showErrorMessage(`无效的 command: ${error}`);
return;
}
const commands = commandString.split(' ');
const command = commands[0];
const args = commands.slice(1);
const filePath = await getFirstValidPathFromCommand(commandString, cwd || '');
// 保存连接配置
return {
type: 'STDIO',
name: `STDIO-${Date.now()}`,
command: command,
args,
cwd: cwd || '',
filePath
};
} else if (connectionType === 'SSE') {
// 获取 url
const url = await vscode.window.showInputBox({
prompt: '请输入连接的 URL',
placeHolder: '例如: https://127.0.0.1:8282'
});
if (!url) {
return; // 用户取消输入
}
// 获取 oauth
const oauth = await vscode.window.showInputBox({
prompt: '请输入 OAuth 令牌,可选',
placeHolder: '例如: your-oauth-token'
});
// 保存连接配置
return {
type: 'SSE',
name: `SSE-${Date.now()}`,
version: '1.0', // 假设默认版本为 1.0,可根据实际情况修改
url: url,
oauth: oauth || ''
}
}
}
export async function deleteUserConnection(item: IConnectionItem) {
// 弹出确认对话框 // 弹出确认对话框
const masterNode = Array.isArray(item) ? item[0] : item;
const name = masterNode.name;
const confirm = await vscode.window.showWarningMessage( const confirm = await vscode.window.showWarningMessage(
`确定要删除连接 "${item.name}" 吗?`, `确定要删除连接 "${name}" 吗?`,
{ modal: true }, { modal: true },
'确定' '确定'
); );
@ -108,12 +28,12 @@ export async function deleteUserConnection(item: IConnectionItem) {
// 刷新侧边栏视图 // 刷新侧边栏视图
vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh'); vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh');
panels.delete(item.name);
// 如果该连接有对应的webview面板则关闭它 // 如果该连接有对应的webview面板则关闭它
if (panels.has(item.filePath || item.name)) { const filePath = masterNode.filePath || '';
const panel = panels.get(item.filePath || item.name); const panel = panels.get(filePath);
panel?.dispose(); panel?.dispose();
} panels.delete(filePath);
} }
} }
@ -129,3 +49,109 @@ export async function validateAndGetCommandPath(command: string, cwd?: string):
throw new Error(`无法找到命令: ${command.split(' ')[0]}`); throw new Error(`无法找到命令: ${command.split(' ')[0]}`);
} }
} }
export async function acquireUserCustomConnection(): Promise<McpOptions[]> {
// 让用户选择连接类型
const connectionType = await vscode.window.showQuickPick(['STDIO', 'SSE'], {
placeHolder: '请选择连接类型'
});
if (!connectionType) {
return []; // 用户取消选择
}
if (connectionType === 'STDIO') {
// 获取 command
const commandString = await vscode.window.showInputBox({
prompt: '请输入连接的 command',
placeHolder: '例如: mcp run main.py'
});
if (!commandString) {
return []; // 用户取消输入
}
// 获取 cwd
const cwd = await vscode.window.showInputBox({
prompt: '请输入工作目录 (cwd),可选',
placeHolder: '例如: /path/to/project'
});
// 校验 command + cwd 是否有效
try {
const commandPath = await validateAndGetCommandPath(commandString, cwd);
console.log('Command Path:', commandPath);
} catch (error) {
vscode.window.showErrorMessage(`无效的 command: ${error}`);
return [];
}
const commands = commandString.split(' ');
const command = commands[0];
const args = commands.slice(1);
const filePath = await getFirstValidPathFromCommand(commandString, cwd || '');
// 保存连接配置
return [{
connectionType: 'STDIO',
name: `STDIO-${Date.now()}`,
command: command,
args,
cwd: cwd || '',
filePath
}];
} else if (connectionType === 'SSE') {
// 获取 url
const url = await vscode.window.showInputBox({
prompt: '请输入连接的 URL',
placeHolder: '例如: https://127.0.0.1:8282/sse'
});
if (!url) {
return []; // 用户取消输入
}
// 获取 oauth
const oauth = await vscode.window.showInputBox({
prompt: '请输入 OAuth 令牌,可选',
placeHolder: '例如: your-oauth-token'
});
// 保存连接配置
return [{
connectionType: 'SSE',
name: `SSE-${Date.now()}`,
version: '1.0', // 假设默认版本为 1.0,可根据实际情况修改
url: url,
oauth: oauth || ''
}];
} else if (connectionType === 'STREAMABLE_HTTP') {
// 获取 url
const url = await vscode.window.showInputBox({
prompt: '请输入连接的 URL',
placeHolder: '例如: https://127.0.0.1:8282/stream'
});
if (!url) {
return []; // 用户取消输入
}
// 获取 oauth
const oauth = await vscode.window.showInputBox({
prompt: '请输入 OAuth 令牌,可选',
placeHolder: '例如: your-oauth-token'
});
// 保存连接配置
return [{
connectionType: 'STREAMABLE_HTTP',
name: `STREAMABLE_HTTP-${Date.now()}`,
version: '1.0', // 假设默认版本为 1.0,可根据实际情况修改
url: url,
oauth: oauth || ''
}];
}
return [];
}

View File

@ -21,7 +21,7 @@ export class WebviewController {
} }
revealOpenMcpWebviewPanel(context, 'workspace', uri.fsPath, { revealOpenMcpWebviewPanel(context, 'workspace', uri.fsPath, {
type: 'STDIO', connectionType: 'STDIO',
name: 'OpenMCP', name: 'OpenMCP',
command: signature.command, command: signature.command,
args: signature.args, args: signature.args,

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 { IConnectionItem, ILaunchSigature, panels, updateInstalledConnectionConfig, updateWorkspaceConnectionConfig } from '../global'; import { McpOptions, panels, updateInstalledConnectionConfig, updateWorkspaceConnectionConfig } from '../global';
import { routeMessage } from '../../openmcp-sdk/service'; import { routeMessage } from '../../openmcp-sdk/service';
export function getWebviewContent(context: vscode.ExtensionContext, panel: vscode.WebviewPanel): string | undefined { export function getWebviewContent(context: vscode.ExtensionContext, panel: vscode.WebviewPanel): string | undefined {
@ -35,12 +35,7 @@ export function revealOpenMcpWebviewPanel(
context: vscode.ExtensionContext, context: vscode.ExtensionContext,
type: 'workspace' | 'installed', type: 'workspace' | 'installed',
panelKey: string, panelKey: string,
option: IConnectionItem = { option: McpOptions[] | McpOptions
type: 'STDIO',
name: 'OpenMCP',
command: 'mcp',
args: ['run', 'main.py']
}
) { ) {
if (panels.has(panelKey)) { if (panels.has(panelKey)) {
const panel = panels.get(panelKey); const panel = panels.get(panelKey);
@ -75,21 +70,9 @@ export function revealOpenMcpWebviewPanel(
// 拦截消息,注入额外信息 // 拦截消息,注入额外信息
switch (command) { switch (command) {
case 'vscode/launch-signature': case 'vscode/launch-signature':
const launchResultMessage: ILaunchSigature = option.type === 'STDIO' ?
{
type: 'STDIO',
commandString: option.command + ' ' + option.args.join(' '),
cwd: option.cwd || ''
} :
{
type: 'SSE',
url: option.url,
oauth: option.oauth || ''
};
const launchResult = { const launchResult = {
code: 200, code: 200,
msg: launchResultMessage msg: option
}; };
panel.webview.postMessage({ panel.webview.postMessage({
@ -118,6 +101,8 @@ export function revealOpenMcpWebviewPanel(
// 删除 // 删除
panels.delete(panelKey); panels.delete(panelKey);
// TODO: 通过引用计数器关闭后端的 clientMap
// 退出 // 退出
panel.dispose(); panel.dispose();
}); });