This commit is contained in:
li1553770945 2025-05-19 23:39:29 +08:00
commit 8b3816d05c
50 changed files with 1214 additions and 880 deletions

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,5 +1,11 @@
# 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 返回的是索引而非真实内容
- 测试新的发布管线 - 测试新的发布管线

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.9", "version": "0.1.0",
"publisher": "kirigaya", "publisher": "kirigaya",
"author": { "author": {
"name": "kirigaya", "name": "kirigaya",

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
```

Binary file not shown.

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

@ -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,64 +13,55 @@ 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) {
const bridge = useMessageBridge();
return new Promise((resolve, reject) => { const { code, msg } = await bridge.commandRequest<SaveTab>('panel/load', {
const bridge = useMessageBridge(); clientId: client.clientId
bridge.addCommandListener('panel/load', data => {
if (data.code !== 200) {
pinkLog('tabs 加载失败');
console.log(data.msg);
} else {
const persistTab = data.msg as SaveTab;
pinkLog('tabs 加载成功');
if (persistTab.tabs.length === 0) {
// 空的,直接返回不需要管
panelLoaded.value = true;
resolve(void 0);
return;
}
tabs.activeIndex = 0;
tabs.content = [];
for (const tab of persistTab.tabs || []) {
const component = tab.componentIndex >= 0? markRaw(debugModes[tab.componentIndex]) : undefined;
tabs.content.push({
id: uuidv4(),
name: tab.name,
icon: tab.icon,
type: tab.type,
componentIndex: tab.componentIndex,
component: component,
storage: tab.storage
});
}
tabs.activeIndex = persistTab.currentIndex;
}
panelLoaded.value = true;
resolve(void 0);
}, { once: true });
bridge.postMessage({
command: 'panel/load'
});
}); });
if (code !== 200) {
pinkLog('tabs 加载失败');
console.log(msg);
} else {
const persistTab = msg;
pinkLog('tabs 加载成功');
if (persistTab.tabs.length === 0) {
// 空的,直接返回不需要管
panelLoaded.value = true;
return;
}
tabs.activeIndex = 0;
tabs.content = [];
for (const tab of persistTab.tabs || []) {
const component = tab.componentIndex >= 0? markRaw(debugModes[tab.componentIndex]) : undefined;
tabs.content.push({
id: uuidv4(),
name: tab.name,
icon: tab.icon,
type: tab.type,
componentIndex: tab.componentIndex,
component: component,
storage: tab.storage
});
}
tabs.activeIndex = persistTab.currentIndex;
}
panelLoaded.value = true;
} }
let debounceHandler: number; let debounceHandler: number;

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

@ -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="server-list">
<div class="connect-panel-container" <div v-for="(client, index) in mcpClientAdapter.clients" :key="index" class="server-item"
:ref="el => connectionSettingRef = el" :class="{ 'active': mcpClientAdapter.currentClientIndex === index }" @click="selectServer(index)">
> <span class="connect-status">
<ConnectionMethod></ConnectionMethod> <span v-if="client.connectionResult.success">
<ConnectionArgs></ConnectionArgs> <span class="iconfont icon-connect"></span>
<EnvVar></EnvVar> <span class="iconfont icon-dui"></span>
</span>
<div class="connect-action"> <span v-else>
<el-button type="primary" size="large" :loading="isLoading" :disabled="!connectionResult" <span class="iconfont icon-connect"></span>
@click="suitableConnect()"> <span class="server-name"> Unconnected </span>
<span class="iconfont icon-connect" v-if="!isLoading"></span> </span>
{{ t('connect.appearance.connect') }} </span>
</el-button> </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]);

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 = [

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) {
return enabledTools; const enabledTools = clientTools.tools.map((tool) => {
}); const enabledTools = {...tool, enabled: true };
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,69 +1,180 @@
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;
} } catch (error) {
return null; return error instanceof Error ? error.message : String(error);
} catch (error) { }
return error instanceof Error ? error.message : String(error);
}
} }
function getCWD(option: McpOptions) {
if (option.cwd) {
return option.cwd;
}
const file = option.args?.at(-1);
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) {
option.args = option.args.map(arg => {
if (arg.startsWith('~/')) {
return arg.replace('~', process.env.HOME || '');
}
return arg;
});
}
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( export async function connectService(
_client: RequestClientType, option: McpOptions
option: McpOptions
): Promise<RestfulResponse> { ): Promise<RestfulResponse> {
try { try {
console.log('ready to connect', option); console.log('ready to connect', option);
// 对于特殊表示的路径,进行特殊的支持 const info = preprocessCommand(option);
if (option.args) {
option.args = option.args.map(arg => { const client = await connect(option);
if (arg.startsWith('~/')) { const uuid = randomUUID();
return arg.replace('~', process.env.HOME || ''); clientMap.set(uuid, client);
}
return arg; const versionInfo = client.getServerVersion();
});
}
client = await connect(option);
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)
let errorMsg = ''; let errorMsg = '';
@ -78,6 +189,6 @@ export async function connectService(
msg: errorMsg msg: errorMsg
}; };
return connectResult; return connectResult;
} }
} }

View File

@ -1,13 +1,16 @@
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);
return { return {
code: 200, code: 200,
@ -15,11 +18,11 @@ 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);
return { return {
code: 200, code: 200,
@ -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

@ -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

@ -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

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

View File

@ -36,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']
@ -75,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 || ''
}; };
@ -99,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 {

View File

@ -2,11 +2,6 @@
"$schema": "https://turborepo.com/schema.json", "$schema": "https://turborepo.com/schema.json",
"tasks": { "tasks": {
"serve": { "serve": {
"dependsOn": ["^build"],
"cache": false
},
"dev": {
"with": ["@openmcp/renderer#serve", "@openmcp/service#serve"],
"cache": false "cache": false
}, },
"build": { "build": {