实现连接参数的局部保存

This commit is contained in:
锦恢 2025-04-23 03:31:16 +08:00
parent b2b80c1a3f
commit 5bac8f8726
5 changed files with 249 additions and 48 deletions

View File

@ -31,6 +31,20 @@
"light": "./icons/light/protocol.svg", "light": "./icons/light/protocol.svg",
"dark": "./icons/dark/protocol.svg" "dark": "./icons/dark/protocol.svg"
} }
},
{
"command": "openmcp.sidebar.workspace-connection.revealWebviewPanel",
"title": "展示 OpenMCP",
"category": "openmcp",
"icon": {
"light": "./icons/light/protocol.svg",
"dark": "./icons/dark/protocol.svg"
}
},
{
"command": "openmcp.sidebar.workspace-connection.refresh",
"title": "刷新",
"category": "openmcp"
} }
], ],
"menus": { "menus": {
@ -40,6 +54,13 @@
"group": "navigation", "group": "navigation",
"when": "editorLangId == python || editorLangId == javascript || editorLangId == typescript || editorLangId == java || editorLangId == csharp" "when": "editorLangId == python || editorLangId == javascript || editorLangId == typescript || editorLangId == java || editorLangId == csharp"
} }
],
"view/item/context": [
{
"command": "openmcp.sidebar.workspace-connection.revealWebviewPanel",
"group": "navigation",
"when": "view == openmcp.sidebar-view.workspace-connection"
}
] ]
}, },
"viewsContainers": { "viewsContainers": {
@ -54,7 +75,7 @@
"views": { "views": {
"openmcp-sidebar": [ "openmcp-sidebar": [
{ {
"id": "openmcp.sidebar.connect", "id": "openmcp.sidebar-view.workspace-connection",
"icon": "./icons/protocol.svg", "icon": "./icons/protocol.svg",
"name": "MCP 连接", "name": "MCP 连接",
"type": "tree" "type": "tree"

View File

@ -4,6 +4,7 @@ import * as fspath from 'path';
import * as OpenMCPService from '../resources/service'; import * as OpenMCPService from '../resources/service';
import { getLaunchCWD, revealOpenMcpWebviewPanel } from './webview'; import { getLaunchCWD, revealOpenMcpWebviewPanel } from './webview';
import { registerSidebar } from './sidebar'; import { registerSidebar } from './sidebar';
import { getWorkspaceConnectionConfigItemByPath, ISSEConnectionItem, IStdioConnectionItem } from './global';
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
console.log('activate openmcp'); console.log('activate openmcp');
@ -16,25 +17,37 @@ export function activate(context: vscode.ExtensionContext) {
registerSidebar(context); registerSidebar(context);
// 注册 showOpenMCP 命令 context.subscriptions.push(
vscode.commands.registerCommand('openmcp.sidebar.workspace-connection.revealWebviewPanel', (item: IStdioConnectionItem | ISSEConnectionItem) => {
revealOpenMcpWebviewPanel(context, item.name, item);
})
);
context.subscriptions.push( context.subscriptions.push(
vscode.commands.registerCommand('openmcp.showOpenMCP', async (uri: vscode.Uri) => { vscode.commands.registerCommand('openmcp.showOpenMCP', async (uri: vscode.Uri) => {
const cwd = getLaunchCWD(context, uri); const connectionItem = getWorkspaceConnectionConfigItemByPath(uri.fsPath);
// 获取 uri 相对于 cwd 的路径 if (!connectionItem) {
const relativePath = fspath.relative(cwd, uri.fsPath); // 项目不存在连接信息
const cwd = getLaunchCWD(context, uri);
// TODO: 实现从 connection.json 中读取配置,然后启动对应的 connection
const command = 'mcp'; // 获取 uri 相对于 cwd 的路径
const args = ['run', relativePath]; const relativePath = fspath.relative(cwd, uri.fsPath);
revealOpenMcpWebviewPanel(context, uri.fsPath, { // TODO: 实现从 connection.json 中读取配置,然后启动对应的 connection
type: 'stdio', const command = 'mcp';
name: 'OpenMCP', const args = ['run', relativePath];
command,
args, revealOpenMcpWebviewPanel(context, uri.fsPath, {
cwd type: 'stdio',
}); name: 'OpenMCP',
command,
args,
cwd
});
} else {
revealOpenMcpWebviewPanel(context, uri.fsPath, connectionItem);
}
}) })
); );

View File

@ -14,6 +14,7 @@ export interface IStdioConnectionItem {
args: string[]; args: string[];
cwd?: string; cwd?: string;
env?: { [key: string]: string }; env?: { [key: string]: string };
filePath?: string;
} }
export interface ISSEConnectionItem { export interface ISSEConnectionItem {
@ -22,16 +23,29 @@ export interface ISSEConnectionItem {
url: string; url: string;
oauth?: string; oauth?: string;
env?: { [key: string]: string }; env?: { [key: string]: string };
filePath?: string;
} }
export interface IConnectionConfig { export interface IConnectionConfig {
items: (IStdioConnectionItem | ISSEConnectionItem)[]; items: (IStdioConnectionItem | ISSEConnectionItem)[];
} }
export const CONNECTION_CONFIG_NAME = 'openmcp_connection.json';
let _connectionConfig: IConnectionConfig | undefined;
let _workspaceConnectionConfig: IConnectionConfig | undefined;
/**
* @description
* @returns
*/
export function getConnectionConfig() { export function getConnectionConfig() {
if (_connectionConfig) {
return _connectionConfig;
}
const homeDir = os.homedir(); const homeDir = os.homedir();
const configDir = fspath.join(homeDir, '.openmcp'); const configDir = fspath.join(homeDir, '.openmcp');
const connectionConfig = fspath.join(configDir, 'connection.json'); const connectionConfig = fspath.join(configDir, CONNECTION_CONFIG_NAME);
if (!fs.existsSync(connectionConfig)) { if (!fs.existsSync(connectionConfig)) {
fs.mkdirSync(configDir, { recursive: true }); fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(connectionConfig, JSON.stringify({ items: [] }), 'utf-8'); fs.writeFileSync(connectionConfig, JSON.stringify({ items: [] }), 'utf-8');
@ -39,5 +53,153 @@ export function getConnectionConfig() {
const rawConnectionString = fs.readFileSync(connectionConfig, 'utf-8'); const rawConnectionString = fs.readFileSync(connectionConfig, 'utf-8');
const connection = JSON.parse(rawConnectionString) as IConnectionConfig; const connection = JSON.parse(rawConnectionString) as IConnectionConfig;
_connectionConfig = connection;
return connection; return connection;
}
/**
* @description {workspace}
* @param workspace
*/
export function getWorkspaceConnectionConfig() {
const workspace = getWorkspacePath();
if (_workspaceConnectionConfig) {
return _workspaceConnectionConfig;
}
const configDir = fspath.join(workspace, '.vscode');
const connectionConfig = fspath.join(configDir, CONNECTION_CONFIG_NAME);
if (!fs.existsSync(connectionConfig)) {
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(connectionConfig, JSON.stringify({ items: [] }), 'utf-8');
}
const rawConnectionString = fs.readFileSync(connectionConfig, 'utf-8');
const connection = JSON.parse(rawConnectionString) as IConnectionConfig;
const workspacePath = getWorkspacePath();
for (const item of connection.items) {
if (item.filePath && item.filePath.startsWith('{workspace}')) {
item.filePath = item.filePath.replace('{workspace}', workspacePath).replace('\\', '/');
}
if (item.type === 'stdio' && item.cwd && item.cwd.startsWith('{workspace}')) {
item.cwd = item.cwd.replace('{workspace}', workspacePath).replace('\\', '/');
}
}
_workspaceConnectionConfig = connection;
return connection;
}
export function saveWorkspaceConnectionConfig(workspace: string) {
if (!_workspaceConnectionConfig) {
return;
}
const connectionConfig = JSON.parse(JSON.stringify(_workspaceConnectionConfig)) as IConnectionConfig;
const configDir = fspath.join(workspace, '.vscode');
const connectionConfigPath = fspath.join(configDir, CONNECTION_CONFIG_NAME);
const workspacePath = getWorkspacePath();
for (const item of connectionConfig.items) {
if (item.filePath && item.filePath.startsWith(workspacePath)) {
item.filePath = item.filePath.replace(workspacePath, '{workspace}').replace('\\', '/');
}
if (item.type ==='stdio' && item.cwd && item.cwd.startsWith(workspacePath)) {
item.cwd = item.cwd.replace(workspacePath, '{workspace}').replace('\\', '/');
}
}
fs.writeFileSync(connectionConfigPath, JSON.stringify(connectionConfig), 'utf-8');
}
interface ClientStdioConnectionItem {
command: string;
args: string[];
clientName: string;
clientVersion: string;
connectionType: 'STDIO';
cwd: string;
env: { [key: string]: string };
}
interface ClientSseConnectionItem {
url: string;
clientName: string;
clientVersion: string;
connectionType: 'SSE';
env: { [key: string]: string };
}
export function updateWorkspaceConnectionConfig(absPath: string, data: ClientStdioConnectionItem | ClientSseConnectionItem) {
const connectionItem = getWorkspaceConnectionConfigItemByPath(absPath);
const workspaceConnectionConfig = getWorkspaceConnectionConfig();
// 如果存在,删除老的 connectionItem
if (connectionItem) {
const index = workspaceConnectionConfig.items.indexOf(connectionItem);
if (index !== -1) {
workspaceConnectionConfig.items.splice(index, 1);
}
}
if (data.connectionType === 'STDIO') {
const connectionItem: IStdioConnectionItem = {
type:'stdio',
name: data.clientName,
command: data.command,
args: data.args,
cwd: data.cwd.replace('\\', '/'),
env: data.env,
filePath: absPath.replace('\\', '/')
};
// 插入到第一个
workspaceConnectionConfig.items.unshift(connectionItem);
const workspacePath = getWorkspacePath();
saveWorkspaceConnectionConfig(workspacePath);
vscode.commands.executeCommand('openmcp.sidebar.workspace-connection.refresh');
} else {
}
}
function normaliseConnectionFilePath(item: IStdioConnectionItem | ISSEConnectionItem, workspace: string) {
if (item.filePath) {
if (item.filePath.startsWith('{workspace}')) {
return item.filePath.replace('{workspace}', workspace).replace('\\', '/');
} else {
return item.filePath.replace('\\', '/');
}
}
return undefined;
}
export function getWorkspacePath() {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
return (workspaceFolder?.uri.fsPath || '').replace('\\', '/');
}
/**
* @description mcp
* @param absPath
*/
export function getWorkspaceConnectionConfigItemByPath(absPath: string) {
const workspacePath = getWorkspacePath();
const workspaceConnectionConfig = getWorkspaceConnectionConfig();
const normaliseAbsPath = absPath.replace('\\', '/');
for (const item of workspaceConnectionConfig.items) {
const filePath = normaliseConnectionFilePath(item, workspacePath);
if (filePath === normaliseAbsPath) {
return item;
}
}
return undefined;
} }

View File

@ -1,27 +1,9 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { getConnectionConfig, ISSEConnectionItem, IStdioConnectionItem } from './global'; import { getConnectionConfig, getWorkspaceConnectionConfig } from './global';
import { revealOpenMcpWebviewPanel } from './webview';
export function registerSidebar(context: vscode.ExtensionContext) { class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<SidebarItem> {
private _onDidChangeTreeData: vscode.EventEmitter<SidebarItem | undefined | null | void> = new vscode.EventEmitter<SidebarItem | undefined | null | void>();
context.subscriptions.push( readonly onDidChangeTreeData: vscode.Event<SidebarItem | undefined | null | void> = this._onDidChangeTreeData.event;
vscode.commands.registerCommand('openmcp.sidebar.revealOpenMcpWebviewPanel', (item: IStdioConnectionItem | ISSEConnectionItem) => {
revealOpenMcpWebviewPanel(context, item.name, item);
})
)
// 注册 MCP 连接的 sidebar 视图
context.subscriptions.push(
vscode.window.registerTreeDataProvider('openmcp.sidebar.connect', new McpConnectProvider(context))
);
// 注册 入门与帮助的 sidebar 视图
context.subscriptions.push(
vscode.window.registerTreeDataProvider('openmcp.sidebar.help', new HelpProvider(context))
);
}
class McpConnectProvider implements vscode.TreeDataProvider<SidebarItem> {
constructor(private context: vscode.ExtensionContext) { constructor(private context: vscode.ExtensionContext) {
} }
@ -33,21 +15,42 @@ class McpConnectProvider implements vscode.TreeDataProvider<SidebarItem> {
getChildren(element?: SidebarItem): Thenable<SidebarItem[]> { getChildren(element?: SidebarItem): Thenable<SidebarItem[]> {
// TODO: 读取 configDir 下的所有文件,作为子节点 // TODO: 读取 configDir 下的所有文件,作为子节点
const connection = getConnectionConfig(); const connection = getWorkspaceConnectionConfig();
const sidebarItems = connection.items.map((item, index) => { const sidebarItems = connection.items.map((item, index) => {
return new SidebarItem(item.name, vscode.TreeItemCollapsibleState.None, { return new SidebarItem(item.name, vscode.TreeItemCollapsibleState.None);
command: 'openmcp.sidebar.revealOpenMcpWebviewPanel',
title: 'OpenMCP',
arguments: [item]
}, 'server');
}) })
// 返回子节点 // 返回子节点
return Promise.resolve(sidebarItems); return Promise.resolve(sidebarItems);
} }
// 添加 refresh 方法
public refresh(): void {
this._onDidChangeTreeData.fire();
}
} }
// 在 registerSidebar 函数中注册 refresh 命令
export function registerSidebar(context: vscode.ExtensionContext) {
const workspaceConnectionProvider = new McpWorkspaceConnectProvider(context);
// 注册 refresh 命令
context.subscriptions.push(
vscode.commands.registerCommand('openmcp.sidebar.workspace-connection.refresh', () => {
workspaceConnectionProvider.refresh();
})
);
// 注册 MCP 连接的 sidebar 视图
context.subscriptions.push(
vscode.window.registerTreeDataProvider('openmcp.sidebar-view.workspace-connection', workspaceConnectionProvider)
);
// 注册 入门与帮助的 sidebar 视图
context.subscriptions.push(
vscode.window.registerTreeDataProvider('openmcp.sidebar.help', new HelpProvider(context))
);
}
class HelpProvider implements vscode.TreeDataProvider<SidebarItem> { class HelpProvider implements vscode.TreeDataProvider<SidebarItem> {

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 { ISSEConnectionItem, IStdioConnectionItem, panels } from './global'; import { getWorkspaceConnectionConfigItemByPath, ISSEConnectionItem, IStdioConnectionItem, panels, updateWorkspaceConnectionConfig } from './global';
import * as OpenMCPService from '../resources/service'; import * as OpenMCPService from '../resources/service';
@ -92,7 +92,9 @@ export function revealOpenMcpWebviewPanel(
}); });
break; break;
case 'connect':
updateWorkspaceConnectionConfig(panelKey, data);
default: default:
OpenMCPService.messageController(command, data, panel.webview); OpenMCPService.messageController(command, data, panel.webview);
break; break;