dsh-mcp-panel 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.es.md +129 -0
- package/README.hi.md +129 -0
- package/README.md +129 -0
- package/README.pt.md +129 -0
- package/README.zh.md +129 -0
- package/cordis.patch.yml +23 -0
- package/lib/client.js +5045 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +1079 -0
- package/lib/typert.host.js +4261 -0
- package/lib/types/aggregate.d.ts +92 -0
- package/lib/types/aggregate.d.ts.map +1 -0
- package/lib/types/client/McpPanelTab.d.ts +16 -0
- package/lib/types/client/McpPanelTab.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +35 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +90 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/present.d.ts +79 -0
- package/lib/types/client/present.d.ts.map +1 -0
- package/lib/types/client/remote.d.ts +119 -0
- package/lib/types/client/remote.d.ts.map +1 -0
- package/lib/types/client/styles.d.ts +12 -0
- package/lib/types/client/styles.d.ts.map +1 -0
- package/lib/types/command.d.ts +122 -0
- package/lib/types/command.d.ts.map +1 -0
- package/lib/types/config.d.ts +63 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/grouping.d.ts +49 -0
- package/lib/types/grouping.d.ts.map +1 -0
- package/lib/types/index.d.ts +41 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/probe.d.ts +67 -0
- package/lib/types/probe.d.ts.map +1 -0
- package/lib/types/sanitize.d.ts +42 -0
- package/lib/types/sanitize.d.ts.map +1 -0
- package/lib/types/service.d.ts +107 -0
- package/lib/types/service.d.ts.map +1 -0
- package/lib/types/typert.host.d.ts +110 -0
- package/lib/types/typert.host.d.ts.map +1 -0
- package/lib/types/upstream.d.ts +67 -0
- package/lib/types/upstream.d.ts.map +1 -0
- package/lib/types/wire.d.ts +342 -0
- package/lib/types/wire.d.ts.map +1 -0
- package/package.json +111 -0
- package/src/aggregate.ts +248 -0
- package/src/client/McpPanelTab.tsx +257 -0
- package/src/client/index.ts +87 -0
- package/src/client/locales.ts +92 -0
- package/src/client/present.ts +127 -0
- package/src/client/remote.ts +35 -0
- package/src/client/styles.ts +235 -0
- package/src/command.ts +387 -0
- package/src/config.ts +110 -0
- package/src/grouping.ts +109 -0
- package/src/index.ts +86 -0
- package/src/probe.ts +198 -0
- package/src/sanitize.ts +115 -0
- package/src/service.ts +279 -0
- package/src/typert.host.ts +25 -0
- package/src/upstream.ts +72 -0
- package/src/wire.ts +221 -0
package/src/command.ts
ADDED
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/mcp` human command over the official MCP client's observable facts.
|
|
3
|
+
* Output is a standard `CommandResult` (model-readable, logged by the
|
|
4
|
+
* commands service as `command/run` + `command/done`, so every line is
|
|
5
|
+
* reconstructable from the session log).
|
|
6
|
+
*
|
|
7
|
+
* - `/mcp` — one row per server: transport, target, tool count, connection
|
|
8
|
+
* status (honest: `unknown` until the upstream seam ships), recent error,
|
|
9
|
+
* reconnect count.
|
|
10
|
+
* - `/mcp <server>` — that server's row.
|
|
11
|
+
* - `/mcp <server> tools` — model-visible tool names + one-line descriptions.
|
|
12
|
+
* - `/mcp <server> disable|enable` — a controlled patch suggestion (the exact
|
|
13
|
+
* `cordis.patch.yml` line + the reload path). The command never edits
|
|
14
|
+
* configuration files and never fakes a runtime effect.
|
|
15
|
+
*
|
|
16
|
+
* Renderers are pure functions of the snapshot plus a message dictionary, so
|
|
17
|
+
* the output language is a config choice (`outputLanguage: en|zh`) without
|
|
18
|
+
* touching the command lifecycle.
|
|
19
|
+
*
|
|
20
|
+
* @module dsh-mcp-panel/command
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { CommandDefinition } from '@deepseek-ai/dsh-commands'
|
|
24
|
+
import type { McpPanelService } from './service.ts'
|
|
25
|
+
import type { McpPanelSnapshot, McpServerView } from './wire.ts'
|
|
26
|
+
|
|
27
|
+
/** Placeholder for fields the panel cannot observe yet. */
|
|
28
|
+
const UNKNOWN = '—'
|
|
29
|
+
|
|
30
|
+
/** Display language for the `/mcp` output. */
|
|
31
|
+
export type CommandLanguage = 'en' | 'zh' | 'es' | 'pt' | 'hi'
|
|
32
|
+
|
|
33
|
+
/** Every display string the renderers emit, per language. */
|
|
34
|
+
export interface CommandMessages {
|
|
35
|
+
enabled: string
|
|
36
|
+
disabled: string
|
|
37
|
+
status: string
|
|
38
|
+
reconnects: string
|
|
39
|
+
lastError: string
|
|
40
|
+
retryIn: string
|
|
41
|
+
cordisFiberFailed: string
|
|
42
|
+
tools: string
|
|
43
|
+
serversHeader: (count: number) => string
|
|
44
|
+
noServers: string
|
|
45
|
+
noteNoSeam: string
|
|
46
|
+
noteProposal: string
|
|
47
|
+
noTools: (server: string) => string
|
|
48
|
+
toolsHeader: (server: string, count: number) => string
|
|
49
|
+
noDescription: string
|
|
50
|
+
patchIntro: (action: string, server: string, entryId: string, patchFile: string | null) => string
|
|
51
|
+
patchNoRuntimeToggle: string
|
|
52
|
+
patchReloadPath: string
|
|
53
|
+
usage: string
|
|
54
|
+
probeStarted: (server: string, jobId: string) => string
|
|
55
|
+
unknownServer: (server: string, known: string) => string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** English output dictionary (default). */
|
|
59
|
+
export const EN_MESSAGES: CommandMessages = {
|
|
60
|
+
enabled: 'enabled',
|
|
61
|
+
disabled: 'disabled',
|
|
62
|
+
status: 'status',
|
|
63
|
+
reconnects: 'reconnects',
|
|
64
|
+
lastError: 'last error',
|
|
65
|
+
retryIn: 'retry in',
|
|
66
|
+
cordisFiberFailed: 'cordis fiber: failed',
|
|
67
|
+
tools: 'tools',
|
|
68
|
+
serversHeader: count => `MCP servers (${count}):`,
|
|
69
|
+
noServers: 'No MCP servers configured (no @deepseek-ai/dsh-mcp-client rows in this profile).',
|
|
70
|
+
noteNoSeam: 'Note: connection status/reconnect counts are not observable yet — @deepseek-ai/dsh-mcp-client exposes no status seam.',
|
|
71
|
+
noteProposal: 'Upstream proposal: docs/upstream-proposal.md (deepseek-harness). Row facts above are derived from config and the tool registry.',
|
|
72
|
+
noTools: server => `No tools registered for "${server}" (server down, sync failed, or reconnect budget exhausted).`,
|
|
73
|
+
toolsHeader: (server, count) => `Tools of "${server}" (${count}, model-visible public names):`,
|
|
74
|
+
noDescription: '(no description)',
|
|
75
|
+
patchIntro: (action, server, entryId, patchFile) =>
|
|
76
|
+
`To ${action} "${server}" (entry ${entryId}), add this line to the profile patch layer${patchFile === null ? '' : ` (${patchFile})`}:`,
|
|
77
|
+
patchNoRuntimeToggle: '@deepseek-ai/dsh-mcp-client has no runtime toggle; the Loader applies the patch on reload.',
|
|
78
|
+
patchReloadPath: 'The web surface hot-reloads cordis.patch.yml edits; other surfaces restart. This command never edits your config.',
|
|
79
|
+
usage: 'Usage: /mcp | /mcp <server> | /mcp <server> tools | /mcp <server> disable | /mcp <server> enable | /mcp <server> probe',
|
|
80
|
+
probeStarted: (server, jobId) =>
|
|
81
|
+
`Probe started for "${server}" (background job ${jobId}). Read the result in the MCP panel: Settings → Plugins → MCP.`,
|
|
82
|
+
unknownServer: (server, known) =>
|
|
83
|
+
`Unknown MCP server "${server}" (configured: ${known === '' ? 'none' : known})`,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Simplified Chinese output dictionary. */
|
|
87
|
+
export const ZH_MESSAGES: CommandMessages = {
|
|
88
|
+
enabled: '已启用',
|
|
89
|
+
disabled: '已停用',
|
|
90
|
+
status: '状态',
|
|
91
|
+
reconnects: '重连',
|
|
92
|
+
lastError: '最近错误',
|
|
93
|
+
retryIn: '重试等待',
|
|
94
|
+
cordisFiberFailed: 'cordis fiber: 失败',
|
|
95
|
+
tools: '工具',
|
|
96
|
+
serversHeader: count => `MCP 服务器(${count} 个):`,
|
|
97
|
+
noServers: '此 profile 未配置官方 MCP 服务器(@deepseek-ai/dsh-mcp-client 行)。',
|
|
98
|
+
noteNoSeam: '说明:连接状态/重连计数尚不可观测——@deepseek-ai/dsh-mcp-client 未暴露状态 seam。',
|
|
99
|
+
noteProposal: '上游提案:docs/upstream-proposal.md(deepseek-harness)。以上行数据来自配置与工具注册表。',
|
|
100
|
+
noTools: server => `"${server}" 未注册任何工具(服务器宕机、同步失败或重连预算耗尽)。`,
|
|
101
|
+
toolsHeader: (server, count) => `"${server}" 的工具(${count} 个,模型可见公开名):`,
|
|
102
|
+
noDescription: '(无描述)',
|
|
103
|
+
patchIntro: (action, server, entryId, patchFile) => {
|
|
104
|
+
const verb = action === 'disable' ? '停用' : '启用'
|
|
105
|
+
return `要${verb} "${server}"(条目 ${entryId}),把下面这行加到 profile patch 层${patchFile === null ? '' : `(${patchFile})`}:`
|
|
106
|
+
},
|
|
107
|
+
patchNoRuntimeToggle: '@deepseek-ai/dsh-mcp-client 没有运行时开关;Loader 在重载时应用该 patch。',
|
|
108
|
+
patchReloadPath: 'web 面板会热重载 cordis.patch.yml 的修改;其他面板重启生效。本命令绝不修改你的配置。',
|
|
109
|
+
usage: '用法:/mcp | /mcp <server> | /mcp <server> tools | /mcp <server> disable | /mcp <server> enable | /mcp <server> probe',
|
|
110
|
+
probeStarted: (server, jobId) =>
|
|
111
|
+
`已对 "${server}" 启动探测(后台任务 ${jobId})。结果仅面板可见:设置 → 插件 → MCP。`,
|
|
112
|
+
unknownServer: (server, known) =>
|
|
113
|
+
`未知 MCP 服务器 "${server}"(已配置:${known === '' ? '无' : known})`,
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Spanish output dictionary. */
|
|
117
|
+
export const ES_MESSAGES: CommandMessages = {
|
|
118
|
+
enabled: 'habilitado',
|
|
119
|
+
disabled: 'deshabilitado',
|
|
120
|
+
status: 'estado',
|
|
121
|
+
reconnects: 'reconexiones',
|
|
122
|
+
lastError: 'último error',
|
|
123
|
+
retryIn: 'reintento en',
|
|
124
|
+
cordisFiberFailed: 'cordis fiber: falló',
|
|
125
|
+
tools: 'herramientas',
|
|
126
|
+
serversHeader: count => `Servidores MCP (${count}):`,
|
|
127
|
+
noServers: 'No hay servidores MCP configurados (sin filas @deepseek-ai/dsh-mcp-client en este perfil).',
|
|
128
|
+
noteNoSeam: 'Nota: el estado de conexión y los conteos de reconexión aún no son observables — @deepseek-ai/dsh-mcp-client no expone una costura de estado.',
|
|
129
|
+
noteProposal: 'Propuesta upstream: docs/upstream-proposal.md (deepseek-harness). Los datos de arriba derivan de la configuración y del registro de herramientas.',
|
|
130
|
+
noTools: server => `Sin herramientas registradas para "${server}" (servidor caído, sincronización fallida o presupuesto de reconexión agotado).`,
|
|
131
|
+
toolsHeader: (server, count) => `Herramientas de "${server}" (${count}, nombres públicos visibles al modelo):`,
|
|
132
|
+
noDescription: '(sin descripción)',
|
|
133
|
+
patchIntro: (action, server, entryId, patchFile) => {
|
|
134
|
+
const verb = action === 'disable' ? 'deshabilitar' : 'habilitar'
|
|
135
|
+
return `Para ${verb} "${server}" (entrada ${entryId}), añade esta línea a la capa de parches del perfil${patchFile === null ? '' : ` (${patchFile})`}:`
|
|
136
|
+
},
|
|
137
|
+
patchNoRuntimeToggle: '@deepseek-ai/dsh-mcp-client no tiene conmutador en tiempo de ejecución; el Loader aplica el parche al recargar.',
|
|
138
|
+
patchReloadPath: 'La superficie web recarga en caliente los cambios de cordis.patch.yml; otras superficies se reinician. Este comando nunca edita tu configuración.',
|
|
139
|
+
usage: 'Uso: /mcp | /mcp <server> | /mcp <server> tools | /mcp <server> disable | /mcp <server> enable | /mcp <server> probe',
|
|
140
|
+
probeStarted: (server, jobId) =>
|
|
141
|
+
`Sonda iniciada para "${server}" (tarea en segundo plano ${jobId}). Lee el resultado en el panel MCP: Ajustes → Plugins → MCP.`,
|
|
142
|
+
unknownServer: (server, known) =>
|
|
143
|
+
`Servidor MCP desconocido "${server}" (configurados: ${known === '' ? 'ninguno' : known})`,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Portuguese output dictionary. */
|
|
147
|
+
export const PT_MESSAGES: CommandMessages = {
|
|
148
|
+
enabled: 'habilitado',
|
|
149
|
+
disabled: 'desabilitado',
|
|
150
|
+
status: 'status',
|
|
151
|
+
reconnects: 'reconexões',
|
|
152
|
+
lastError: 'último erro',
|
|
153
|
+
retryIn: 'nova tentativa em',
|
|
154
|
+
cordisFiberFailed: 'cordis fiber: falhou',
|
|
155
|
+
tools: 'ferramentas',
|
|
156
|
+
serversHeader: count => `Servidores MCP (${count}):`,
|
|
157
|
+
noServers: 'Nenhum servidor MCP configurado (nenhuma linha @deepseek-ai/dsh-mcp-client neste perfil).',
|
|
158
|
+
noteNoSeam: 'Nota: o status de conexão e as contagens de reconexão ainda não são observáveis — @deepseek-ai/dsh-mcp-client não expõe uma costura de status.',
|
|
159
|
+
noteProposal: 'Proposta upstream: docs/upstream-proposal.md (deepseek-harness). Os fatos acima derivam da configuração e do registro de ferramentas.',
|
|
160
|
+
noTools: server => `Nenhuma ferramenta registrada para "${server}" (servidor fora do ar, sincronização falhou ou orçamento de reconexão esgotado).`,
|
|
161
|
+
toolsHeader: (server, count) => `Ferramentas de "${server}" (${count}, nomes públicos visíveis ao modelo):`,
|
|
162
|
+
noDescription: '(sem descrição)',
|
|
163
|
+
patchIntro: (action, server, entryId, patchFile) => {
|
|
164
|
+
const verb = action === 'disable' ? 'desabilitar' : 'habilitar'
|
|
165
|
+
return `Para ${verb} "${server}" (entrada ${entryId}), adicione esta linha à camada de patches do perfil${patchFile === null ? '' : ` (${patchFile})`}:`
|
|
166
|
+
},
|
|
167
|
+
patchNoRuntimeToggle: '@deepseek-ai/dsh-mcp-client não tem alternância em tempo de execução; o Loader aplica o patch ao recarregar.',
|
|
168
|
+
patchReloadPath: 'A superfície web recarrega em quente as edições de cordis.patch.yml; outras superfícies reiniciam. Este comando nunca edita sua configuração.',
|
|
169
|
+
usage: 'Uso: /mcp | /mcp <server> | /mcp <server> tools | /mcp <server> disable | /mcp <server> enable | /mcp <server> probe',
|
|
170
|
+
probeStarted: (server, jobId) =>
|
|
171
|
+
`Sonda iniciada para "${server}" (tarefa em segundo plano ${jobId}). Leia o resultado no painel MCP: Configurações → Plugins → MCP.`,
|
|
172
|
+
unknownServer: (server, known) =>
|
|
173
|
+
`Servidor MCP desconhecido "${server}" (configurados: ${known === '' ? 'nenhum' : known})`,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Hindi output dictionary. */
|
|
177
|
+
export const HI_MESSAGES: CommandMessages = {
|
|
178
|
+
enabled: 'सक्षम',
|
|
179
|
+
disabled: 'अक्षम',
|
|
180
|
+
status: 'स्थिति',
|
|
181
|
+
reconnects: 'रीकनेक्ट',
|
|
182
|
+
lastError: 'अंतिम त्रुटि',
|
|
183
|
+
retryIn: 'पुनः प्रयास',
|
|
184
|
+
cordisFiberFailed: 'cordis fiber: विफल',
|
|
185
|
+
tools: 'टूल',
|
|
186
|
+
serversHeader: count => `MCP सर्वर (${count}):`,
|
|
187
|
+
noServers: 'कोई MCP सर्वर कॉन्फ़िगर नहीं (इस प्रोफ़ाइल में कोई @deepseek-ai/dsh-mcp-client पंक्ति नहीं)।',
|
|
188
|
+
noteNoSeam: 'नोट: कनेक्शन स्थिति/रीकनेक्ट गणना अभी देखने योग्य नहीं — @deepseek-ai/dsh-mcp-client कोई स्थिति सीम उजागर नहीं करता।',
|
|
189
|
+
noteProposal: 'अपस्ट्रीम प्रस्ताव: docs/upstream-proposal.md (deepseek-harness)। ऊपर के तथ्य कॉन्फ़िगरेशन और टूल रजिस्ट्री से प्राप्त हैं।',
|
|
190
|
+
noTools: server => `"${server}" के लिए कोई टूल पंजीकृत नहीं (सर्वर डाउन, सिंक विफल या रीकनेक्ट बजट समाप्त)।`,
|
|
191
|
+
toolsHeader: (server, count) => `"${server}" के टूल (${count}, मॉडल-दृश्य सार्वजनिक नाम):`,
|
|
192
|
+
noDescription: '(कोई विवरण नहीं)',
|
|
193
|
+
patchIntro: (action, server, entryId, patchFile) => {
|
|
194
|
+
const verb = action === 'disable' ? 'अक्षम' : 'सक्षम'
|
|
195
|
+
return `"${server}" (एंट्री ${entryId}) को ${verb} करने के लिए प्रोफ़ाइल पैच परत${patchFile === null ? '' : ` (${patchFile})`} में यह पंक्ति जोड़ें:`
|
|
196
|
+
},
|
|
197
|
+
patchNoRuntimeToggle: '@deepseek-ai/dsh-mcp-client के पास रनटाइम टॉगल नहीं है; Loader रीलोड पर पैच लागू करता है।',
|
|
198
|
+
patchReloadPath: 'वेब सतह cordis.patch.yml के बदलाव हॉट-रीलोड करती है; अन्य सतहें रीस्टार्ट करें। यह कमांड आपका कॉन्फ़िगरेशन कभी नहीं बदलती।',
|
|
199
|
+
usage: 'उपयोग: /mcp | /mcp <server> | /mcp <server> tools | /mcp <server> disable | /mcp <server> enable | /mcp <server> probe',
|
|
200
|
+
probeStarted: (server, jobId) =>
|
|
201
|
+
`"${server}" के लिए प्रोब शुरू (बैकग्राउंड जॉब ${jobId})। परिणाम MCP पैनल में पढ़ें: सेटिंग्स → प्लगइन्स → MCP।`,
|
|
202
|
+
unknownServer: (server, known) =>
|
|
203
|
+
`अज्ञात MCP सर्वर "${server}" (कॉन्फ़िगर: ${known === '' ? 'कोई नहीं' : known})`,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Every output dictionary indexed by the configured language. */
|
|
207
|
+
const MESSAGES: Record<CommandLanguage, CommandMessages> = {
|
|
208
|
+
en: EN_MESSAGES,
|
|
209
|
+
zh: ZH_MESSAGES,
|
|
210
|
+
es: ES_MESSAGES,
|
|
211
|
+
pt: PT_MESSAGES,
|
|
212
|
+
hi: HI_MESSAGES,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** One-line count of the reconnection attempts observed this process. */
|
|
216
|
+
function reconnectText(view: McpServerView): string {
|
|
217
|
+
return view.reconnectCount < 0 ? UNKNOWN : String(view.reconnectCount)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** One-line recent-error summary. */
|
|
221
|
+
function lastErrorText(view: McpServerView): string {
|
|
222
|
+
return view.lastError ?? UNKNOWN
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Human status phrase with its provenance. */
|
|
226
|
+
function statusText(view: McpServerView): string {
|
|
227
|
+
const phase = view.phase === 'unknown' ? 'unknown' : view.phase
|
|
228
|
+
return `${phase} (source: ${view.statusSource})`
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Render one server row: `name [entryId] transport target | N tools | …`.
|
|
233
|
+
*
|
|
234
|
+
* @param view - the assembled server view.
|
|
235
|
+
* @param messages - the output dictionary.
|
|
236
|
+
* @returns the single display line.
|
|
237
|
+
*/
|
|
238
|
+
export function renderServer(view: McpServerView, messages: CommandMessages = EN_MESSAGES): string {
|
|
239
|
+
const state = view.enabled ? messages.enabled : messages.disabled
|
|
240
|
+
const details = [
|
|
241
|
+
`${messages.status}: ${statusText(view)}`,
|
|
242
|
+
`${messages.reconnects}: ${reconnectText(view)}`,
|
|
243
|
+
`${messages.lastError}: ${lastErrorText(view)}`,
|
|
244
|
+
]
|
|
245
|
+
if (view.fiberPhase === 'failed') details.push(messages.cordisFiberFailed)
|
|
246
|
+
if (view.delayMs !== null) details.push(`${messages.retryIn} ${view.delayMs}ms`)
|
|
247
|
+
return `- ${view.serverName} [${view.entryId}] ${view.transport} ${view.target} | ${view.toolCount} ${messages.tools} | ${state} | ${details.join(' | ')}`
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Render the no-argument listing.
|
|
252
|
+
*
|
|
253
|
+
* @param snapshot - the current snapshot.
|
|
254
|
+
* @param messages - the output dictionary.
|
|
255
|
+
* @returns the full listing text.
|
|
256
|
+
*/
|
|
257
|
+
export function renderList(snapshot: McpPanelSnapshot, messages: CommandMessages = EN_MESSAGES): string {
|
|
258
|
+
if (snapshot.servers.length === 0) {
|
|
259
|
+
return messages.noServers
|
|
260
|
+
}
|
|
261
|
+
const lines = [messages.serversHeader(snapshot.servers.length)]
|
|
262
|
+
for (const view of snapshot.servers) lines.push(renderServer(view, messages))
|
|
263
|
+
if (!snapshot.observed) {
|
|
264
|
+
lines.push(messages.noteNoSeam, messages.noteProposal)
|
|
265
|
+
}
|
|
266
|
+
return lines.join('\n')
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Render the tool list for one server.
|
|
271
|
+
*
|
|
272
|
+
* @param view - the assembled server view.
|
|
273
|
+
* @param messages - the output dictionary.
|
|
274
|
+
* @returns the tool listing text.
|
|
275
|
+
*/
|
|
276
|
+
export function renderTools(view: McpServerView, messages: CommandMessages = EN_MESSAGES): string {
|
|
277
|
+
if (view.tools.length === 0) {
|
|
278
|
+
return messages.noTools(view.serverName)
|
|
279
|
+
}
|
|
280
|
+
const lines = [messages.toolsHeader(view.serverName, view.tools.length)]
|
|
281
|
+
for (const tool of view.tools) {
|
|
282
|
+
const description = tool.description.trim() === '' ? messages.noDescription : tool.description
|
|
283
|
+
lines.push(`- ${tool.name} — ${description}`)
|
|
284
|
+
}
|
|
285
|
+
return lines.join('\n')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Render the controlled enable/disable patch suggestion. Reads only; the user
|
|
290
|
+
* applies the line themselves. The web surface hot-reloads the profile patch
|
|
291
|
+
* layer; other surfaces apply it on restart.
|
|
292
|
+
*
|
|
293
|
+
* @param view - the assembled server view.
|
|
294
|
+
* @param action - which direction the suggestion flips.
|
|
295
|
+
* @param patchFile - absolute profile patch-layer path, or null when unknown.
|
|
296
|
+
* @param messages - the output dictionary.
|
|
297
|
+
* @returns the suggestion text.
|
|
298
|
+
*/
|
|
299
|
+
export function renderPatchSuggestion(
|
|
300
|
+
view: McpServerView,
|
|
301
|
+
action: 'disable' | 'enable',
|
|
302
|
+
patchFile: string | null,
|
|
303
|
+
messages: CommandMessages = EN_MESSAGES,
|
|
304
|
+
): string {
|
|
305
|
+
const disabled = action === 'disable'
|
|
306
|
+
const patch = `- set: { id: ${view.entryId}, name: '@deepseek-ai/dsh-mcp-client', disabled: ${disabled} }`
|
|
307
|
+
const lines = [
|
|
308
|
+
messages.patchIntro(action, view.serverName, view.entryId, patchFile),
|
|
309
|
+
'',
|
|
310
|
+
patch,
|
|
311
|
+
'',
|
|
312
|
+
messages.patchNoRuntimeToggle,
|
|
313
|
+
messages.patchReloadPath,
|
|
314
|
+
]
|
|
315
|
+
return lines.join('\n')
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Parsed `/mcp` arguments. */
|
|
319
|
+
export type McpCommandArgs =
|
|
320
|
+
| { readonly kind: 'list' }
|
|
321
|
+
| { readonly kind: 'server'; readonly server: string; readonly action: 'detail' | 'tools' | 'disable' | 'enable' | 'probe' }
|
|
322
|
+
| { readonly kind: 'usage' }
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Parse the free-form command input.
|
|
326
|
+
*
|
|
327
|
+
* @param rawInput - text after `/mcp`, including leading whitespace.
|
|
328
|
+
* @returns the parsed intent; malformed input becomes `usage`.
|
|
329
|
+
*/
|
|
330
|
+
export function parseMcpArgs(rawInput: string): McpCommandArgs {
|
|
331
|
+
const tokens = rawInput.trim().split(/\s+/u).filter(token => token !== '')
|
|
332
|
+
if (tokens.length === 0) return { kind: 'list' }
|
|
333
|
+
const server = tokens[0]
|
|
334
|
+
const action = tokens[1]
|
|
335
|
+
if (action === undefined) return { kind: 'server', server: server ?? '', action: 'detail' }
|
|
336
|
+
if (tokens.length !== 2) return { kind: 'usage' }
|
|
337
|
+
if (action === 'tools' || action === 'disable' || action === 'enable' || action === 'probe') {
|
|
338
|
+
return { kind: 'server', server: server ?? '', action }
|
|
339
|
+
}
|
|
340
|
+
return { kind: 'usage' }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Build the `/mcp` command definition over one service instance.
|
|
345
|
+
*
|
|
346
|
+
* @param service - the panel service supplying snapshots.
|
|
347
|
+
* @param language - output language for the rendered text.
|
|
348
|
+
* @returns the registration-ready definition.
|
|
349
|
+
*/
|
|
350
|
+
export function mcpCommand(service: McpPanelService, language: CommandLanguage = 'en'): CommandDefinition {
|
|
351
|
+
const messages = MESSAGES[language] ?? EN_MESSAGES
|
|
352
|
+
return {
|
|
353
|
+
name: 'mcp',
|
|
354
|
+
description: 'Show MCP server status, tools, and enable/disable patch suggestions (read-only)',
|
|
355
|
+
input: { hint: '[server] [tools|disable|enable|probe]' },
|
|
356
|
+
handler: ({ rawInput }) => {
|
|
357
|
+
const parsed = parseMcpArgs(rawInput)
|
|
358
|
+
if (parsed.kind === 'usage') return { kind: 'error', text: messages.usage }
|
|
359
|
+
const snapshot = service.status()
|
|
360
|
+
if (parsed.kind === 'list') return { kind: 'success', text: renderList(snapshot, messages) }
|
|
361
|
+
const view = snapshot.servers.find(candidate => candidate.serverName === parsed.server)
|
|
362
|
+
if (view === undefined) {
|
|
363
|
+
const known = snapshot.servers.map(candidate => candidate.serverName).join(', ')
|
|
364
|
+
return {
|
|
365
|
+
kind: 'error',
|
|
366
|
+
text: messages.unknownServer(parsed.server, known),
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
switch (parsed.action) {
|
|
370
|
+
case 'tools': return { kind: 'success', text: renderTools(view, messages) }
|
|
371
|
+
case 'disable': return { kind: 'success', text: renderPatchSuggestion(view, 'disable', snapshot.patchFile, messages) }
|
|
372
|
+
case 'enable': return { kind: 'success', text: renderPatchSuggestion(view, 'enable', snapshot.patchFile, messages) }
|
|
373
|
+
case 'probe': {
|
|
374
|
+
// The service throws for stdio rows and for a missing job registry;
|
|
375
|
+
// the command reports those as errors without touching any config.
|
|
376
|
+
try {
|
|
377
|
+
const started = service.probe(parsed.server)
|
|
378
|
+
return { kind: 'success', text: messages.probeStarted(parsed.server, started.jobId) }
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return { kind: 'error', text: error instanceof Error ? error.message : String(error) }
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
default: return { kind: 'success', text: renderServer(view, messages) }
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
}
|
|
387
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin configuration and its explicit resolve step. `resolveConfig` re-judges
|
|
3
|
+
* every default and bound so programmatic construction that bypasses
|
|
4
|
+
* Schemastery normalization still fails loud instead of running with hidden
|
|
5
|
+
* defaults (the explicit-resolve contract).
|
|
6
|
+
*
|
|
7
|
+
* @module dsh-mcp-panel/config
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import z from '@deepseek-ai/schemastery'
|
|
11
|
+
|
|
12
|
+
/** Default per-probe timeout in milliseconds. */
|
|
13
|
+
export const DEFAULT_PROBE_TIMEOUT_MS = 10_000
|
|
14
|
+
|
|
15
|
+
/** Ceiling for a single probe timeout: a probe is a one-shot HTTP call. */
|
|
16
|
+
export const MAX_PROBE_TIMEOUT_MS = 300_000
|
|
17
|
+
|
|
18
|
+
/** Default cap on probe records shown in the panel. */
|
|
19
|
+
export const DEFAULT_MAX_PROBES = 10
|
|
20
|
+
|
|
21
|
+
/** Ceiling on the suggested panel refresh interval (1 hour). */
|
|
22
|
+
export const MAX_REFRESH_INTERVAL_MS = 3_600_000
|
|
23
|
+
|
|
24
|
+
/** Languages the `/mcp` command renders in (mirrors the five-language READMEs). */
|
|
25
|
+
export type OutputLanguage = 'en' | 'zh' | 'es' | 'pt' | 'hi'
|
|
26
|
+
|
|
27
|
+
/** Configuration for the MCP management panel. */
|
|
28
|
+
export interface Config {
|
|
29
|
+
/** Register the optional `mcp_probe` connectivity tool (default true). */
|
|
30
|
+
probeEnabled?: boolean
|
|
31
|
+
/** Per-probe timeout in milliseconds (default 10000). */
|
|
32
|
+
probeTimeoutMs?: number
|
|
33
|
+
/** Cap on probe records shown in the panel (default 10). */
|
|
34
|
+
maxProbes?: number
|
|
35
|
+
/** Suggested panel refresh interval in ms; 0 = on demand only (default 0). */
|
|
36
|
+
refreshIntervalMs?: number
|
|
37
|
+
/** Output language of the `/mcp` command (default en). */
|
|
38
|
+
outputLanguage?: OutputLanguage
|
|
39
|
+
/** Periodically probe streamable-http servers in the background (default false). */
|
|
40
|
+
passiveProbeEnabled?: boolean
|
|
41
|
+
/** Passive probe interval in milliseconds (default 60000). */
|
|
42
|
+
passiveProbeIntervalMs?: number
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Fully resolved configuration captured at plugin load. */
|
|
46
|
+
export interface ResolvedConfig {
|
|
47
|
+
/** Whether the `mcp_probe` tool is registered. */
|
|
48
|
+
probeEnabled: boolean
|
|
49
|
+
/** Per-probe timeout in milliseconds. */
|
|
50
|
+
probeTimeoutMs: number
|
|
51
|
+
/** Cap on probe records shown in the panel. */
|
|
52
|
+
maxProbes: number
|
|
53
|
+
/** Suggested panel refresh interval in ms (0 = on demand). */
|
|
54
|
+
refreshIntervalMs: number
|
|
55
|
+
/** Output language of the `/mcp` command. */
|
|
56
|
+
outputLanguage: OutputLanguage
|
|
57
|
+
/** Whether the passive probe loop runs. */
|
|
58
|
+
passiveProbeEnabled: boolean
|
|
59
|
+
/** Passive probe interval in milliseconds. */
|
|
60
|
+
passiveProbeIntervalMs: number
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Schemastery schema for loader-validated configuration. */
|
|
64
|
+
export const Config: z<Config> = z.object({
|
|
65
|
+
probeEnabled: z.boolean().default(true),
|
|
66
|
+
probeTimeoutMs: z.number().min(1).max(MAX_PROBE_TIMEOUT_MS).default(DEFAULT_PROBE_TIMEOUT_MS),
|
|
67
|
+
maxProbes: z.number().min(1).max(100).default(DEFAULT_MAX_PROBES),
|
|
68
|
+
refreshIntervalMs: z.number().min(0).max(MAX_REFRESH_INTERVAL_MS).default(0),
|
|
69
|
+
outputLanguage: z.union(['en', 'zh', 'es', 'pt', 'hi'] as const).default('en'),
|
|
70
|
+
passiveProbeEnabled: z.boolean().default(false),
|
|
71
|
+
passiveProbeIntervalMs: z.number().min(1_000).max(MAX_REFRESH_INTERVAL_MS).default(60_000),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve raw config to the runtime policy, re-validating defaults and bounds.
|
|
76
|
+
*
|
|
77
|
+
* @param config - raw loader config; `undefined` for a bare row.
|
|
78
|
+
* @returns the frozen resolved config.
|
|
79
|
+
*/
|
|
80
|
+
export function resolveConfig(config: Config | undefined): ResolvedConfig {
|
|
81
|
+
const probeEnabled = config?.probeEnabled ?? true
|
|
82
|
+
if (typeof probeEnabled !== 'boolean') {
|
|
83
|
+
throw new TypeError('dsh-mcp-panel: config.probeEnabled must be a boolean')
|
|
84
|
+
}
|
|
85
|
+
const probeTimeoutMs = config?.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS
|
|
86
|
+
if (!Number.isFinite(probeTimeoutMs) || probeTimeoutMs < 1 || probeTimeoutMs > MAX_PROBE_TIMEOUT_MS) {
|
|
87
|
+
throw new Error(`dsh-mcp-panel: config.probeTimeoutMs must be a finite number between 1 and ${MAX_PROBE_TIMEOUT_MS}`)
|
|
88
|
+
}
|
|
89
|
+
const maxProbes = config?.maxProbes ?? DEFAULT_MAX_PROBES
|
|
90
|
+
if (!Number.isInteger(maxProbes) || maxProbes < 1 || maxProbes > 100) {
|
|
91
|
+
throw new Error('dsh-mcp-panel: config.maxProbes must be an integer between 1 and 100')
|
|
92
|
+
}
|
|
93
|
+
const refreshIntervalMs = config?.refreshIntervalMs ?? 0
|
|
94
|
+
if (!Number.isFinite(refreshIntervalMs) || refreshIntervalMs < 0 || refreshIntervalMs > MAX_REFRESH_INTERVAL_MS) {
|
|
95
|
+
throw new Error(`dsh-mcp-panel: config.refreshIntervalMs must be a finite number between 0 and ${MAX_REFRESH_INTERVAL_MS}`)
|
|
96
|
+
}
|
|
97
|
+
const outputLanguage = config?.outputLanguage ?? 'en'
|
|
98
|
+
if (outputLanguage !== 'en' && outputLanguage !== 'zh' && outputLanguage !== 'es' && outputLanguage !== 'pt' && outputLanguage !== 'hi') {
|
|
99
|
+
throw new Error(`dsh-mcp-panel: config.outputLanguage must be one of "en", "zh", "es", "pt", "hi", got ${JSON.stringify(outputLanguage)}`)
|
|
100
|
+
}
|
|
101
|
+
const passiveProbeEnabled = config?.passiveProbeEnabled ?? false
|
|
102
|
+
if (typeof passiveProbeEnabled !== 'boolean') {
|
|
103
|
+
throw new TypeError('dsh-mcp-panel: config.passiveProbeEnabled must be a boolean')
|
|
104
|
+
}
|
|
105
|
+
const passiveProbeIntervalMs = config?.passiveProbeIntervalMs ?? 60_000
|
|
106
|
+
if (!Number.isFinite(passiveProbeIntervalMs) || passiveProbeIntervalMs < 1_000 || passiveProbeIntervalMs > MAX_REFRESH_INTERVAL_MS) {
|
|
107
|
+
throw new Error(`dsh-mcp-panel: config.passiveProbeIntervalMs must be a finite number between 1000 and ${MAX_REFRESH_INTERVAL_MS}`)
|
|
108
|
+
}
|
|
109
|
+
return Object.freeze({ probeEnabled, probeTimeoutMs, maxProbes, refreshIntervalMs, outputLanguage, passiveProbeEnabled, passiveProbeIntervalMs })
|
|
110
|
+
}
|
package/src/grouping.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enumeration of model-facing MCP tools from the ToolRuntime schema list,
|
|
3
|
+
* grouped by server namespace. The official bridge registers every MCP tool
|
|
4
|
+
* under `mcp__<serverName>__<rawName>` (normalized deterministically), so a
|
|
5
|
+
* prefix match against the configured server names is exact; leftover
|
|
6
|
+
* `mcp__`-prefixed registrations from foreign plugins are preserved in
|
|
7
|
+
* separate unconfigured groups instead of being mis-attributed or dropped.
|
|
8
|
+
*
|
|
9
|
+
* Pure functions of the schema snapshot — no registry reads.
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-mcp-panel/grouping
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { McpToolView } from './wire.ts'
|
|
15
|
+
|
|
16
|
+
/** The `mcp__` namespace prefix every bridged MCP tool name starts with. */
|
|
17
|
+
export const MCP_TOOL_PREFIX = 'mcp__'
|
|
18
|
+
|
|
19
|
+
/** Mutable bucket used while grouping; published groups are readonly. */
|
|
20
|
+
interface ToolBucket {
|
|
21
|
+
serverName: string
|
|
22
|
+
configured: boolean
|
|
23
|
+
tools: McpToolView[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** One tool group: a server namespace with its model-visible tools. */
|
|
27
|
+
export interface McpToolGroup {
|
|
28
|
+
/** Server namespace (`mcp__<serverName>__…`); the best-effort segment for leftovers. */
|
|
29
|
+
serverName: string
|
|
30
|
+
/** True when `serverName` is a configured mcp-client namespace. */
|
|
31
|
+
configured: boolean
|
|
32
|
+
/** Model-visible tools under that namespace, sorted by public name. */
|
|
33
|
+
tools: readonly McpToolView[]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The schema face this module reads; the ToolRuntime snapshot satisfies it. */
|
|
37
|
+
export interface ToolSchemaFace {
|
|
38
|
+
/** Registered public tool name. */
|
|
39
|
+
readonly name: string
|
|
40
|
+
/** One-line model-facing description; may be absent on hostile or partial input. */
|
|
41
|
+
readonly description?: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Split one registered tool schema list into per-server groups.
|
|
46
|
+
*
|
|
47
|
+
* @param schemas - the `ctx.tools.schemas()` snapshot (or any subset).
|
|
48
|
+
* @param configuredNames - server namespaces from the loader's mcp-client rows.
|
|
49
|
+
* @returns one group per configured namespace (even with zero tools) plus one
|
|
50
|
+
* group per unmatched `mcp__` namespace, sorted by server name.
|
|
51
|
+
*/
|
|
52
|
+
export function groupMcpTools(
|
|
53
|
+
schemas: readonly ToolSchemaFace[],
|
|
54
|
+
configuredNames: readonly string[],
|
|
55
|
+
): McpToolGroup[] {
|
|
56
|
+
const groups = new Map<string, ToolBucket>()
|
|
57
|
+
const configured = new Set(configuredNames)
|
|
58
|
+
for (const serverName of configuredNames) {
|
|
59
|
+
if (!groups.has(serverName)) {
|
|
60
|
+
groups.set(serverName, { serverName, configured: true, tools: [] })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const schema of schemas) {
|
|
64
|
+
const name = typeof schema.name === 'string' ? schema.name : ''
|
|
65
|
+
if (!name.startsWith(MCP_TOOL_PREFIX)) continue
|
|
66
|
+
const server = matchConfigured(name, configured)
|
|
67
|
+
const key = server ?? leftoverSegment(name)
|
|
68
|
+
// A bare `mcp__` name has no server segment — not a bridge registration.
|
|
69
|
+
if (key === '') continue
|
|
70
|
+
const description = typeof schema.description === 'string' ? schema.description : ''
|
|
71
|
+
const group = groups.get(key)
|
|
72
|
+
?? { serverName: key, configured: server !== undefined, tools: [] as McpToolView[] }
|
|
73
|
+
group.tools.push({ name, description })
|
|
74
|
+
groups.set(key, group)
|
|
75
|
+
}
|
|
76
|
+
return [...groups.values()]
|
|
77
|
+
.map(group => ({ ...group, tools: [...group.tools].sort((left, right) => left.name < right.name ? -1 : 1) }))
|
|
78
|
+
.sort((left, right) => left.serverName < right.serverName ? -1 : 1)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Match `mcp__<server>__<raw>` against the configured namespaces. */
|
|
82
|
+
function matchConfigured(name: string, configured: ReadonlySet<string>): string | undefined {
|
|
83
|
+
for (const serverName of configured) {
|
|
84
|
+
if (name.startsWith(`${MCP_TOOL_PREFIX}${serverName}__`)) return serverName
|
|
85
|
+
}
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Best-effort namespace segment for a foreign `mcp__`-prefixed registration. */
|
|
90
|
+
function leftoverSegment(name: string): string {
|
|
91
|
+
const separator = name.indexOf('__', MCP_TOOL_PREFIX.length)
|
|
92
|
+
return separator < 0 ? name.slice(MCP_TOOL_PREFIX.length) : name.slice(MCP_TOOL_PREFIX.length, separator)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Count the tools registered under one configured server namespace.
|
|
97
|
+
*
|
|
98
|
+
* @param schemas - the `ctx.tools.schemas()` snapshot.
|
|
99
|
+
* @param serverName - the configured namespace.
|
|
100
|
+
* @returns the number of matching registrations.
|
|
101
|
+
*/
|
|
102
|
+
export function countServerTools(schemas: readonly ToolSchemaFace[], serverName: string): number {
|
|
103
|
+
const prefix = `${MCP_TOOL_PREFIX}${serverName}__`
|
|
104
|
+
let count = 0
|
|
105
|
+
for (const schema of schemas) {
|
|
106
|
+
if (schema.name.startsWith(prefix)) count += 1
|
|
107
|
+
}
|
|
108
|
+
return count
|
|
109
|
+
}
|