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 { acquireVsCodeApi, electronApi, getPlatform } from './platform';
import { isReactive } from 'vue';
export interface VSCodeMessage {
command: string;
@ -208,6 +209,21 @@ export class MessageBridge {
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
* @param command
@ -215,6 +231,7 @@ export class MessageBridge {
* @returns
*/
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);
@ -222,7 +239,7 @@ export class MessageBridge {
this.postMessage({
command,
data
data: this.deserializeReactiveData(data)
});
});
}

View File

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

View File

@ -84,7 +84,11 @@ const handleKeydown = (event: KeyboardEvent) => {
const copy = async () => {
try {
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 {
const textarea = document.createElement('textarea');
textarea.value = userInput.value;

View File

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

View File

@ -367,7 +367,9 @@ export class McpClient {
*/
public async lookupEnvVar(varNames: string[]) {
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) {
this.connectionResult.logString.push({

View File

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

View File

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

View File

@ -16,8 +16,22 @@ export class ConnectController {
const { keys } = data;
const values = keys.map((key: string) => {
// TODO: 在 Windows 上测试
if (process.platform === 'win32' && key.toLowerCase() === 'path') {
key = 'Path'; // 确保正确匹配环境变量的 ke
console.log(key);
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] || '';

View File

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

View File

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

View File

@ -6,47 +6,43 @@ import * as fs from 'fs';
export type FsPath = string;
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 {
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';
let _connectionConfig: IConnectionConfig | undefined;
@ -71,11 +67,11 @@ export function getConnectionConfig() {
const rawConnectionString = fs.readFileSync(connectionConfig, 'utf-8');
let connection;
try {
connection = JSON.parse(rawConnectionString) as IConnectionConfig;
connection = JSON.parse(rawConnectionString) as IConnectionConfig;
} catch (error) {
connection = { items: [] };
}
_connectionConfig = connection;
return connection;
}
@ -113,17 +109,20 @@ export function getWorkspaceConnectionConfig() {
let connection;
try {
connection = JSON.parse(rawConnectionString) as IConnectionConfig;
connection = JSON.parse(rawConnectionString) as IConnectionConfig;
} catch (error) {
connection = { items: [] };
}
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}')) {
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, '/');
}
}
@ -165,42 +164,28 @@ export function saveWorkspaceConnectionConfig(workspace: string) {
const connectionConfigPath = fspath.join(configDir, CONNECTION_CONFIG_NAME);
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)) {
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, '/');
}
}
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(
absPath: string,
data: (ClientStdioConnectionItem | ClientSseConnectionItem) & { serverInfo: ServerInfo }
data: McpOptions[]
) {
const connectionItem = getWorkspaceConnectionConfigItemByPath(absPath);
const connectionItem = getWorkspaceConnectionConfigItemByPath(absPath);
const workspaceConnectionConfig = getWorkspaceConnectionConfig();
// 如果存在,删除老的 connectionItem
@ -211,48 +196,29 @@ export function updateWorkspaceConnectionConfig(
}
}
if (data.connectionType === 'STDIO') {
const connectionItem: IStdioConnectionItem = {
type: 'STDIO',
name: data.serverInfo.name,
version: data.serverInfo.version,
command: data.command,
args: data.args,
cwd: data.cwd.replace(/\\/g, '/'),
env: data.env,
filePath: absPath.replace(/\\/g, '/')
};
// 对于第一个 item 添加 filePath
// 对路径进行标准化
data.forEach(item => {
item.filePath = absPath.replace(/\\/g, '/');
item.cwd = item.cwd?.replace(/\\/g, '/');
item.name = item.serverInfo?.name;
item.version = item.serverInfo?.version;
item.type = undefined;
});
console.log('get connectionItem: ', connectionItem);
console.log('get connectionItem: ', data);
// 插入到第一个
workspaceConnectionConfig.items.unshift(connectionItem);
const workspacePath = getWorkspacePath();
saveWorkspaceConnectionConfig(workspacePath);
vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh');
// 插入到第一个
workspaceConnectionConfig.items.unshift(data);
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(
absPath: string,
data: (ClientStdioConnectionItem | ClientSseConnectionItem) & { serverInfo: ServerInfo }
data: McpOptions[]
) {
const connectionItem = getInstalledConnectionConfigItemByPath(absPath);
const installedConnectionConfig = getConnectionConfig();
@ -265,45 +231,26 @@ export function updateInstalledConnectionConfig(
}
}
if (data.connectionType === 'STDIO') {
const connectionItem: IStdioConnectionItem = {
type: 'STDIO',
name: data.serverInfo.name,
version: data.serverInfo.version,
command: data.command,
args: data.args,
cwd: data.cwd.replace(/\\/g, '/'),
env: data.env,
filePath: absPath.replace(/\\/g, '/')
};
// 对于第一个 item 添加 filePath
// 对路径进行标准化
data.forEach(item => {
item.filePath = absPath.replace(/\\/g, '/');
item.cwd = item.cwd?.replace(/\\/g, '/');
item.name = item.serverInfo?.name;
item.version = item.serverInfo?.version;
item.type = undefined;
});
console.log('get connectionItem: ', connectionItem);
console.log('get connectionItem: ', data);
// 插入到第一个
installedConnectionConfig.items.unshift(connectionItem);
saveConnectionConfig();
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');
}
// 插入到第一个
installedConnectionConfig.items.unshift(data);
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.startsWith('{workspace}')) {
return item.filePath.replace('{workspace}', workspace).replace(/\\/g, '/');
@ -329,7 +276,9 @@ export function getWorkspaceConnectionConfigItemByPath(absPath: string) {
const workspaceConnectionConfig = getWorkspaceConnectionConfig();
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);
if (filePath === normaliseAbsPath) {
return item;
@ -347,7 +296,9 @@ export function getInstalledConnectionConfigItemByPath(absPath: string) {
const installedConnectionConfig = getConnectionConfig();
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, '/');
if (filePath === normaliseAbsPath) {
return item;
@ -361,14 +312,14 @@ export function getInstalledConnectionConfigItemByPath(absPath: string) {
export async function getFirstValidPathFromCommand(command: string, cwd: string): Promise<string | undefined> {
// 分割命令字符串
const parts = command.split(' ');
// 遍历命令部分,寻找第一个可能是路径的部分
for (let i = 1; i < parts.length; i++) {
const part = parts[i];
// 跳过以 '-' 开头的参数
if (part.startsWith('-')) continue;
// 处理相对路径
let fullPath = part;
if (!fspath.isAbsolute(part)) {
@ -381,6 +332,6 @@ export async function getFirstValidPathFromCommand(command: string, cwd: string)
return fullPath;
}
}
return undefined;
}

View File

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

View File

@ -22,6 +22,7 @@ export class McpInstalledConnectProvider implements vscode.TreeDataProvider<Conn
const connection = getConnectionConfig();
const sidebarItems = connection.items.map((item, index) => {
// 连接的名字
item = Array.isArray(item)? item[0] : item;
const itemName = `${item.name} (${item.type})`
return new ConnectionViewItem(itemName, vscode.TreeItemCollapsibleState.None, item, 'server');
})
@ -33,7 +34,9 @@ export class McpInstalledConnectProvider implements vscode.TreeDataProvider<Conn
@RegisterCommand('revealWebviewPanel')
public revealWebviewPanel(context: vscode.ExtensionContext, view: ConnectionViewItem) {
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')
@ -45,7 +48,7 @@ export class McpInstalledConnectProvider implements vscode.TreeDataProvider<Conn
public async addConnection(context: vscode.ExtensionContext) {
const item = await acquireInstalledConnection();
if (!item) {
if (item.length === 0) {
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 * 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(
`确定要删除连接 "${item.name}" 吗?`,
`确定要删除连接 "${name}" 吗?`,
{ modal: true },
'确定'
);
@ -26,12 +28,12 @@ export async function deleteInstalledConnection(item: IConnectionItem) {
// 刷新侧边栏视图
vscode.commands.executeCommand('openmcp.sidebar.installed-connection.refresh');
panels.delete(item.name);
// 如果该连接有对应的webview面板则关闭它
if (panels.has(item.filePath || item.name)) {
const panel = panels.get(item.filePath || item.name);
panel?.dispose();
}
const filePath = masterNode.filePath || '';
const panel = panels.get(filePath);
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: '请选择连接类型',
canPickMany: false
});
if (!connectionType) {
return; // 用户取消选择
return []; // 用户取消选择
}
if (connectionType === 'STDIO') {
@ -69,7 +71,7 @@ export async function acquireInstalledConnection(): Promise<IConnectionItem | un
});
if (!commandString) {
return; // 用户取消输入
return []; // 用户取消输入
}
// 获取 cwd
@ -84,7 +86,7 @@ export async function acquireInstalledConnection(): Promise<IConnectionItem | un
console.log('Command Path:', commandPath);
} catch (error) {
vscode.window.showErrorMessage(`无效的 command: ${error}`);
return;
return [];
}
const commands = commandString.split(' ');
@ -92,28 +94,28 @@ export async function acquireInstalledConnection(): Promise<IConnectionItem | un
const args = commands.slice(1);
console.log('Command:', command);
const filePath = await getFirstValidPathFromCommand(commandString, cwd || '');
const filePath = await getFirstValidPathFromCommand(commandString, cwd || '');
// 保存连接配置
return {
type: 'STDIO',
return [{
connectionType: 'STDIO',
name: `STDIO-${Date.now()}`,
command: command,
args,
cwd: cwd || '',
filePath: filePath,
};
}];
} else if (connectionType === 'SSE') {
// 获取 url
const url = await vscode.window.showInputBox({
prompt: '请输入连接的 URL',
placeHolder: '例如: https://127.0.0.1:8282'
placeHolder: '例如: https://127.0.0.1:8282/sse'
});
if (!url) {
return; // 用户取消输入
return []; // 用户取消输入
}
// 获取 oauth
@ -123,13 +125,40 @@ export async function acquireInstalledConnection(): Promise<IConnectionItem | un
});
// 保存连接配置
return {
type: 'SSE',
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

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

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';
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) {
export async function deleteUserConnection(item: McpOptions[] | McpOptions) {
// 弹出确认对话框
const masterNode = Array.isArray(item) ? item[0] : item;
const name = masterNode.name;
const confirm = await vscode.window.showWarningMessage(
`确定要删除连接 "${item.name}" 吗?`,
`确定要删除连接 "${name}" 吗?`,
{ modal: true },
'确定'
);
@ -108,12 +28,12 @@ export async function deleteUserConnection(item: IConnectionItem) {
// 刷新侧边栏视图
vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh');
panels.delete(item.name);
// 如果该连接有对应的webview面板则关闭它
if (panels.has(item.filePath || item.name)) {
const panel = panels.get(item.filePath || item.name);
panel?.dispose();
}
const filePath = masterNode.filePath || '';
const panel = panels.get(filePath);
panel?.dispose();
panels.delete(filePath);
}
}
@ -128,4 +48,110 @@ export async function validateAndGetCommandPath(command: string, cwd?: string):
} catch (error) {
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, {
type: 'STDIO',
connectionType: 'STDIO',
name: 'OpenMCP',
command: signature.command,
args: signature.args,

View File

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