重构完成基础设施
This commit is contained in:
parent
4d459464d3
commit
468ce23b66
@ -54,7 +54,7 @@
|
|||||||
| `ext` | 支持基本的 MCP 项目管理 | `迭代版本` | 100% | `P0` |
|
| `ext` | 支持基本的 MCP 项目管理 | `迭代版本` | 100% | `P0` |
|
||||||
| `service` | 支持自定义支持 openai 接口协议的大模型接入 | `完整版本` | 100% | `Done` |
|
| `service` | 支持自定义支持 openai 接口协议的大模型接入 | `完整版本` | 100% | `Done` |
|
||||||
| `service` | 支持自定义接口协议的大模型接入 | `MVP` | 0% | `P1` |
|
| `service` | 支持自定义接口协议的大模型接入 | `MVP` | 0% | `P1` |
|
||||||
| `all` | 支持同时调试多个 MCP Server | `MVP` | 0% | `P1` |
|
| `all` | 支持同时调试多个 MCP Server | `MVP` | 80% | `P0` |
|
||||||
| `all` | 支持通过大模型进行在线验证 | `迭代版本` | 100% | `Done` |
|
| `all` | 支持通过大模型进行在线验证 | `迭代版本` | 100% | `Done` |
|
||||||
| `all` | 支持对用户对应服务器的调试工作内容进行保存 | `迭代版本` | 100% | `Done` |
|
| `all` | 支持对用户对应服务器的调试工作内容进行保存 | `迭代版本` | 100% | `Done` |
|
||||||
| `render` | 高危操作权限确认 | `MVP` | 0% | `P1` |
|
| `render` | 高危操作权限确认 | `MVP` | 0% | `P1` |
|
||||||
|
@ -55,13 +55,8 @@ onMounted(async () => {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// 进行桥接
|
// 进行桥接
|
||||||
console.log('enter');
|
|
||||||
|
|
||||||
await bridge.awaitForWebsocket();
|
await bridge.awaitForWebsocket();
|
||||||
|
|
||||||
console.log('enter2');
|
|
||||||
|
|
||||||
|
|
||||||
// 根据是否需要密码进行后续的选择
|
// 根据是否需要密码进行后续的选择
|
||||||
if (!privilegeStatus.allow) {
|
if (!privilegeStatus.allow) {
|
||||||
return;
|
return;
|
||||||
|
10
renderer/src/components/log-block/index.vue
Normal file
10
renderer/src/components/log-block/index.vue
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
@ -8,7 +8,7 @@ import { pinkLog, redLog } from "@/views/setting/util";
|
|||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { handleToolCalls, type ToolCallResult } from "./handle-tool-calls";
|
import { handleToolCalls, type ToolCallResult } from "./handle-tool-calls";
|
||||||
import { getPlatform } from "@/api/platform";
|
import { getPlatform } from "@/api/platform";
|
||||||
import { getSystemPrompt, systemPrompts } from "../chat-box/options/system-prompt";
|
import { getSystemPrompt } from "../chat-box/options/system-prompt";
|
||||||
|
|
||||||
export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
|
export type ChatCompletionChunk = OpenAI.Chat.Completions.ChatCompletionChunk;
|
||||||
export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string };
|
export type ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string };
|
||||||
|
@ -83,10 +83,20 @@ const handleKeydown = (event: KeyboardEvent) => {
|
|||||||
|
|
||||||
const copy = async () => {
|
const copy = async () => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(userInput.value);
|
if (navigator.clipboard) {
|
||||||
|
await navigator.clipboard.writeText(userInput.value);
|
||||||
|
} else {
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.value = userInput.value;
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
}
|
||||||
ElMessage.success('内容已复制到剪贴板');
|
ElMessage.success('内容已复制到剪贴板');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('无法复制内容: ', err);
|
console.error('无法复制内容: ', err);
|
||||||
|
ElMessage.error('复制失败,请手动复制');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -40,6 +40,7 @@ import { promptsManager, type PromptStorage } from './prompts';
|
|||||||
import type { PromptsGetResponse } from '@/hook/type';
|
import type { PromptsGetResponse } from '@/hook/type';
|
||||||
import { useMessageBridge } from '@/api/message-bridge';
|
import { useMessageBridge } from '@/api/message-bridge';
|
||||||
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
|
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
|
||||||
|
import { mcpClientAdapter } from '@/views/connect/core';
|
||||||
|
|
||||||
defineComponent({ name: 'prompt-reader' });
|
defineComponent({ name: 'prompt-reader' });
|
||||||
|
|
||||||
@ -131,16 +132,14 @@ const resetForm = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
const bridge = useMessageBridge();
|
|
||||||
|
|
||||||
const { code, msg } = await bridge.commandRequest('prompts/get', {
|
const res = await mcpClientAdapter.readPromptTemplate(
|
||||||
promptId: currentPrompt.value.name,
|
currentPrompt.value.name,
|
||||||
args: JSON.parse(JSON.stringify(tabStorage.formData))
|
JSON.parse(JSON.stringify(tabStorage.formData))
|
||||||
});
|
);
|
||||||
|
|
||||||
tabStorage.lastPromptGetResponse = msg;
|
tabStorage.lastPromptGetResponse = res;
|
||||||
|
emits('prompt-get-response', res);
|
||||||
emits('prompt-get-response', msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.tabId >= 0) {
|
if (props.tabId >= 0) {
|
||||||
|
@ -42,6 +42,7 @@ import { parseResourceTemplate, resourcesManager, type ResourceStorage } from '.
|
|||||||
import type{ ResourcesReadResponse } from '@/hook/type';
|
import type{ ResourcesReadResponse } from '@/hook/type';
|
||||||
import { useMessageBridge } from '@/api/message-bridge';
|
import { useMessageBridge } from '@/api/message-bridge';
|
||||||
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
|
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
|
||||||
|
import { mcpClientAdapter } from '@/views/connect/core';
|
||||||
|
|
||||||
defineComponent({ name: 'resource-reader' });
|
defineComponent({ name: 'resource-reader' });
|
||||||
|
|
||||||
@ -154,11 +155,10 @@ function getUri() {
|
|||||||
// 提交表单
|
// 提交表单
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
const uri = getUri();
|
const uri = getUri();
|
||||||
|
const res = await mcpClientAdapter.readResource(uri);
|
||||||
|
|
||||||
const bridge = useMessageBridge();
|
tabStorage.lastResourceReadResponse = res;
|
||||||
const { code, msg } = await bridge.commandRequest('resources/read', { resourceUri: uri });
|
emits('resource-get-response', res);
|
||||||
tabStorage.lastResourceReadResponse = msg;
|
|
||||||
emits('resource-get-response', msg);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.tabId >= 0) {
|
if (props.tabId >= 0) {
|
||||||
|
@ -60,10 +60,11 @@ import { defineComponent, defineProps, watch, ref, computed } from 'vue';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import type { FormInstance, FormRules } from 'element-plus';
|
import type { FormInstance, FormRules } from 'element-plus';
|
||||||
import { tabs } from '../panel';
|
import { tabs } from '../panel';
|
||||||
import { callTool, toolsManager, type ToolStorage } from './tools';
|
import type { ToolStorage } from './tools';
|
||||||
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
|
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
|
||||||
|
|
||||||
import KInputObject from '@/components/k-input-object/index.vue';
|
import KInputObject from '@/components/k-input-object/index.vue';
|
||||||
|
import { mcpClientAdapter } from '@/views/connect/core';
|
||||||
|
|
||||||
defineComponent({ name: 'tool-executor' });
|
defineComponent({ name: 'tool-executor' });
|
||||||
|
|
||||||
@ -90,7 +91,10 @@ const formRef = ref<FormInstance>();
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
||||||
const currentTool = computed(() => {
|
const currentTool = computed(() => {
|
||||||
return toolsManager.tools.find(tool => tool.name === tabStorage.currentToolName);
|
for (const client of mcpClientAdapter.clients) {
|
||||||
|
const tool = client.tools?.get(tabStorage.currentToolName);
|
||||||
|
if (tool) return tool;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -146,7 +150,7 @@ async function handleExecute() {
|
|||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
tabStorage.lastToolCallResponse = undefined;
|
tabStorage.lastToolCallResponse = undefined;
|
||||||
const toolResponse = await callTool(tabStorage.currentToolName, tabStorage.formData);
|
const toolResponse = await mcpClientAdapter.callTool(tabStorage.currentToolName, tabStorage.formData);
|
||||||
tabStorage.lastToolCallResponse = toolResponse;
|
tabStorage.lastToolCallResponse = toolResponse;
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
|
@ -1,42 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<h3 class="resource-template">
|
|
||||||
<code>tools/list</code>
|
|
||||||
<span
|
|
||||||
class="iconfont icon-restart"
|
|
||||||
@click="reloadTools({ first: false })"
|
|
||||||
></span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div class="tool-list-container-scrollbar">
|
<el-collapse :expand-icon-position="'left'" v-model="activeNames">
|
||||||
<el-scrollbar height="500px">
|
<el-collapse-item v-for="(client, index) in mcpClientAdapter.clients" :name="index" :class="[]">
|
||||||
<div class="tool-list-container">
|
|
||||||
<div
|
<!-- header -->
|
||||||
class="item"
|
<template #title>
|
||||||
:class="{ 'active': tabStorage.currentToolName === tool.name }"
|
<h3 class="resource-template">
|
||||||
v-for="tool of toolsManager.tools"
|
<code>tools/list</code>
|
||||||
:key="tool.name"
|
<span class="iconfont icon-restart" @click="reloadTools({ first: false })"></span>
|
||||||
@click="handleClick(tool)"
|
</h3>
|
||||||
>
|
</template>
|
||||||
<span>{{ tool.name }}</span>
|
|
||||||
<span>{{ tool.description || '' }}</span>
|
<!-- body -->
|
||||||
</div>
|
|
||||||
|
<div class="tool-list-container-scrollbar">
|
||||||
|
<el-scrollbar height="500px">
|
||||||
|
<div class="tool-list-container">
|
||||||
|
<div class="item" :class="{ 'active': tabStorage.currentToolName === tool.name }"
|
||||||
|
v-for="tool of client.tools?.values()" :key="tool.name" @click="handleClick(tool)">
|
||||||
|
<span>{{ tool.name }}</span>
|
||||||
|
<span>{{ tool.description || '' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
</el-scrollbar>
|
</el-collapse-item>
|
||||||
</div>
|
</el-collapse>
|
||||||
|
|
||||||
<div>
|
|
||||||
<!-- resources/list -->
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useMessageBridge } from '@/api/message-bridge';
|
import { useMessageBridge } from '@/api/message-bridge';
|
||||||
import type { CasualRestAPI, ToolsListResponse } from '@/hook/type';
|
import type { CasualRestAPI, ToolsListResponse } from '@/hook/type';
|
||||||
import { onMounted, onUnmounted, defineProps } from 'vue';
|
import { onMounted, onUnmounted, defineProps, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { toolsManager, type ToolStorage } from './tools';
|
import type { ToolStorage } from './tools';
|
||||||
import { tabs } from '../panel';
|
import { tabs } from '../panel';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { mcpClientAdapter } from '@/views/connect/core';
|
||||||
|
|
||||||
const bridge = useMessageBridge();
|
const bridge = useMessageBridge();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@ -51,6 +51,8 @@ const props = defineProps({
|
|||||||
const tab = tabs.content[props.tabId];
|
const tab = tabs.content[props.tabId];
|
||||||
const tabStorage = tab.storage as ToolStorage;
|
const tabStorage = tab.storage as ToolStorage;
|
||||||
|
|
||||||
|
const activeNames = ref<any[]>([0]);
|
||||||
|
|
||||||
function reloadTools(option: { first: boolean }) {
|
function reloadTools(option: { first: boolean }) {
|
||||||
bridge.postMessage({
|
bridge.postMessage({
|
||||||
command: 'tools/list'
|
command: 'tools/list'
|
||||||
@ -71,30 +73,12 @@ function handleClick(tool: { name: string }) {
|
|||||||
tabStorage.lastToolCallResponse = undefined;
|
tabStorage.lastToolCallResponse = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let commandCancel: (() => void);
|
onMounted(async () => {
|
||||||
|
for (const client of mcpClientAdapter.clients) {
|
||||||
onMounted(() => {
|
await client.getTools();
|
||||||
commandCancel = bridge.addCommandListener('tools/list', (data: CasualRestAPI<ToolsListResponse>) => {
|
}
|
||||||
toolsManager.tools = data.msg.tools || [];
|
|
||||||
|
|
||||||
const targetTool = toolsManager.tools.find((tool) => {
|
|
||||||
return tool.name === tabStorage.currentToolName;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (targetTool === undefined) {
|
|
||||||
tabStorage.currentToolName = toolsManager.tools[0].name;
|
|
||||||
tabStorage.lastToolCallResponse = undefined;
|
|
||||||
}
|
|
||||||
}, { once: false });
|
|
||||||
|
|
||||||
reloadTools({ first: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
if (commandCancel){
|
|
||||||
commandCancel();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -122,7 +106,7 @@ onUnmounted(() => {
|
|||||||
width: 175px;
|
width: 175px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-list-container > .item {
|
.tool-list-container>.item {
|
||||||
margin: 3px;
|
margin: 3px;
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
border-radius: .3em;
|
border-radius: .3em;
|
||||||
@ -134,24 +118,24 @@ onUnmounted(() => {
|
|||||||
transition: var(--animation-3s);
|
transition: var(--animation-3s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-list-container > .item:hover {
|
.tool-list-container>.item:hover {
|
||||||
background-color: var(--main-light-color);
|
background-color: var(--main-light-color);
|
||||||
transition: var(--animation-3s);
|
transition: var(--animation-3s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-list-container > .item.active {
|
.tool-list-container>.item.active {
|
||||||
background-color: var(--main-light-color);
|
background-color: var(--main-light-color);
|
||||||
transition: var(--animation-3s);
|
transition: var(--animation-3s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-list-container > .item > span:first-child {
|
.tool-list-container>.item>span:first-child {
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tool-list-container > .item > span:last-child {
|
.tool-list-container>.item>span:last-child {
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
font-size: 12.5px;
|
font-size: 12.5px;
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
|
@ -1,14 +1,7 @@
|
|||||||
import { useMessageBridge } from '@/api/message-bridge';
|
import { useMessageBridge } from '@/api/message-bridge';
|
||||||
import { mcpSetting } from '@/hook/mcp';
|
import { mcpSetting } from '@/hook/mcp';
|
||||||
import type { ToolsListResponse, ToolCallResponse, CasualRestAPI } from '@/hook/type';
|
import type { ToolsListResponse, ToolCallResponse, CasualRestAPI } from '@/hook/type';
|
||||||
import { pinkLog } from '@/views/setting/util';
|
import { mcpClientAdapter } from '@/views/connect/core';
|
||||||
import { reactive } from 'vue';
|
|
||||||
|
|
||||||
export const toolsManager = reactive<{
|
|
||||||
tools: ToolsListResponse['tools']
|
|
||||||
}>({
|
|
||||||
tools: []
|
|
||||||
});
|
|
||||||
|
|
||||||
export interface ToolStorage {
|
export interface ToolStorage {
|
||||||
currentToolName: string;
|
currentToolName: string;
|
||||||
@ -16,29 +9,24 @@ export interface ToolStorage {
|
|||||||
formData: Record<string, any>;
|
formData: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 根据工具名字和参数调取工具
|
||||||
|
* @param toolName
|
||||||
|
* @param toolArgs
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export async function callTool(toolName: string, toolArgs: Record<string, any>) {
|
||||||
|
|
||||||
|
mcpClientAdapter
|
||||||
|
|
||||||
export function callTool(toolName: string, toolArgs: Record<string, any>) {
|
|
||||||
const bridge = useMessageBridge();
|
const bridge = useMessageBridge();
|
||||||
return new Promise<ToolCallResponse>((resolve, reject) => {
|
const { msg } = await bridge.commandRequest<ToolCallResponse>('tools/call', {
|
||||||
bridge.addCommandListener('tools/call', (data: CasualRestAPI<ToolCallResponse>) => {
|
toolName,
|
||||||
console.log(data.msg);
|
toolArgs: JSON.parse(JSON.stringify(toolArgs)),
|
||||||
|
callToolOption: {
|
||||||
if (data.code !== 200) {
|
timeout: mcpSetting.timeout * 1000
|
||||||
resolve(data.msg);
|
}
|
||||||
} else {
|
|
||||||
resolve(data.msg);
|
|
||||||
}
|
|
||||||
}, { once: true });
|
|
||||||
|
|
||||||
bridge.postMessage({
|
|
||||||
command: 'tools/call',
|
|
||||||
data: {
|
|
||||||
toolName,
|
|
||||||
toolArgs: JSON.parse(JSON.stringify(toolArgs)),
|
|
||||||
callToolOption: {
|
|
||||||
timeout: mcpSetting.timeout * 1000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return msg;
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
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 } from "vue";
|
import { markRaw, ref, type Reactive } from "vue";
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import type { McpClient } from "@/views/connect/core";
|
import type { McpClient } from "@/views/connect/core";
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ export interface SaveTab {
|
|||||||
|
|
||||||
export const panelLoaded = ref(false);
|
export const panelLoaded = ref(false);
|
||||||
|
|
||||||
export async function loadPanels(client: McpClient) {
|
export async function loadPanels(client: McpClient | Reactive<McpClient>) {
|
||||||
const bridge = useMessageBridge();
|
const bridge = useMessageBridge();
|
||||||
const { code, msg } = await bridge.commandRequest<SaveTab>('panel/load', {
|
const { code, msg } = await bridge.commandRequest<SaveTab>('panel/load', {
|
||||||
clientId: client.clientId
|
clientId: client.clientId
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
OpenMCP Client 0.0.9 由 OpenMCP@<a href="https://www.zhihu.com/people/can-meng-zhong-de-che-xian">锦恢</a> 开发
|
OpenMCP Client 0.1.0 由 OpenMCP@<a href="https://www.zhihu.com/people/can-meng-zhong-de-che-xian">锦恢</a> 开发
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
@ -1,11 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="connection-option">
|
<div class="connection-option">
|
||||||
<span>{{ t('log') }}</span>
|
<div class="header">
|
||||||
|
<span>{{ t('log') }}</span>
|
||||||
|
<span class="iconfont icon-delete" @click="clearLogs"></span>
|
||||||
|
</div>
|
||||||
<el-scrollbar height="90%">
|
<el-scrollbar height="90%">
|
||||||
<div class="output-content">
|
<div class="output-content">
|
||||||
<div v-for="(log, index) in client.connectionResult.logString" :key="index" :class="log.type">
|
<el-collapse :expand-icon-position="'left'">
|
||||||
<span class="log-message">{{ log.message }}</span>
|
<el-collapse-item v-for="(log, index) in logString" :name="index" :class="['item', log.type]">
|
||||||
</div>
|
<template #title>
|
||||||
|
<div class="tool-calls">
|
||||||
|
<div class="tool-call-header">
|
||||||
|
<span>{{ log.message.split('\n')[0] }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="logger-inner">
|
||||||
|
{{ log.message }}
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
</div>
|
</div>
|
||||||
</el-scrollbar>
|
</el-scrollbar>
|
||||||
</div>
|
</div>
|
||||||
@ -24,10 +39,15 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const client = computed(() => mcpClientAdapter.clients[props.index]);
|
const logString = computed(() => {
|
||||||
|
return mcpClientAdapter.clients[props.index].connectionResult.logString;
|
||||||
|
});
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
function clearLogs() {
|
||||||
|
mcpClientAdapter.clients[props.index].connectionResult.logString = [];
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@ -55,28 +75,22 @@ const { t } = useI18n();
|
|||||||
height: 95%;
|
height: 95%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.output-content .item {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 0px 9px;
|
||||||
|
border-radius: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
.output-content .info {
|
.output-content .info {
|
||||||
background-color: rgba(103, 194, 58, 0.5);
|
background-color: rgba(103, 194, 58, 0.5);
|
||||||
margin: 8px 0;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
padding: 5px 9px;
|
|
||||||
border-radius: .5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.output-content .error {
|
.output-content .error {
|
||||||
background-color: rgba(245, 108, 108, 0.5);
|
background-color: rgba(245, 108, 108, 0.5);
|
||||||
margin: 8px 0;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
padding: 5px 9px;
|
|
||||||
border-radius: .5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.output-content .warning {
|
.output-content .warning {
|
||||||
background-color: rgba(230, 162, 60, 0.5);
|
background-color: rgba(230, 162, 60, 0.5);
|
||||||
margin: 8px 0;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
padding: 5px 9px;
|
|
||||||
border-radius: .5em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-icon {
|
.log-icon {
|
||||||
@ -104,4 +118,34 @@ const { t } = useI18n();
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.output-content .el-collapse-item__header,
|
||||||
|
.output-content .el-collapse-item__wrap {
|
||||||
|
background-color: unset !important;
|
||||||
|
border-bottom: unset !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.output-content .el-collapse-item__content {
|
||||||
|
padding-bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logger-inner {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .icon-delete {
|
||||||
|
margin-left: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .icon-delete:hover {
|
||||||
|
color: var(--el-color-error);
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
@ -1,9 +1,11 @@
|
|||||||
import { useMessageBridge } from "@/api/message-bridge";
|
import { useMessageBridge } from "@/api/message-bridge";
|
||||||
import { reactive } from "vue";
|
import { reactive, type Reactive } from "vue";
|
||||||
import type { IConnectionResult, ConnectionTypeOptionItem, IConnectionArgs, IConnectionEnvironment, McpOptions } from "./type";
|
import type { IConnectionResult, ConnectionTypeOptionItem, IConnectionArgs, IConnectionEnvironment, McpOptions } from "./type";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { loadPanels } from "@/hook/panel";
|
import { loadPanels } from "@/hook/panel";
|
||||||
import { getPlatform } from "@/api/platform";
|
import { getPlatform } from "@/api/platform";
|
||||||
|
import type { PromptsGetResponse, PromptsListResponse, PromptTemplate, Resources, ResourcesListResponse, ResourcesReadResponse, ResourceTemplate, ResourceTemplatesListResponse, ToolCallResponse, ToolItem, ToolsListResponse } from "@/hook/type";
|
||||||
|
import { mcpSetting } from "@/hook/mcp";
|
||||||
|
|
||||||
export const connectionSelectDataViewOption: ConnectionTypeOptionItem[] = [
|
export const connectionSelectDataViewOption: ConnectionTypeOptionItem[] = [
|
||||||
{
|
{
|
||||||
@ -37,6 +39,11 @@ export class McpClient {
|
|||||||
// setting 面板的 ref
|
// setting 面板的 ref
|
||||||
public connectionSettingRef: any = null;
|
public connectionSettingRef: any = null;
|
||||||
|
|
||||||
|
public tools: Map<string, ToolItem> | null = null;
|
||||||
|
public promptTemplates: Map<string, PromptTemplate> | null = null;
|
||||||
|
public resources: Map<string, Resources> | null = null;
|
||||||
|
public resourceTemplates: Map<string, ResourceTemplate> | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public clientVersion: string = '0.0.1',
|
public clientVersion: string = '0.0.1',
|
||||||
public clientNamePrefix: string = 'openmcp.connect'
|
public clientNamePrefix: string = 'openmcp.connect'
|
||||||
@ -104,6 +111,83 @@ export class McpClient {
|
|||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getTools() {
|
||||||
|
if (this.tools) {
|
||||||
|
return this.tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bridge = useMessageBridge();
|
||||||
|
|
||||||
|
const { code, msg } = await bridge.commandRequest<ToolsListResponse>('tools/list', { clientId: this.clientId });
|
||||||
|
if (code!== 200) {
|
||||||
|
return new Map<string, ToolItem>();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tools = new Map<string, ToolItem>();
|
||||||
|
msg.tools.forEach(tool => {
|
||||||
|
this.tools!.set(tool.name, tool);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.tools;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPromptTemplates() {
|
||||||
|
if (this.promptTemplates) {
|
||||||
|
return this.promptTemplates;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bridge = useMessageBridge();
|
||||||
|
|
||||||
|
const { code, msg } = await bridge.commandRequest<PromptsListResponse>('prompts/list', { clientId: this.clientId });
|
||||||
|
if (code!== 200) {
|
||||||
|
return new Map<string, PromptTemplate>();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.promptTemplates = new Map<string, PromptTemplate>();
|
||||||
|
msg.prompts.forEach(template => {
|
||||||
|
this.promptTemplates!.set(template.name, template);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.promptTemplates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getResources() {
|
||||||
|
if (this.resources) {
|
||||||
|
return this.resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bridge = useMessageBridge();
|
||||||
|
|
||||||
|
const { code, msg } = await bridge.commandRequest<ResourcesListResponse>('resources/list', { clientId: this.clientId });
|
||||||
|
if (code!== 200) {
|
||||||
|
return new Map<string, Resources>();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resources = new Map<string, Resources>();
|
||||||
|
msg.resources.forEach(resource => {
|
||||||
|
this.resources!.set(resource.name, resource);
|
||||||
|
});
|
||||||
|
return this.resources;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getResourceTemplates() {
|
||||||
|
if (this.resourceTemplates) {
|
||||||
|
return this.resourceTemplates;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bridge = useMessageBridge();
|
||||||
|
|
||||||
|
const { code, msg } = await bridge.commandRequest<ResourceTemplatesListResponse>('resources/templates/list', { clientId: this.clientId });
|
||||||
|
if (code!== 200) {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
this.resourceTemplates = new Map<string, ResourceTemplate>();
|
||||||
|
msg.resourceTemplates.forEach(template => {
|
||||||
|
this.resourceTemplates!.set(template.name, template);
|
||||||
|
});
|
||||||
|
return this.resourceTemplates;
|
||||||
|
}
|
||||||
|
|
||||||
private get commandAndArgs() {
|
private get commandAndArgs() {
|
||||||
const commandString = this.connectionArgs.commandString;
|
const commandString = this.connectionArgs.commandString;
|
||||||
|
|
||||||
@ -163,14 +247,6 @@ export class McpClient {
|
|||||||
ElMessage.error(message);
|
ElMessage.error(message);
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
const info = msg.info || '';
|
|
||||||
if (info) {
|
|
||||||
this.connectionResult.logString.push({
|
|
||||||
type: 'info',
|
|
||||||
message: msg.info || ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.connectionResult.logString.push({
|
this.connectionResult.logString.push({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
message: msg.name + ' ' + msg.version + ' 连接成功'
|
message: msg.name + ' ' + msg.version + ' 连接成功'
|
||||||
@ -247,10 +323,11 @@ export class McpClient {
|
|||||||
|
|
||||||
|
|
||||||
class McpClientAdapter {
|
class McpClientAdapter {
|
||||||
public clients: McpClient[] = [];
|
public clients: Reactive<McpClient[]> = [];
|
||||||
public currentClientIndex: number = 0;
|
public currentClientIndex: number = 0;
|
||||||
|
|
||||||
private defaultClient: McpClient = new McpClient();
|
private defaultClient: McpClient = new McpClient();
|
||||||
|
public connectLogListenerCancel: (() => void) | null = null;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public platform: string
|
public platform: string
|
||||||
@ -298,6 +375,28 @@ class McpClientAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async launch() {
|
public async launch() {
|
||||||
|
// 创建对于 log/output 的监听
|
||||||
|
if (!this.connectLogListenerCancel) {
|
||||||
|
const bridge = useMessageBridge();
|
||||||
|
this.connectLogListenerCancel = bridge.addCommandListener('connect/log', (message) => {
|
||||||
|
const { code, msg } = message;
|
||||||
|
|
||||||
|
console.log(code, msg);
|
||||||
|
const client = this.clients.at(-1);
|
||||||
|
console.log(client);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
client.connectionResult.logString.push({
|
||||||
|
type: code === 200 ? 'info': 'error',
|
||||||
|
message: msg
|
||||||
|
});
|
||||||
|
|
||||||
|
}, { once: false });
|
||||||
|
}
|
||||||
|
|
||||||
const launchSignature = await this.getLaunchSignature();
|
const launchSignature = await this.getLaunchSignature();
|
||||||
console.log('launchSignature', launchSignature);
|
console.log('launchSignature', launchSignature);
|
||||||
|
|
||||||
@ -306,7 +405,7 @@ class McpClientAdapter {
|
|||||||
for (const item of launchSignature) {
|
for (const item of launchSignature) {
|
||||||
|
|
||||||
// 创建一个新的客户端
|
// 创建一个新的客户端
|
||||||
const client = new McpClient();
|
const client = reactive(new McpClient());
|
||||||
|
|
||||||
// 同步连接参数
|
// 同步连接参数
|
||||||
await client.acquireConnectionSignature(item);
|
await client.acquireConnectionSignature(item);
|
||||||
@ -314,11 +413,11 @@ class McpClientAdapter {
|
|||||||
// 同步环境变量
|
// 同步环境变量
|
||||||
await client.handleEnvSwitch(true);
|
await client.handleEnvSwitch(true);
|
||||||
|
|
||||||
|
this.clients.push(client);
|
||||||
|
|
||||||
// 连接
|
// 连接
|
||||||
const ok = await client.connect();
|
const ok = await client.connect();
|
||||||
allOk &&= ok;
|
allOk &&= ok;
|
||||||
|
|
||||||
this.clients.push(client);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果全部成功,保存连接参数
|
// 如果全部成功,保存连接参数
|
||||||
@ -327,6 +426,76 @@ class McpClientAdapter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async readResource(resourceUri?: string) {
|
||||||
|
if (!resourceUri) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 如果遇到不同服务器的同名 tool,请拓展解决方案
|
||||||
|
// 目前只找到第一个匹配 toolName 的工具进行调用
|
||||||
|
let clientId = this.clients[0].clientId;
|
||||||
|
|
||||||
|
for (const client of this.clients) {
|
||||||
|
const resources = await client.getResources();
|
||||||
|
const resource = resources.get(resourceUri);
|
||||||
|
if (resource) {
|
||||||
|
clientId = client.clientId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bridge = useMessageBridge();
|
||||||
|
const { code, msg } = await bridge.commandRequest<ResourcesReadResponse>('resources/read', { clientId, resourceUri });
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async readPromptTemplate(promptId: string, args: Record<string, any>) {
|
||||||
|
// TODO: 如果遇到不同服务器的同名 tool,请拓展解决方案
|
||||||
|
// 目前只找到第一个匹配 toolName 的工具进行调用
|
||||||
|
let clientId = this.clients[0].clientId;
|
||||||
|
|
||||||
|
for (const client of this.clients) {
|
||||||
|
const promptTemplates = await client.getPromptTemplates();
|
||||||
|
const promptTemplate = promptTemplates.get(promptId);
|
||||||
|
if (promptTemplate) {
|
||||||
|
clientId = client.clientId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bridge = useMessageBridge();
|
||||||
|
const { code, msg } = await bridge.commandRequest<PromptsGetResponse>('prompts/get', { clientId, promptId, args });
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async callTool(toolName: string, toolArgs: Record<string, any>) {
|
||||||
|
// TODO: 如果遇到不同服务器的同名 tool,请拓展解决方案
|
||||||
|
// 目前只找到第一个匹配 toolName 的工具进行调用
|
||||||
|
let clientId = this.clients[0].clientId;
|
||||||
|
|
||||||
|
for (const client of this.clients) {
|
||||||
|
const tools = await client.getTools();
|
||||||
|
const tool = tools.get(toolName);
|
||||||
|
if (tool) {
|
||||||
|
clientId = client.clientId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bridge = useMessageBridge();
|
||||||
|
const { msg } = await bridge.commandRequest<ToolCallResponse>('tools/call', {
|
||||||
|
clientId,
|
||||||
|
toolName,
|
||||||
|
toolArgs: JSON.parse(JSON.stringify(toolArgs)),
|
||||||
|
callToolOption: {
|
||||||
|
timeout: mcpSetting.timeout * 1000
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
public async loadPanels() {
|
public async loadPanels() {
|
||||||
const masterNode = this.clients[0];
|
const masterNode = this.clients[0];
|
||||||
await loadPanels(masterNode);
|
await loadPanels(masterNode);
|
||||||
@ -337,3 +506,12 @@ const platform = getPlatform();
|
|||||||
export const mcpClientAdapter = reactive(
|
export const mcpClientAdapter = reactive(
|
||||||
new McpClientAdapter(platform)
|
new McpClientAdapter(platform)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export interface ISegmentViewItem {
|
||||||
|
value: any;
|
||||||
|
label: string;
|
||||||
|
client: McpClient;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const segmentsView = reactive<ISegmentViewItem[]>([]);
|
@ -20,11 +20,11 @@ export async function initialise() {
|
|||||||
// 获取引导状态
|
// 获取引导状态
|
||||||
await getTour();
|
await getTour();
|
||||||
|
|
||||||
|
loading.close();
|
||||||
|
|
||||||
// 尝试进行初始化连接
|
// 尝试进行初始化连接
|
||||||
await mcpClientAdapter.launch();
|
await mcpClientAdapter.launch();
|
||||||
|
|
||||||
// loading panels
|
// loading panels
|
||||||
await mcpClientAdapter.loadPanels();
|
await mcpClientAdapter.loadPanels();
|
||||||
|
|
||||||
loading.close();
|
|
||||||
}
|
}
|
@ -24,7 +24,9 @@
|
|||||||
<span class="iconfont icon-cuo"></span>
|
<span class="iconfont icon-cuo"></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="delete-btn" @click.stop="deleteServer(scope.item.index)">
|
<span
|
||||||
|
v-if="scope.item.index > 0"
|
||||||
|
class="delete-btn" @click.stop="deleteServer(scope.item.index)">
|
||||||
<span class="iconfont icon-delete"></span>
|
<span class="iconfont icon-delete"></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@ -41,7 +43,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent, computed } from 'vue';
|
import { defineComponent, computed, watch, toRef } from 'vue';
|
||||||
import ConnectionPanel from './connection-panel.vue';
|
import ConnectionPanel from './connection-panel.vue';
|
||||||
import { McpClient, mcpClientAdapter } from './core';
|
import { McpClient, mcpClientAdapter } from './core';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
@ -58,14 +60,15 @@ function addServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const serverOptions = computed(() => {
|
const serverOptions = computed(() => {
|
||||||
return mcpClientAdapter.clients.map((client, index) => ({
|
return mcpClientAdapter.clients.map((client, index) => ({
|
||||||
value: index,
|
value: index,
|
||||||
label: `Server ${index + 1}`,
|
label: `Server ${index + 1}`,
|
||||||
client,
|
client,
|
||||||
index
|
index
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
function deleteServer(index: number) {
|
function deleteServer(index: number) {
|
||||||
if (mcpClientAdapter.clients.length <= 1) {
|
if (mcpClientAdapter.clients.length <= 1) {
|
||||||
ElMessage.warning('至少需要保留一个服务器连接');
|
ElMessage.warning('至少需要保留一个服务器连接');
|
||||||
@ -75,6 +78,7 @@ function deleteServer(index: number) {
|
|||||||
if (mcpClientAdapter.currentClientIndex >= mcpClientAdapter.clients.length) {
|
if (mcpClientAdapter.currentClientIndex >= mcpClientAdapter.clients.length) {
|
||||||
mcpClientAdapter.currentClientIndex = mcpClientAdapter.clients.length - 1;
|
mcpClientAdapter.currentClientIndex = mcpClientAdapter.clients.length - 1;
|
||||||
}
|
}
|
||||||
|
mcpClientAdapter.saveLaunchSignature();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -89,6 +93,7 @@ function deleteServer(index: number) {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: 150px;
|
width: 150px;
|
||||||
|
height: 50px;
|
||||||
border-right: 1px solid var(--border-color);
|
border-right: 1px solid var(--border-color);
|
||||||
padding: 15px 25px;
|
padding: 15px 25px;
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ export class ConnectController {
|
|||||||
|
|
||||||
@Controller('connect')
|
@Controller('connect')
|
||||||
async connect(data: any, webview: PostMessageble) {
|
async connect(data: any, webview: PostMessageble) {
|
||||||
const res = await connectService(data);
|
const res = await connectService(data, webview);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import { randomUUID } from 'node:crypto';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
|
import { PostMessageble } from '../hook/adapter';
|
||||||
|
|
||||||
export const clientMap: Map<string, RequestClientType> = new Map();
|
export const clientMap: Map<string, RequestClientType> = new Map();
|
||||||
export function getClient(clientId?: string): RequestClientType | undefined {
|
export function getClient(clientId?: string): RequestClientType | undefined {
|
||||||
@ -14,15 +15,11 @@ export function getClient(clientId?: string): RequestClientType | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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);
|
const commandString = command + ' ' + args.join(' ');
|
||||||
console.log('current args', args);
|
|
||||||
|
|
||||||
const commandString = command + ' ' + args.join(' ');
|
|
||||||
|
|
||||||
const result = spawnSync(commandString, {
|
const result = spawnSync(commandString, {
|
||||||
cwd: cwd || process.cwd(),
|
cwd: cwd || process.cwd(),
|
||||||
STDIO: 'pipe',
|
stdio: 'pipe',
|
||||||
encoding: 'utf-8'
|
encoding: 'utf-8'
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -33,10 +30,9 @@ export function tryGetRunCommandError(command: string, args: string[] = [], cwd?
|
|||||||
return result.stderr || `Command failed with code ${result.status}`;
|
return result.stderr || `Command failed with code ${result.status}`;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
} catch (error) {
|
||||||
} catch (error) {
|
return error instanceof Error ? error.message : String(error);
|
||||||
return error instanceof Error ? error.message : String(error);
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCWD(option: McpOptions) {
|
function getCWD(option: McpOptions) {
|
||||||
@ -61,12 +57,15 @@ function getCommandFileExt(option: McpOptions) {
|
|||||||
function collectAllOutputExec(command: string, cwd: string) {
|
function collectAllOutputExec(command: string, cwd: string) {
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
exec(command, { cwd }, (error, stdout, stderr) => {
|
exec(command, { cwd }, (error, stdout, stderr) => {
|
||||||
resolve(error + stdout + stderr);
|
const errorString = error || '';
|
||||||
|
const stdoutString = stdout || '';
|
||||||
|
const stderrString = stderr || '';
|
||||||
|
resolve(errorString + stdoutString + stderrString);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function preprocessCommand(option: McpOptions): Promise<[McpOptions, string]> {
|
async function preprocessCommand(option: McpOptions, webview?: PostMessageble) {
|
||||||
// 对于特殊表示的路径,进行特殊的支持
|
// 对于特殊表示的路径,进行特殊的支持
|
||||||
if (option.args) {
|
if (option.args) {
|
||||||
option.args = option.args.map(arg => {
|
option.args = option.args.map(arg => {
|
||||||
@ -78,17 +77,17 @@ async function preprocessCommand(option: McpOptions): Promise<[McpOptions, strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (option.connectionType === 'SSE' || option.connectionType === 'STREAMABLE_HTTP') {
|
if (option.connectionType === 'SSE' || option.connectionType === 'STREAMABLE_HTTP') {
|
||||||
return [option, ''];
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cwd = getCWD(option);
|
const cwd = getCWD(option);
|
||||||
if (!cwd) {
|
if (!cwd) {
|
||||||
return [option, ''];
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = getCommandFileExt(option);
|
const ext = getCommandFileExt(option);
|
||||||
if (!ext) {
|
if (!ext) {
|
||||||
return [option, ''];
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// STDIO 模式下,对不同类型的项目进行额外支持
|
// STDIO 模式下,对不同类型的项目进行额外支持
|
||||||
@ -96,25 +95,21 @@ async function preprocessCommand(option: McpOptions): Promise<[McpOptions, strin
|
|||||||
// npm:如果没有初始化,则进行 npm init,将 mcp 设置为虚拟环境
|
// npm:如果没有初始化,则进行 npm init,将 mcp 设置为虚拟环境
|
||||||
// go:如果没有初始化,则进行 go mod init
|
// go:如果没有初始化,则进行 go mod init
|
||||||
|
|
||||||
let info: string = '';
|
|
||||||
|
|
||||||
switch (ext) {
|
switch (ext) {
|
||||||
case '.py':
|
case '.py':
|
||||||
info = await initUv(option, cwd);
|
await initUv(option, cwd, webview);
|
||||||
break;
|
break;
|
||||||
case '.js':
|
case '.js':
|
||||||
case '.ts':
|
case '.ts':
|
||||||
info = await initNpm(option, cwd);
|
await initNpm(option, cwd, webview);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [option, ''];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initUv(option: McpOptions, cwd: string) {
|
async function initUv(option: McpOptions, cwd: string, webview?: PostMessageble) {
|
||||||
let projectDir = cwd;
|
let projectDir = cwd;
|
||||||
|
|
||||||
while (projectDir!== path.dirname(projectDir)) {
|
while (projectDir!== path.dirname(projectDir)) {
|
||||||
@ -140,16 +135,27 @@ async function initUv(option: McpOptions, cwd: string) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
let info = '';
|
const syncOutput = await collectAllOutputExec('uv sync', projectDir);
|
||||||
|
webview?.postMessage({
|
||||||
|
command: 'connect/log',
|
||||||
|
data: {
|
||||||
|
code: syncOutput.toLowerCase().startsWith('error') ? 501: 200,
|
||||||
|
msg: syncOutput
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
info += await collectAllOutputExec('uv sync', projectDir) + '\n';
|
const addOutput = await collectAllOutputExec('uv add mcp "mcp[cli]"', projectDir);
|
||||||
info += await collectAllOutputExec('uv add mcp "mcp[cli]"', projectDir) + '\n';
|
webview?.postMessage({
|
||||||
|
command: 'connect/log',
|
||||||
return info;
|
data: {
|
||||||
|
code: addOutput.toLowerCase().startsWith('error') ? 501: 200,
|
||||||
|
msg: addOutput
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function initNpm(option: McpOptions, cwd: string) {
|
async function initNpm(option: McpOptions, cwd: string, webview?: PostMessageble) {
|
||||||
let projectDir = cwd;
|
let projectDir = cwd;
|
||||||
|
|
||||||
while (projectDir !== path.dirname(projectDir)) {
|
while (projectDir !== path.dirname(projectDir)) {
|
||||||
@ -164,18 +170,26 @@ async function initNpm(option: McpOptions, cwd: string) {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return execSync('npm i', { cwd: projectDir }).toString('utf-8') + '\n';
|
const installOutput = execSync('npm i', { cwd: projectDir }).toString('utf-8') + '\n';
|
||||||
|
webview?.postMessage({
|
||||||
|
command: 'connect/log',
|
||||||
|
data: {
|
||||||
|
code: installOutput.toLowerCase().startsWith('error')? 200: 501,
|
||||||
|
msg: installOutput
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function connectService(
|
export async function connectService(
|
||||||
option: McpOptions
|
option: McpOptions,
|
||||||
|
webview?: PostMessageble
|
||||||
): Promise<RestfulResponse> {
|
): Promise<RestfulResponse> {
|
||||||
try {
|
try {
|
||||||
const { env, ...others } = option;
|
const { env, ...others } = option;
|
||||||
console.log('ready to connect', others);
|
console.log('ready to connect', others);
|
||||||
|
|
||||||
const [_, info] = await preprocessCommand(option);
|
await preprocessCommand(option, webview);
|
||||||
|
|
||||||
const client = await connect(option);
|
const client = await connect(option);
|
||||||
const uuid = randomUUID();
|
const uuid = randomUUID();
|
||||||
@ -189,8 +203,7 @@ export async function connectService(
|
|||||||
status: 'success',
|
status: 'success',
|
||||||
clientId: uuid,
|
clientId: uuid,
|
||||||
name: versionInfo?.name,
|
name: versionInfo?.name,
|
||||||
version: versionInfo?.version,
|
version: versionInfo?.version
|
||||||
info
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user