重构完成基础设施
This commit is contained in:
parent
4d459464d3
commit
468ce23b66
@ -54,7 +54,7 @@
|
||||
| `ext` | 支持基本的 MCP 项目管理 | `迭代版本` | 100% | `P0` |
|
||||
| `service` | 支持自定义支持 openai 接口协议的大模型接入 | `完整版本` | 100% | `Done` |
|
||||
| `service` | 支持自定义接口协议的大模型接入 | `MVP` | 0% | `P1` |
|
||||
| `all` | 支持同时调试多个 MCP Server | `MVP` | 0% | `P1` |
|
||||
| `all` | 支持同时调试多个 MCP Server | `MVP` | 80% | `P0` |
|
||||
| `all` | 支持通过大模型进行在线验证 | `迭代版本` | 100% | `Done` |
|
||||
| `all` | 支持对用户对应服务器的调试工作内容进行保存 | `迭代版本` | 100% | `Done` |
|
||||
| `render` | 高危操作权限确认 | `MVP` | 0% | `P1` |
|
||||
|
@ -54,13 +54,8 @@ onMounted(async () => {
|
||||
// router.push(targetRoute);
|
||||
// }
|
||||
|
||||
// 进行桥接
|
||||
console.log('enter');
|
||||
|
||||
await bridge.awaitForWebsocket();
|
||||
|
||||
console.log('enter2');
|
||||
|
||||
// 进行桥接
|
||||
await bridge.awaitForWebsocket();
|
||||
|
||||
// 根据是否需要密码进行后续的选择
|
||||
if (!privilegeStatus.allow) {
|
||||
|
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 { handleToolCalls, type ToolCallResult } from "./handle-tool-calls";
|
||||
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 ChatCompletionCreateParamsBase = OpenAI.Chat.Completions.ChatCompletionCreateParams & { id?: string };
|
||||
|
@ -83,10 +83,20 @@ const handleKeydown = (event: KeyboardEvent) => {
|
||||
|
||||
const copy = async () => {
|
||||
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('内容已复制到剪贴板');
|
||||
} catch (err) {
|
||||
console.error('无法复制内容: ', err);
|
||||
ElMessage.error('复制失败,请手动复制');
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -40,6 +40,7 @@ import { promptsManager, type PromptStorage } from './prompts';
|
||||
import type { PromptsGetResponse } from '@/hook/type';
|
||||
import { useMessageBridge } from '@/api/message-bridge';
|
||||
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
|
||||
import { mcpClientAdapter } from '@/views/connect/core';
|
||||
|
||||
defineComponent({ name: 'prompt-reader' });
|
||||
|
||||
@ -131,16 +132,14 @@ const resetForm = () => {
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const bridge = useMessageBridge();
|
||||
|
||||
const { code, msg } = await bridge.commandRequest('prompts/get', {
|
||||
promptId: currentPrompt.value.name,
|
||||
args: JSON.parse(JSON.stringify(tabStorage.formData))
|
||||
});
|
||||
const res = await mcpClientAdapter.readPromptTemplate(
|
||||
currentPrompt.value.name,
|
||||
JSON.parse(JSON.stringify(tabStorage.formData))
|
||||
);
|
||||
|
||||
tabStorage.lastPromptGetResponse = msg;
|
||||
|
||||
emits('prompt-get-response', msg);
|
||||
tabStorage.lastPromptGetResponse = res;
|
||||
emits('prompt-get-response', res);
|
||||
}
|
||||
|
||||
if (props.tabId >= 0) {
|
||||
|
@ -42,6 +42,7 @@ import { parseResourceTemplate, resourcesManager, type ResourceStorage } from '.
|
||||
import type{ ResourcesReadResponse } from '@/hook/type';
|
||||
import { useMessageBridge } from '@/api/message-bridge';
|
||||
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
|
||||
import { mcpClientAdapter } from '@/views/connect/core';
|
||||
|
||||
defineComponent({ name: 'resource-reader' });
|
||||
|
||||
@ -154,11 +155,10 @@ function getUri() {
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
const uri = getUri();
|
||||
const res = await mcpClientAdapter.readResource(uri);
|
||||
|
||||
const bridge = useMessageBridge();
|
||||
const { code, msg } = await bridge.commandRequest('resources/read', { resourceUri: uri });
|
||||
tabStorage.lastResourceReadResponse = msg;
|
||||
emits('resource-get-response', msg);
|
||||
tabStorage.lastResourceReadResponse = res;
|
||||
emits('resource-get-response', res);
|
||||
}
|
||||
|
||||
if (props.tabId >= 0) {
|
||||
|
@ -60,10 +60,11 @@ import { defineComponent, defineProps, watch, ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { FormInstance, FormRules } from 'element-plus';
|
||||
import { tabs } from '../panel';
|
||||
import { callTool, toolsManager, type ToolStorage } from './tools';
|
||||
import type { ToolStorage } from './tools';
|
||||
import { getDefaultValue, normaliseJavascriptType } from '@/hook/mcp';
|
||||
|
||||
import KInputObject from '@/components/k-input-object/index.vue';
|
||||
import { mcpClientAdapter } from '@/views/connect/core';
|
||||
|
||||
defineComponent({ name: 'tool-executor' });
|
||||
|
||||
@ -90,7 +91,10 @@ const formRef = ref<FormInstance>();
|
||||
const loading = ref(false);
|
||||
|
||||
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;
|
||||
try {
|
||||
tabStorage.lastToolCallResponse = undefined;
|
||||
const toolResponse = await callTool(tabStorage.currentToolName, tabStorage.formData);
|
||||
const toolResponse = await mcpClientAdapter.callTool(tabStorage.currentToolName, tabStorage.formData);
|
||||
tabStorage.lastToolCallResponse = toolResponse;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
|
@ -1,42 +1,42 @@
|
||||
<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-scrollbar height="500px">
|
||||
<div class="tool-list-container">
|
||||
<div
|
||||
class="item"
|
||||
:class="{ 'active': tabStorage.currentToolName === tool.name }"
|
||||
v-for="tool of toolsManager.tools"
|
||||
:key="tool.name"
|
||||
@click="handleClick(tool)"
|
||||
>
|
||||
<span>{{ tool.name }}</span>
|
||||
<span>{{ tool.description || '' }}</span>
|
||||
</div>
|
||||
<el-collapse :expand-icon-position="'left'" v-model="activeNames">
|
||||
<el-collapse-item v-for="(client, index) in mcpClientAdapter.clients" :name="index" :class="[]">
|
||||
|
||||
<!-- header -->
|
||||
<template #title>
|
||||
<h3 class="resource-template">
|
||||
<code>tools/list</code>
|
||||
<span class="iconfont icon-restart" @click="reloadTools({ first: false })"></span>
|
||||
</h3>
|
||||
</template>
|
||||
|
||||
<!-- body -->
|
||||
|
||||
<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>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- resources/list -->
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useMessageBridge } from '@/api/message-bridge';
|
||||
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 { toolsManager, type ToolStorage } from './tools';
|
||||
import type { ToolStorage } from './tools';
|
||||
import { tabs } from '../panel';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { mcpClientAdapter } from '@/views/connect/core';
|
||||
|
||||
const bridge = useMessageBridge();
|
||||
const { t } = useI18n();
|
||||
@ -51,7 +51,9 @@ const props = defineProps({
|
||||
const tab = tabs.content[props.tabId];
|
||||
const tabStorage = tab.storage as ToolStorage;
|
||||
|
||||
function reloadTools(option: { first: boolean }) {
|
||||
const activeNames = ref<any[]>([0]);
|
||||
|
||||
function reloadTools(option: { first: boolean }) {
|
||||
bridge.postMessage({
|
||||
command: 'tools/list'
|
||||
});
|
||||
@ -71,30 +73,12 @@ function handleClick(tool: { name: string }) {
|
||||
tabStorage.lastToolCallResponse = undefined;
|
||||
}
|
||||
|
||||
let commandCancel: (() => void);
|
||||
|
||||
onMounted(() => {
|
||||
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 });
|
||||
onMounted(async () => {
|
||||
for (const client of mcpClientAdapter.clients) {
|
||||
await client.getTools();
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (commandCancel){
|
||||
commandCancel();
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@ -122,7 +106,7 @@ onUnmounted(() => {
|
||||
width: 175px;
|
||||
}
|
||||
|
||||
.tool-list-container > .item {
|
||||
.tool-list-container>.item {
|
||||
margin: 3px;
|
||||
padding: 5px 10px;
|
||||
border-radius: .3em;
|
||||
@ -134,24 +118,24 @@ onUnmounted(() => {
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.tool-list-container > .item:hover {
|
||||
.tool-list-container>.item:hover {
|
||||
background-color: var(--main-light-color);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.tool-list-container > .item.active {
|
||||
.tool-list-container>.item.active {
|
||||
background-color: var(--main-light-color);
|
||||
transition: var(--animation-3s);
|
||||
}
|
||||
|
||||
.tool-list-container > .item > span:first-child {
|
||||
.tool-list-container>.item>span:first-child {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tool-list-container > .item > span:last-child {
|
||||
.tool-list-container>.item>span:last-child {
|
||||
opacity: 0.6;
|
||||
font-size: 12.5px;
|
||||
max-width: 200px;
|
||||
|
@ -1,14 +1,7 @@
|
||||
import { useMessageBridge } from '@/api/message-bridge';
|
||||
import { mcpSetting } from '@/hook/mcp';
|
||||
import type { ToolsListResponse, ToolCallResponse, CasualRestAPI } from '@/hook/type';
|
||||
import { pinkLog } from '@/views/setting/util';
|
||||
import { reactive } from 'vue';
|
||||
|
||||
export const toolsManager = reactive<{
|
||||
tools: ToolsListResponse['tools']
|
||||
}>({
|
||||
tools: []
|
||||
});
|
||||
import { mcpClientAdapter } from '@/views/connect/core';
|
||||
|
||||
export interface ToolStorage {
|
||||
currentToolName: string;
|
||||
@ -16,29 +9,24 @@ export interface ToolStorage {
|
||||
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();
|
||||
return new Promise<ToolCallResponse>((resolve, reject) => {
|
||||
bridge.addCommandListener('tools/call', (data: CasualRestAPI<ToolCallResponse>) => {
|
||||
console.log(data.msg);
|
||||
|
||||
if (data.code !== 200) {
|
||||
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
|
||||
}
|
||||
}
|
||||
});
|
||||
const { msg } = await bridge.commandRequest<ToolCallResponse>('tools/call', {
|
||||
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 { pinkLog } from "@/views/setting/util";
|
||||
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 type { McpClient } from "@/views/connect/core";
|
||||
|
||||
@ -20,7 +20,7 @@ export interface SaveTab {
|
||||
|
||||
export const panelLoaded = ref(false);
|
||||
|
||||
export async function loadPanels(client: McpClient) {
|
||||
export async function loadPanels(client: McpClient | Reactive<McpClient>) {
|
||||
const bridge = useMessageBridge();
|
||||
const { code, msg } = await bridge.commandRequest<SaveTab>('panel/load', {
|
||||
clientId: client.clientId
|
||||
|
@ -6,7 +6,7 @@
|
||||
</span>
|
||||
|
||||
<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>
|
||||
|
@ -1,11 +1,26 @@
|
||||
<template>
|
||||
<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%">
|
||||
<div class="output-content">
|
||||
<div v-for="(log, index) in client.connectionResult.logString" :key="index" :class="log.type">
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
</div>
|
||||
<el-collapse :expand-icon-position="'left'">
|
||||
<el-collapse-item v-for="(log, index) in logString" :name="index" :class="['item', log.type]">
|
||||
<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>
|
||||
</el-scrollbar>
|
||||
</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();
|
||||
|
||||
function clearLogs() {
|
||||
mcpClientAdapter.clients[props.index].connectionResult.logString = [];
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@ -55,28 +75,22 @@ const { t } = useI18n();
|
||||
height: 95%;
|
||||
}
|
||||
|
||||
.output-content .item {
|
||||
margin-bottom: 12px;
|
||||
padding: 0px 9px;
|
||||
border-radius: .5em;
|
||||
}
|
||||
|
||||
.output-content .info {
|
||||
background-color: rgba(103, 194, 58, 0.5);
|
||||
margin: 8px 0;
|
||||
margin-bottom: 12px;
|
||||
padding: 5px 9px;
|
||||
border-radius: .5em;
|
||||
}
|
||||
|
||||
.output-content .error {
|
||||
background-color: rgba(245, 108, 108, 0.5);
|
||||
margin: 8px 0;
|
||||
margin-bottom: 12px;
|
||||
padding: 5px 9px;
|
||||
border-radius: .5em;
|
||||
}
|
||||
|
||||
.output-content .warning {
|
||||
background-color: rgba(230, 162, 60, 0.5);
|
||||
margin: 8px 0;
|
||||
margin-bottom: 12px;
|
||||
padding: 5px 9px;
|
||||
border-radius: .5em;
|
||||
}
|
||||
|
||||
.log-icon {
|
||||
@ -104,4 +118,34 @@ const { t } = useI18n();
|
||||
display: inline-block;
|
||||
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>
|
@ -1,9 +1,11 @@
|
||||
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 { ElMessage } from "element-plus";
|
||||
import { loadPanels } from "@/hook/panel";
|
||||
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[] = [
|
||||
{
|
||||
@ -37,6 +39,11 @@ export class McpClient {
|
||||
// setting 面板的 ref
|
||||
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(
|
||||
public clientVersion: string = '0.0.1',
|
||||
public clientNamePrefix: string = 'openmcp.connect'
|
||||
@ -104,6 +111,83 @@ export class McpClient {
|
||||
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() {
|
||||
const commandString = this.connectionArgs.commandString;
|
||||
|
||||
@ -163,14 +247,6 @@ export class McpClient {
|
||||
ElMessage.error(message);
|
||||
return false;
|
||||
} else {
|
||||
const info = msg.info || '';
|
||||
if (info) {
|
||||
this.connectionResult.logString.push({
|
||||
type: 'info',
|
||||
message: msg.info || ''
|
||||
});
|
||||
}
|
||||
|
||||
this.connectionResult.logString.push({
|
||||
type: 'info',
|
||||
message: msg.name + ' ' + msg.version + ' 连接成功'
|
||||
@ -247,10 +323,11 @@ export class McpClient {
|
||||
|
||||
|
||||
class McpClientAdapter {
|
||||
public clients: McpClient[] = [];
|
||||
public clients: Reactive<McpClient[]> = [];
|
||||
public currentClientIndex: number = 0;
|
||||
|
||||
private defaultClient: McpClient = new McpClient();
|
||||
public connectLogListenerCancel: (() => void) | null = null;
|
||||
|
||||
constructor(
|
||||
public platform: string
|
||||
@ -298,6 +375,28 @@ class McpClientAdapter {
|
||||
}
|
||||
|
||||
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();
|
||||
console.log('launchSignature', launchSignature);
|
||||
|
||||
@ -306,7 +405,7 @@ class McpClientAdapter {
|
||||
for (const item of launchSignature) {
|
||||
|
||||
// 创建一个新的客户端
|
||||
const client = new McpClient();
|
||||
const client = reactive(new McpClient());
|
||||
|
||||
// 同步连接参数
|
||||
await client.acquireConnectionSignature(item);
|
||||
@ -314,11 +413,11 @@ class McpClientAdapter {
|
||||
// 同步环境变量
|
||||
await client.handleEnvSwitch(true);
|
||||
|
||||
this.clients.push(client);
|
||||
|
||||
// 连接
|
||||
const ok = await client.connect();
|
||||
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() {
|
||||
const masterNode = this.clients[0];
|
||||
await loadPanels(masterNode);
|
||||
@ -336,4 +505,13 @@ class McpClientAdapter {
|
||||
const platform = getPlatform();
|
||||
export const mcpClientAdapter = reactive(
|
||||
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();
|
||||
|
||||
loading.close();
|
||||
|
||||
// 尝试进行初始化连接
|
||||
await mcpClientAdapter.launch();
|
||||
|
||||
// loading panels
|
||||
await mcpClientAdapter.loadPanels();
|
||||
|
||||
loading.close();
|
||||
}
|
@ -24,7 +24,9 @@
|
||||
<span class="iconfont icon-cuo"></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>
|
||||
</div>
|
||||
@ -41,7 +43,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { defineComponent, computed } from 'vue';
|
||||
import { defineComponent, computed, watch, toRef } from 'vue';
|
||||
import ConnectionPanel from './connection-panel.vue';
|
||||
import { McpClient, mcpClientAdapter } from './core';
|
||||
import { ElMessage } from 'element-plus';
|
||||
@ -58,14 +60,15 @@ function addServer() {
|
||||
}
|
||||
|
||||
const serverOptions = computed(() => {
|
||||
return mcpClientAdapter.clients.map((client, index) => ({
|
||||
value: index,
|
||||
label: `Server ${index + 1}`,
|
||||
client,
|
||||
index
|
||||
}));
|
||||
return mcpClientAdapter.clients.map((client, index) => ({
|
||||
value: index,
|
||||
label: `Server ${index + 1}`,
|
||||
client,
|
||||
index
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
function deleteServer(index: number) {
|
||||
if (mcpClientAdapter.clients.length <= 1) {
|
||||
ElMessage.warning('至少需要保留一个服务器连接');
|
||||
@ -75,6 +78,7 @@ function deleteServer(index: number) {
|
||||
if (mcpClientAdapter.currentClientIndex >= mcpClientAdapter.clients.length) {
|
||||
mcpClientAdapter.currentClientIndex = mcpClientAdapter.clients.length - 1;
|
||||
}
|
||||
mcpClientAdapter.saveLaunchSignature();
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -89,6 +93,7 @@ function deleteServer(index: number) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 150px;
|
||||
height: 50px;
|
||||
border-right: 1px solid var(--border-color);
|
||||
padding: 15px 25px;
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ export class ConnectController {
|
||||
|
||||
@Controller('connect')
|
||||
async connect(data: any, webview: PostMessageble) {
|
||||
const res = await connectService(data);
|
||||
const res = await connectService(data, webview);
|
||||
return res;
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import { randomUUID } from 'node:crypto';
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import * as os from 'os';
|
||||
import { PostMessageble } from '../hook/adapter';
|
||||
|
||||
export const clientMap: Map<string, RequestClientType> = new Map();
|
||||
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 {
|
||||
try {
|
||||
console.log('current command', command);
|
||||
console.log('current args', args);
|
||||
|
||||
const commandString = command + ' ' + args.join(' ');
|
||||
|
||||
try {
|
||||
const commandString = command + ' ' + args.join(' ');
|
||||
const result = spawnSync(commandString, {
|
||||
cwd: cwd || process.cwd(),
|
||||
STDIO: 'pipe',
|
||||
stdio: 'pipe',
|
||||
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 null;
|
||||
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
} catch (error) {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
|
||||
function getCWD(option: McpOptions) {
|
||||
@ -61,12 +57,15 @@ function getCommandFileExt(option: McpOptions) {
|
||||
function collectAllOutputExec(command: string, cwd: string) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
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) {
|
||||
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') {
|
||||
return [option, ''];
|
||||
return;
|
||||
}
|
||||
|
||||
const cwd = getCWD(option);
|
||||
if (!cwd) {
|
||||
return [option, ''];
|
||||
return;
|
||||
}
|
||||
|
||||
const ext = getCommandFileExt(option);
|
||||
if (!ext) {
|
||||
return [option, ''];
|
||||
return;
|
||||
}
|
||||
|
||||
// STDIO 模式下,对不同类型的项目进行额外支持
|
||||
@ -96,25 +95,21 @@ async function preprocessCommand(option: McpOptions): Promise<[McpOptions, strin
|
||||
// npm:如果没有初始化,则进行 npm init,将 mcp 设置为虚拟环境
|
||||
// go:如果没有初始化,则进行 go mod init
|
||||
|
||||
let info: string = '';
|
||||
|
||||
switch (ext) {
|
||||
case '.py':
|
||||
info = await initUv(option, cwd);
|
||||
await initUv(option, cwd, webview);
|
||||
break;
|
||||
case '.js':
|
||||
case '.ts':
|
||||
info = await initNpm(option, cwd);
|
||||
await initNpm(option, cwd, webview);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return [option, ''];
|
||||
}
|
||||
}
|
||||
|
||||
async function initUv(option: McpOptions, cwd: string) {
|
||||
async function initUv(option: McpOptions, cwd: string, webview?: PostMessageble) {
|
||||
let projectDir = cwd;
|
||||
|
||||
while (projectDir!== path.dirname(projectDir)) {
|
||||
@ -140,16 +135,27 @@ async function initUv(option: McpOptions, cwd: string) {
|
||||
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';
|
||||
info += await collectAllOutputExec('uv add mcp "mcp[cli]"', projectDir) + '\n';
|
||||
|
||||
return info;
|
||||
const addOutput = await collectAllOutputExec('uv add mcp "mcp[cli]"', projectDir);
|
||||
webview?.postMessage({
|
||||
command: 'connect/log',
|
||||
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;
|
||||
|
||||
while (projectDir !== path.dirname(projectDir)) {
|
||||
@ -164,18 +170,26 @@ async function initNpm(option: McpOptions, cwd: string) {
|
||||
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(
|
||||
option: McpOptions
|
||||
option: McpOptions,
|
||||
webview?: PostMessageble
|
||||
): Promise<RestfulResponse> {
|
||||
try {
|
||||
const { env, ...others } = option;
|
||||
console.log('ready to connect', others);
|
||||
|
||||
const [_, info] = await preprocessCommand(option);
|
||||
await preprocessCommand(option, webview);
|
||||
|
||||
const client = await connect(option);
|
||||
const uuid = randomUUID();
|
||||
@ -189,8 +203,7 @@ export async function connectService(
|
||||
status: 'success',
|
||||
clientId: uuid,
|
||||
name: versionInfo?.name,
|
||||
version: versionInfo?.version,
|
||||
info
|
||||
version: versionInfo?.version
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user