Compare commits

..

18 Commits

Author SHA1 Message Date
c218dcba03 完成兼容 2025-05-19 23:34:10 +08:00
355b25b9b7 完成兼容 2025-05-19 22:14:13 +08:00
be48449df8 merge multi clients 2025-05-19 21:11:34 +08:00
4597904796 update 2025-05-19 21:10:09 +08:00
c9bc58ca9c finish 0.0.9; prepare test TIP CI system 2025-05-19 20:46:04 +08:00
9a687e4432 fix oom 2025-05-19 20:10:23 +08:00
kirigaya
c8451241f3 更新 vite 后调整 task-loop 的打包策略 2025-05-19 17:22:06 +08:00
kirigaya
3924ee0224 更新 vite 后调整 task-loop 的打包策略 2025-05-19 17:08:39 +08:00
kirigaya
13fd6a00e1 update 2025-05-19 16:47:44 +08:00
19857a5c73 update 2025-05-19 16:07:50 +08:00
23d0ce1d93 update 2025-05-19 04:38:36 +08:00
9b45c272b4 update 2025-05-18 21:05:49 +08:00
c1cad24790 Merge branch 'appli456-chore/cross-platform-script' 2025-05-17 00:49:47 +08:00
e9e9d7e119 merge 2025-05-17 00:49:37 +08:00
lirz
e72b03a995 chore: service模块给electron继续使用原编译方案;修改文档 2025-05-09 09:23:00 +08:00
lirun
7c30463062 chore: lock update 2025-05-09 00:17:07 +08:00
lirz
72248864da chore:turbo config 2025-05-08 22:53:17 +08:00
lirz
86cd49fae6 chore:turbo config 2025-05-08 22:53:03 +08:00
77 changed files with 9801 additions and 16403 deletions

1
.gitignore vendored
View File

@ -14,3 +14,4 @@ resources/ocr/*.wasm
resources/renderer resources/renderer
resources/service resources/service
*.traineddata *.traineddata
.turbo

View File

@ -12,5 +12,8 @@
"typescript.tsc.autoDetect": "off", "typescript.tsc.autoDetect": "off",
"i18n-haru.root": "renderer/src/i18n", "i18n-haru.root": "renderer/src/i18n",
"i18n-haru.main": "zh" "i18n-haru.main": "zh",
"i18n-ally.localesPaths": [
"renderer/src/i18n"
]
} }

View File

@ -331,6 +331,34 @@
"state": "success", "state": "success",
"serverName": "Huoshan DeepSeek" "serverName": "Huoshan DeepSeek"
} }
},
{
"role": "user",
"content": "请将下面的更新日志翻译成 GitHub release 风格的英文说明,请只返回翻译后的结果,不要出现任何多余的前缀: ",
"extraInfo": {
"created": 1747598248965,
"state": "success",
"serverName": "deepseek"
}
},
{
"role": "assistant",
"content": "Heres the translation in GitHub release style:\n\n---\n\n### **New Features**\n- Added support for real-time data synchronization.\n- Introduced a new dashboard for analytics.\n\n### **Improvements**\n- Optimized query performance for large datasets.\n- Enhanced error handling for API requests.\n\n### **Bug Fixes**\n- Fixed an issue where the system crashed on invalid input.\n- Resolved a memory leak in the background service.\n\n### **Breaking Changes**\n- Removed deprecated endpoints (`/v1/old`).\n- Updated the authentication mechanism to use OAuth 2.0.\n\n### **Documentation**\n- Added detailed guides for API integration.\n- Improved inline code comments.",
"extraInfo": {
"created": 1747598260799,
"state": "success",
"serverName": "deepseek",
"usage": {
"prompt_tokens": 7946,
"completion_tokens": 136,
"total_tokens": 8082,
"prompt_tokens_details": {
"cached_tokens": 896
},
"prompt_cache_hit_tokens": 896,
"prompt_cache_miss_tokens": 7050
}
}
} }
], ],
"settings": { "settings": {

View File

@ -1,8 +1,14 @@
# Change Log # Change Log
## [main] 0.1.0
- 新特性:支持同时连入多个 mcp server
- 新特性:更新协议内容,支持 streamable http 协议,未来将逐步取代 SSE 的连接方式
- 对于 uv 创建的 py 项目进行特殊支持:自动初始化项目,并将 mcp 定向到 .venv/bin/mcp 中,不再需要用户全局安装 mcp
- 对于 npm 创建的 js/ts 项目进行特殊支持:自动初始化项目
## [main] 0.0.9 ## [main] 0.0.9
- 修复 0.0.8 引入的bugsystem prompt 返回的是索引而非真实内容 - 修复 0.0.8 引入的bugsystem prompt 返回的是索引而非真实内容
- - 测试新的发布管线
## [main] 0.0.8 ## [main] 0.0.8
- 大模型 API 测试时更加完整的报错 - 大模型 API 测试时更加完整的报错

View File

@ -136,19 +136,13 @@ B <--mcp--> m(MCP Server)
配置项目 配置项目
```bash ```bash
## linux npm run setup
./configure.sh
## windows
./configure.ps1
``` ```
启动 dev server 启动 dev server
```bash ```bash
## linux npm run dev
./dev.sh
## windows
./dev.ps1
``` ```
> 端口占用: 8282 (renderer) + 8081 (service) > 端口占用: 8282 (renderer) + 8081 (service)
@ -163,11 +157,7 @@ B <--mcp--> m(MCP Server)
负载部署 负载部署
```bash ```bash
## linux npm run build
./build_service.sh
## windows
./build_service.ps1
``` ```
and just press f5, いただきます and just press f5, いただきます

View File

@ -1,38 +0,0 @@
# 创建并清理资源目录
New-Item -ItemType Directory -Path ./openmcp-sdk -Force
Remove-Item -Recurse -Force ./openmcp-sdk/* -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Path ./openmcp-sdk -Force
# 获取当前工作目录的绝对路径
$currentDir = (Get-Location).Path
# 并行构建 renderer 和 service
$rendererJob = Start-Job -ScriptBlock {
param($workDir)
Set-Location -Path "$workDir\renderer"
npm run build
Move-Item -Path "./dist" -Destination "$workDir\openmcp-sdk\renderer" -Force
} -ArgumentList $currentDir
$serviceJob = Start-Job -ScriptBlock {
param($workDir)
Set-Location -Path "$workDir\service"
npm run build
Move-Item -Path "./dist" -Destination "$workDir\openmcp-sdk\service" -Force
} -ArgumentList $currentDir
# 等待任务完成
$rendererJob | Wait-Job | Receive-Job
$serviceJob | Wait-Job | Receive-Job
# 将 openmcp-sdk 目录复制到 software/openmcp-sdk
New-Item -ItemType Directory -Path ./software/openmcp-sdk -Force
Remove-Item -Recurse -Force ./software/openmcp-sdk/* -ErrorAction SilentlyContinue
Copy-Item -Recurse -Path ./openmcp-sdk -Destination ./software/ -Force
$serviceJob = Start-Job -ScriptBlock {
param($workDir)
npm run build:task-loop
} -ArgumentList $currentDir
Write-Output "finish building services in ./openmcp-sdk"

View File

@ -1,18 +0,0 @@
#!/bin/bash
mkdir -p ./openmcp-sdk
rm -rf ./openmcp-sdk/
mkdir -p ./openmcp-sdk
(cd ./renderer && npm run build && mv ./dist ../openmcp-sdk/renderer) &
(cd ./service && npm run build && mv ./dist ../openmcp-sdk/service) &
wait
mkdir -p ./software/openmcp-sdk
rm -rf ./software/openmcp-sdk
cp -r ./openmcp-sdk ./software/
npm run build:task-loop
echo "finish building services in ./openmcp-sdk"

View File

@ -1,17 +0,0 @@
# 安装 renderer 依赖
Set-Location renderer
npm i
Set-Location ..
# 安装 service 依赖并打补丁
Set-Location service
npm i
node patch-mcp-sdk.js
Set-Location ..
Set-Location servers
uv sync
Set-Location ..
# 安装根目录依赖
npm i

View File

@ -1,5 +0,0 @@
cd renderer && npm i && cd ..
cd service && npm i && cd ..
cd servers && uv sync
npm i
npm run prepare:ocr

View File

@ -1,7 +0,0 @@
npx concurrently `
-n "renderer,service" `
-p " {name} " `
-c "black.bgBlue,black.bgGreen" `
--kill-others `
"cd renderer && npm run serve" `
"cd service && npm run serve"

9
dev.sh
View File

@ -1,9 +0,0 @@
#!/bin/bash
npx concurrently \
-n "renderer,service" \
-p " {name} " \
-c "black.bgBlue,black.bgGreen" \
--kill-others \
"cd renderer && npm run serve" \
"cd service && npm run serve"

8589
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "openmcp", "name": "openmcp",
"displayName": "OpenMCP", "displayName": "OpenMCP",
"description": "An all in one MCP Client/TestTool", "description": "An all in one MCP Client/TestTool",
"version": "0.0.8", "version": "0.1.0",
"publisher": "kirigaya", "publisher": "kirigaya",
"author": { "author": {
"name": "kirigaya", "name": "kirigaya",
@ -212,7 +212,17 @@
] ]
} }
}, },
"workspaces": [
"service",
"renderer",
"software"
],
"scripts": { "scripts": {
"setup": "npm i && npm run prepare:ocr",
"serve": "turbo serve",
"build": "turbo build --filter=!@openmcp/electron",
"build:electron": "turbo build --filter=@openmcp/electron",
"build:all": "turbo build",
"vscode:prepublish": "webpack --mode production", "vscode:prepublish": "webpack --mode production",
"compile": "tsc -p ./", "compile": "tsc -p ./",
"watch": "tsc -watch -p ./", "watch": "tsc -watch -p ./",
@ -242,8 +252,10 @@
"fork-ts-checker-webpack-plugin": "^9.1.0", "fork-ts-checker-webpack-plugin": "^9.1.0",
"null-loader": "^4.0.1", "null-loader": "^4.0.1",
"ts-loader": "^9.5.1", "ts-loader": "^9.5.1",
"turbo": "^2.5.3",
"typescript": "^5.4.2", "typescript": "^5.4.2",
"webpack": "^5.99.5", "webpack": "^5.99.5",
"webpack-cli": "^5.1.4" "webpack-cli": "^5.1.4"
} },
"packageManager": "npm@10.0.0"
} }

View File

@ -1 +1,2 @@
VITE_USE_AUTH=false
VITE_WEBSOCKET_URL=ws://localhost:8282 VITE_WEBSOCKET_URL=ws://localhost:8282

View File

@ -1 +1,2 @@
VITE_USE_AUTH=false
VITE_WEBSOCKET_URL=ws://localhost:8282 VITE_WEBSOCKET_URL=ws://localhost:8282

View File

@ -1,33 +1,18 @@
# test-vite ## dev
This template should help get you started developing with Vue 3 in Vite. 如果想要部署到公网中,想要通过密码认证才能进入,进行如下步骤:
## Recommended IDE Setup ```bash
touch .env.website.local
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Type Support for `.vue` Imports in TS
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
``` ```
### Compile and Hot-Reload for Development 写入:
```sh ```toml
npm run dev VITE_USE_AUTH=true
VITE_WEBSOCKET_URL=wss://<IP>/<路径>
``` ```
### Type-Check, Compile and Minify for Production 使用 `npm run serve:website` 进行测试(服务端使用 ts-node src/server.ts
```sh 使用 `npm run build:website` 进行打包
npm run build
```

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,12 @@
{ {
"name": "test-vite", "name": "@openmcp/renderer",
"version": "0.0.0", "version": "0.1.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite",
"postbuild": "node scripts/post-build.mjs",
"lint": "vue-cli-service lint",
"serve": "vite", "serve": "vite",
"serve:website": "vite --mode website", "serve:website": "vite --mode website",
"build": "run-p type-check \"build-only {@}\" --", "build": "run-p type-check \"build-only {@}\" --",
@ -28,13 +31,26 @@
"vue-router": "^4.5.0" "vue-router": "^4.5.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.27.1",
"@babel/plugin-proposal-decorators": "^7.27.1",
"@babel/plugin-syntax-import-attributes": "^7.27.1",
"@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/plugin-transform-typescript": "^7.27.1",
"@originjs/vite-plugin-commonjs": "^1.0.3", "@originjs/vite-plugin-commonjs": "^1.0.3",
"@rollup/pluginutils": "^5.1.4",
"@tsconfig/node22": "^22.0.1", "@tsconfig/node22": "^22.0.1",
"@types/markdown-it": "^14.1.2",
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",
"@vue/babel-plugin-jsx": "^1.4.0",
"@vue/devtools-core": "^7.7.6",
"@vue/tsconfig": "^0.7.0", "@vue/tsconfig": "^0.7.0",
"error-stack-parser-es": "^0.1.5",
"kolorist": "^1.8.0",
"npm-run-all2": "^7.0.2", "npm-run-all2": "^7.0.2",
"rollup": "^4.41.0",
"sirv": "^3.0.1",
"typescript": "~5.8.0", "typescript": "~5.8.0",
"vite": "^6.2.4", "vite": "^6.2.4",
"vite-plugin-vue-devtools": "^7.7.2", "vite-plugin-vue-devtools": "^7.7.2",

Binary file not shown.

View File

@ -0,0 +1,43 @@
import fsPath from 'node:path';
import fs from 'node:fs';
import * as process from "node:process";
function createDirIfExists(filePath) {
if (!fs.existsSync(filePath)) {
fs.mkdirSync(filePath, { recursive: true })
}
}
function recreateDir(filePath) {
if (fs.existsSync(filePath)) {
fs.rmSync(filePath, { recursive: true, force: true });
}
fs.mkdirSync(filePath, { recursive: true });
}
const currentDir = process.cwd();
// 确保上级目录的 openmcp-sdk 存在
const openMCPSdkPath = fsPath.join(currentDir, '..', 'openmcp-sdk');
createDirIfExists(openMCPSdkPath);
const sdkRenderPath = fsPath.join(openMCPSdkPath, 'renderer');
recreateDir(sdkRenderPath);
const sourceDist = fsPath.join(currentDir, 'dist');
// 如果源目录不存在则报错
if (!fs.existsSync(sourceDist)) {
throw new Error(`Source directory not found: ${sourceDist}`)
}
fs.cpSync(sourceDist, sdkRenderPath, { recursive: true })
// electron目录
const electronOpenMcpSdkPath = fsPath.join(currentDir, '..', 'software', 'openmcp-sdk');
createDirIfExists(electronOpenMcpSdkPath);
const electronRendererPath = fsPath.join(electronOpenMcpSdkPath, 'renderer');
recreateDir(electronRendererPath);
fs.cpSync(sourceDist, electronRendererPath, { recursive: true })

View File

@ -4,7 +4,7 @@
<MainPanel></MainPanel> <MainPanel></MainPanel>
<Tour v-if="!userHasReadGuide"/> <Tour v-if="!userHasReadGuide"/>
<PasswordDialog v-if="password"/> <PasswordDialog v-if="useAuth"/>
</div> </div>
</template> </template>
@ -18,8 +18,7 @@ import MainPanel from '@/components/main-panel/index.vue';
import { setDefaultCss } from './hook/css'; import { setDefaultCss } from './hook/css';
import { greenLog, pinkLog } from './views/setting/util'; import { greenLog, pinkLog } from './views/setting/util';
import { useMessageBridge } from './api/message-bridge'; import { useMessageBridge } from './api/message-bridge';
import { initialise } from './views/connect/connection'; import { initialise } from './views/connect';
import { getPlatform } from './api/platform';
import Tour from '@/components/guide/tour.vue'; import Tour from '@/components/guide/tour.vue';
import { userHasReadGuide } from './components/guide/tour'; import { userHasReadGuide } from './components/guide/tour';
@ -37,8 +36,10 @@ bridge.addCommandListener('hello', data => {
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const password = Boolean(import.meta.env.VITE_USE_PASSWORD); const useAuth = Boolean(import.meta.env.VITE_USE_AUTH !== "false");
privilegeStatus.allow = !Boolean(password); console.log(import.meta.env.VITE_USE_AUTH, useAuth);
privilegeStatus.allow = !Boolean(useAuth);
onMounted(async () => { onMounted(async () => {
// css // css
@ -47,11 +48,11 @@ onMounted(async () => {
pinkLog('OpenMCP Client 启动'); pinkLog('OpenMCP Client 启动');
// //
if (route.name !== 'debug') { // if (route.name !== 'debug') {
const targetRoute = import.meta.env.BASE_URL + 'debug'; // const targetRoute = import.meta.env.BASE_URL + 'debug';
console.log('go to ' + targetRoute); // console.log('go to ' + targetRoute);
router.push(targetRoute); // router.push(targetRoute);
} // }
// //
await bridge.awaitForWebsocket(); await bridge.awaitForWebsocket();

View File

@ -7,9 +7,9 @@ export interface VSCodeMessage {
callbackId?: string; callbackId?: string;
} }
export interface RestFulResponse { export interface RestFulResponse<T = any> {
code: number; code: number;
msg: any; msg: T;
} }
export type MessageHandler = (message: VSCodeMessage) => void; export type MessageHandler = (message: VSCodeMessage) => void;
@ -19,6 +19,11 @@ interface AddCommandListenerOption {
once: boolean // 只调用一次就销毁 once: boolean // 只调用一次就销毁
} }
export interface ICommandRequestData {
clientId?: string;
[key: string]: any;
}
export class MessageBridge { export class MessageBridge {
private ws: WebSocket | null = null; private ws: WebSocket | null = null;
private handlers = new Map<string, Set<CommandHandler>>(); private handlers = new Map<string, Set<CommandHandler>>();
@ -206,7 +211,7 @@ export class MessageBridge {
* @param data * @param data
* @returns * @returns
*/ */
public commandRequest(command: string, data?: any) { 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);

View File

@ -58,7 +58,7 @@
</el-tour-step> </el-tour-step>
<el-tour-step <el-tour-step
:target="connectionSettingRef" :target="client.connectionSettingRef"
:prev-button-props="{ children: '上一步' }" :prev-button-props="{ children: '上一步' }"
:next-button-props="{ children: '下一步' }" :next-button-props="{ children: '下一步' }"
:show-close="false" :show-close="false"
@ -78,7 +78,7 @@
</el-tour-step> </el-tour-step>
<el-tour-step <el-tour-step
:target="connectionLogRef" :target="client.connectionLogRef"
:prev-button-props="{ children: '上一步' }" :prev-button-props="{ children: '上一步' }"
:next-button-props="{ children: '下一步' }" :next-button-props="{ children: '下一步' }"
:show-close="false" :show-close="false"
@ -249,16 +249,17 @@ import TourTitle from './tour-title.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { welcomeRef } from '@/views/debug/welcome'; import { welcomeRef } from '@/views/debug/welcome';
import { connectionLogRef, connectionSettingRef } from '@/views/connect/connection';
import { llmSettingRef } from '@/views/setting/api'; import { llmSettingRef } from '@/views/setting/api';
import { userHasReadGuide } from './tour'; import { userHasReadGuide } from './tour';
import { setTour } from '@/hook/setting'; import { setTour } from '@/hook/setting';
import { mcpClientAdapter } from '@/views/connect/core';
const openTour = ref(true); const openTour = ref(true);
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const client = mcpClientAdapter.masterNode;
const baseUrl = import.meta.env.BASE_URL; const baseUrl = import.meta.env.BASE_URL;

View File

@ -44,6 +44,8 @@ import { panelLoaded } from '@/hook/panel';
defineComponent({ name: 'main-panel' }); defineComponent({ name: 'main-panel' });
const baseURL = import.meta.env.BASE_URL;
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -52,7 +54,7 @@ function pageAddNewTab() {
// debug debug // debug debug
if (route.name !== 'debug') { if (route.name !== 'debug') {
router.replace('/debug'); router.push(baseURL + 'debug');
} }
} }
@ -61,7 +63,7 @@ function setActiveTab(index: number) {
tabs.activeIndex = index; tabs.activeIndex = index;
// debug // debug
if (route.name !== 'debug') { if (route.name !== 'debug') {
router.replace('/debug'); router.push(baseURL + 'debug');
} }
} }
} }

View File

@ -48,7 +48,7 @@
import { defineComponent, defineProps, computed, ref } from 'vue'; import { defineComponent, defineProps, computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { tabs } from '../panel'; import { tabs } from '../panel';
import { ResourceStorage } from './resources'; import type { ResourceStorage } from './resources';
import { getImageBlobUrlByBase64 } from '@/hook/util'; import { getImageBlobUrlByBase64 } from '@/hook/util';
import JsonRender from '@/components/json-render/index.vue'; import JsonRender from '@/components/json-render/index.vue';

View File

@ -18,7 +18,7 @@ import { ref, onMounted } from 'vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { privilegeStatus } from './status'; import { privilegeStatus } from './status';
import { useMessageBridge } from '@/api/message-bridge'; import { useMessageBridge } from '@/api/message-bridge';
import { initialise } from '@/views/connect/connection'; import { initialise } from '@/views/connect';
const dialogVisible = ref(true); const dialogVisible = ref(true);

View File

@ -2,7 +2,7 @@
<div class="connected-status-container" <div class="connected-status-container"
id="connected-status-container" id="connected-status-container"
@click.stop="toggleConnectionPanel()" @click.stop="toggleConnectionPanel()"
:class="{ 'connected': connectionResult.success }" :class="{ 'connected': client.connectionResult.success }"
> >
<span class="mcp-server-info"> <span class="mcp-server-info">
<el-tooltip <el-tooltip
@ -15,7 +15,7 @@
</el-tooltip> </el-tooltip>
</span> </span>
<span class="connect-status"> <span class="connect-status">
<span v-if="connectionResult.success"> <span v-if="client.connectionResult.success">
<span class="iconfont icon-connect"></span> <span class="iconfont icon-connect"></span>
<span class="iconfont icon-dui"></span> <span class="iconfont icon-dui"></span>
</span> </span>
@ -32,18 +32,19 @@
import { defineComponent, computed } from 'vue'; import { defineComponent, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { Connection } from './sidebar'; import { Connection } from './sidebar';
import { connectionResult } from '@/views/connect/connection'; import { mcpClientAdapter } from '@/views/connect/core';
defineComponent({ name: 'connected' }); defineComponent({ name: 'connected' });
const { t } = useI18n(); const { t } = useI18n();
const client = mcpClientAdapter.masterNode;
const fullDisplayServerName = computed(() => { const fullDisplayServerName = computed(() => {
return connectionResult.serverInfo.name + '/' + connectionResult.serverInfo.version; return client.connectionResult.name + '/' + client.connectionResult.version;
}); });
const displayServerName = computed(() => { const displayServerName = computed(() => {
const name = connectionResult.serverInfo.name; const name = client.connectionResult.name;
if (name.length <= 3) return name; if (name.length <= 3) return name;
// //

View File

@ -1,8 +1,9 @@
import { useMessageBridge } from "@/api/message-bridge"; import { useMessageBridge } from "@/api/message-bridge";
import { pinkLog } from "@/views/setting/util"; import { pinkLog } from "@/views/setting/util";
import { debugModes, tabs } from "@/components/main-panel/panel"; import { debugModes, tabs } from "@/components/main-panel/panel";
import { markRaw, ref, nextTick } from "vue"; import { markRaw, ref } from "vue";
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import type { McpClient } from "@/views/connect/core";
interface SaveTabItem { interface SaveTabItem {
name: string; name: string;
@ -12,32 +13,30 @@ interface SaveTabItem {
storage: Record<string, any>; storage: Record<string, any>;
} }
interface SaveTab { export interface SaveTab {
tabs: SaveTabItem[] tabs: SaveTabItem[]
currentIndex: number currentIndex: number
} }
export const panelLoaded = ref(false); export const panelLoaded = ref(false);
export function loadPanels() { export async function loadPanels(client: McpClient) {
return new Promise((resolve, reject) => {
const bridge = useMessageBridge(); const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest<SaveTab>('panel/load', {
bridge.addCommandListener('panel/load', data => { clientId: client.clientId
if (data.code !== 200) { });
if (code !== 200) {
pinkLog('tabs 加载失败'); pinkLog('tabs 加载失败');
console.log(data.msg); console.log(msg);
} else { } else {
const persistTab = data.msg as SaveTab; const persistTab = msg;
pinkLog('tabs 加载成功'); pinkLog('tabs 加载成功');
if (persistTab.tabs.length === 0) { if (persistTab.tabs.length === 0) {
// 空的,直接返回不需要管 // 空的,直接返回不需要管
panelLoaded.value = true; panelLoaded.value = true;
resolve(void 0);
return; return;
} }
@ -63,16 +62,9 @@ export function loadPanels() {
} }
panelLoaded.value = true; panelLoaded.value = true;
resolve(void 0);
}, { once: true });
bridge.postMessage({
command: 'panel/load'
});
});
} }
let debounceHandler: NodeJS.Timeout; let debounceHandler: number;
export function safeSavePanels() { export function safeSavePanels() {
clearTimeout(debounceHandler); clearTimeout(debounceHandler);

View File

@ -149,7 +149,7 @@ export type APIRequest =
| ToolCallRequest; | ToolCallRequest;
export interface IStdioConnectionItem { export interface IStdioConnectionItem {
type: 'stdio'; type: 'STDIO';
name: string; name: string;
command: string; command: string;
args: string[]; args: string[];
@ -159,7 +159,7 @@ export interface IStdioConnectionItem {
} }
export interface ISSEConnectionItem { export interface ISSEConnectionItem {
type: 'sse'; type: 'SSE';
name: string; name: string;
url: string; url: string;
oauth?: string; oauth?: string;
@ -169,13 +169,13 @@ export interface ISSEConnectionItem {
export interface IStdioLaunchSignature { export interface IStdioLaunchSignature {
type: 'stdio'; type: 'STDIO';
commandString: string; commandString: string;
cwd: string; cwd: string;
} }
export interface ISSELaunchSignature { export interface ISSELaunchSignature {
type:'sse'; type:'SSE';
url: string; url: string;
oauth: string; oauth: string;
} }

View File

@ -6,28 +6,28 @@ const routes: Array<RouteRecordRaw> = [
{ {
name : "default", name : "default",
path : "/", path : "/",
redirect : baseURL + "/debug" redirect : baseURL + "debug"
}, },
{ {
path: baseURL + "/debug", path: baseURL + "debug",
name: "debug", name: "debug",
component: () => import( /* webpackMode: "eager" */ "@/views/debug/index.vue"), component: () => import( /* webpackMode: "eager" */ "@/views/debug/index.vue"),
meta: { title: "Debug" } meta: { title: "Debug" }
}, },
{ {
path: baseURL + "/connect", path: baseURL + "connect",
name: "connect", name: "connect",
component: () => import( /* webpackMode: "eager" */ "@/views/connect/index.vue"), component: () => import( /* webpackMode: "eager" */ "@/views/connect/index.vue"),
meta: { title: "Connect" } meta: { title: "Connect" }
}, },
{ {
path: baseURL + "/setting", path: baseURL + "setting",
name: "setting", name: "setting",
component: () => import( /* webpackMode: "eager" */ "@/views/setting/index.vue"), component: () => import( /* webpackMode: "eager" */ "@/views/setting/index.vue"),
meta: { title: "Setting" } meta: { title: "Setting" }
}, },
{ {
path: baseURL + "/about", path: baseURL + "about",
name: "about", name: "about",
component: () => import( /* webpackMode: "eager" */ "@/views/about/index.vue"), component: () => import( /* webpackMode: "eager" */ "@/views/about/index.vue"),
meta: { title: "Tools" } meta: { title: "Tools" }

View File

@ -6,7 +6,7 @@
</span> </span>
<p> <p>
OpenMCP Client 0.0.8 OpenMCP@<a href="https://www.zhihu.com/people/can-meng-zhong-de-che-xian">锦恢</a> 开发 OpenMCP Client 0.0.9 OpenMCP@<a href="https://www.zhihu.com/people/can-meng-zhong-de-che-xian">锦恢</a> 开发
</p> </p>
<p> <p>

View File

@ -1,19 +1,19 @@
<template> <template>
<!-- STDIO 模式下的命令输入 --> <!-- STDIO 模式下的命令输入 -->
<div class="connection-option" v-if="connectionMethods.current === 'STDIO'"> <div class="connection-option" v-if="client.connectionArgs.type === 'STDIO'">
<span>{{ t('connect-sigature') }}</span> <span>{{ t('connect-sigature') }}</span>
<span style="width: 310px;"> <span style="width: 310px;">
<el-form :model="connectionArgs" :rules="rules" ref="stdioForm"> <el-form :model="client.connectionArgs" :rules="rules" ref="stdioForm">
<el-form-item prop="commandString"> <el-form-item prop="commandString">
<div class="input-with-label"> <div class="input-with-label">
<span class="input-label">{{ t("command") }}</span> <span class="input-label">{{ t("command") }}</span>
<el-input v-model="connectionArgs.commandString" placeholder="mcp run <your script>"></el-input> <el-input v-model="client.connectionArgs.commandString" placeholder="mcp run <your script>"></el-input>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item prop="cwd"> <el-form-item prop="cwd">
<div class="input-with-label"> <div class="input-with-label">
<span class="input-label">{{ t('cwd') }}</span> <span class="input-label">{{ t('cwd') }}</span>
<el-input v-model="connectionArgs.cwd" placeholder="cwd, 可为空"></el-input> <el-input v-model="client.connectionArgs.cwd" placeholder="cwd, 可为空"></el-input>
</div> </div>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -24,17 +24,17 @@
<div class="connection-option" v-else> <div class="connection-option" v-else>
<span>{{ t('connect-sigature') }}</span> <span>{{ t('connect-sigature') }}</span>
<span style="width: 310px;"> <span style="width: 310px;">
<el-form :model="connectionArgs" :rules="rules" ref="urlForm"> <el-form :model="client.connectionArgs" :rules="rules" ref="urlForm">
<el-form-item prop="urlString"> <el-form-item prop="url">
<div class="input-with-label"> <div class="input-with-label">
<span class="input-label">URL</span> <span class="input-label">URL</span>
<el-input v-model="connectionArgs.urlString" placeholder="http://"></el-input> <el-input v-model="client.connectionArgs.url" placeholder="http://"></el-input>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item prop="oauth"> <el-form-item prop="oauth">
<div class="input-with-label"> <div class="input-with-label">
<span class="input-label">OAuth</span> <span class="input-label">OAuth</span>
<el-input v-model="connectionArgs.oauth" placeholder="认证签名, 可为空"></el-input> <el-input v-model="client.connectionArgs.oauth" placeholder="认证签名, 可为空"></el-input>
</div> </div>
</el-form-item> </el-form-item>
</el-form> </el-form>
@ -43,14 +43,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus'; import type { FormInstance, FormRules } from 'element-plus';
import { connectionArgs, connectionMethods } from './connection'; import { mcpClientAdapter } from './core';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({
index: {
type: Number,
required: true
}
});
const client = mcpClientAdapter.clients[props.index];
const stdioForm = ref<FormInstance>() const stdioForm = ref<FormInstance>()
const urlForm = ref<FormInstance>() const urlForm = ref<FormInstance>()
@ -65,7 +74,7 @@ const rules = reactive<FormRules>({
oauth: [ oauth: [
{ required: false, trigger: 'blur' } { required: false, trigger: 'blur' }
], ],
urlString: [ url: [
{ required: true, message: 'URL不能为空', trigger: 'blur' } { required: true, message: 'URL不能为空', trigger: 'blur' }
] ]
}) })
@ -73,7 +82,7 @@ const rules = reactive<FormRules>({
// //
const validateForm = async () => { const validateForm = async () => {
try { try {
if (connectionMethods.current === 'STDIO') { if (client.connectionArgs.type === 'STDIO') {
await stdioForm.value?.validate() await stdioForm.value?.validate()
} else { } else {
await urlForm.value?.validate() await urlForm.value?.validate()

View File

@ -5,7 +5,7 @@
<el-switch <el-switch
v-model="envEnabled" v-model="envEnabled"
@change="handleEnvSwitch" @change="(enable: boolean) => client.handleEnvSwitch(enable)"
inline-prompt inline-prompt
active-text="预设" active-text="预设"
inactive-text="预设" inactive-text="预设"
@ -14,10 +14,10 @@
<div class="input-env"> <div class="input-env">
<span class="input-env-container"> <span class="input-env-container">
<span> <span>
<el-input v-model="connectionEnv.newKey" @keyup.enter="addEnvVar"></el-input> <el-input v-model="client.connectionEnvironment.newKey" @keyup.enter="addEnvVar"></el-input>
</span> </span>
<span> <span>
<el-input v-model="connectionEnv.newValue" @keyup.enter="addEnvVar"></el-input> <el-input v-model="client.connectionEnvironment.newValue" @keyup.enter="addEnvVar"></el-input>
</span> </span>
<span> <span>
<div @click="addEnvVar"> <div @click="addEnvVar">
@ -28,7 +28,7 @@
</div> </div>
<el-scrollbar height="200px" width="350px" class="display-env-container"> <el-scrollbar height="200px" width="350px" class="display-env-container">
<div class="display-env"> <div class="display-env">
<div class="input-env-container" v-for="option of connectionEnv.data" :key="option.key"> <div class="input-env-container" v-for="option of client.connectionEnvironment.data" :key="option.key">
<span> <el-input v-model="option.key"></el-input></span> <span> <el-input v-model="option.key"></el-input></span>
<span> <el-input v-model="option.value" show-password></el-input></span> <span> <el-input v-model="option.value" show-password></el-input></span>
<span @click="deleteEnvVar(option)"> <span @click="deleteEnvVar(option)">
@ -44,9 +44,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, ref } from 'vue'; import { defineComponent, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { connectionEnv, type EnvItem, handleEnvSwitch } from './connection'; import { mcpClientAdapter } from './core';
import type { EnvItem } from './type';
defineComponent({ name: 'env-var' }); defineComponent({ name: 'env-var' });
const props = defineProps({
index: {
type: Number,
required: true
}
});
const client = mcpClientAdapter.clients[props.index];
const { t } = useI18n(); const { t } = useI18n();
@ -55,24 +64,24 @@ const { t } = useI18n();
*/ */
function addEnvVar() { function addEnvVar() {
// key // key
const currentKey = connectionEnv.newKey; const currentKey = client.connectionEnvironment.newKey;
const currentValue = connectionEnv.newValue; const currentValue = client.connectionEnvironment.newValue;
if (currentKey.length === 0 || currentValue.length === 0) { if (currentKey.length === 0 || currentValue.length === 0) {
return; return;
} }
const sameNameItems = connectionEnv.data.filter(item => item.key === currentKey); const sameNameItems = client.connectionEnvironment.data.filter(item => item.key === currentKey);
if (sameNameItems.length > 0) { if (sameNameItems.length > 0) {
const conflictItem = sameNameItems[0]; const conflictItem = sameNameItems[0];
conflictItem.value = currentValue; conflictItem.value = currentValue;
} else { } else {
connectionEnv.data.push({ client.connectionEnvironment.data.push({
key: currentKey, value: currentValue key: currentKey, value: currentValue
}); });
connectionEnv.newKey = ''; client.connectionEnvironment.newKey = '';
connectionEnv.newValue = ''; client.connectionEnvironment.newValue = '';
} }
} }
@ -81,11 +90,10 @@ function addEnvVar() {
*/ */
function deleteEnvVar(option: EnvItem) { function deleteEnvVar(option: EnvItem) {
const currentKey = option.key; const currentKey = option.key;
const reserveItems = connectionEnv.data.filter(item => item.key !== currentKey); const reserveItems = client.connectionEnvironment.data.filter(item => item.key !== currentKey);
connectionEnv.data = reserveItems; client.connectionEnvironment.data = reserveItems;
} }
const envEnabled = ref(true); const envEnabled = ref(true);
</script> </script>

View File

@ -3,7 +3,7 @@
<span>{{ t('log') }}</span> <span>{{ t('log') }}</span>
<el-scrollbar height="90%"> <el-scrollbar height="90%">
<div class="output-content"> <div class="output-content">
<div v-for="(log, index) in connectionResult.logString" :key="index" :class="log.type"> <div v-for="(log, index) in client.connectionResult.logString" :key="index" :class="log.type">
<span class="log-message">{{ log.message }}</span> <span class="log-message">{{ log.message }}</span>
</div> </div>
</div> </div>
@ -14,9 +14,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { connectionResult } from './connection'; import { mcpClientAdapter } from './core';
defineComponent({ name: 'connection-log' }); defineComponent({ name: 'connection-log' });
const props = defineProps({
index: {
type: Number,
required: true
}
});
const client = mcpClientAdapter.clients[props.index];
const { t } = useI18n(); const { t } = useI18n();

View File

@ -2,8 +2,8 @@
<div class="connection-option"> <div class="connection-option">
<span>{{ t('connection-method') }}</span> <span>{{ t('connection-method') }}</span>
<span style="width: 200px;"> <span style="width: 200px;">
<el-select name="language-setting" class="language-setting" v-model="connectionMethods.current"> <el-select name="language-setting" class="language-setting" v-model="client.connectionArgs.type">
<el-option v-for="option in connectionMethods.data" :value="option.value" :label="option.label" <el-option v-for="option in connectionSelectDataViewOption" :value="option.value" :label="option.label"
:key="option.label"></el-option> :key="option.label"></el-option>
</el-select> </el-select>
</span> </span>
@ -13,9 +13,17 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { connectionMethods } from './connection'; import { connectionSelectDataViewOption, mcpClientAdapter } from './core';
defineComponent({ name: 'connection-method' }); defineComponent({ name: 'connection-method' });
const props = defineProps({
index: {
type: Number,
required: true
}
});
const client = mcpClientAdapter.clients[props.index];
const { t } = useI18n(); const { t } = useI18n();

View File

@ -0,0 +1,143 @@
<template>
<el-scrollbar>
<div class="connection-container">
<div class="connect-panel-container"
:ref="el => client.connectionSettingRef = el"
>
<ConnectionMethod :index="props.index" />
<ConnectionArgs :index="props.index" />
<ConnectionEnvironment :index="props.index" />
<div class="connect-action">
<el-button type="primary" size="large" :loading="isLoading" :disabled="!client.connectionResult"
@click="connect()">
<span class="iconfont icon-connect" v-if="!isLoading"></span>
{{ t('connect.appearance.connect') }}
</el-button>
</div>
</div>
<div class="connect-panel-container"
:ref="el => client.connectionLogRef = el"
>
<ConnectionLog :index="props.index" />
</div>
</div>
</el-scrollbar>
</template>
<script setup lang="ts">
import { defineComponent, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import ConnectionMethod from './connection-method.vue';
import ConnectionArgs from './connection-args.vue';
import ConnectionEnvironment from './connection-environment.vue';
import ConnectionLog from './connection-log.vue';
import { getPlatform } from '@/api/platform';
import { mcpClientAdapter } from './core';
defineComponent({ name: 'connection-panel' });
const props = defineProps({
index: {
type: Number,
required: true
}
});
const client = mcpClientAdapter.clients[props.index];
console.log(client);
console.log(client.connectionSettingRef);
const { t } = useI18n();
const isLoading = ref(false);
async function connect() {
isLoading.value = true;
const platform = getPlatform();
const ok = await client.connect();
if (ok) {
mcpClientAdapter.saveLaunchSignature();
}
isLoading.value = false;
}
</script>
<style>
.connection-container {
display: flex;
}
.connect-panel-container {
display: flex;
flex-direction: column;
width: 45%;
min-width: 300px;
padding: 20px;
}
.connection-option {
display: flex;
flex-direction: column;
background-color: var(--background);
padding: 10px;
margin-bottom: 20px;
border-radius: .5em;
border: 1px solid var(--background);
}
.connection-option>span:first-child {
margin-bottom: 5px;
}
.input-env-container {
display: flex;
margin-bottom: 10px;
}
.display-env {
padding-top: 10px;
padding-bottom: 10px;
}
.input-env-container>span {
width: 150px;
margin-right: 10px;
display: flex;
height: 30px;
align-items: center;
}
.input-env-container .iconfont {
font-size: 20px;
border-radius: 99em;
color: var(--foreground);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--animation-3s);
user-select: none;
}
.input-env-container .iconfont:hover {
color: var(--main-color);
transition: var(--animation-3s);
}
.connect-action {
margin-top: 20px;
padding: 10px;
}
</style>

View File

@ -1,383 +0,0 @@
import { useMessageBridge } from '@/api/message-bridge';
import { reactive, ref } from 'vue';
import { pinkLog } from '../setting/util';
import { ElLoading, ElMessage } from 'element-plus';
import { getPlatform, type OpenMcpSupportPlatform } from '@/api/platform';
import { getTour, loadSetting } from '@/hook/setting';
import { loadPanels } from '@/hook/panel';
export const connectionMethods = reactive({
current: 'STDIO',
data: [
{
value: 'STDIO',
label: 'STDIO'
},
{
value: 'SSE',
label: 'SSE'
}
]
});
export const connectionSettingRef = ref<any>(null);
export const connectionLogRef = ref<any>(null);
export const connectionArgs = reactive({
commandString: '',
cwd: '',
oauth: '',
urlString: ''
});
export interface EnvItem {
key: string
value: string
}
export interface IConnectionEnv {
data: EnvItem[]
newKey: string
newValue: string
}
export const connectionEnv = reactive<IConnectionEnv>({
data: [],
newKey: '',
newValue: ''
});
export function makeEnv() {
const env = {} as Record<string, string>;
connectionEnv.data.forEach(item => {
env[item.key] = item.value;
});
return env;
}
// 定义连接类型
type ConnectionType = 'STDIO' | 'SSE';
// 定义命令行参数接口
export interface McpOptions {
connectionType: ConnectionType;
// STDIO 特定选项
command?: string;
args?: string[];
cwd?: string;
env?: Record<string, string>;
// SSE 特定选项
url?: string;
// 通用客户端选项
clientName?: string;
clientVersion?: string;
}
export async function doConnect(
option: {
namespace: OpenMcpSupportPlatform
updateCommandString?: boolean
}
) {
const {
// updateCommandString 为 true 代表是初始化阶段
namespace,
updateCommandString = true
} = option;
if (updateCommandString) {
pinkLog('请求启动参数');
const connectionItem = await getLaunchSignature(namespace + '/launch-signature');
if (connectionItem.type ==='stdio') {
connectionMethods.current = 'STDIO';
connectionArgs.commandString = connectionItem.commandString;
connectionArgs.cwd = connectionItem.cwd;
if (connectionArgs.commandString.length === 0) {
return;
}
} else {
connectionMethods.current = 'SSE';
connectionArgs.urlString = connectionItem.url || '';
if (connectionArgs.urlString.length === 0) {
return;
}
}
}
if (connectionMethods.current === 'STDIO') {
await launchStdio(namespace);
} else {
await launchSSE(namespace);
}
}
async function launchStdio(namespace: string) {
const bridge = useMessageBridge();
const env = makeEnv();
const commandComponents = connectionArgs.commandString.split(/\s+/g);
const command = commandComponents[0];
commandComponents.shift();
const connectOption = {
connectionType: 'STDIO',
command: command,
args: commandComponents,
cwd: connectionArgs.cwd,
clientName: 'openmcp.connect.stdio',
clientVersion: '0.0.1',
env
};
const { code, msg } = await bridge.commandRequest('connect', connectOption);
connectionResult.success = (code === 200);
if (code === 200) {
connectionResult.logString.push({
type: 'info',
message: msg
});
const res = await getServerVersion() as { name: string, version: string };
connectionResult.serverInfo.name = res.name || '';
connectionResult.serverInfo.version = res.version || '';
// 同步信息到 vscode
const commandComponents = connectionArgs.commandString.split(/\s+/g);
const command = commandComponents[0];
commandComponents.shift();
const clientStdioConnectionItem = {
serverInfo: connectionResult.serverInfo,
connectionType: 'STDIO',
name: 'openmcp.connect.stdio',
command: command,
args: commandComponents,
cwd: connectionArgs.cwd,
env
};
bridge.postMessage({
command: namespace + '/update-connection-sigature',
data: JSON.parse(JSON.stringify(clientStdioConnectionItem))
});
} else {
connectionResult.logString.push({
type: 'error',
message: msg
});
ElMessage.error(msg);
}
}
async function launchSSE(namespace: string) {
const bridge = useMessageBridge();
const env = makeEnv();
const connectOption: McpOptions = {
connectionType: 'SSE',
url: connectionArgs.urlString,
clientName: 'openmcp.connect.sse',
clientVersion: '0.0.1',
env
};
const { code, msg } = await bridge.commandRequest('connect', connectOption);
connectionResult.success = (code === 200);
if (code === 200) {
connectionResult.logString.push({
type: 'info',
message: msg
});
const res = await getServerVersion() as { name: string, version: string };
connectionResult.serverInfo.name = res.name || '';
connectionResult.serverInfo.version = res.version || '';
// 同步信息到 vscode
const clientSseConnectionItem = {
serverInfo: connectionResult.serverInfo,
connectionType: 'SSE',
name: 'openmcp.connect.sse',
url: connectionArgs.urlString,
oauth: connectionArgs.oauth,
env: env
};
bridge.postMessage({
command: namespace + '/update-connection-sigature',
data: JSON.parse(JSON.stringify(clientSseConnectionItem))
});
} else {
connectionResult.logString.push({
type: 'error',
message: msg
});
ElMessage.error(msg);
}
}
async function getLaunchSignature(signatureName: string) {
const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest(signatureName);
return msg;
}
export function doReconnect() {
// TODO: finish this
console.log();
}
export const connectionResult = reactive<{
success: boolean,
logString: { type: 'info' | 'error' | 'warning', message: string }[],
serverInfo: {
name: string,
version: string
}
}>({
success: false,
logString: [],
serverInfo: {
name: '',
version: ''
}
});
export function getServerVersion() {
return new Promise((resolve, reject) => {
const bridge = useMessageBridge();
bridge.addCommandListener('server/version', data => {
if (data.code === 200) {
resolve(data.msg);
} else {
reject(data.msg);
}
}, { once: true });
bridge.postMessage({
command: 'server/version',
});
});
}
export const envVarStatus = {
launched: false
};
function lookupEnvVar(varNames: string[]) {
const bridge = useMessageBridge();
return new Promise<string[] | undefined>((resolve, reject) => {
bridge.addCommandListener('lookup-env-var', data => {
const { code, msg } = data;
if (code === 200) {
connectionResult.logString.push({
type: 'info',
message: '预设环境变量同步完成'
});
resolve(msg);
} else {
connectionResult.logString.push({
type: 'error',
message: '预设环境变量同步失败: ' + msg
});
resolve(undefined);
}
}, { once: true });
console.log(varNames);
bridge.postMessage({
command: 'lookup-env-var',
data: {
keys: varNames
}
})
});
}
export async function handleEnvSwitch(enabled: boolean) {
const presetVars = ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER'];
if (enabled) {
const values = await lookupEnvVar(presetVars);
if (values) {
// 将 key values 合并进 connectionEnv.data 中
// 若已有相同的 key, 则替换 value
for (let i = 0; i < presetVars.length; i++) {
const key = presetVars[i];
const value = values[i];
const sameNameItems = connectionEnv.data.filter(item => item.key === key);
if (sameNameItems.length > 0) {
const conflictItem = sameNameItems[0];
conflictItem.value = value;
} else {
connectionEnv.data.push({
key: key, value: value
});
}
}
}
} else {
// 清空 connectionEnv.data 中所有 key 为 presetVars 的项
const reserveItems = connectionEnv.data.filter(item => !presetVars.includes(item.key));
connectionEnv.data = reserveItems;
}
}
export async function loadEnvVar() {
return await handleEnvSwitch(true);
}
export async function initialise() {
pinkLog('准备请求设置');
const loading = ElLoading.service({
fullscreen: true,
lock: true,
text: 'Loading',
background: 'rgba(0, 0, 0, 0.7)'
});
const platform = getPlatform();
// 加载全局设置
loadSetting();
// 设置环境变量
await loadEnvVar();
// 获取引导状态
await getTour();
// 尝试进行初始化连接
await doConnect({
namespace: platform,
updateCommandString: true
});
// loading panels
await loadPanels();
loading.close();
}

View File

@ -0,0 +1,331 @@
import { useMessageBridge } from "@/api/message-bridge";
import { reactive } from "vue";
import type { IConnectionResult, ConnectionTypeOptionItem, IConnectionArgs, IConnectionEnvironment, McpOptions } from "./type";
import { ElMessage } from "element-plus";
import { loadPanels } from "@/hook/panel";
import { getPlatform } from "@/api/platform";
export const connectionSelectDataViewOption: ConnectionTypeOptionItem[] = [
{
value: 'STDIO',
label: 'STDIO'
},
{
value: 'SSE',
label: 'SSE'
},
{
value: 'STREAMABLE_HTTP',
label: 'STREAMABLE_HTTP'
}
]
export class McpClient {
// 连接入参
public connectionArgs: IConnectionArgs;
// 连接出参
public connectionResult: IConnectionResult;
// 预设环境变量,初始化的时候会去获取它们
public presetsEnvironment: string[] = ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER'];
// 环境变量
public connectionEnvironment: IConnectionEnvironment;
// logger 面板的 ref
public connectionLogRef: any = null;
// setting 面板的 ref
public connectionSettingRef: any = null;
constructor(
public clientVersion: string = '0.0.1',
public clientNamePrefix: string = 'openmcp.connect'
) {
// 连接入参
this.connectionArgs = {
type: 'STDIO',
commandString: '',
cwd: '',
url: '',
oauth: ''
};
// 连接出参
this.connectionResult = {
success: false,
status: 'disconnected',
clientId: '',
name: '',
version: '',
logString: []
};
// 环境变量
this.connectionEnvironment = {
data: [],
newKey: '',
newValue: ''
};
}
async acquireConnectionSignature(args: IConnectionArgs) {
this.connectionArgs.type = args.type;
this.connectionArgs.commandString = args.commandString || '';
this.connectionArgs.cwd = args.cwd || '';
this.connectionArgs.url = args.url || '';
this.connectionArgs.oauth = args.oauth || '';
}
get clientId() {
return this.connectionResult.clientId;
}
get name() {
return this.connectionResult.name;
}
get version() {
return this.connectionResult.version;
}
get status() {
return this.connectionResult.status;
}
get connected() {
return this.connectionResult.success;
}
get env() {
const env = {} as Record<string, string>;
this.connectionEnvironment.data.forEach(item => {
env[item.key] = item.value;
});
return env;
}
private get commandAndArgs() {
const commandString = this.connectionArgs.commandString;
if (!commandString) {
return { command: '', args: [] };
}
const args = commandString.split(' ');
const command = args.shift() || '';
return { command, args };
}
get connectOption() {
const { command, args } = this.commandAndArgs;
const env = this.env;
const url = this.connectionArgs.url;
const cwd = this.connectionArgs.cwd;
const oauth = this.connectionArgs.oauth;
const connectionType = this.connectionArgs.type;
const clientName = this.clientNamePrefix + '.' + this.connectionArgs.type;
const clientVersion = this.clientVersion;
const option: McpOptions = {
connectionType,
command,
args,
url,
cwd,
oauth,
clientName,
clientVersion,
env,
serverInfo: {
name: this.connectionResult.name,
version: this.connectionResult.version
}
};
return option;
}
public async connect() {
const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest<IConnectionResult>('connect', this.connectOption);
this.connectionResult.success = (code === 200);
if (code !== 200) {
const message = msg.toString();
this.connectionResult.logString.push({
type: 'error',
message
});
ElMessage.error(message);
return false;
} else {
this.connectionResult.logString.push({
type: 'info',
message: msg.info || ''
})
}
this.connectionResult.status = msg.status;
this.connectionResult.clientId = msg.clientId;
this.connectionResult.name = msg.name;
this.connectionResult.version = msg.version;
return true;
}
/**
* @description
* -
* -
* @param enabled
*/
public async handleEnvSwitch(enabled: boolean) {
const presetVars = this.presetsEnvironment;
if (enabled) {
const values = await this.lookupEnvVar(presetVars);
if (values) {
// 将 key values 合并进 connectionEnv.data 中
// 若已有相同的 key, 则替换 value
for (let i = 0; i < presetVars.length; i++) {
const key = presetVars[i];
const value = values[i];
const sameNameItems = this.connectionEnvironment.data.filter(item => item.key === key);
if (sameNameItems.length > 0) {
const conflictItem = sameNameItems[0];
conflictItem.value = value;
} else {
this.connectionEnvironment.data.push({
key: key, value: value
});
}
}
}
} else {
// 清空 connectionEnv.data 中所有 key 为 presetVars 的项
const reserveItems = this.connectionEnvironment.data.filter(item => !presetVars.includes(item.key));
this.connectionEnvironment.data = reserveItems;
}
}
/**
* @description
* @param varNames
* @returns
*/
public async lookupEnvVar(varNames: string[]) {
const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest('lookup-env-var', { keys: varNames });
if (code === 200) {
this.connectionResult.logString.push({
type: 'info',
message: '预设环境变量同步完成'
});
return msg;
} else {
this.connectionResult.logString.push({
type: 'error',
message: '预设环境变量同步失败: ' + msg
});
}
}
}
class McpClientAdapter {
public clients: McpClient[] = [];
public currentClientIndex: number = 0;
private defaultClient: McpClient = new McpClient();
constructor(
public platform: string
) { }
/**
* @description
* @returns
*/
public async getLaunchSignature(): Promise<IConnectionArgs[]> {
const bridge = useMessageBridge();
const { code, msg } = await bridge.commandRequest(this.platform + '/launch-signature');
if (code !== 200) {
const message = msg.toString();
ElMessage.error(message);
return [];
}
// 判断一下版本,新版本的 msg 应该是数组,老版本是对象
// 返回的数组的第一个为主节点,其余为从节点
if (Array.isArray(msg)) {
return msg;
}
return [msg];
}
get masterNode() {
if (this.clients.length === 0) {
return this.defaultClient;
}
return this.clients[0];
}
public async saveLaunchSignature() {
const bridge = useMessageBridge();
const options: McpOptions[] = this.clients.map(client => client.connectOption);
// 同步成功的连接参数到后端,更新 vscode treeview 中的列表
const deserializeOption = JSON.parse(JSON.stringify(options));
bridge.postMessage({
command: platform + '/update-connection-signature',
data: deserializeOption
});
}
public async launch() {
const launchSignature = await this.getLaunchSignature();
console.log('launchSignature', launchSignature);
let allOk = true;
for (const item of launchSignature) {
// 创建一个新的客户端
const client = new McpClient();
// 同步连接参数
await client.acquireConnectionSignature(item);
// 同步环境变量
await client.handleEnvSwitch(true);
// 连接
const ok = await client.connect(this.platform);
allOk &&= ok;
this.clients.push(client);
}
// 如果全部成功,保存连接参数
if (allOk) {
this.saveLaunchSignature();
}
}
public async loadPanels() {
const masterNode = this.clients[0];
await loadPanels(masterNode);
}
}
const platform = getPlatform();
export const mcpClientAdapter = reactive(
new McpClientAdapter(platform)
);

View File

@ -0,0 +1,30 @@
import { getTour, loadSetting } from "@/hook/setting";
import { ElLoading } from "element-plus";
import { pinkLog } from "../setting/util";
import { mcpClientAdapter } from "./core";
export async function initialise() {
pinkLog('准备请求设置');
const loading = ElLoading.service({
fullscreen: true,
lock: true,
text: 'Loading',
background: 'rgba(0, 0, 0, 0.7)'
});
// 加载全局设置
loadSetting();
// 获取引导状态
await getTour();
// 尝试进行初始化连接
await mcpClientAdapter.launch();
// loading panels
await mcpClientAdapter.loadPanels();
loading.close();
}

View File

@ -1,128 +1,105 @@
<template> <template>
<el-scrollbar>
<div class="connection-container"> <div class="connection-container">
<div class="connect-panel-container" <div class="server-list">
:ref="el => connectionSettingRef = el" <div v-for="(client, index) in mcpClientAdapter.clients" :key="index" class="server-item"
> :class="{ 'active': mcpClientAdapter.currentClientIndex === index }" @click="selectServer(index)">
<ConnectionMethod></ConnectionMethod> <span class="connect-status">
<ConnectionArgs></ConnectionArgs> <span v-if="client.connectionResult.success">
<EnvVar></EnvVar> <span class="iconfont icon-connect"></span>
<span class="iconfont icon-dui"></span>
<div class="connect-action"> </span>
<el-button type="primary" size="large" :loading="isLoading" :disabled="!connectionResult" <span v-else>
@click="suitableConnect()"> <span class="iconfont icon-connect"></span>
<span class="iconfont icon-connect" v-if="!isLoading"></span> <span class="server-name"> Unconnected </span>
{{ t('connect.appearance.connect') }} </span>
</el-button> </span>
</div>
<div class="add-server" @click="addServer">
<span class="iconfont icon-add"></span>
</div> </div>
</div> </div>
<div class="panel-container">
<div class="connect-panel-container" <ConnectionPanel v-if="mcpClientAdapter.clients.length > 0" :index="mcpClientAdapter.currentClientIndex" />
:ref="el => connectionLogRef = el"
>
<ConnectionLog></ConnectionLog>
</div> </div>
</div> </div>
</el-scrollbar>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent, ref } from 'vue'; import { defineComponent } from 'vue';
import { useI18n } from 'vue-i18n'; import ConnectionPanel from './connection-panel.vue';
import { McpClient, mcpClientAdapter } from './core';
import { ElMessage } from 'element-plus';
const { t } = useI18n(); defineComponent({ name: 'connection' });
import { connectionLogRef, connectionResult, connectionSettingRef, doConnect } from './connection'; function selectServer(index: number) {
mcpClientAdapter.currentClientIndex = index;
import ConnectionMethod from './connection-method.vue';
import ConnectionArgs from './connection-args.vue';
import EnvVar from './env-var.vue';
import ConnectionLog from './connection-log.vue';
import { getPlatform } from '@/api/platform';
defineComponent({ name: 'connect' });
const isLoading = ref(false);
async function suitableConnect() {
isLoading.value = true;
const plaform = getPlatform();
await doConnect({ namespace: plaform, updateCommandString: false })
isLoading.value = false;
} }
function addServer() {
ElMessage.info('Add server is not implemented yet');
mcpClientAdapter.clients.push(new McpClient());
mcpClientAdapter.currentClientIndex = mcpClientAdapter.clients.length - 1;
}
</script> </script>
<style> <style>
.connection-container { .connection-container {
display: flex; display: flex;
height: 100%;
} }
.server-list {
width: 150px;
border-right: 1px solid var(--border-color);
padding: 10px;
}
.connect-panel-container { .server-name {
font-size: 15px;
}
.server-item {
padding: 10px;
margin-bottom: 5px;
cursor: pointer;
border-radius: 4px;
display: flex; display: flex;
flex-direction: column; justify-content: space-between;
width: 45%; align-items: center;
min-width: 300px; }
.server-item.active {
background-color: var(--main-color);
color: white;
}
.server-status {
font-size: 12px;
}
.server-status.connected {
color: green;
}
.server-status.disconnected {
color: red;
}
.add-server {
padding: 10px;
text-align: center;
cursor: pointer;
border-radius: 4px;
border: 1px dashed var(--border-color);
}
.add-server:hover {
background-color: var(--background);
}
.panel-container {
flex: 1;
padding: 20px; padding: 20px;
} }
.connection-option {
display: flex;
flex-direction: column;
background-color: var(--background);
padding: 10px;
margin-bottom: 20px;
border-radius: .5em;
border: 1px solid var(--background);
}
.connection-option>span:first-child {
margin-bottom: 5px;
}
.input-env-container {
display: flex;
margin-bottom: 10px;
}
.display-env {
padding-top: 10px;
padding-bottom: 10px;
}
.input-env-container>span {
width: 150px;
margin-right: 10px;
display: flex;
height: 30px;
align-items: center;
}
.input-env-container .iconfont {
font-size: 20px;
border-radius: 99em;
color: var(--foreground);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: var(--animation-3s);
user-select: none;
}
.input-env-container .iconfont:hover {
color: var(--main-color);
transition: var(--animation-3s);
}
.connect-action {
margin-top: 20px;
padding: 10px;
}
</style> </style>

View File

@ -0,0 +1,81 @@
export type ConnectionType = 'STDIO' | 'SSE' | 'STREAMABLE_HTTP';
export interface ConnectionTypeOptionItem {
value: ConnectionType;
label: string;
}
export interface IConnectionArgs {
type: ConnectionType;
commandString?: string;
cwd?: string;
url?: string;
oauth?: string;
}
export interface IConnectionResult {
info?: string;
success: boolean;
status: string
clientId: string
name: string
version: string
logString: {
type: 'info' | 'error' | 'warning',
message: string
}[]
}
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
}
}
export interface EnvItem {
key: string
value: string
}
export interface IConnectionEnvironment {
data: EnvItem[]
newKey: string
newValue: string
}
export interface IConnectionArgs {
type: ConnectionType;
commandString?: string;
cwd?: string;
url?: string;
oauth?: string;
}
export interface ConnectionResult {
status: string
clientId: string
name: string
version: string
}

View File

@ -7,7 +7,7 @@
<!-- TODO: 支持更多的 server --> <!-- TODO: 支持更多的 server -->
<span <span
class="debug-option" class="debug-option"
:class="{ 'disable': !connectionResult.success }" :class="{ 'disable': !client.connectionResult.success }"
v-for="(option, index) of debugOptions" v-for="(option, index) of debugOptions"
:key="index" :key="index"
@click="chooseDebugMode(index)" @click="chooseDebugMode(index)"
@ -25,13 +25,14 @@
import { debugModes, tabs } from '@/components/main-panel/panel'; import { debugModes, tabs } from '@/components/main-panel/panel';
import { defineComponent, markRaw, computed } from 'vue'; import { defineComponent, markRaw, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { connectionResult } from '../connect/connection';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
import { welcomeRef } from './welcome'; import { welcomeRef } from './welcome';
import { mcpClientAdapter } from '../connect/core';
defineComponent({ name: 'welcome' }); defineComponent({ name: 'welcome' });
const { t } = useI18n(); const { t } = useI18n();
const client = mcpClientAdapter.masterNode;
const debugOptions = [ const debugOptions = [
{ {
@ -59,7 +60,7 @@ const debugOptions = [
function chooseDebugMode(index: number) { function chooseDebugMode(index: number) {
// TODO: server // TODO: server
if (connectionResult.success) { if (client.connectionResult.success) {
const activeTab = tabs.activeTab; const activeTab = tabs.activeTab;
activeTab.component = markRaw(debugModes[index]); activeTab.component = markRaw(debugModes[index]);

View File

@ -0,0 +1,41 @@
{
"compilerOptions": {
"target": "esnext",
"module": "nodenext",
"strict": true,
"jsx": "preserve",
"moduleResolution": "nodenext",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"useDefineForClassFields": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

View File

@ -15,5 +15,14 @@ export default defineConfig(({ mode }) => ({
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url))
}, },
}, },
base: mode === 'website' ? '/mcp/' : '/' base: mode === 'website' ? '/mcp/' : '/',
build: {
cssCodeSplit: false, // 禁用 CSS 代码分割
rollupOptions: {
output: {
inlineDynamicImports: true, // 将动态导入的内容内联
manualChunks: undefined, // 禁用手动分块
},
},
},
})) }))

View File

@ -0,0 +1,59 @@
const { TaskLoop } = require('../../openmcp-sdk/task-loop');
const { TaskLoopAdapter } = require('../../openmcp-sdk/service');
async function main() {
// 创建适配器,负责通信和 mcp 连接
const adapter = new TaskLoopAdapter();
// 创建事件循环驱动器
const taskLoop = new TaskLoop({ adapter });
// 配置改次事件循环使用的大模型
taskLoop.setLlmConfig({
id: 'deepseek',
baseUrl: 'https://api.deepseek.com/v1',
userToken: process.env['DEEPSEEK_API_TOKEN'],
userModel: 'deepseek-chat'
});
// 创建当前事件循环对应的上下文,并且配置当前上下文的设置
const storage = {
messages: [],
settings: {
temperature: 0.7,
enableTools: [],
systemPrompt: 'you are a clever bot',
contextLength: 20
}
};
// 本次发出的问题
const message = 'hello world';
// 事件循环结束的句柄
taskLoop.registerOnDone(() => {
console.log('taskLoop done');
});
// 事件循环每一次 epoch 开始的句柄
taskLoop.registerOnError((error) => {
console.log('taskLoop error', error);
});
// 事件循环出现 error 时的句柄(出现 error 不一定会停止事件循环)
taskLoop.registerOnEpoch(() => {
console.log('taskLoop epoch');
});
// 开启事件循环
await taskLoop.start(storage, message);
// 打印上下文,最终的回答在 messages.at(-1) 中
console.log(storage.messages);
const content = storage.messages.at(-1).content;
console.log(content);
}
main();

10
servers/uv.lock generated
View File

@ -175,10 +175,10 @@ wheels = [
] ]
[[package]] [[package]]
name = "httpx-sse" name = "httpx-SSE"
version = "0.4.0" version = "0.4.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-SSE-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 },
] ]
@ -258,10 +258,10 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "anyio" }, { name = "anyio" },
{ name = "httpx" }, { name = "httpx" },
{ name = "httpx-sse" }, { name = "httpx-SSE" },
{ name = "pydantic" }, { name = "pydantic" },
{ name = "pydantic-settings" }, { name = "pydantic-settings" },
{ name = "sse-starlette" }, { name = "SSE-starlette" },
{ name = "starlette" }, { name = "starlette" },
{ name = "uvicorn" }, { name = "uvicorn" },
] ]
@ -477,7 +477,7 @@ wheels = [
] ]
[[package]] [[package]]
name = "sse-starlette" name = "SSE-starlette"
version = "2.2.1" version = "2.2.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [

5023
service/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,16 @@
{ {
"name": "openmcp-service", "name": "@openmcp/service",
"version": "0.0.1", "version": "0.0.1",
"description": "", "description": "",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"type": "commonjs",
"scripts": { "scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/main.ts",
"serve": "ts-node-dev --respawn --transpile-only src/main.ts", "serve": "ts-node-dev --respawn --transpile-only src/main.ts",
"build": "tsc", "build": "tsc",
"build:watch": "tsc --watch", "build:watch": "tsc --watch",
"postbuild": "node ./scripts/post-build.mjs",
"start": "node dist/main.js", "start": "node dist/main.js",
"start:prod": "NODE_ENV=production node dist/main.js", "start:prod": "NODE_ENV=production node dist/main.js",
"debug": "node --inspect -r ts-node/register src/main.ts", "debug": "node --inspect -r ts-node/register src/main.ts",

View File

@ -0,0 +1,46 @@
import fs from "node:fs";
import process from "node:process";
import fsPath from "node:path";
function createDirIfExists(filePath) {
if (!fs.existsSync(filePath)) {
fs.mkdirSync(filePath, { recursive: true })
}
}
function recreateDir(filePath) {
if (fs.existsSync(filePath)) {
fs.rmSync(filePath, { recursive: true, force: true });
}
fs.mkdirSync(filePath, { recursive: true });
}
const currentDir = process.cwd();
const sourceDist = fsPath.join(currentDir, 'dist');
// 如果源目录不存在则报错
if (!fs.existsSync(sourceDist)) {
throw new Error(`Source directory not found: ${sourceDist}`)
}
// electron目录
const electronOpenMcpSdkPath = fsPath.join(currentDir, '..', 'software', 'openmcp-sdk');
createDirIfExists(electronOpenMcpSdkPath);
const electronServicePath = fsPath.join(electronOpenMcpSdkPath, 'service');
recreateDir(electronServicePath);
fs.cpSync(sourceDist, electronServicePath, { recursive: true })
const openMCPSdkPath = fsPath.join(currentDir, '..', 'openmcp-sdk');
createDirIfExists(openMCPSdkPath);
const sdkRenderPath = fsPath.join(openMCPSdkPath, 'service');
recreateDir(sdkRenderPath);
// 如果源目录不存在则报错
if (!fs.existsSync(sourceDist)) {
throw new Error(`Source directory not found: ${sourceDist}`)
}
fs.cpSync(sourceDist, sdkRenderPath, { recursive: true })

View File

@ -3,9 +3,13 @@ import { McpClient } from "../mcp/client.service";
export type RequestClientType = McpClient | undefined; export type RequestClientType = McpClient | undefined;
export interface RequestData {
clientId?: string;
[key: string]: any;
}
export type RequestHandler<T, R> = ( export type RequestHandler<T, R> = (
client: RequestClientType, data: T & RequestData,
data: T,
webview: PostMessageble webview: PostMessageble
) => Promise<R>; ) => Promise<R>;

View File

@ -3,7 +3,6 @@ import type { PostMessageble } from "../hook/adapter";
import { LlmController } from "../llm/llm.controller"; import { LlmController } from "../llm/llm.controller";
import { ClientController } from "../mcp/client.controller"; import { ClientController } from "../mcp/client.controller";
import { ConnectController } from "../mcp/connect.controller"; import { ConnectController } from "../mcp/connect.controller";
import { client } from "../mcp/connect.service";
import { OcrController } from "../mcp/ocr.controller"; import { OcrController } from "../mcp/ocr.controller";
import { PanelController } from "../panel/panel.controller"; import { PanelController } from "../panel/panel.controller";
import { SettingController } from "../setting/setting.controller"; import { SettingController } from "../setting/setting.controller";
@ -24,7 +23,7 @@ export async function routeMessage(command: string, data: any, webview: PostMess
try { try {
// TODO: select client based on something // TODO: select client based on something
const res = await handler(client, data, webview); const res = await handler(data, webview);
// res.code = -1 代表当前请求不需要返回发送 // res.code = -1 代表当前请求不需要返回发送
if (res.code >= 0) { if (res.code >= 0) {

View File

@ -2,7 +2,7 @@ import { WebSocket } from 'ws';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { routeMessage } from '../common/router'; import { routeMessage } from '../common/router';
import { McpOptions } from '../mcp/client.dto'; import { McpOptions } from '../mcp/client.dto';
import { client, connectService } from '../mcp/connect.service'; import { clientMap, connectService } from '../mcp/connect.service';
// WebSocket 消息格式 // WebSocket 消息格式
export interface WebSocketMessage { export interface WebSocketMessage {
@ -114,9 +114,12 @@ export class TaskLoopAdapter {
* @param mcpOption * @param mcpOption
*/ */
public async connectMcpServer(mcpOption: McpOptions) { public async connectMcpServer(mcpOption: McpOptions) {
const res = await connectService(undefined, mcpOption); const res = await connectService(mcpOption);
if (res.code === 200) { if (res.code === 200) {
console.log('✅ 成功连接 mcp 服务器: ' + res.msg); console.log('✅ 成功连接 mcp 服务器: ' + res.msg);
const uuid = res.msg.uuid;
const client = clientMap.get(uuid);
const version = client?.getServerVersion(); const version = client?.getServerVersion();
console.log(version); console.log(version);
} else { } else {
@ -129,14 +132,19 @@ export class TaskLoopAdapter {
* @returns * @returns
*/ */
public async listTools() { public async listTools() {
const tools = await client?.listTools(); const tools = [];
if (tools?.tools) { for (const client of clientMap.values()) {
return tools.tools.map((tool) => { const clientTools = await client?.listTools();
const enabledTools = { ...tool, enabled: true }; if (clientTools?.tools) {
const enabledTools = clientTools.tools.map((tool) => {
const enabledTools = {...tool, enabled: true };
return enabledTools; return enabledTools;
}); });
tools.push(...enabledTools);
} }
return []; }
return tools;
} }
} }

0
service/src/hook/util.ts Normal file
View File

View File

@ -1,13 +1,17 @@
import { Controller, RequestClientType } from "../common"; import { Controller, RequestClientType } from "../common";
import { RequestData } from "../common/index.dto";
import { PostMessageble } from "../hook/adapter"; import { PostMessageble } from "../hook/adapter";
import { getClient } from "../mcp/connect.service";
import { abortMessageService, streamingChatCompletion } from "./llm.service"; import { abortMessageService, streamingChatCompletion } from "./llm.service";
export class LlmController { export class LlmController {
@Controller('llm/chat/completions') @Controller('llm/chat/completions')
async chatCompletion(client: RequestClientType, data: any, webview: PostMessageble) { async chatCompletion(data: RequestData, webview: PostMessageble) {
let { tools = [] } = data; let { tools = [] } = data;
const client = getClient(data.clientId);
if (tools.length > 0 && !client) { if (tools.length > 0 && !client) {
return { return {
code: 501, code: 501,
@ -37,7 +41,7 @@ export class LlmController {
} }
@Controller('llm/chat/completions/abort') @Controller('llm/chat/completions/abort')
async abortChatCompletion(client: RequestClientType, data: any, webview: PostMessageble) { async abortChatCompletion(data: RequestData, webview: PostMessageble) {
return abortMessageService(data, webview); return abortMessageService(data, webview);
} }

View File

@ -28,13 +28,13 @@ const logger = pino({
export type MessageHandler = (message: VSCodeMessage) => void; export type MessageHandler = (message: VSCodeMessage) => void;
interface IStdioLaunchSignature { interface IStdioLaunchSignature {
type: 'stdio'; type: 'STDIO';
commandString: string; commandString: string;
cwd: string; cwd: string;
} }
interface ISSELaunchSignature { interface ISSELaunchSignature {
type:'sse'; type:'SSE';
url: string; url: string;
oauth: string; oauth: string;
} }
@ -42,19 +42,20 @@ interface ISSELaunchSignature {
export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature; export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature;
function refreshConnectionOption(envPath: string) { function refreshConnectionOption(envPath: string) {
const serverPath = path.join(__dirname, '..', '..', 'servers');
const defaultOption = { const defaultOption = {
type:'stdio', type:'STDIO',
command: 'mcp', commandString: 'mcp run main.py',
args: ['run', 'main.py'], cwd: serverPath
cwd: '../server'
}; };
fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4)); fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4));
return defaultOption; return { data: [ defaultOption ] };
} }
function getInitConnectionOption() { function acquireConnectionOption() {
const envPath = path.join(__dirname, '..', '.env'); const envPath = path.join(__dirname, '..', '.env');
if (!fs.existsSync(envPath)) { if (!fs.existsSync(envPath)) {
@ -63,6 +64,15 @@ function getInitConnectionOption() {
try { try {
const option = JSON.parse(fs.readFileSync(envPath, 'utf-8')); const option = JSON.parse(fs.readFileSync(envPath, 'utf-8'));
if (!option.data) {
return refreshConnectionOption(envPath);
}
if (option.data && option.data.length === 0) {
return refreshConnectionOption(envPath);
}
return option; return option;
} catch (error) { } catch (error) {
@ -73,27 +83,11 @@ function getInitConnectionOption() {
function updateConnectionOption(data: any) { function updateConnectionOption(data: any) {
const envPath = path.join(__dirname, '..', '.env'); const envPath = path.join(__dirname, '..', '.env');
const connection = { data };
if (data.connectionType === 'STDIO') { fs.writeFileSync(envPath, JSON.stringify(connection, null, 4));
const connectionItem = {
type: 'stdio',
command: data.command,
args: data.args,
cwd: data.cwd.replace(/\\/g, '/')
};
fs.writeFileSync(envPath, JSON.stringify(connectionItem, null, 4));
} else {
const connectionItem = {
type: 'sse',
url: data.url,
oauth: data.oauth
};
fs.writeFileSync(envPath, JSON.stringify(connectionItem, null, 4));
}
} }
const devHome = path.join(__dirname, '..', '..'); const devHome = path.join(__dirname, '..', '..');
setRunningCWD(devHome); setRunningCWD(devHome);
@ -115,7 +109,7 @@ wss.on('connection', (ws: any) => {
} }
}); });
const option = getInitConnectionOption(); const option = acquireConnectionOption();
// 注册消息接受的管线 // 注册消息接受的管线
webview.onDidReceiveMessage(message => { webview.onDidReceiveMessage(message => {
@ -124,21 +118,9 @@ wss.on('connection', (ws: any) => {
switch (command) { switch (command) {
case 'web/launch-signature': case 'web/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.data
}; };
webview.postMessage({ webview.postMessage({
@ -148,7 +130,7 @@ wss.on('connection', (ws: any) => {
break; break;
case 'web/update-connection-sigature': case 'web/update-connection-signature':
updateConnectionOption(data); updateConnectionOption(data);
break; break;

View File

@ -1,11 +1,14 @@
import { Controller, RequestClientType } from "../common"; import { Controller } from "../common";
import { RequestData } from "../common/index.dto";
import { PostMessageble } from "../hook/adapter"; import { PostMessageble } from "../hook/adapter";
import { postProcessMcpToolcallResponse } from "./client.service"; import { postProcessMcpToolcallResponse } from "./client.service";
import { getClient } from "./connect.service";
export class ClientController { export class ClientController {
@Controller('server/version') @Controller('server/version')
async getServerVersion(client: RequestClientType, data: any, webview: PostMessageble) { async getServerVersion(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
if (!client) { if (!client) {
return { return {
code: 501, code: 501,
@ -21,7 +24,8 @@ export class ClientController {
} }
@Controller('prompts/list') @Controller('prompts/list')
async listPrompts(client: RequestClientType, data: any, webview: PostMessageble) { async listPrompts(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
if (!client) { if (!client) {
const connectResult = { const connectResult = {
code: 501, code: 501,
@ -39,7 +43,8 @@ export class ClientController {
} }
@Controller('prompts/get') @Controller('prompts/get')
async getPrompt(client: RequestClientType, option: any, webview: PostMessageble) { async getPrompt(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
if (!client) { if (!client) {
return { return {
code: 501, code: 501,
@ -47,7 +52,7 @@ export class ClientController {
}; };
} }
const prompt = await client.getPrompt(option.promptId, option.args || {}); const prompt = await client.getPrompt(data.promptId, data.args || {});
return { return {
code: 200, code: 200,
msg: prompt msg: prompt
@ -55,7 +60,8 @@ export class ClientController {
} }
@Controller('resources/list') @Controller('resources/list')
async listResources(client: RequestClientType, data: any, webview: PostMessageble) { async listResources(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
if (!client) { if (!client) {
return { return {
code: 501, code: 501,
@ -71,8 +77,8 @@ export class ClientController {
} }
@Controller('resources/templates/list') @Controller('resources/templates/list')
async listResourceTemplates(client: RequestClientType, data: any, webview: PostMessageble) { async listResourceTemplates(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
if (!client) { if (!client) {
return { return {
code: 501, code: 501,
@ -88,7 +94,8 @@ export class ClientController {
} }
@Controller('resources/read') @Controller('resources/read')
async readResource(client: RequestClientType, option: any, webview: PostMessageble) { async readResource(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
if (!client) { if (!client) {
return { return {
code: 501, code: 501,
@ -96,7 +103,7 @@ export class ClientController {
}; };
} }
const resource = await client.readResource(option.resourceUri); const resource = await client.readResource(data.resourceUri);
console.log(resource); console.log(resource);
return { return {
@ -106,7 +113,8 @@ export class ClientController {
} }
@Controller('tools/list') @Controller('tools/list')
async listTools(client: RequestClientType, data: any, webview: PostMessageble) { async listTools(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
if (!client) { if (!client) {
return { return {
code: 501, code: 501,
@ -122,7 +130,8 @@ export class ClientController {
} }
@Controller('tools/call') @Controller('tools/call')
async callTool(client: RequestClientType, option: any, webview: PostMessageble) { async callTool(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
if (!client) { if (!client) {
return { return {
code: 501, code: 501,
@ -131,18 +140,13 @@ export class ClientController {
} }
const toolResult = await client.callTool({ const toolResult = await client.callTool({
name: option.toolName, name: data.toolName,
arguments: option.toolArgs, arguments: data.toolArgs,
callToolOption: option.callToolOption callToolOption: data.callToolOption
}); });
// console.log(JSON.stringify(toolResult, null, 2));
postProcessMcpToolcallResponse(toolResult, webview); postProcessMcpToolcallResponse(toolResult, webview);
// console.log(JSON.stringify(toolResult, null, 2));
return { return {
code: 200, code: 200,
msg: toolResult msg: toolResult

View File

@ -1,5 +1,5 @@
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/STDIO.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/SSE.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { Implementation } from "@modelcontextprotocol/sdk/types"; import { Implementation } from "@modelcontextprotocol/sdk/types";

View File

@ -1,7 +1,7 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/STDIO.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/SSE.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import type { McpOptions, McpTransport, IServerVersion, ToolCallResponse, ToolCallContent } from './client.dto'; import type { McpOptions, McpTransport, IServerVersion, ToolCallResponse, ToolCallContent } from './client.dto';
import { PostMessageble } from "../hook/adapter"; import { PostMessageble } from "../hook/adapter";

View File

@ -1,17 +1,19 @@
import { Controller, RequestClientType } from '../common'; import { Controller } from '../common';
import { PostMessageble } from '../hook/adapter'; import { PostMessageble } from '../hook/adapter';
import { connectService } from './connect.service'; import { RequestData } from '../common/index.dto';
import { connectService, getClient } from './connect.service';
export class ConnectController { export class ConnectController {
@Controller('connect') @Controller('connect')
async connect(client: RequestClientType, data: any, webview: PostMessageble) { async connect(data: any, webview: PostMessageble) {
const res = await connectService(client, data); const res = await connectService(data);
return res; return res;
} }
@Controller('lookup-env-var') @Controller('lookup-env-var')
async lookupEnvVar(client: RequestClientType, data: any, webview: PostMessageble) { async lookupEnvVar(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
const { keys } = data; const { keys } = data;
const values = keys.map((key: string) => process.env[key] || ''); const values = keys.map((key: string) => process.env[key] || '');
@ -22,7 +24,8 @@ export class ConnectController {
} }
@Controller('ping') @Controller('ping')
async ping(client: RequestClientType, data: any, webview: PostMessageble) { async ping(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
if (!client) { if (!client) {
const connectResult = { const connectResult = {
code: 501, code: 501,

View File

@ -1,43 +1,55 @@
import { spawnSync } from 'node:child_process'; import { execSync, spawnSync } from 'node:child_process';
import { RequestClientType } from '../common'; import { RequestClientType } from '../common';
import { connect } from './client.service'; import { connect } from './client.service';
import { RestfulResponse } from '../common/index.dto'; import { RestfulResponse } from '../common/index.dto';
import { McpOptions } from './client.dto'; import { McpOptions } from './client.dto';
import { randomUUID } from 'node:crypto';
import path from 'node:path';
import fs from 'node:fs';
export const clientMap: Map<string, RequestClientType> = new Map();
// TODO: 更多的 client export function getClient(clientId?: string): RequestClientType | undefined {
export let client: RequestClientType = undefined; return clientMap.get(clientId || '');
}
export function tryGetRunCommandError(command: string, args: string[] = [], cwd?: string): string | null { export function tryGetRunCommandError(command: string, args: string[] = [], cwd?: string): string | null {
try { try {
console.log('current command', command); console.log('current command', command);
console.log('current args', args); console.log('current args', args);
const result = spawnSync(command, args, { const commandString = [command, ...args].join(' ');
cwd: cwd || process.cwd(),
stdio: 'pipe',
encoding: 'utf-8'
});
if (result.error) { const result = execSync(commandString, {
return result.error.message; cwd: cwd || process.cwd()
} }).toString('utf-8');
if (result.status !== 0) {
return result.stderr || `Command failed with code ${result.status}`; return result;
}
return null;
} catch (error) { } catch (error) {
return error instanceof Error ? error.message : String(error); return error instanceof Error ? error.message : String(error);
} }
} }
export async function connectService( function getCWD(option: McpOptions) {
_client: RequestClientType, if (option.cwd) {
option: McpOptions return option.cwd;
): Promise<RestfulResponse> { }
try { const file = option.args?.at(-1);
console.log('ready to connect', option); if (file) {
return path.dirname(file);
}
return undefined;
}
function getCommandFileExt(option: McpOptions) {
const file = option.args?.at(-1);
if (file) {
return path.extname(file);
}
return undefined;
}
function preprocessCommand(option: McpOptions): [McpOptions, string] {
// 对于特殊表示的路径,进行特殊的支持 // 对于特殊表示的路径,进行特殊的支持
if (option.args) { if (option.args) {
option.args = option.args.map(arg => { option.args = option.args.map(arg => {
@ -48,19 +60,118 @@ export async function connectService(
}); });
} }
client = await connect(option); if (option.connectionType === 'SSE' || option.connectionType === 'STREAMABLE_HTTP') {
return [option, ''];
}
const cwd = getCWD(option);
if (!cwd) {
return [option, ''];
}
const ext = getCommandFileExt(option);
if (!ext) {
return [option, ''];
}
// STDIO 模式下,对不同类型的项目进行额外支持
// uv如果没有初始化则进行 uv sync将 mcp 设置为虚拟环境的
// npm如果没有初始化则进行 npm init将 mcp 设置为虚拟环境
// go如果没有初始化则进行 go mod init
let info: string = '';
switch (ext) {
case '.py':
info = initUv(cwd);
break;
case '.js':
case '.ts':
info = initNpm(cwd);
break;
default:
break;
}
return [option, info];
}
function initUv(cwd: string) {
let projectDir = cwd;
while (projectDir!== path.dirname(projectDir)) {
if (fs.readFileSync(projectDir).includes('pyproject.toml')) {
break;
}
projectDir = path.dirname(projectDir);
}
console.log(projectDir);
const venv = path.join(projectDir, '.venv');
const mcpCli = path.join(venv, 'bin', 'mcp');
if (fs.existsSync(mcpCli)) {
return '';
}
let info = '';
info += execSync('uv sync', { cwd: projectDir }).toString('utf-8') + '\n';
info += execSync('uv add mcp "mcp[cli]"', { cwd: projectDir }).toString('utf-8') + '\n';
return info;
}
function initNpm(cwd: string) {
let projectDir = cwd;
while (projectDir !== path.dirname(projectDir)) {
if (fs.readFileSync(projectDir).includes('package.json')) {
break;
}
projectDir = path.dirname(projectDir);
}
const nodeModulesPath = path.join(projectDir, 'node_modules');
if (fs.existsSync(nodeModulesPath)) {
return '';
}
return execSync('npm i', { cwd: projectDir }).toString('utf-8') + '\n';
}
export async function connectService(
option: McpOptions
): Promise<RestfulResponse> {
try {
console.log('ready to connect', option);
const info = preprocessCommand(option);
const client = await connect(option);
const uuid = randomUUID();
clientMap.set(uuid, client);
const versionInfo = client.getServerVersion();
const connectResult = { const connectResult = {
code: 200, code: 200,
msg: 'Connect to OpenMCP successfully\nWelcome back, Kirigaya' msg: {
status: 'success',
clientId: uuid,
name: versionInfo?.name,
version: versionInfo?.version,
info
}
}; };
return connectResult; return connectResult;
} catch (error) { } catch (error) {
console.log('meet error');
console.log(error);
// TODO: 这边获取到的 error 不够精致,如何才能获取到更加精准的错误 // TODO: 这边获取到的 error 不够精致,如何才能获取到更加精准的错误
// 比如 error: Failed to spawn: `server.py` // 比如 error: Failed to spawn: `server.py`
// Caused by: No such file or directory (os error 2) // Caused by: No such file or directory (os error 2)

View File

@ -1,11 +1,14 @@
import { Controller, RequestClientType } from "../common"; import { Controller } from "../common";
import { PostMessageble } from "../hook/adapter"; import { PostMessageble } from "../hook/adapter";
import { RequestData } from "../common/index.dto";
import { getClient } from "../mcp/connect.service";
import { systemPromptDB } from "../hook/db"; import { systemPromptDB } from "../hook/db";
import { loadTabSaveConfig, saveTabSaveConfig } from "./panel.service"; import { loadTabSaveConfig, saveTabSaveConfig } from "./panel.service";
export class PanelController { export class PanelController {
@Controller('panel/save') @Controller('panel/save')
async savePanel(client: RequestClientType, data: any, webview: PostMessageble) { async savePanel(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
const serverInfo = client?.getServerVersion(); const serverInfo = client?.getServerVersion();
saveTabSaveConfig(serverInfo, data); saveTabSaveConfig(serverInfo, data);
@ -15,9 +18,9 @@ export class PanelController {
}; };
} }
@Controller('panel/load') @Controller('panel/load')
async loadPanel(client: RequestClientType, data: any, webview: PostMessageble) { async loadPanel(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
const serverInfo = client?.getServerVersion(); const serverInfo = client?.getServerVersion();
const config = loadTabSaveConfig(serverInfo); const config = loadTabSaveConfig(serverInfo);
@ -28,7 +31,8 @@ export class PanelController {
} }
@Controller('system-prompts/set') @Controller('system-prompts/set')
async setSystemPrompt(client: RequestClientType, data: any, webview: PostMessageble) { async setSystemPrompt(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
const { name, content } = data; const { name, content } = data;
await systemPromptDB.insert({ await systemPromptDB.insert({
@ -44,7 +48,8 @@ export class PanelController {
} }
@Controller('system-prompts/delete') @Controller('system-prompts/delete')
async deleteSystemPrompt(client: RequestClientType, data: any, webview: PostMessageble) { async deleteSystemPrompt(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
const { name } = data; const { name } = data;
await systemPromptDB.delete(name); await systemPromptDB.delete(name);
return { return {
@ -54,7 +59,8 @@ export class PanelController {
} }
@Controller('system-prompts/save') @Controller('system-prompts/save')
async saveSystemPrompts(client: RequestClientType, data: any, webview: PostMessageble) { async saveSystemPrompts(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
const { prompts } = data; const { prompts } = data;
await Promise.all(prompts.map((prompt: any) => { await Promise.all(prompts.map((prompt: any) => {
@ -72,8 +78,8 @@ export class PanelController {
} }
@Controller('system-prompts/load') @Controller('system-prompts/load')
async loadSystemPrompts(client: RequestClientType, data: any, webview: PostMessageble) { async loadSystemPrompts(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
const queryPrompts = await systemPromptDB.findAll(); const queryPrompts = await systemPromptDB.findAll();
const prompts = []; const prompts = [];
for (const prompt of queryPrompts) { for (const prompt of queryPrompts) {

View File

@ -29,13 +29,13 @@ const logger = pino({
export type MessageHandler = (message: VSCodeMessage) => void; export type MessageHandler = (message: VSCodeMessage) => void;
interface IStdioLaunchSignature { interface IStdioLaunchSignature {
type: 'stdio'; type: 'STDIO';
commandString: string; commandString: string;
cwd: string; cwd: string;
} }
interface ISSELaunchSignature { interface ISSELaunchSignature {
type: 'sse'; type: 'SSE';
url: string; url: string;
oauth: string; oauth: string;
} }
@ -43,19 +43,20 @@ interface ISSELaunchSignature {
export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature; export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature;
function refreshConnectionOption(envPath: string) { function refreshConnectionOption(envPath: string) {
const serverPath = path.join(__dirname, '..', '..', 'servers');
const defaultOption = { const defaultOption = {
type: 'stdio', type:'STDIO',
command: 'mcp', commandString: 'mcp run main.py',
args: ['run', 'main.py'], cwd: serverPath
cwd: '../server'
}; };
fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4)); fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4));
return defaultOption; return { data: [ defaultOption ] };
} }
function getInitConnectionOption() { function acquireConnectionOption() {
const envPath = path.join(__dirname, '..', '.env'); const envPath = path.join(__dirname, '..', '.env');
if (!fs.existsSync(envPath)) { if (!fs.existsSync(envPath)) {
@ -64,6 +65,15 @@ function getInitConnectionOption() {
try { try {
const option = JSON.parse(fs.readFileSync(envPath, 'utf-8')); const option = JSON.parse(fs.readFileSync(envPath, 'utf-8'));
if (!option.data) {
return refreshConnectionOption(envPath);
}
if (option.data && option.data.length === 0) {
return refreshConnectionOption(envPath);
}
return option; return option;
} catch (error) { } catch (error) {
@ -81,25 +91,8 @@ 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 };
if (data.connectionType === 'STDIO') { fs.writeFileSync(envPath, JSON.stringify(connection, null, 4));
const connectionItem = {
type: 'stdio',
command: data.command,
args: data.args,
cwd: data.cwd.replace(/\\/g, '/')
};
fs.writeFileSync(envPath, JSON.stringify(connectionItem, null, 4));
} else {
const connectionItem = {
type: 'sse',
url: data.url,
oauth: data.oauth
};
fs.writeFileSync(envPath, JSON.stringify(connectionItem, null, 4));
}
} }
const devHome = path.join(__dirname, '..', '..'); const devHome = path.join(__dirname, '..', '..');
@ -146,7 +139,7 @@ wss.on('connection', (ws: any) => {
} }
}); });
const option = getInitConnectionOption(); const option = acquireConnectionOption();
// 注册消息接受的管线 // 注册消息接受的管线
webview.onDidReceiveMessage(message => { webview.onDidReceiveMessage(message => {
@ -155,21 +148,9 @@ wss.on('connection', (ws: any) => {
switch (command) { switch (command) {
case 'web/launch-signature': case 'web/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.data
}; };
webview.postMessage({ webview.postMessage({
@ -179,7 +160,7 @@ wss.on('connection', (ws: any) => {
break; break;
case 'web/update-connection-sigature': case 'web/update-connection-signature':
updateConnectionOption(data); updateConnectionOption(data);
break; break;

View File

@ -1,11 +1,14 @@
import { Controller, RequestClientType } from "../common"; import { Controller } from "../common";
import { PostMessageble } from "../hook/adapter"; import { PostMessageble } from "../hook/adapter";
import { RequestData } from "../common/index.dto";
import { getClient } from "../mcp/connect.service";
import { getTour, loadSetting, saveSetting, setTour } from "./setting.service"; import { getTour, loadSetting, saveSetting, setTour } from "./setting.service";
export class SettingController { export class SettingController {
@Controller('setting/save') @Controller('setting/save')
async saveSetting(client: RequestClientType, data: any, webview: PostMessageble) { async saveSetting(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
saveSetting(data); saveSetting(data);
console.log('Settings saved successfully'); console.log('Settings saved successfully');
@ -16,8 +19,8 @@ export class SettingController {
} }
@Controller('setting/load') @Controller('setting/load')
async loadSetting(client: RequestClientType, data: any, webview: PostMessageble) { async loadSetting(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
const config = loadSetting(); const config = loadSetting();
return { return {
code: 200, code: 200,
@ -26,10 +29,8 @@ export class SettingController {
} }
@Controller('setting/set-tour') @Controller('setting/set-tour')
async setTourController(client: RequestClientType, data: any, webview: PostMessageble) { async setTourController(data: any, webview: PostMessageble) {
const { userHasReadGuide } = data; const { userHasReadGuide } = data;
setTour(userHasReadGuide); setTour(userHasReadGuide);
return { return {
@ -39,7 +40,7 @@ export class SettingController {
} }
@Controller('setting/get-tour') @Controller('setting/get-tour')
async getTourController(client: RequestClientType, data: any, webview: PostMessageble) { async getTourController(data: any, webview: PostMessageble) {
const { userHasReadGuide } = getTour(); const { userHasReadGuide } = getTour();

View File

@ -10,122 +10,20 @@
"messages": [ "messages": [
{ {
"role": "user", "role": "user",
"content": "https://challenge.isic-archive.com/data/#2020", "content": "hello",
"extraInfo": { "extraInfo": {
"created": 1747382016210, "created": 1747658023940,
"state": "success", "state": "success",
"serverName": "deepseek" "serverName": "Huoshan DeepSeek"
} }
}, },
{ {
"role": "assistant", "role": "assistant",
"content": "", "content": "Hello! How can I assist you today?",
"tool_calls": [
{
"id": "call_0_9dedd2c3-32ba-4f8c-993f-bb381b9845b0",
"index": 0,
"type": "function",
"function": {
"name": "k_navigate",
"arguments": "{\"url\":\"https://challenge.isic-archive.com/data/#2020\"}"
}
}
],
"extraInfo": { "extraInfo": {
"created": 1747382021478, "created": 1747658024544,
"state": "success", "state": "success",
"serverName": "deepseek" "serverName": "Huoshan DeepSeek"
}
},
{
"role": "tool",
"index": 0,
"tool_call_id": "call_0_9dedd2c3-32ba-4f8c-993f-bb381b9845b0",
"content": [
{
"type": "text",
"text": "Navigated to https://challenge.isic-archive.com/data/#2020"
}
],
"extraInfo": {
"created": 1747382026814,
"state": "success",
"serverName": "deepseek",
"usage": {
"prompt_tokens": 1422,
"completion_tokens": 31,
"total_tokens": 1453,
"prompt_tokens_details": {
"cached_tokens": 0
},
"prompt_cache_hit_tokens": 0,
"prompt_cache_miss_tokens": 1422
}
}
},
{
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_0_b4cd47c1-ad08-46a1-9a0e-d202dd2aef0a",
"index": 0,
"type": "function",
"function": {
"name": "k_get_full_page_text",
"arguments": "{}"
}
}
],
"extraInfo": {
"created": 1747382031777,
"state": "success",
"serverName": "deepseek"
}
},
{
"role": "tool",
"index": 0,
"tool_call_id": "call_0_b4cd47c1-ad08-46a1-9a0e-d202dd2aef0a",
"content": [
{
"type": "text",
"text": "Execution result:\n\"mail_outline\\nSUBSCRIBE\\nLOGIN\\nSIGN UP\\nISIC Challenge Datasets\\n2016\\n2017\\n2018\\n2019\\n2020\\nTraining Data\\tTraining Ground Truth\\tTest Data\\tTest Ground Truth\\tLicense\\n\\n\\nDownload DICOM (48.9GB)\\n33,126 DICOM images with embedded metadata.\\n\\nDownload DICOM Corrected* (23.0GB)\\n33,126 DICOM images with embedded metadata.\\n\\nDownload JPEG (23GB)\\n33,126 JPEG images.\\n\\nDownload metadata (2MB)\\n33,126 metadata entries of patient ID, sex, age, and general anatomic site.\\n\\nDownload metadata v2 (2MB)\\n33,126 metadata entries of patient ID, lesion ID, sex, age, and general anatomic site.\\n\\nDownload duplicate image list (2MB)\\nList of 425 duplicate images.\\n\\n\\tDownload (2MB)\\n33,126 entries of gold standard lesion diagnoses.\\t\\n\\nDownload DICOM (15.3GB)\\n10,982 DICOM images with embedded metadata.\\n\\nDownload DICOM Corrected* (6.7GB)\\n10,982 DICOM images with embedded metadata.\\n\\nDownload JPEG (6.7GB)\\n10,982 JPEG images.\\n\\nDownload metadata (458KB)\\n10,982 metadata entries of patient ID, sex, age, and general anatomic site.\\n\\n\\t\\nNot Available\\nhelp_outline\\n\\tCC-BY-NC\\n\\n*The newer version of the DICOM files are provided to avoid potential errors stemming from readers implementing a strict DICOM verification, as implemented in http://dclunie.com/dicom3tools/dciodvfy.html.\\n\\nCiting 2020 datasets:\\nTo comply with the attribution requirements of the CC-BY-NC license , the aggregate \\\"ISIC 2020\\\" data must be cited as:\\n\\nInternational Skin Imaging Collaboration. SIIM-ISIC 2020 Challenge Dataset. International Skin Imaging Collaboration https://doi.org/10.34970/2020-ds01 (2020).\\n\\nCreative Commons Attribution-Non Commercial 4.0 International License.\\n\\nThe dataset was generated by the International Skin Imaging Collaboration (ISIC) and images are from the following sources: Hospital Clínic de Barcelona, Medical University of Vienna, Memorial Sloan Kettering Cancer Center, Melanoma Institute Australia, The University of Queensland, and the University of Athens Medical School.\\n\\nYou should have received a copy of the license along with this work.\\n\\nIf not, see https://creativecommons.org/licenses/by-nc/4.0/legalcode.txt .\\n\\nWhen referencing this dataset in your own manuscripts and publications, please use the following full citation. Please note this is a preprint and has not undergone peer review. It is being prepared for submission and if accepted to a peer reviewed journal the below will be updated accordingly:\\n\\n[1] Rotemberg, V., Kurtansky, N., Betz-Stablein, B., Caffery, L., Chousakos, E., Codella, N., Combalia, M., Dusza, S., Guitera, P., Gutman, D., Halpern, A., Helba, B., Kittler, H., Kose, K., Langer, S., Lioprys, K., Malvehy, J., Musthaq, S., Nanda, J., Reiter, O., Shih, G., Stratigos, A., Tschandl, P., Weber, J. & Soyer, P. A patient-centric dataset of images and metadata for identifying melanomas using clinical context. Sci Data 8, 34 (2021). https://doi.org/10.1038/s41597-021-00815-z\\n\\nTerms of Use|Contact\\nREPORT AN ISSUE\"\n\nConsole output:\n"
}
],
"extraInfo": {
"created": 1747382031793,
"state": "success",
"serverName": "deepseek",
"usage": {
"prompt_tokens": 1484,
"completion_tokens": 17,
"total_tokens": 1501,
"prompt_tokens_details": {
"cached_tokens": 1408
},
"prompt_cache_hit_tokens": 1408,
"prompt_cache_miss_tokens": 76
}
}
},
{
"role": "assistant",
"content": "### ISIC 2020 皮肤影像数据集 \n该数据集由国际皮肤影像协作组ISIC提供用于训练和测试AI模型以识别皮肤病变尤其是黑色素瘤。数据集包含大量皮肤镜图像及其相关元数据适用于医学影像分析和皮肤病学研究。\n\n**主要类型**:医学影像(皮肤镜图像) \n**涉及器官**:皮肤 \n\n🔗 **开源链接** \n- [DICOM格式下载](https://challenge.isic-archive.com/data/#2020) \n- [JPEG格式下载](https://challenge.isic-archive.com/data/#2020) \n- [元数据下载](https://challenge.isic-archive.com/data/#2020) \n\n**数据集特点** \n- 包含33,126张训练图像和10,982张测试图像。 \n- 提供患者ID、性别、年龄和病变部位等元数据。 \n- 遵循CC-BY-NC 4.0国际许可协议。 \n\n**引用方式** \n如需引用此数据集请参考以下文献 \nRotemberg, V., et al. (2021). *A patient-centric dataset of images and metadata for identifying melanomas using clinical context*. Sci Data 8, 34. [DOI链接](https://doi.org/10.1038/s41597-021-00815-z) \n\n**注意事项** \n- 数据集仅限非商业用途。 \n- 使用前需确认已阅读并遵守许可协议。 ",
"extraInfo": {
"created": 1747382050707,
"state": "success",
"serverName": "deepseek",
"usage": {
"prompt_tokens": 2425,
"completion_tokens": 317,
"total_tokens": 2742,
"prompt_tokens_details": {
"cached_tokens": 1472
},
"prompt_cache_hit_tokens": 1472,
"prompt_cache_miss_tokens": 953
}
} }
} }
], ],
@ -176,7 +74,7 @@
"enableWebSearch": false, "enableWebSearch": false,
"temperature": 0.7, "temperature": 0.7,
"contextLength": 20, "contextLength": 20,
"systemPrompt": "medical data" "systemPrompt": ""
} }
} }
} }

View File

@ -9,7 +9,8 @@
"outDir": "./dist", "outDir": "./dist",
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"experimentalDecorators": true "experimentalDecorators": true,
"moduleResolution": "node"
}, },
"paths": { "paths": {
"@/*": [ "@/*": [

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{ {
"name": "openmcp-electron", "name": "@openmcp/electron",
"version": "0.0.1", "version": "0.0.1",
"description": "", "description": "",
"main": "dist/main.js", "main": "dist/main.js",
@ -13,6 +13,7 @@
"email": "1193466151@qq.com" "email": "1193466151@qq.com"
}, },
"dependencies": { "dependencies": {
"@openmcp/service": "*",
"@modelcontextprotocol/sdk": "^1.10.2", "@modelcontextprotocol/sdk": "^1.10.2",
"@seald-io/nedb": "^4.1.1", "@seald-io/nedb": "^4.1.1",
"axios": "^1.7.7", "axios": "^1.7.7",

View File

@ -40,14 +40,14 @@ function createWindow(): void {
switch (command) { switch (command) {
case 'electron/launch-signature': case 'electron/launch-signature':
const launchResultMessage: ILaunchSigature = option.type === 'stdio' ? const launchResultMessage: ILaunchSigature = option.type === 'STDIO' ?
{ {
type: 'stdio', type: 'STDIO',
commandString: option.command + ' ' + option.args.join(' '), commandString: option.command + ' ' + option.args.join(' '),
cwd: option.cwd || '' cwd: option.cwd || ''
} : } :
{ {
type: 'sse', type: 'SSE',
url: option.url, url: option.url,
oauth: option.oauth || '' oauth: option.oauth || ''
}; };
@ -64,7 +64,7 @@ function createWindow(): void {
break; break;
case 'electron/update-connection-sigature': case 'electron/update-connection-signature':
updateConnectionOption(data); updateConnectionOption(data);
break; break;

View File

@ -24,13 +24,13 @@ export class ElectronIPCLike {
interface IStdioLaunchSignature { interface IStdioLaunchSignature {
type: 'stdio'; type: 'STDIO';
commandString: string; commandString: string;
cwd: string; cwd: string;
} }
interface ISSELaunchSignature { interface ISSELaunchSignature {
type:'sse'; type:'SSE';
url: string; url: string;
oauth: string; oauth: string;
} }
@ -39,7 +39,7 @@ export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature;
export function refreshConnectionOption(envPath: string) { export function refreshConnectionOption(envPath: string) {
const defaultOption = { const defaultOption = {
type:'stdio', type:'STDIO',
command: 'mcp', command: 'mcp',
args: ['run', 'main.py'], args: ['run', 'main.py'],
cwd: '../server' cwd: '../server'
@ -80,7 +80,7 @@ export function updateConnectionOption(data: any) {
if (data.connectionType === 'STDIO') { if (data.connectionType === 'STDIO') {
const connectionItem = { const connectionItem = {
type: 'stdio', type: 'STDIO',
command: data.command, command: data.command,
args: data.args, args: data.args,
cwd: data.cwd.replace(/\\/g, '/') cwd: data.cwd.replace(/\\/g, '/')
@ -89,7 +89,7 @@ export function updateConnectionOption(data: any) {
fs.writeFileSync(envPath, JSON.stringify(connectionItem, null, 4)); fs.writeFileSync(envPath, JSON.stringify(connectionItem, null, 4));
} else { } else {
const connectionItem = { const connectionItem = {
type: 'sse', type: 'SSE',
url: data.url, url: data.url,
oauth: data.oauth oauth: data.oauth
}; };

View File

@ -1,5 +1,5 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as OpenMCPService from '../openmcp-sdk/service'; import { setRunningCWD, setVscodeWorkspace } from '../openmcp-sdk/service';
import { launch } from './common/entry'; import { launch } from './common/entry';
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
@ -10,8 +10,8 @@ export function activate(context: vscode.ExtensionContext) {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
const workspace = workspaceFolder?.uri.fsPath || ''; const workspace = workspaceFolder?.uri.fsPath || '';
OpenMCPService.setVscodeWorkspace(workspace); setVscodeWorkspace(workspace);
OpenMCPService.setRunningCWD(context.extensionPath); setRunningCWD(context.extensionPath);
launch(context); launch(context);
} }

View File

@ -7,7 +7,7 @@ export type FsPath = string;
export const panels = new Map<FsPath, vscode.WebviewPanel>(); export const panels = new Map<FsPath, vscode.WebviewPanel>();
export interface IStdioConnectionItem { export interface IStdioConnectionItem {
type: 'stdio'; type: 'STDIO';
name: string; name: string;
version?: string; version?: string;
command: string; command: string;
@ -18,7 +18,7 @@ export interface IStdioConnectionItem {
} }
export interface ISSEConnectionItem { export interface ISSEConnectionItem {
type: 'sse'; type: 'SSE';
name: string; name: string;
version: string; version: string;
url: string; url: string;
@ -29,13 +29,13 @@ export interface ISSEConnectionItem {
interface IStdioLaunchSignature { interface IStdioLaunchSignature {
type: 'stdio'; type: 'STDIO';
commandString: string; commandString: string;
cwd: string; cwd: string;
} }
interface ISSELaunchSignature { interface ISSELaunchSignature {
type:'sse'; type:'SSE';
url: string; url: string;
oauth: string; oauth: string;
} }
@ -123,7 +123,7 @@ export function getWorkspaceConnectionConfig() {
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 (item.type === '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, '/');
} }
} }
@ -169,7 +169,7 @@ export function saveWorkspaceConnectionConfig(workspace: string) {
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, '/');
} }
} }
@ -213,7 +213,7 @@ export function updateWorkspaceConnectionConfig(
if (data.connectionType === 'STDIO') { if (data.connectionType === 'STDIO') {
const connectionItem: IStdioConnectionItem = { const connectionItem: IStdioConnectionItem = {
type: 'stdio', type: 'STDIO',
name: data.serverInfo.name, name: data.serverInfo.name,
version: data.serverInfo.version, version: data.serverInfo.version,
command: data.command, command: data.command,
@ -234,7 +234,7 @@ export function updateWorkspaceConnectionConfig(
} else { } else {
const connectionItem: ISSEConnectionItem = { const connectionItem: ISSEConnectionItem = {
type: 'sse', type: 'SSE',
name: data.serverInfo.name, name: data.serverInfo.name,
version: data.serverInfo.version, version: data.serverInfo.version,
url: data.url, url: data.url,
@ -267,7 +267,7 @@ export function updateInstalledConnectionConfig(
if (data.connectionType === 'STDIO') { if (data.connectionType === 'STDIO') {
const connectionItem: IStdioConnectionItem = { const connectionItem: IStdioConnectionItem = {
type: 'stdio', type: 'STDIO',
name: data.serverInfo.name, name: data.serverInfo.name,
version: data.serverInfo.version, version: data.serverInfo.version,
command: data.command, command: data.command,
@ -287,7 +287,7 @@ export function updateInstalledConnectionConfig(
} else { } else {
const connectionItem: ISSEConnectionItem = { const connectionItem: ISSEConnectionItem = {
type: 'sse', type: 'SSE',
name: data.serverInfo.name, name: data.serverInfo.name,
version: data.serverInfo.version, version: data.serverInfo.version,
url: data.url, url: data.url,

View File

@ -52,7 +52,7 @@ export async function validateAndGetCommandPath(commandString: string, cwd?: str
export async function acquireInstalledConnection(): Promise<IConnectionItem | undefined> { export async function acquireInstalledConnection(): Promise<IConnectionItem | undefined> {
// 让用户选择连接类型 // 让用户选择连接类型
const connectionType = await vscode.window.showQuickPick(['stdio', 'sse'], { const connectionType = await vscode.window.showQuickPick(['STDIO', 'SSE'], {
placeHolder: '请选择连接类型', placeHolder: '请选择连接类型',
canPickMany: false canPickMany: false
}); });
@ -61,7 +61,7 @@ export async function acquireInstalledConnection(): Promise<IConnectionItem | un
return; // 用户取消选择 return; // 用户取消选择
} }
if (connectionType === 'stdio') { if (connectionType === 'STDIO') {
// 获取 command // 获取 command
const commandString = await vscode.window.showInputBox({ const commandString = await vscode.window.showInputBox({
prompt: '请输入连接的 command', prompt: '请输入连接的 command',
@ -97,15 +97,15 @@ export async function acquireInstalledConnection(): Promise<IConnectionItem | un
// 保存连接配置 // 保存连接配置
return { return {
type: 'stdio', type: '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',
@ -124,8 +124,8 @@ export async function acquireInstalledConnection(): Promise<IConnectionItem | un
// 保存连接配置 // 保存连接配置
return { return {
type: 'sse', type: '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 || ''

View File

@ -6,7 +6,7 @@ import * as vscode from 'vscode';
export async function acquireUserCustomConnection(): Promise<IConnectionItem | undefined> { export async function acquireUserCustomConnection(): Promise<IConnectionItem | undefined> {
// 让用户选择连接类型 // 让用户选择连接类型
const connectionType = await vscode.window.showQuickPick(['stdio', 'sse'], { const connectionType = await vscode.window.showQuickPick(['STDIO', 'SSE'], {
placeHolder: '请选择连接类型' placeHolder: '请选择连接类型'
}); });
@ -14,7 +14,7 @@ export async function acquireUserCustomConnection(): Promise<IConnectionItem | u
return; // 用户取消选择 return; // 用户取消选择
} }
if (connectionType === 'stdio') { if (connectionType === 'STDIO') {
// 获取 command // 获取 command
const commandString = await vscode.window.showInputBox({ const commandString = await vscode.window.showInputBox({
prompt: '请输入连接的 command', prompt: '请输入连接的 command',
@ -47,15 +47,15 @@ export async function acquireUserCustomConnection(): Promise<IConnectionItem | u
// 保存连接配置 // 保存连接配置
return { return {
type: 'stdio', type: 'STDIO',
name: `stdio-${Date.now()}`, name: `STDIO-${Date.now()}`,
command: command, command: command,
args, args,
cwd: cwd || '', cwd: cwd || '',
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',
@ -74,8 +74,8 @@ export async function acquireUserCustomConnection(): Promise<IConnectionItem | u
// 保存连接配置 // 保存连接配置
return { return {
type: 'sse', type: '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 || ''

View File

@ -7,22 +7,24 @@ export class WebviewController {
@RegisterCommand('openmcp.showOpenMCP') @RegisterCommand('openmcp.showOpenMCP')
async showOpenMCP(context: vscode.ExtensionContext, uri: vscode.Uri) { async showOpenMCP(context: vscode.ExtensionContext, uri: vscode.Uri) {
const connectionItem = getWorkspaceConnectionConfigItemByPath(uri.fsPath); const connectionItem = getWorkspaceConnectionConfigItemByPath(uri.fsPath);
if (!connectionItem) { if (!connectionItem) {
// 项目不存在连接信息 // 项目不存在连接信息
const cwd = getLaunchCWD(context, uri); const cwd = getLaunchCWD(context, uri);
const sigature = getDefaultLanunchSignature(uri.fsPath, cwd); const signature = getDefaultLanunchSignature(uri.fsPath, cwd);
if (!sigature) { if (!signature) {
vscode.window.showInformationMessage('OpenMCP: 无法获取启动参数');
vscode.window.showErrorMessage('OpenMCP: 无法获取启动参数'); vscode.window.showErrorMessage('OpenMCP: 无法获取启动参数');
return; return;
} }
revealOpenMcpWebviewPanel(context, 'workspace', uri.fsPath, { revealOpenMcpWebviewPanel(context, 'workspace', uri.fsPath, {
type: 'stdio', type: 'STDIO',
name: 'OpenMCP', name: 'OpenMCP',
command: sigature.command, command: signature.command,
args: sigature.args, args: signature.args,
cwd cwd
}); });
} else { } else {

View File

@ -2,18 +2,25 @@ 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 { IConnectionItem, ILaunchSigature, panels, updateInstalledConnectionConfig, updateWorkspaceConnectionConfig } from '../global';
import * as OpenMCPService 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 {
const viewRoot = fspath.join(context.extensionPath, 'openmcp-sdk', 'renderer'); const viewRoot = fspath.join(context.extensionPath, 'openmcp-sdk', 'renderer');
const htmlIndexPath = fspath.join(viewRoot, 'index.html'); const htmlIndexPath = fspath.join(viewRoot, 'index.html');
const html = fs.readFileSync(htmlIndexPath, { encoding: 'utf-8' })?.replace(/(<link.+?href="|<script.+?src="|<img.+?src="|url\()(.+?)(\)|")/g, (m, $1, $2) => {
const absLocalPath = fspath.resolve(viewRoot, $2);
const webviewUri = panel.webview.asWebviewUri(vscode.Uri.file(absLocalPath));
const html = fs.readFileSync(htmlIndexPath, { encoding: 'utf-8' })?.replace(/(<link.+?href="|<script.+?src="|<img.+?src="|url\()(.+?)(\)|")/g, (m, $1, $2) => {
const importFile = $2 as string;
const rel = importFile.startsWith('/') ? importFile.substring(1) : importFile;
const absLocalPath = fspath.resolve(viewRoot, rel);
const webviewUri = panel.webview.asWebviewUri(vscode.Uri.file(absLocalPath));
const replaceHref = $1 + webviewUri?.toString() + '"'; const replaceHref = $1 + webviewUri?.toString() + '"';
return replaceHref; return replaceHref;
}); });
console.log(html);
return html; return html;
} }
@ -29,7 +36,7 @@ export function revealOpenMcpWebviewPanel(
type: 'workspace' | 'installed', type: 'workspace' | 'installed',
panelKey: string, panelKey: string,
option: IConnectionItem = { option: IConnectionItem = {
type: 'stdio', type: 'STDIO',
name: 'OpenMCP', name: 'OpenMCP',
command: 'mcp', command: 'mcp',
args: ['run', 'main.py'] args: ['run', 'main.py']
@ -68,14 +75,14 @@ export function revealOpenMcpWebviewPanel(
// 拦截消息,注入额外信息 // 拦截消息,注入额外信息
switch (command) { switch (command) {
case 'vscode/launch-signature': case 'vscode/launch-signature':
const launchResultMessage: ILaunchSigature = option.type === 'stdio' ? const launchResultMessage: ILaunchSigature = option.type === 'STDIO' ?
{ {
type: 'stdio', type: 'STDIO',
commandString: option.command + ' ' + option.args.join(' '), commandString: option.command + ' ' + option.args.join(' '),
cwd: option.cwd || '' cwd: option.cwd || ''
} : } :
{ {
type: 'sse', type: 'SSE',
url: option.url, url: option.url,
oauth: option.oauth || '' oauth: option.oauth || ''
}; };
@ -92,7 +99,7 @@ export function revealOpenMcpWebviewPanel(
break; break;
case 'vscode/update-connection-sigature': case 'vscode/update-connection-signature':
if (type === 'installed') { if (type === 'installed') {
updateInstalledConnectionConfig(panelKey, data); updateInstalledConnectionConfig(panelKey, data);
} else { } else {
@ -101,7 +108,7 @@ export function revealOpenMcpWebviewPanel(
break; break;
default: default:
OpenMCPService.routeMessage(command, data, panel.webview); routeMessage(command, data, panel.webview);
break; break;
} }

38
turbo.json Normal file
View File

@ -0,0 +1,38 @@
{
"$schema": "https://turborepo.com/schema.json",
"tasks": {
"serve": {
"cache": false
},
"build": {
"dependsOn": ["@openmcp/renderer#build", "@openmcp/service#build", "//#build:task-loop"],
"cache": false
},
"//#build:task-loop": {
"cache": false
},
"@openmcp/renderer#serve": {
"cache": false,
"persistent": true
},
"@openmcp/renderer#build": {
"cache": false
},
"@openmcp/service#serve": {
"persistent": true,
"cache": false
},
"@openmcp/service#build": {
"cache": false
},
"@openmcp/electron#dev": {
"dependsOn": ["@openmcp/renderer#build"],
"persistent": true,
"cache": false
},
"@openmcp/electron#build": {
"dependsOn": ["@openmcp/renderer#build"],
"outputs": ["dist/**"]
}
}
}

View File

@ -23,7 +23,12 @@ module.exports = {
rules: [ rules: [
{ {
test: /\.ts$/, test: /\.ts$/,
use: 'ts-loader', use: {
loader: 'ts-loader',
options: {
configFile: path.resolve(__dirname, '../tsconfig.json') // 指定 tsconfig.json 路径
}
},
exclude: /node_modules/, exclude: /node_modules/,
}, },
{ {