修复全局安装的 mcp 服务器 name 更新的问题

This commit is contained in:
锦恢 2025-05-02 18:00:22 +08:00
parent dca2a9c820
commit 56145bfdf9
24 changed files with 183 additions and 37 deletions

View File

@ -4,7 +4,7 @@
## [main] 0.0.6
- 修复部分因为服务器名称特殊字符而导致的保存实效的错误
- 插件模式下左侧管理面板中的「MCP连接工作区」视图可以进行增删改查了
- 新增「MCP连接全局」,用于安装全局范围的 mcp server
- 新增「安装的 MCP 服务器」,用于安装全局范围的 mcp server
- 增加引导页面
## [main] 0.0.5

View File

@ -6,11 +6,10 @@
<a href="https://qm.qq.com/cgi-bin/qm/qr?k=C6ZUTZvfqWoI12lWe7L93cWa1hUsuVT0&jump_from=webapi&authKey=McW6B1ogTPjPDrCyGttS890tMZGQ1KB3QLuG4aqVNRaYp4vlTSgf2c6dMcNjMuBD" target="_blank" style="display: inline-block; padding: 8px 16px; background-color: #CB81DA; color: white; border-radius: .5em; text-decoration: none;">👉 加入 OpenMCP正式级技术组</a>
<a href="https://qm.qq.com/q/qyVJ189OUg" target="_blank" style="display: inline-block; padding: 8px 16px; background-color: rgb(84, 176, 84); color: white; border-radius: .5em; text-decoration: none;">加入 OpenMCP咖啡厅</a>
<a href="https://qm.qq.com/q/AO0sJS3r7U" target="_blank" style="display: inline-block; padding: 8px 16px; background-color: rgb(84, 176, 84); color: white; border-radius: .5em; text-decoration: none;">加入 OpenMCP正式级宣传组</a>
</div>
## OpenMCP
一款用于 MCP 服务端调试的一体化 vscode/trae 插件。

View File

@ -2,7 +2,7 @@
"name": "openmcp",
"displayName": "OpenMCP",
"description": "An all in one MCP Client/TestTool",
"version": "0.0.5",
"version": "0.0.6",
"publisher": "kirigaya",
"author": {
"name": "kirigaya",
@ -65,6 +65,15 @@
"category": "openmcp",
"icon": "$(gear)"
},
{
"command": "openmcp.sidebar.installed-connection.revealWebviewPanel",
"title": "连接",
"category": "openmcp",
"icon": {
"light": "./icons/light/protocol.svg",
"dark": "./icons/dark/protocol.svg"
}
},
{
"command": "openmcp.sidebar.installed-connection.deleteConnection",
"title": "删除连接",
@ -134,7 +143,7 @@
{
"command": "openmcp.sidebar.workspace-connection.revealWebviewPanel",
"group": "inline@1",
"when": "view == openmcp.sidebar.workspace-connection || view == openmcp.sidebar.installed-connection",
"when": "view == openmcp.sidebar.workspace-connection",
"args": {
"view": "${viewItem}"
}
@ -147,6 +156,14 @@
"view": "${viewItem}"
}
},
{
"command": "openmcp.sidebar.installed-connection.revealWebviewPanel",
"group": "inline@1",
"when": "view == openmcp.sidebar.installed-connection",
"args": {
"view": "${viewItem}"
}
},
{
"command": "openmcp.sidebar.installed-connection.deleteConnection",
"group": "inline@2",

View File

@ -205,7 +205,7 @@ a {
}
.openmcp-image {
background-image: url('./images/openmcp.png');
background-image: url('https://picx.zhimg.com/80/v2-a0aa51e8a61f86586e374520995b5df5_1440w.png?source=d16d100b');
background-size: contain;
background-repeat: no-repeat;
background-position: center;

View File

@ -232,7 +232,8 @@
<TourTitle>终章</TourTitle>
</template>
<div class="tour-common-text">
<pre><code>(base) <span style="color: greenyellow"></span> <span style="color: #6AC2CF">.openmcp</span> <span style="color: #6BC34B">cat</span> <span style="color: #D357DB">KEY</span>
<pre><code style="color: unset !important; background-color: unset !important;"
>(base) <span style="color: greenyellow"></span> <span style="color: #6AC2CF">.openmcp</span> <span style="color: #6BC34B">cat</span> <span style="color: #D357DB">KEY</span>
直面恐惧创造未来
Face your fears, create the future
恐怖に直面し未来を創り出</code></pre>
@ -282,4 +283,9 @@ function finishTour() {
padding: 5px;
margin: 5px 0;
}
.tour-common-text code {
color: unset!important;
background-color: unset!important;
}
</style>

View File

@ -56,7 +56,7 @@
<KCuteTextarea
v-model="userInput"
placeholder="输入消息..."
:placeholder="t('enter-message-dot')"
:customClass="'chat-input'"
@press-enter="handleSend()"
/>

View File

@ -9,7 +9,7 @@
<KCuteTextarea v-else
v-model="userInput"
placeholder="输入消息..."
:placeholder="t('enter-message-dot')"
@press-enter="handleKeydown"
/>
<div class="message-actions" v-if="!isEditing">
@ -39,6 +39,9 @@ import { ChatStorage, IRenderMessage } from '../chat';
import KCuteTextarea from '@/components/k-cute-textarea/index.vue';
import { ElMessage } from 'element-plus';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
message: {
type: Object as PropType<IRenderMessage>,

View File

@ -146,5 +146,6 @@
"press-and-run": "اكتب سؤالاً لبدء الاختبار",
"connect-sigature": "توقيع الاتصال",
"finish-refresh": "تم التحديث",
"add-system-prompt.name-placeholder": "عنوان prompt المخصص"
"add-system-prompt.name-placeholder": "عنوان prompt المخصص",
"enter-message-dot": "أدخل الرسالة..."
}

View File

@ -146,5 +146,6 @@
"press-and-run": "Geben Sie eine Frage ein, um den Test zu starten",
"connect-sigature": "Verbindungssignatur",
"finish-refresh": "Aktualisierung abgeschlossen",
"add-system-prompt.name-placeholder": "Titel für benutzerdefinierte Eingabeaufforderung"
"add-system-prompt.name-placeholder": "Titel für benutzerdefinierte Eingabeaufforderung",
"enter-message-dot": "Nachricht eingeben..."
}

View File

@ -146,5 +146,6 @@
"press-and-run": "Type a question to start the test",
"connect-sigature": "Connection signature",
"finish-refresh": "Refresh completed",
"add-system-prompt.name-placeholder": "Title for custom prompt"
"add-system-prompt.name-placeholder": "Title for custom prompt",
"enter-message-dot": "Enter message..."
}

View File

@ -146,5 +146,6 @@
"press-and-run": "Tapez une question pour commencer le test",
"connect-sigature": "Signature de connexion",
"finish-refresh": "Actualisation terminée",
"add-system-prompt.name-placeholder": "Titre de l'invite personnalisée"
"add-system-prompt.name-placeholder": "Titre de l'invite personnalisée",
"enter-message-dot": "Entrez un message..."
}

View File

@ -146,5 +146,6 @@
"press-and-run": "テストを開始するには質問を入力してください",
"connect-sigature": "接続署名",
"finish-refresh": "更新が完了しました",
"add-system-prompt.name-placeholder": "カスタムプロンプトのタイトル"
"add-system-prompt.name-placeholder": "カスタムプロンプトのタイトル",
"enter-message-dot": "メッセージを入力..."
}

View File

@ -146,5 +146,6 @@
"press-and-run": "테스트를 시작하려면 질문을 입력하세요",
"connect-sigature": "연결 서명",
"finish-refresh": "새로 고침 완료",
"add-system-prompt.name-placeholder": "사용자 지정 프롬프트 제목"
"add-system-prompt.name-placeholder": "사용자 지정 프롬프트 제목",
"enter-message-dot": "메시지를 입력하세요..."
}

View File

@ -146,5 +146,6 @@
"press-and-run": "Введите вопрос, чтобы начать тест",
"connect-sigature": "Подпись соединения",
"finish-refresh": "Обновление завершено",
"add-system-prompt.name-placeholder": "Заголовок пользовательского prompt"
"add-system-prompt.name-placeholder": "Заголовок пользовательского prompt",
"enter-message-dot": "Введите сообщение..."
}

View File

@ -146,5 +146,6 @@
"press-and-run": "键入问题以开始测试",
"connect-sigature": "连接签名",
"finish-refresh": "完成刷新",
"add-system-prompt.name-placeholder": "输入自定义 prompt 的标题"
"add-system-prompt.name-placeholder": "输入自定义 prompt 的标题",
"enter-message-dot": "输入消息..."
}

View File

@ -146,5 +146,6 @@
"press-and-run": "輸入問題以開始測試",
"connect-sigature": "連接簽名",
"finish-refresh": "刷新完成",
"add-system-prompt.name-placeholder": "自定義提示的標題"
"add-system-prompt.name-placeholder": "自定義提示的標題",
"enter-message-dot": "輸入訊息..."
}

View File

@ -5,11 +5,7 @@ export const registerTreeDataProviders = new Map<string, IRegisterTreeDataProvid
export function RegisterCommand(command: string, options?: any) {
return function (target: any, propertyKey: string, descriptor: CommandHandlerDescriptor) {
const handler = descriptor.value;
console.log(propertyKey);
console.log(descriptor);
const handler = descriptor.value;
if (handler) {
registerCommands.push([command, { handler, target, propertyKey, options }]);

View File

@ -250,6 +250,59 @@ export function updateWorkspaceConnectionConfig(
}
}
export function updateInstalledConnectionConfig(
absPath: string,
data: (ClientStdioConnectionItem | ClientSseConnectionItem) & { serverInfo: ServerInfo }
) {
const connectionItem = getInstalledConnectionConfigItemByPath(absPath);
const installedConnectionConfig = getConnectionConfig();
// 如果存在,删除老的 connectionItem
if (connectionItem) {
const index = installedConnectionConfig.items.indexOf(connectionItem);
if (index !== -1) {
installedConnectionConfig.items.splice(index, 1);
}
}
if (data.connectionType === 'STDIO') {
const connectionItem: IStdioConnectionItem = {
type: 'stdio',
name: data.serverInfo.name,
version: data.serverInfo.version,
command: data.command,
args: data.args,
cwd: data.cwd.replace(/\\/g, '/'),
env: data.env,
filePath: absPath.replace(/\\/g, '/')
};
console.log('get connectionItem: ', connectionItem);
// 插入到第一个
installedConnectionConfig.items.unshift(connectionItem);
saveConnectionConfig();
vscode.commands.executeCommand('openmcp.sidebar.installed-connection.refresh');
} else {
const connectionItem: ISSEConnectionItem = {
type: 'sse',
name: data.serverInfo.name,
version: data.serverInfo.version,
url: data.url,
oauth: data.oauth,
filePath: absPath.replace(/\\/g, '/')
};
// 插入到第一个
installedConnectionConfig.items.unshift(connectionItem);
saveConnectionConfig();
vscode.commands.executeCommand('openmcp.sidebar.installed-connection.refresh');
}
}
function normaliseConnectionFilePath(item: IConnectionItem, workspace: string) {
if (item.filePath) {
if (item.filePath.startsWith('{workspace}')) {
@ -284,4 +337,50 @@ export function getWorkspaceConnectionConfigItemByPath(absPath: string) {
}
return undefined;
}
}
/**
* @description mcp
* @param absPath
*/
export function getInstalledConnectionConfigItemByPath(absPath: string) {
const installedConnectionConfig = getConnectionConfig();
const normaliseAbsPath = absPath.replace(/\\/g, '/');
for (const item of installedConnectionConfig.items) {
const filePath = (item.filePath || '').replace(/\\/g, '/');
if (filePath === normaliseAbsPath) {
return item;
}
}
return undefined;
}
export async function getFirstValidPathFromCommand(command: string, cwd: string): Promise<string | undefined> {
// 分割命令字符串
const parts = command.split(' ');
// 遍历命令部分,寻找第一个可能是路径的部分
for (let i = 1; i < parts.length; i++) {
const part = parts[i];
// 跳过以 '-' 开头的参数
if (part.startsWith('-')) continue;
// 处理相对路径
let fullPath = part;
if (!fspath.isAbsolute(part)) {
fullPath = fspath.join(cwd, part);
}
console.log(fullPath);
if (fs.existsSync(fullPath)) {
return fullPath;
}
}
return undefined;
}

View File

@ -3,6 +3,7 @@ import { RegisterCommand, RegisterTreeDataProvider } from '../common';
import { ConnectionViewItem } from './common';
import { getConnectionConfig, getInstalledConnectionConfigPath, saveConnectionConfig } from '../global';
import { acquireInstalledConnection, deleteInstalledConnection } from './installed.service';
import { revealOpenMcpWebviewPanel } from '../webview/webview.service';
@RegisterTreeDataProvider('openmcp.sidebar.installed-connection')
export class McpInstalledConnectProvider implements vscode.TreeDataProvider<ConnectionViewItem> {
@ -29,6 +30,11 @@ export class McpInstalledConnectProvider implements vscode.TreeDataProvider<Conn
return Promise.resolve(sidebarItems);
}
@RegisterCommand('revealWebviewPanel')
public revealWebviewPanel(context: vscode.ExtensionContext, view: ConnectionViewItem) {
const item = view.item;
revealOpenMcpWebviewPanel(context, 'installed', item.filePath || item.name, item);
}
@RegisterCommand('refresh')
public refresh(context: vscode.ExtensionContext): void {

View File

@ -1,4 +1,4 @@
import { getConnectionConfig, IConnectionItem, panels, saveConnectionConfig } from "../global";
import { getConnectionConfig, IConnectionItem, panels, saveConnectionConfig, getFirstValidPathFromCommand } from "../global";
import * as vscode from 'vscode';
@ -51,7 +51,8 @@ export async function validateAndGetCommandPath(command: string, cwd?: string):
export async function acquireInstalledConnection(): Promise<IConnectionItem | undefined> {
// 让用户选择连接类型
const connectionType = await vscode.window.showQuickPick(['stdio', 'sse'], {
placeHolder: '请选择连接类型'
placeHolder: '请选择连接类型',
canPickMany: false
});
if (!connectionType) {
@ -88,13 +89,16 @@ export async function acquireInstalledConnection(): Promise<IConnectionItem | un
const command = commands[0];
const args = commands.slice(1);
const filePath = await getFirstValidPathFromCommand(commandString, cwd || '');
// 保存连接配置
return {
type: 'stdio',
name: `stdio-${Date.now()}`,
command: command,
args,
cwd: cwd || ''
cwd: cwd || '',
filePath: filePath,
};
} else if (connectionType === 'sse') {

View File

@ -33,7 +33,7 @@ export class McpWorkspaceConnectProvider implements vscode.TreeDataProvider<Conn
@RegisterCommand('revealWebviewPanel')
public revealWebviewPanel(context: vscode.ExtensionContext, view: ConnectionViewItem) {
const item = view.item;
revealOpenMcpWebviewPanel(context, item.filePath || item.name, item);
revealOpenMcpWebviewPanel(context, 'workspace', item.filePath || item.name, item);
}
@RegisterCommand('refresh')

View File

@ -1,4 +1,4 @@
import { getWorkspaceConnectionConfig, getWorkspacePath, IConnectionItem, panels, saveWorkspaceConnectionConfig } from "../global";
import { getFirstValidPathFromCommand, getWorkspaceConnectionConfig, getWorkspacePath, IConnectionItem, panels, saveWorkspaceConnectionConfig } from "../global";
import * as vscode from 'vscode';
@ -43,6 +43,7 @@ export async function acquireUserCustomConnection(): Promise<IConnectionItem | u
const commands = commandString.split(' ');
const command = commands[0];
const args = commands.slice(1);
const filePath = await getFirstValidPathFromCommand(commandString, cwd || '');
// 保存连接配置
return {
@ -50,7 +51,8 @@ export async function acquireUserCustomConnection(): Promise<IConnectionItem | u
name: `stdio-${Date.now()}`,
command: command,
args,
cwd: cwd || ''
cwd: cwd || '',
filePath
};
} else if (connectionType === 'sse') {

View File

@ -18,7 +18,7 @@ export class WebviewController {
return;
}
revealOpenMcpWebviewPanel(context, uri.fsPath, {
revealOpenMcpWebviewPanel(context, 'workspace', uri.fsPath, {
type: 'stdio',
name: 'OpenMCP',
command: sigature.command,
@ -26,7 +26,7 @@ export class WebviewController {
cwd
});
} else {
revealOpenMcpWebviewPanel(context, uri.fsPath, connectionItem);
revealOpenMcpWebviewPanel(context, 'workspace', uri.fsPath, connectionItem);
}
}

View File

@ -1,19 +1,19 @@
import * as vscode from 'vscode';
import * as fs from 'fs';
import * as fspath from 'path';
import { IConnectionItem, ILaunchSigature, panels, updateWorkspaceConnectionConfig } from '../global';
import { IConnectionItem, ILaunchSigature, panels, updateInstalledConnectionConfig, updateWorkspaceConnectionConfig } from '../global';
import * as OpenMCPService from '../../resources/service';
export function getWebviewContent(context: vscode.ExtensionContext, panel: vscode.WebviewPanel): string | undefined {
const viewRoot = fspath.join(context.extensionPath, 'resources', 'renderer');
const htmlIndexPath = fspath.join(viewRoot, 'index.html');
const html = fs.readFileSync(htmlIndexPath, { encoding: 'utf-8' })?.replace(/(<link.+?href="|<script.+?src="|<img.+?src=")(.+?)"/g, (m, $1, $2) => {
const html = fs.readFileSync(htmlIndexPath, { encoding: 'utf-8' })?.replace(/(<link.+?href="|<script.+?src="|<img.+?src="|url\()(.+?)(\)|")/g, (m, $1, $2) => {
const absLocalPath = fspath.resolve(viewRoot, $2);
const webviewUri = panel.webview.asWebviewUri(vscode.Uri.file(absLocalPath));
const replaceHref = $1 + webviewUri?.toString() + '"';
return replaceHref;
});
});
return html;
}
@ -26,6 +26,7 @@ export function getLaunchCWD(context: vscode.ExtensionContext, uri: vscode.Uri)
export function revealOpenMcpWebviewPanel(
context: vscode.ExtensionContext,
type: 'workspace' | 'installed',
panelKey: string,
option: IConnectionItem = {
type: 'stdio',
@ -92,7 +93,11 @@ export function revealOpenMcpWebviewPanel(
break;
case 'vscode/update-connection-sigature':
updateWorkspaceConnectionConfig(panelKey, data);
if (type === 'installed') {
updateInstalledConnectionConfig(panelKey, data);
} else {
updateWorkspaceConnectionConfig(panelKey, data);
}
break;
default: