diff --git a/README.md b/README.md index fa77fda..349ca07 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ |---------|---------|--------|---------|-----------| | `all` | 完成最基本的各类基础设施 | `完整版本` | 100% | `Done` | | `render` | chat 模式下支持进行成本分析 | `迭代版本` | 100% | `Done` | -| `ext` | 支持基本的 MCP 项目管理 | `MVP` | 0% | `P0` | +| `ext` | 支持基本的 MCP 项目管理 | `MVP` | 70% | `P0` | | `service` | 支持自定义支持 openai 接口协议的大模型接入 | `完整版本` | 100% | `Done` | | `service` | 支持自定义接口协议的大模型接入 | `MVP` | 0% | `P1` | | `all` | 支持同时调试多个 MCP Server | `MVP` | 0% | `P1` | diff --git a/configure.ps1 b/configure.ps1 index 55b21e6..805146f 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -9,5 +9,9 @@ npm i node patch-mcp-sdk.js Set-Location .. +Set-Location servers +uv sync +Set-Location .. + # 安装根目录依赖 npm i \ No newline at end of file diff --git a/package.json b/package.json index bf79d2e..e2a0a53 100644 --- a/package.json +++ b/package.json @@ -54,10 +54,16 @@ "views": { "openmcp-sidebar": [ { - "id": "webview-sidebar.view", + "id": "openmcp.sidebar.connect", "icon": "./icons/protocol.svg", - "name": "chatbot", - "type": "webview" + "name": "MCP 连接", + "type": "tree" + }, + { + "id": "openmcp.sidebar.help", + "icon": "./icons/protocol.svg", + "name": "入门与帮助", + "type": "tree" } ] } diff --git a/src/extension.ts b/src/extension.ts index 9bb4abf..44a4fbf 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,10 +1,9 @@ import * as vscode from 'vscode'; -import * as fs from 'fs'; import * as fspath from 'path'; import * as OpenMCPService from '../resources/service'; -import { getLaunchCWD, getWebviewContent } from './webview'; -import { panels } from './global'; +import { getLaunchCWD, revealOpenMcpWebviewPanel } from './webview'; +import { registerSidebar } from './sidebar'; export function activate(context: vscode.ExtensionContext) { console.log('activate openmcp'); @@ -15,81 +14,28 @@ export function activate(context: vscode.ExtensionContext) { const workspace = workspaceFolder?.uri.fsPath || ''; OpenMCPService.setVscodeWorkspace(workspace); + registerSidebar(context); + // 注册 showOpenMCP 命令 context.subscriptions.push( vscode.commands.registerCommand('openmcp.showOpenMCP', async (uri: vscode.Uri) => { - if (panels.has(uri.fsPath)) { - const panel = panels.get(uri.fsPath); - panel?.reveal(); - return; - } - - const panel = vscode.window.createWebviewPanel( - 'OpenMCP', - 'OpenMCP', - vscode.ViewColumn.One, - { - enableScripts: true, - retainContextWhenHidden: true, - enableFindWidget: true - } - ); - - panels.set(uri.fsPath, panel); - const cwd = getLaunchCWD(context, uri); // 获取 uri 相对于 cwd 的路径 const relativePath = fspath.relative(cwd, uri.fsPath); + + // TODO: 实现从 connection.json 中读取配置,然后启动对应的 connection + const command = 'mcp'; + const args = ['run', relativePath]; + + revealOpenMcpWebviewPanel(context, uri.fsPath, { + type: 'stdio', + name: 'OpenMCP', + command, + args, + cwd + }); - console.log('current file' + uri.fsPath); - console.log(`relativePath: ${relativePath}`); - - // 根据 relativePath 先去 setting 中进行选择 - - // 设置HTML内容 - const html = getWebviewContent(context, panel); - panel.webview.html = html || ''; - panel.iconPath = vscode.Uri.file(fspath.join(context.extensionPath, 'resources', 'renderer', 'images', 'openmcp.png')); - - // 处理来自webview的消息 - panel.webview.onDidReceiveMessage(message => { - const { command, data } = message; - console.log('receive message', message); - - // 拦截消息,注入额外信息 - switch (command) { - case 'vscode/launch-command': - const commandString = 'mcp run ' + relativePath; - const launchResult = { - code: 200, - msg: { - commandString: commandString, - cwd: cwd - } - } - - panel.webview.postMessage({ - command: 'vscode/launch-command', - data: launchResult - }); - - break; - - default: - OpenMCPService.messageController(command, data, panel.webview); - break; - } - - }); - - panel.onDidDispose(async () => { - // 删除 - panels.delete(uri.fsPath); - - // 退出 - panel.dispose(); - }); }) ); } diff --git a/src/global.ts b/src/global.ts index 3d4693d..3a4e7a7 100644 --- a/src/global.ts +++ b/src/global.ts @@ -1,4 +1,43 @@ import * as vscode from 'vscode'; +import * as os from 'os'; +import * as fspath from 'path'; +import * as fs from 'fs'; export type FsPath = string; -export const panels = new Map(); \ No newline at end of file +export const panels = new Map(); + + +export interface IStdioConnectionItem { + type: 'stdio'; + name: string; + command: string; + args: string[]; + cwd?: string; + env?: { [key: string]: string }; +} + +export interface ISSEConnectionItem { + type: 'sse'; + name: string; + url: string; + oauth?: string; + env?: { [key: string]: string }; +} + +export interface IConnectionConfig { + items: (IStdioConnectionItem | ISSEConnectionItem)[]; +} + +export function getConnectionConfig() { + const homeDir = os.homedir(); + const configDir = fspath.join(homeDir, '.openmcp'); + const connectionConfig = fspath.join(configDir, 'connection.json'); + 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; + return connection; +} \ No newline at end of file diff --git a/src/sidebar.ts b/src/sidebar.ts new file mode 100644 index 0000000..cda96d7 --- /dev/null +++ b/src/sidebar.ts @@ -0,0 +1,105 @@ +import * as vscode from 'vscode'; +import { getConnectionConfig, ISSEConnectionItem, IStdioConnectionItem } from './global'; +import { revealOpenMcpWebviewPanel } from './webview'; + +export function registerSidebar(context: vscode.ExtensionContext) { + + context.subscriptions.push( + 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 { + + constructor(private context: vscode.ExtensionContext) { + } + + // 实现 TreeDataProvider 接口 + getTreeItem(element: SidebarItem): vscode.TreeItem { + return element; + } + + getChildren(element?: SidebarItem): Thenable { + // TODO: 读取 configDir 下的所有文件,作为子节点 + const connection = getConnectionConfig(); + const sidebarItems = connection.items.map((item, index) => { + return new SidebarItem(item.name, vscode.TreeItemCollapsibleState.None, { + command: 'openmcp.sidebar.revealOpenMcpWebviewPanel', + title: 'OpenMCP', + arguments: [item] + }, 'server'); + }) + + // 返回子节点 + return Promise.resolve(sidebarItems); + } +} + + + +class HelpProvider implements vscode.TreeDataProvider { + + constructor(private context: vscode.ExtensionContext) { + } + + // 实现 TreeDataProvider 接口 + getTreeItem(element: SidebarItem): vscode.TreeItem { + return element; + } + + getChildren(element?: SidebarItem): Thenable { + // 返回子节点 + return Promise.resolve([ + new SidebarItem('入门', vscode.TreeItemCollapsibleState.None, { + command: 'vscode.open', + title: 'Open Guide', + arguments: [vscode.Uri.parse('https://zhuanlan.zhihu.com/p/1894785817186121106')] + }, 'book'), + new SidebarItem('阅读文档', vscode.TreeItemCollapsibleState.None, { + command: 'vscode.open', + title: 'Open Documentation', + arguments: [vscode.Uri.parse('https://document.kirigaya.cn/blogs/openmcp/main.html')] + }, 'file-text'), + new SidebarItem('报告问题', vscode.TreeItemCollapsibleState.None, { + command: 'vscode.open', + title: 'Report Issue', + arguments: [vscode.Uri.parse('https://github.com/LSTM-Kirigaya/openmcp-client/issues')] + }, 'bug'), + new SidebarItem('参与项目', vscode.TreeItemCollapsibleState.None, { + command: 'vscode.open', + title: 'Join Project', + arguments: [vscode.Uri.parse('https://qm.qq.com/cgi-bin/qm/qr?k=C6ZUTZvfqWoI12lWe7L93cWa1hUsuVT0&jump_from=webapi&authKey=McW6B1ogTPjPDrCyGttS890tMZGQ1KB3QLuG4aqVNRaYp4vlTSgf2c6dMcNjMuBD')] + }, 'organization'), + new SidebarItem('评论插件', vscode.TreeItemCollapsibleState.None, { + command: 'vscode.open', + title: 'Review Extension', + arguments: [vscode.Uri.parse('https://marketplace.visualstudio.com/items?itemName=kirigaya.openmcp&ssr=false#review-details')] + }, 'feedback') + ]); + } +} + +class SidebarItem extends vscode.TreeItem { + constructor( + public readonly label: string, + public readonly collapsibleState: vscode.TreeItemCollapsibleState, + public readonly command?: vscode.Command, + public readonly icon?: string + ) { + super(label, collapsibleState); + this.command = command; + this.iconPath = new vscode.ThemeIcon(icon || 'circle-outline'); + } +} \ No newline at end of file diff --git a/src/webview.ts b/src/webview.ts index 416f008..5509931 100644 --- a/src/webview.ts +++ b/src/webview.ts @@ -1,6 +1,9 @@ import * as vscode from 'vscode'; import * as fs from 'fs'; import * as fspath from 'path'; +import { ISSEConnectionItem, IStdioConnectionItem, panels } from './global'; +import * as OpenMCPService from '../resources/service'; + export function getWebviewContent(context: vscode.ExtensionContext, panel: vscode.WebviewPanel): string | undefined { const viewRoot = fspath.join(context.extensionPath, 'resources', 'renderer'); @@ -21,3 +24,89 @@ export function getLaunchCWD(context: vscode.ExtensionContext, uri: vscode.Uri) const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); return workspaceFolder?.uri.fsPath || ''; } + + +export function revealOpenMcpWebviewPanel( + context: vscode.ExtensionContext, + panelKey: string, + option: (IStdioConnectionItem | ISSEConnectionItem) = { + type: 'stdio', + name: 'OpenMCP', + command: 'mcp', + args: ['run', 'main.py'] + } +) { + if (panels.has(panelKey)) { + const panel = panels.get(panelKey); + panel?.reveal(); + return panel; + } + + const panel = vscode.window.createWebviewPanel( + 'OpenMCP', + 'OpenMCP', + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true, + enableFindWidget: true + } + ); + + panels.set(panelKey, panel); + + + // 设置HTML内容 + const html = getWebviewContent(context, panel); + panel.webview.html = html || ''; + panel.iconPath = vscode.Uri.file(fspath.join(context.extensionPath, 'resources', 'renderer', 'images', 'openmcp.png')); + + // 处理来自webview的消息 + panel.webview.onDidReceiveMessage(message => { + const { command, data } = message; + console.log('receive message', message); + + // 拦截消息,注入额外信息 + switch (command) { + case 'vscode/launch-command': + const laucnResultMessage = 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: laucnResultMessage + }; + + panel.webview.postMessage({ + command: 'vscode/launch-command', + data: launchResult + }); + + break; + + default: + OpenMCPService.messageController(command, data, panel.webview); + break; + } + + }); + + panel.onDidDispose(async () => { + // 删除 + panels.delete(panelKey); + + // 退出 + panel.dispose(); + }); + + return panel; +} \ No newline at end of file