Compare commits

..

3 Commits

Author SHA1 Message Date
4d459464d3 update 2025-05-20 03:47:15 +08:00
li1553770945
8b3816d05c Merge branch 'main' of https://github.com/LSTM-Kirigaya/openmcp-client 2025-05-19 23:39:29 +08:00
kirigaya
f84834a97f update lock 2025-05-19 20:49:40 +08:00
17 changed files with 223 additions and 142 deletions

5
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "openmcp",
"version": "0.0.9",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openmcp",
"version": "0.0.9",
"version": "0.1.0",
"workspaces": [
"service",
"renderer",
@ -11601,7 +11601,6 @@
"service": {
"name": "@openmcp/service",
"version": "0.0.1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.10.2",

View File

@ -55,7 +55,12 @@ onMounted(async () => {
// }
//
await bridge.awaitForWebsocket();
console.log('enter');
await bridge.awaitForWebsocket();
console.log('enter2');
//
if (!privilegeStatus.allow) {

View File

@ -77,6 +77,8 @@ export class MessageBridge {
throw new Error('setupSignature must be a string');
}
console.log(wsUrl);
this.ws = new WebSocket(wsUrl);
const ws = this.ws;
@ -117,6 +119,7 @@ export class MessageBridge {
}
public async awaitForWebsocket() {
if (this.isConnected) {
return await this.isConnected;
}

View File

@ -37,14 +37,16 @@ import { mcpClientAdapter } from '@/views/connect/core';
defineComponent({ name: 'connected' });
const { t } = useI18n();
const client = mcpClientAdapter.masterNode;
const client = computed(() => mcpClientAdapter.masterNode);
console.log(client);
const fullDisplayServerName = computed(() => {
return client.connectionResult.name + '/' + client.connectionResult.version;
return client.value.connectionResult.name + '/' + client.value.connectionResult.version;
});
const displayServerName = computed(() => {
const name = client.connectionResult.name;
const name = client.value.connectionResult.name;
if (name.length <= 3) return name;
//

View File

@ -1,6 +1,6 @@
<template>
<!-- STDIO 模式下的命令输入 -->
<div class="connection-option" v-if="client.connectionArgs.type === 'STDIO'">
<div class="connection-option" v-if="client.connectionArgs.connectionType === 'STDIO'">
<span>{{ t('connect-sigature') }}</span>
<span style="width: 310px;">
<el-form :model="client.connectionArgs" :rules="rules" ref="stdioForm">
@ -43,7 +43,7 @@
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { computed, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { ElMessage } from 'element-plus';
import type { FormInstance, FormRules } from 'element-plus';
@ -58,7 +58,7 @@ const props = defineProps({
}
});
const client = mcpClientAdapter.clients[props.index];
const client = computed(() => mcpClientAdapter.clients[props.index]);
const stdioForm = ref<FormInstance>()
const urlForm = ref<FormInstance>()
@ -82,7 +82,7 @@ const rules = reactive<FormRules>({
//
const validateForm = async () => {
try {
if (client.connectionArgs.type === 'STDIO') {
if (client.value.connectionArgs.connectionType === 'STDIO') {
await stdioForm.value?.validate()
} else {
await urlForm.value?.validate()

View File

@ -42,7 +42,7 @@
<script setup lang="ts">
import { defineComponent, ref } from 'vue';
import { computed, defineComponent, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { mcpClientAdapter } from './core';
import type { EnvItem } from './type';
@ -55,7 +55,7 @@ const props = defineProps({
}
});
const client = mcpClientAdapter.clients[props.index];
const client = computed(() => mcpClientAdapter.clients[props.index]);
const { t } = useI18n();
@ -64,24 +64,24 @@ const { t } = useI18n();
*/
function addEnvVar() {
// key
const currentKey = client.connectionEnvironment.newKey;
const currentValue = client.connectionEnvironment.newValue;
const currentKey = client.value.connectionEnvironment.newKey;
const currentValue = client.value.connectionEnvironment.newValue;
if (currentKey.length === 0 || currentValue.length === 0) {
return;
}
const sameNameItems = client.connectionEnvironment.data.filter(item => item.key === currentKey);
const sameNameItems = client.value.connectionEnvironment.data.filter(item => item.key === currentKey);
if (sameNameItems.length > 0) {
const conflictItem = sameNameItems[0];
conflictItem.value = currentValue;
} else {
client.connectionEnvironment.data.push({
client.value.connectionEnvironment.data.push({
key: currentKey, value: currentValue
});
client.connectionEnvironment.newKey = '';
client.connectionEnvironment.newValue = '';
client.value.connectionEnvironment.newKey = '';
client.value.connectionEnvironment.newValue = '';
}
}
@ -90,8 +90,8 @@ function addEnvVar() {
*/
function deleteEnvVar(option: EnvItem) {
const currentKey = option.key;
const reserveItems = client.connectionEnvironment.data.filter(item => item.key !== currentKey);
client.connectionEnvironment.data = reserveItems;
const reserveItems = client.value.connectionEnvironment.data.filter(item => item.key !== currentKey);
client.value.connectionEnvironment.data = reserveItems;
}
const envEnabled = ref(true);

View File

@ -12,7 +12,7 @@
</template>
<script setup lang="ts">
import { defineComponent } from 'vue';
import { computed, defineComponent } from 'vue';
import { useI18n } from 'vue-i18n';
import { mcpClientAdapter } from './core';
@ -24,7 +24,7 @@ const props = defineProps({
}
});
const client = mcpClientAdapter.clients[props.index];
const client = computed(() => mcpClientAdapter.clients[props.index]);
const { t } = useI18n();
@ -32,7 +32,7 @@ const { t } = useI18n();
<style>
.connection-option {
height: 90%;
height: 98%;
}
.connection-option .el-scrollbar__view {

View File

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

View File

@ -28,7 +28,7 @@
</template>
<script setup lang="ts">
import { defineComponent, ref } from 'vue';
import { computed, defineComponent, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import ConnectionMethod from './connection-method.vue';
@ -48,11 +48,7 @@ const props = defineProps({
}
});
const client = mcpClientAdapter.clients[props.index];
console.log(client);
console.log(client.connectionSettingRef);
const client = computed(() => mcpClientAdapter.clients[props.index]);
const { t } = useI18n();
@ -62,7 +58,7 @@ async function connect() {
isLoading.value = true;
const platform = getPlatform();
const ok = await client.connect();
const ok = await client.value.connect();
if (ok) {
mcpClientAdapter.saveLaunchSignature();
@ -83,8 +79,9 @@ async function connect() {
display: flex;
flex-direction: column;
width: 45%;
max-height: 85vh;
min-width: 300px;
padding: 20px;
padding: 5px 20px;
}
.connection-option {

View File

@ -43,7 +43,7 @@ export class McpClient {
) {
// 连接入参
this.connectionArgs = {
type: 'STDIO',
connectionType: 'STDIO',
commandString: '',
cwd: '',
url: '',
@ -69,7 +69,7 @@ export class McpClient {
}
async acquireConnectionSignature(args: IConnectionArgs) {
this.connectionArgs.type = args.type;
this.connectionArgs.connectionType = args.connectionType;
this.connectionArgs.commandString = args.commandString || '';
this.connectionArgs.cwd = args.cwd || '';
this.connectionArgs.url = args.url || '';
@ -123,9 +123,9 @@ export class McpClient {
const url = this.connectionArgs.url;
const cwd = this.connectionArgs.cwd;
const oauth = this.connectionArgs.oauth;
const connectionType = this.connectionArgs.type;
const connectionType = this.connectionArgs.connectionType;
const clientName = this.clientNamePrefix + '.' + this.connectionArgs.type;
const clientName = this.clientNamePrefix + '.' + this.connectionArgs.connectionType;
const clientVersion = this.clientVersion;
const option: McpOptions = {
@ -163,10 +163,18 @@ 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.info || ''
})
message: msg.name + ' ' + msg.version + ' 连接成功'
});
}
this.connectionResult.status = msg.status;
@ -307,7 +315,7 @@ class McpClientAdapter {
await client.handleEnvSwitch(true);
// 连接
const ok = await client.connect(this.platform);
const ok = await client.connect();
allOk &&= ok;
this.clients.push(client);

View File

@ -1,19 +1,35 @@
<template>
<div class="connection-container">
<div class="connection-container-wrapper">
<div class="server-list">
<div v-for="(client, index) in mcpClientAdapter.clients" :key="index" class="server-item"
:class="{ 'active': mcpClientAdapter.currentClientIndex === index }" @click="selectServer(index)">
<span class="connect-status">
<span v-if="client.connectionResult.success">
<span class="iconfont icon-connect"></span>
<span class="iconfont icon-dui"></span>
</span>
<span v-else>
<span class="iconfont icon-connect"></span>
<span class="server-name"> Unconnected </span>
</span>
</span>
</div>
<el-segmented
v-model="mcpClientAdapter.currentClientIndex"
:options="serverOptions"
style="background-color: var(--background);"
>
<template #default="scope"
@click="selectServer(scope.item.index)"
>
<div class="server-item" :class="{ 'active': mcpClientAdapter.currentClientIndex === scope.index }">
<span class="connect-status">
<span v-if="scope.item.client.connectionResult.success"
class="success"
>
<span class="name">{{ scope.item.client.connectionResult.name }}</span>
<span class="iconfont icon-dui"></span>
</span>
<span v-else>
<span class="server-name" style="margin-right: 60px;">
<span class="iconfont icon-blank"></span>
</span>
<span class="iconfont icon-cuo"></span>
</span>
</span>
<span class="delete-btn" @click.stop="deleteServer(scope.item.index)">
<span class="iconfont icon-delete"></span>
</span>
</div>
</template>
</el-segmented>
<div class="add-server" @click="addServer">
<span class="iconfont icon-add"></span>
</div>
@ -25,7 +41,7 @@
</template>
<script setup lang="ts">
import { defineComponent } from 'vue';
import { defineComponent, computed } from 'vue';
import ConnectionPanel from './connection-panel.vue';
import { McpClient, mcpClientAdapter } from './core';
import { ElMessage } from 'element-plus';
@ -37,22 +53,44 @@ function selectServer(index: number) {
}
function addServer() {
ElMessage.info('Add server is not implemented yet');
mcpClientAdapter.clients.push(new McpClient());
mcpClientAdapter.currentClientIndex = mcpClientAdapter.clients.length - 1;
}
const serverOptions = computed(() => {
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('至少需要保留一个服务器连接');
return;
}
mcpClientAdapter.clients.splice(index, 1);
if (mcpClientAdapter.currentClientIndex >= mcpClientAdapter.clients.length) {
mcpClientAdapter.currentClientIndex = mcpClientAdapter.clients.length - 1;
}
}
</script>
<style>
.connection-container {
.connection-container-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
.server-list {
display: flex;
align-items: center;
width: 150px;
border-right: 1px solid var(--border-color);
padding: 10px;
padding: 15px 25px;
}
.server-name {
@ -74,6 +112,19 @@ function addServer() {
color: white;
}
.server-item .name {
width: 100px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 5px;
}
.server-item .success {
display: flex;
align-items: center;
}
.server-status {
font-size: 12px;
}
@ -100,6 +151,15 @@ function addServer() {
.panel-container {
flex: 1;
padding: 20px;
padding: 5px;
}
.delete-btn {
margin-left: 10px;
cursor: pointer;
color: var(--error-color);
}
.delete-btn:hover {
opacity: 0.8;
}
</style>

View File

@ -6,14 +6,6 @@ export interface ConnectionTypeOptionItem {
label: string;
}
export interface IConnectionArgs {
type: ConnectionType;
commandString?: string;
cwd?: string;
url?: string;
oauth?: string;
}
export interface IConnectionResult {
info?: string;
@ -64,7 +56,7 @@ export interface IConnectionEnvironment {
}
export interface IConnectionArgs {
type: ConnectionType;
connectionType: ConnectionType;
commandString?: string;
cwd?: string;
url?: string;

View File

@ -2,17 +2,13 @@
<el-scrollbar height="100%">
<div class="setting-container">
<div>
<el-segmented
v-model="settingSections.current"
:options="settingSections.data"
size="large"
style="margin: 10px; font-size: 16px; background-color: var(--background);"
>
<template #default="scope">
<div class="setting-section-option">
{{ scope.item.label }}
</div>
</template>
<el-segmented v-model="settingSections.current" :options="settingSections.data" size="large"
style="margin: 10px; font-size: 16px; background-color: var(--background);">
<template #default="scope">
<div class="setting-section-option">
{{ scope.item.label }}
</div>
</template>
</el-segmented>
<div>

View File

@ -27,25 +27,11 @@ const logger = pino({
export type MessageHandler = (message: VSCodeMessage) => void;
interface IStdioLaunchSignature {
type: 'STDIO';
commandString: string;
cwd: string;
}
interface ISSELaunchSignature {
type:'SSE';
url: string;
oauth: string;
}
export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature;
function refreshConnectionOption(envPath: string) {
const serverPath = path.join(__dirname, '..', '..', 'servers');
const defaultOption = {
type:'STDIO',
connectionType: 'STDIO',
commandString: 'mcp run main.py',
cwd: serverPath
};
@ -73,6 +59,17 @@ function acquireConnectionOption() {
return refreshConnectionOption(envPath);
}
// 按照前端的规范,整理成 commandString 样式
option.data = option.data.map((item: any) => {
if (item.connectionType === 'STDIO') {
item.commandString = [item.command, ...item.args]?.join(' ');
} else {
item.url = item.url;
}
return item;
});
return option;
} catch (error) {

View File

@ -1,4 +1,4 @@
import { execSync, spawnSync } from 'node:child_process';
import { exec, execSync, spawnSync } from 'node:child_process';
import { RequestClientType } from '../common';
import { connect } from './client.service';
import { RestfulResponse } from '../common/index.dto';
@ -6,6 +6,7 @@ import { McpOptions } from './client.dto';
import { randomUUID } from 'node:crypto';
import path from 'node:path';
import fs from 'node:fs';
import * as os from 'os';
export const clientMap: Map<string, RequestClientType> = new Map();
export function getClient(clientId?: string): RequestClientType | undefined {
@ -17,13 +18,22 @@ export function tryGetRunCommandError(command: string, args: string[] = [], cwd?
console.log('current command', command);
console.log('current args', args);
const commandString = [command, ...args].join(' ');
const commandString = command + ' ' + args.join(' ');
const result = execSync(commandString, {
cwd: cwd || process.cwd()
}).toString('utf-8');
const result = spawnSync(commandString, {
cwd: cwd || process.cwd(),
STDIO: 'pipe',
encoding: 'utf-8'
});
if (result.error) {
return result.error.message;
}
if (result.status !== 0) {
return result.stderr || `Command failed with code ${result.status}`;
}
return null;
return result;
} catch (error) {
return error instanceof Error ? error.message : String(error);
}
@ -48,8 +58,15 @@ function getCommandFileExt(option: McpOptions) {
return undefined;
}
function collectAllOutputExec(command: string, cwd: string) {
return new Promise<string>((resolve, reject) => {
exec(command, { cwd }, (error, stdout, stderr) => {
resolve(error + stdout + stderr);
});
});
}
function preprocessCommand(option: McpOptions): [McpOptions, string] {
async function preprocessCommand(option: McpOptions): Promise<[McpOptions, string]> {
// 对于特殊表示的路径,进行特殊的支持
if (option.args) {
option.args = option.args.map(arg => {
@ -83,49 +100,56 @@ function preprocessCommand(option: McpOptions): [McpOptions, string] {
switch (ext) {
case '.py':
info = initUv(cwd);
info = await initUv(option, cwd);
break;
case '.js':
case '.ts':
info = initNpm(cwd);
info = await initNpm(option, cwd);
break;
default:
break;
}
}
return [option, info];
return [option, ''];
}
function initUv(cwd: string) {
async function initUv(option: McpOptions, cwd: string) {
let projectDir = cwd;
while (projectDir!== path.dirname(projectDir)) {
if (fs.readFileSync(projectDir).includes('pyproject.toml')) {
if (fs.readdirSync(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');
// judge by OS
const mcpCli = os.platform() === 'win32' ?
path.join(venv, 'Scripts','mcp.exe') :
path.join(venv, 'bin', 'mcp');
if (option.command === 'mcp') {
option.command = mcpCli;
option.cwd = projectDir;
}
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';
info += await collectAllOutputExec('uv sync', projectDir) + '\n';
info += await collectAllOutputExec('uv add mcp "mcp[cli]"', projectDir) + '\n';
return info;
}
function initNpm(cwd: string) {
async function initNpm(option: McpOptions, cwd: string) {
let projectDir = cwd;
while (projectDir !== path.dirname(projectDir)) {
@ -148,9 +172,10 @@ export async function connectService(
option: McpOptions
): Promise<RestfulResponse> {
try {
console.log('ready to connect', option);
const info = preprocessCommand(option);
const { env, ...others } = option;
console.log('ready to connect', others);
const [_, info] = await preprocessCommand(option);
const client = await connect(option);
const uuid = randomUUID();
@ -172,6 +197,8 @@ export async function connectService(
return connectResult;
} catch (error) {
console.log(error);
// TODO: 这边获取到的 error 不够精致,如何才能获取到更加精准的错误
// 比如 error: Failed to spawn: `server.py`
// Caused by: No such file or directory (os error 2)

View File

@ -28,32 +28,18 @@ const logger = pino({
export type MessageHandler = (message: VSCodeMessage) => void;
interface IStdioLaunchSignature {
type: 'STDIO';
commandString: string;
cwd: string;
}
interface ISSELaunchSignature {
type: 'SSE';
url: string;
oauth: string;
}
export type ILaunchSigature = IStdioLaunchSignature | ISSELaunchSignature;
function refreshConnectionOption(envPath: string) {
const serverPath = path.join(__dirname, '..', '..', 'servers');
const defaultOption = {
type:'STDIO',
connectionType: 'STDIO',
commandString: 'mcp run main.py',
cwd: serverPath
};
fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4));
fs.writeFileSync(envPath, JSON.stringify(defaultOption, null, 4));
return { data: [ defaultOption ] };
return { data: [defaultOption] };
}
function acquireConnectionOption() {
@ -74,6 +60,17 @@ function acquireConnectionOption() {
return refreshConnectionOption(envPath);
}
// 按照前端的规范,整理成 commandString 样式
option.data = option.data.map((item: any) => {
if (item.connectionType === 'STDIO') {
item.commandString = [item.command, ...item.args]?.join(' ');
} else {
item.url = item.url;
}
return item;
});
return option;
} catch (error) {
@ -112,7 +109,7 @@ const wss = new WebSocketServer(
port: 8282,
verifyClient: (info, callback) => {
console.log(info.req.url);
const ok = verifyToken(info.req.url || '');
const ok = verifyToken(info.req.url || '');
if (!ok) {
callback(false, 401, 'Unauthorized: Invalid token');

View File

@ -8,7 +8,6 @@ export class SettingController {
@Controller('setting/save')
async saveSetting(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
saveSetting(data);
console.log('Settings saved successfully');
@ -20,7 +19,6 @@ export class SettingController {
@Controller('setting/load')
async loadSetting(data: RequestData, webview: PostMessageble) {
const client = getClient(data.clientId);
const config = loadSetting();
return {
code: 200,