zerochat 7.3.0__py3-none-any.whl
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.
- zerochat-7.3.0.dist-info/METADATA +70 -0
- zerochat-7.3.0.dist-info/RECORD +130 -0
- zerochat-7.3.0.dist-info/WHEEL +5 -0
- zerochat-7.3.0.dist-info/entry_points.txt +2 -0
- zerochat-7.3.0.dist-info/licenses/LICENSE +34 -0
- zerochat-7.3.0.dist-info/top_level.txt +2 -0
- zerochat.py +2004 -0
- zerochat_runtime/__init__.py +1 -0
- zerochat_runtime/assets/css/base.css +199 -0
- zerochat_runtime/assets/css/components/composer.css +1258 -0
- zerochat_runtime/assets/css/components/debug.css +777 -0
- zerochat_runtime/assets/css/components/header.css +97 -0
- zerochat_runtime/assets/css/components/icons.css +39 -0
- zerochat_runtime/assets/css/components/markdown.css +685 -0
- zerochat_runtime/assets/css/components/messages.css +473 -0
- zerochat_runtime/assets/css/components/modals.css +1319 -0
- zerochat_runtime/assets/css/components/sidebar.css +622 -0
- zerochat_runtime/assets/css/components/tools.css +1223 -0
- zerochat_runtime/assets/css/layout.css +171 -0
- zerochat_runtime/assets/css/print.css +120 -0
- zerochat_runtime/assets/css/styles.css +18 -0
- zerochat_runtime/assets/css/theme-overrides.css +555 -0
- zerochat_runtime/assets/css/tokens.css +165 -0
- zerochat_runtime/assets/help/architecture.html +247 -0
- zerochat_runtime/assets/help/composer.html +134 -0
- zerochat_runtime/assets/help/conversations.html +105 -0
- zerochat_runtime/assets/help/debug.html +108 -0
- zerochat_runtime/assets/help/en/architecture.html +247 -0
- zerochat_runtime/assets/help/en/composer.html +134 -0
- zerochat_runtime/assets/help/en/conversations.html +104 -0
- zerochat_runtime/assets/help/en/debug.html +105 -0
- zerochat_runtime/assets/help/en/gemini-free.html +39 -0
- zerochat_runtime/assets/help/en/index.html +191 -0
- zerochat_runtime/assets/help/en/learning.html +71 -0
- zerochat_runtime/assets/help/en/mcp.html +143 -0
- zerochat_runtime/assets/help/en/openrouter-free.html +39 -0
- zerochat_runtime/assets/help/en/profiles.html +173 -0
- zerochat_runtime/assets/help/en/rag.html +130 -0
- zerochat_runtime/assets/help/en/reasoning-telemetry.html +108 -0
- zerochat_runtime/assets/help/en/tools-agent.html +133 -0
- zerochat_runtime/assets/help/en/webllm.html +355 -0
- zerochat_runtime/assets/help/gemini-free.html +39 -0
- zerochat_runtime/assets/help/help.css +604 -0
- zerochat_runtime/assets/help/help.js +51 -0
- zerochat_runtime/assets/help/index.html +194 -0
- zerochat_runtime/assets/help/learning.html +71 -0
- zerochat_runtime/assets/help/mcp.html +143 -0
- zerochat_runtime/assets/help/openrouter-free.html +39 -0
- zerochat_runtime/assets/help/profiles.html +174 -0
- zerochat_runtime/assets/help/rag.html +130 -0
- zerochat_runtime/assets/help/reasoning-telemetry.html +111 -0
- zerochat_runtime/assets/help/tools-agent.html +133 -0
- zerochat_runtime/assets/help/webllm.html +359 -0
- zerochat_runtime/assets/js/agent-core.js +1553 -0
- zerochat_runtime/assets/js/api.js +844 -0
- zerochat_runtime/assets/js/app.js +2558 -0
- zerochat_runtime/assets/js/attachments.js +220 -0
- zerochat_runtime/assets/js/charts.js +338 -0
- zerochat_runtime/assets/js/chat-engine.js +566 -0
- zerochat_runtime/assets/js/config-store.js +207 -0
- zerochat_runtime/assets/js/context-manager.js +584 -0
- zerochat_runtime/assets/js/conversation-service.js +484 -0
- zerochat_runtime/assets/js/cookies.js +688 -0
- zerochat_runtime/assets/js/data-reset-service.js +136 -0
- zerochat_runtime/assets/js/debug.js +508 -0
- zerochat_runtime/assets/js/defaults.js +16 -0
- zerochat_runtime/assets/js/export.js +179 -0
- zerochat_runtime/assets/js/file-parser.js +2088 -0
- zerochat_runtime/assets/js/generation-controller.js +417 -0
- zerochat_runtime/assets/js/i18n.js +1483 -0
- zerochat_runtime/assets/js/icons.js +156 -0
- zerochat_runtime/assets/js/ingestionEngine.js +436 -0
- zerochat_runtime/assets/js/markdown.js +588 -0
- zerochat_runtime/assets/js/mcp.js +1271 -0
- zerochat_runtime/assets/js/message-turns.js +62 -0
- zerochat_runtime/assets/js/profile-backup.js +118 -0
- zerochat_runtime/assets/js/profile-export-bundle.js +481 -0
- zerochat_runtime/assets/js/profile-repository.js +213 -0
- zerochat_runtime/assets/js/providers-webllm.js +486 -0
- zerochat_runtime/assets/js/providers.js +1560 -0
- zerochat_runtime/assets/js/rag-index.js +295 -0
- zerochat_runtime/assets/js/rag-service.js +530 -0
- zerochat_runtime/assets/js/rag-ui.js +997 -0
- zerochat_runtime/assets/js/ragStorage.js +676 -0
- zerochat_runtime/assets/js/sandbox.js +449 -0
- zerochat_runtime/assets/js/state.js +704 -0
- zerochat_runtime/assets/js/storage-db.js +144 -0
- zerochat_runtime/assets/js/tool-cards.js +330 -0
- zerochat_runtime/assets/js/tool-security.js +653 -0
- zerochat_runtime/assets/js/tools/README.md +50 -0
- zerochat_runtime/assets/js/tools/builtin/agent-checkpoint.tool.js +212 -0
- zerochat_runtime/assets/js/tools/builtin/download-pdf.tool.js +111 -0
- zerochat_runtime/assets/js/tools/builtin/execute-javascript.tool.js +219 -0
- zerochat_runtime/assets/js/tools/builtin/fetch-web-page.tool.js +112 -0
- zerochat_runtime/assets/js/tools/builtin/list-documents.tool.js +117 -0
- zerochat_runtime/assets/js/tools/builtin/read-knowledge-chunk.tool.js +129 -0
- zerochat_runtime/assets/js/tools/builtin/read-knowledge-image.tool.js +95 -0
- zerochat_runtime/assets/js/tools/builtin/render-chart.tool.js +90 -0
- zerochat_runtime/assets/js/tools/builtin/search-knowledge-base.tool.js +124 -0
- zerochat_runtime/assets/js/tools/builtin/search-web.tool.js +125 -0
- zerochat_runtime/assets/js/tools/tool-manifest.js +53 -0
- zerochat_runtime/assets/js/tools/tool-runtime.js +77 -0
- zerochat_runtime/assets/js/ui-composer.js +319 -0
- zerochat_runtime/assets/js/ui-conversation.js +655 -0
- zerochat_runtime/assets/js/ui-dialogs.js +132 -0
- zerochat_runtime/assets/js/ui-generation-status.js +119 -0
- zerochat_runtime/assets/js/ui-inspector.js +804 -0
- zerochat_runtime/assets/js/ui-mcp.js +604 -0
- zerochat_runtime/assets/js/ui-profiles.js +824 -0
- zerochat_runtime/assets/js/ui-reasoning.js +146 -0
- zerochat_runtime/assets/js/ui-settings.js +825 -0
- zerochat_runtime/assets/js/ui-shell.js +215 -0
- zerochat_runtime/assets/js/ui-sidebar.js +429 -0
- zerochat_runtime/assets/js/ui-telemetry.js +404 -0
- zerochat_runtime/assets/js/ui-transfer.js +262 -0
- zerochat_runtime/assets/js/utils.js +216 -0
- zerochat_runtime/assets/js/vendor/orama.browser.js +11 -0
- zerochat_runtime/assets/js/web-browser.js +569 -0
- zerochat_runtime/assets/js/web-search.js +519 -0
- zerochat_runtime/assets/manifest.webmanifest +20 -0
- zerochat_runtime/assets/services/dummy_mcp/dummy_mcp_server.py +47 -0
- zerochat_runtime/assets/services/dummy_mcp/service.json +22 -0
- zerochat_runtime/assets/services/lsp/installer.json +9 -0
- zerochat_runtime/assets/services/lsp/service.json +22 -0
- zerochat_runtime/assets/services/memory/installer.json +9 -0
- zerochat_runtime/assets/services/memory/service.json +24 -0
- zerochat_runtime/assets/services/playwright/installer.json +10 -0
- zerochat_runtime/assets/services/playwright/service.json +39 -0
- zerochat_runtime/assets/sw.js +164 -0
- zerochat_runtime/assets/zerochat.html +671 -0
|
@@ -0,0 +1,1271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Módulo de Integración MCP (Model Context Protocol) para ZeroChat.
|
|
3
|
+
* Permite la conexión a servidores MCP locales o remotos vía SSE (Server-Sent Events) o HTTP Stream.
|
|
4
|
+
*
|
|
5
|
+
* Características:
|
|
6
|
+
* - Soporte de transporte HTTP / SSE nativo en navegadores (EventSource / fetch stream).
|
|
7
|
+
* - Dado que los navegadores no pueden lanzar subprocesos `stdio` directamente,
|
|
8
|
+
* las conexiones MCP stdio requieren un proxy / bridge local (mcp-bridge o similar).
|
|
9
|
+
* - Por tanto, ZeroChat implementa el transporte nativo HTTP JSON-RPC 2.0 / SSE / REST para conectarse
|
|
10
|
+
* a servidores MCP remotos o locales (ej. http://localhost:8000/sse o endpoints de herramientas).
|
|
11
|
+
* Los servidores que solo admiten stdio pueden exponerse al navegador utilizando un bridge/proxy SSE.
|
|
12
|
+
*
|
|
13
|
+
* 2. Aislamiento de Credenciales y Seguridad:
|
|
14
|
+
* - Las cabeceras y tokens de autenticación de cada servidor MCP se gestionan exclusivamente en
|
|
15
|
+
* el cliente HTTP y NUNCA se inyectan en el prompt del sistema ni son accesibles para el ejecutor
|
|
16
|
+
* de código JavaScript.
|
|
17
|
+
* - Cada herramienta MCP indica explícitamente el servidor de procedencia para mantener la
|
|
18
|
+
* trazabilidad de ejecución en todo momento.
|
|
19
|
+
* - Se aplica control estricto de timeout (AbortController) y truncado de respuestas para evitar
|
|
20
|
+
* ataques de denegación de servicio o desbordamiento de contexto.
|
|
21
|
+
* ==============================================================================================
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
(function (root, factory) {
|
|
25
|
+
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
|
26
|
+
module.exports = factory();
|
|
27
|
+
} else {
|
|
28
|
+
root.ChatMCP = factory();
|
|
29
|
+
}
|
|
30
|
+
})(typeof self !== 'undefined' ? self : this, function () {
|
|
31
|
+
'use strict';
|
|
32
|
+
|
|
33
|
+
// Shared wire contract with bootstrap.py: escape z and non-lowercase
|
|
34
|
+
// characters as z<hex code point>z; only tool components retain underscores.
|
|
35
|
+
function publicToolName(serverId, originalName) {
|
|
36
|
+
const encode = (value, tool = false) => {
|
|
37
|
+
if (typeof value !== 'string' || !value || value.length > 256) {
|
|
38
|
+
throw new Error('Invalid MCP name component');
|
|
39
|
+
}
|
|
40
|
+
return Array.from(value, ch => /^[a-y0-9]$/.test(ch) || (tool && ch === '_')
|
|
41
|
+
? ch : `z${ch.codePointAt(0).toString(16)}z`).join('');
|
|
42
|
+
};
|
|
43
|
+
const name = serverId === null ? `zmcp_${encode(originalName, true)}`
|
|
44
|
+
: `mcp_${encode(serverId)}_${encode(originalName, true)}`;
|
|
45
|
+
if (name.length > 64) throw new Error('MCP public name exceeds 64 characters');
|
|
46
|
+
return name;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const DEFAULT_TIMEOUT_MS = 15000;
|
|
50
|
+
const MAX_OUTPUT_LENGTH = 60000;
|
|
51
|
+
|
|
52
|
+
function getAgentCore() {
|
|
53
|
+
if (typeof window !== 'undefined' && window.ChatAgentCore) return window.ChatAgentCore;
|
|
54
|
+
if (typeof require !== 'undefined') {
|
|
55
|
+
try { return require('./agent-core.js'); } catch (e) {}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getStorage() {
|
|
61
|
+
if (typeof window !== 'undefined' && window.ChatStorage) return window.ChatStorage;
|
|
62
|
+
if (typeof require !== 'undefined') {
|
|
63
|
+
try { return require('./cookies.js'); } catch (e) {}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getUtils() {
|
|
69
|
+
if (typeof window !== 'undefined' && window.ChatUtils) return window.ChatUtils;
|
|
70
|
+
if (typeof require !== 'undefined') {
|
|
71
|
+
try { return require('./utils.js'); } catch (_) {}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getState() {
|
|
77
|
+
if (typeof window !== 'undefined' && window.ChatState) return window.ChatState;
|
|
78
|
+
if (typeof require !== 'undefined') {
|
|
79
|
+
try { return require('./state.js'); } catch (e) {}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Realiza una petición fetch con timeout controlado mediante AbortController.
|
|
86
|
+
*/
|
|
87
|
+
async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
88
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
89
|
+
const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const response = await fetch(url, {
|
|
93
|
+
...options,
|
|
94
|
+
signal: options.signal || (controller ? controller.signal : undefined)
|
|
95
|
+
});
|
|
96
|
+
if (timer) clearTimeout(timer);
|
|
97
|
+
return response;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (timer) clearTimeout(timer);
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Sondea de forma no destructiva un endpoint MCP / mcp-proxy para verificar conectividad y latencia.
|
|
106
|
+
*/
|
|
107
|
+
async function probeConnection(url, options = {}) {
|
|
108
|
+
const timeoutMs = options.timeoutMs || 4000;
|
|
109
|
+
const startTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
110
|
+
const normalizedUrl = (url || '').trim();
|
|
111
|
+
|
|
112
|
+
if (!normalizedUrl) {
|
|
113
|
+
return { success: false, error: 'URL no válida o vacía', latencyMs: 0 };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let client = null;
|
|
117
|
+
try {
|
|
118
|
+
client = new McpClient({ url: normalizedUrl, timeoutMs, token: options.token });
|
|
119
|
+
const initResult = await client.initialize({ timeoutMs });
|
|
120
|
+
const endTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
121
|
+
const latencyMs = Math.max(1, Math.round(endTime - startTime));
|
|
122
|
+
|
|
123
|
+
if (initResult && initResult.success) {
|
|
124
|
+
return {
|
|
125
|
+
success: true,
|
|
126
|
+
latencyMs,
|
|
127
|
+
serverInfo: initResult.serverInfo || { name: 'mcp-proxy', version: 'unknown' },
|
|
128
|
+
capabilities: initResult.capabilities || {}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let errMsg = initResult?.error || 'No se pudo establecer sesión con el servidor MCP';
|
|
133
|
+
if (errMsg.includes('Failed to fetch') || errMsg.includes('NetworkError') || errMsg.includes('ECONNREFUSED')) {
|
|
134
|
+
errMsg = 'No se puede conectar al proxy en esa dirección y puerto. Asegúrate de haber arrancado mcp-proxy en tu terminal.';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
success: false,
|
|
139
|
+
error: errMsg,
|
|
140
|
+
latencyMs
|
|
141
|
+
};
|
|
142
|
+
} catch (err) {
|
|
143
|
+
const endTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
144
|
+
const latencyMs = Math.max(1, Math.round(endTime - startTime));
|
|
145
|
+
let errMsg = err.message || 'Error de conexión';
|
|
146
|
+
if (errMsg.includes('Failed to fetch') || errMsg.includes('NetworkError') || errMsg.includes('ECONNREFUSED')) {
|
|
147
|
+
errMsg = 'No se puede conectar al proxy en esa dirección y puerto. Asegúrate de haber arrancado mcp-proxy en tu terminal.';
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
success: false,
|
|
151
|
+
error: errMsg,
|
|
152
|
+
latencyMs
|
|
153
|
+
};
|
|
154
|
+
} finally {
|
|
155
|
+
if (client) {
|
|
156
|
+
try {
|
|
157
|
+
client.disconnect();
|
|
158
|
+
} catch (e) {}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function isTransportFailure(error) {
|
|
164
|
+
if (!error || error?.name === 'AbortError') return false;
|
|
165
|
+
const message = String(error?.message || error).toLowerCase();
|
|
166
|
+
return /network|fetch|econnrefused|timeout|conexión mcp cerrada|http 5\d\d|http 404/.test(message);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Cliente de Protocolo MCP (Model Context Protocol) basado en JSON-RPC 2.0 sobre HTTP.
|
|
171
|
+
*/
|
|
172
|
+
class McpClient {
|
|
173
|
+
constructor(config = {}) {
|
|
174
|
+
this.id = config.id || `mcp_server_${Date.now()}`;
|
|
175
|
+
this.name = config.name || 'MCP Server';
|
|
176
|
+
this.url = (config.url || '').trim().replace(/\/+$/, '');
|
|
177
|
+
this.postUrl = null;
|
|
178
|
+
this.isSseActive = false;
|
|
179
|
+
this.sseSource = null;
|
|
180
|
+
this.sseReader = null;
|
|
181
|
+
this.pendingRequests = new Map();
|
|
182
|
+
this.headers = config.headers || {};
|
|
183
|
+
this.timeoutMs = config.timeoutMs || DEFAULT_TIMEOUT_MS;
|
|
184
|
+
this.token = config.token || null;
|
|
185
|
+
this.requestId = 1;
|
|
186
|
+
this.serverCapabilities = null;
|
|
187
|
+
this.serverInfo = null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Cierra de forma limpia la conexión SSE activa y resetea las URLs efímeras de sesión.
|
|
192
|
+
*/
|
|
193
|
+
disconnect() {
|
|
194
|
+
if (this.sseSource) {
|
|
195
|
+
try {
|
|
196
|
+
this.sseSource.close();
|
|
197
|
+
} catch (e) {}
|
|
198
|
+
this.sseSource = null;
|
|
199
|
+
}
|
|
200
|
+
if (this.sseReader) {
|
|
201
|
+
try {
|
|
202
|
+
this.sseReader.cancel();
|
|
203
|
+
} catch (e) {}
|
|
204
|
+
this.sseReader = null;
|
|
205
|
+
}
|
|
206
|
+
this.isSseActive = false;
|
|
207
|
+
this.postUrl = null;
|
|
208
|
+
|
|
209
|
+
if (this.pendingRequests && this.pendingRequests.size > 0) {
|
|
210
|
+
for (const [id, req] of this.pendingRequests.entries()) {
|
|
211
|
+
if (req.timer) clearTimeout(req.timer);
|
|
212
|
+
if (typeof req.reject === 'function') {
|
|
213
|
+
req.reject(new Error('Conexión MCP cerrada'));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
this.pendingRequests.clear();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Construye las cabeceras HTTP necesarias, incluyendo autenticación si está configurada.
|
|
222
|
+
*/
|
|
223
|
+
buildHeaders() {
|
|
224
|
+
const headers = {
|
|
225
|
+
'Content-Type': 'application/json',
|
|
226
|
+
'Accept': 'application/json, text/event-stream, */*',
|
|
227
|
+
'X-ZeroChat-Client': '1'
|
|
228
|
+
};
|
|
229
|
+
if (this.token) {
|
|
230
|
+
headers['Authorization'] = `Bearer ${this.token}`;
|
|
231
|
+
headers['X-ZeroChat-Token'] = this.token;
|
|
232
|
+
}
|
|
233
|
+
if (this.headers && typeof this.headers === 'object') {
|
|
234
|
+
Object.assign(headers, this.headers);
|
|
235
|
+
}
|
|
236
|
+
return headers;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Inicia o reutiliza la conexión SSE persistente para recibir el endpoint y respuestas asíncronas.
|
|
241
|
+
*/
|
|
242
|
+
async connectSseStream(options = {}) {
|
|
243
|
+
if (!options.forceReconnect && this.postUrl && this.isSseActive) {
|
|
244
|
+
return this.postUrl;
|
|
245
|
+
}
|
|
246
|
+
if (options.forceReconnect || !this.isSseActive) {
|
|
247
|
+
this.disconnect();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const timeoutMs = options.timeoutMs || 4000;
|
|
251
|
+
if (!this.pendingRequests) this.pendingRequests = new Map();
|
|
252
|
+
|
|
253
|
+
return new Promise((resolve) => {
|
|
254
|
+
let settled = false;
|
|
255
|
+
const finish = (url) => {
|
|
256
|
+
if (!settled) {
|
|
257
|
+
settled = true;
|
|
258
|
+
this.postUrl = url;
|
|
259
|
+
resolve(url);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const timer = setTimeout(() => finish(this.url), timeoutMs);
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
let sseUrl = this.url;
|
|
267
|
+
if (this.token) {
|
|
268
|
+
const joiner = sseUrl.includes('?') ? '&' : '?';
|
|
269
|
+
sseUrl = `${sseUrl}${joiner}token=${encodeURIComponent(this.token)}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (typeof EventSource !== 'undefined') {
|
|
273
|
+
const es = new EventSource(sseUrl);
|
|
274
|
+
this.sseSource = es;
|
|
275
|
+
this.isSseActive = true;
|
|
276
|
+
|
|
277
|
+
es.addEventListener('endpoint', (evt) => {
|
|
278
|
+
clearTimeout(timer);
|
|
279
|
+
const postPath = (evt.data || '').trim();
|
|
280
|
+
const fullUrl = new URL(postPath, this.url).toString();
|
|
281
|
+
this.postUrl = fullUrl;
|
|
282
|
+
finish(fullUrl);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
es.addEventListener('message', (evt) => {
|
|
286
|
+
this._handleJsonRpcMessage(evt.data);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
es.onerror = () => {
|
|
290
|
+
clearTimeout(timer);
|
|
291
|
+
this.isSseActive = false;
|
|
292
|
+
this.postUrl = null;
|
|
293
|
+
finish(this.url);
|
|
294
|
+
};
|
|
295
|
+
} else {
|
|
296
|
+
// Node.js fallback mediante fetch stream
|
|
297
|
+
fetch(this.url, {
|
|
298
|
+
headers: { 'Accept': 'text/event-stream' }
|
|
299
|
+
}).then(res => {
|
|
300
|
+
if (!res.ok || !res.body) {
|
|
301
|
+
clearTimeout(timer);
|
|
302
|
+
this.isSseActive = false;
|
|
303
|
+
this.postUrl = null;
|
|
304
|
+
return finish(this.url);
|
|
305
|
+
}
|
|
306
|
+
const reader = res.body.getReader ? res.body.getReader() : null;
|
|
307
|
+
if (!reader) {
|
|
308
|
+
clearTimeout(timer);
|
|
309
|
+
this.isSseActive = false;
|
|
310
|
+
this.postUrl = null;
|
|
311
|
+
return finish(this.url);
|
|
312
|
+
}
|
|
313
|
+
this.isSseActive = true;
|
|
314
|
+
this.sseReader = reader;
|
|
315
|
+
const decoder = new TextDecoder();
|
|
316
|
+
let buffer = '';
|
|
317
|
+
|
|
318
|
+
const pump = () => {
|
|
319
|
+
reader.read().then(({ done, value }) => {
|
|
320
|
+
if (done) {
|
|
321
|
+
this.isSseActive = false;
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
buffer += decoder.decode(value, { stream: true });
|
|
325
|
+
const chunks = buffer.split('\n\n');
|
|
326
|
+
buffer = chunks.pop() || '';
|
|
327
|
+
|
|
328
|
+
for (const chunk of chunks) {
|
|
329
|
+
const endpointMatch = chunk.match(/event:\s*endpoint\s*\n\s*data:\s*([^\r\n]+)/);
|
|
330
|
+
if (endpointMatch) {
|
|
331
|
+
clearTimeout(timer);
|
|
332
|
+
const postPath = endpointMatch[1].trim();
|
|
333
|
+
const fullUrl = new URL(postPath, this.url).toString();
|
|
334
|
+
this.postUrl = fullUrl;
|
|
335
|
+
finish(fullUrl);
|
|
336
|
+
}
|
|
337
|
+
const msgMatch = chunk.match(/(?:event:\s*message\s*\n\s*)?data:\s*([^\r\n]+)/);
|
|
338
|
+
if (msgMatch && !endpointMatch) {
|
|
339
|
+
this._handleJsonRpcMessage(msgMatch[1]);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
pump();
|
|
343
|
+
}).catch(() => {
|
|
344
|
+
this.isSseActive = false;
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
pump();
|
|
348
|
+
}).catch(() => {
|
|
349
|
+
clearTimeout(timer);
|
|
350
|
+
this.isSseActive = false;
|
|
351
|
+
finish(this.url);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
} catch (e) {
|
|
355
|
+
clearTimeout(timer);
|
|
356
|
+
this.isSseActive = false;
|
|
357
|
+
finish(this.url);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
_handleJsonRpcMessage(raw) {
|
|
363
|
+
try {
|
|
364
|
+
const data = typeof raw === 'string' ? JSON.parse(raw.trim()) : raw;
|
|
365
|
+
if (data && data.id !== undefined && this.pendingRequests.has(data.id)) {
|
|
366
|
+
const pending = this.pendingRequests.get(data.id);
|
|
367
|
+
this.pendingRequests.delete(data.id);
|
|
368
|
+
if (pending.timer) clearTimeout(pending.timer);
|
|
369
|
+
if (data.error) {
|
|
370
|
+
const code = data.error.code ? ` (${data.error.code})` : '';
|
|
371
|
+
pending.reject(new Error(`Error MCP${code}: ${data.error.message || JSON.stringify(data.error)}`));
|
|
372
|
+
} else {
|
|
373
|
+
pending.resolve(data.result);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
} catch (e) {}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Resuelve la URL adecuada para enviar peticiones POST al servidor MCP.
|
|
381
|
+
*/
|
|
382
|
+
async resolvePostUrl(options = {}) {
|
|
383
|
+
if (!options.forceReconnect && this.postUrl && this.isSseActive) {
|
|
384
|
+
return this.postUrl;
|
|
385
|
+
}
|
|
386
|
+
if (this.url.endsWith('/sse') || options.isSse) {
|
|
387
|
+
return await this.connectSseStream(options);
|
|
388
|
+
}
|
|
389
|
+
return this.postUrl || this.url;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Despacha una petición JSON-RPC 2.0 al endpoint del servidor MCP.
|
|
394
|
+
*/
|
|
395
|
+
async request(method, params = {}, options = {}) {
|
|
396
|
+
if (!this.url) {
|
|
397
|
+
throw new Error(`El servidor MCP '${this.name}' no tiene una URL configurada.`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!this.pendingRequests) this.pendingRequests = new Map();
|
|
401
|
+
|
|
402
|
+
// Si es un endpoint SSE (/sse), conectamos o reutilizamos el canal SSE
|
|
403
|
+
const isSseEndpoint = this.url.endsWith('/sse') || options.isSse;
|
|
404
|
+
let targetUrl = await this.resolvePostUrl(options);
|
|
405
|
+
|
|
406
|
+
const id = this.requestId++;
|
|
407
|
+
const payload = {
|
|
408
|
+
jsonrpc: '2.0',
|
|
409
|
+
id: id,
|
|
410
|
+
method: method,
|
|
411
|
+
params: params
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
const timeoutMs = options.timeoutMs || this.timeoutMs;
|
|
415
|
+
let res;
|
|
416
|
+
try {
|
|
417
|
+
res = await fetchWithTimeout(targetUrl, {
|
|
418
|
+
method: 'POST',
|
|
419
|
+
headers: this.buildHeaders(),
|
|
420
|
+
body: JSON.stringify(payload),
|
|
421
|
+
signal: options.signal
|
|
422
|
+
}, timeoutMs);
|
|
423
|
+
} catch (fetchErr) {
|
|
424
|
+
// En caso de fallo de red en endpoint SSE, reconectar y reintentar una única vez
|
|
425
|
+
if (!options._isRetry && (isSseEndpoint || this.postUrl)) {
|
|
426
|
+
this.disconnect();
|
|
427
|
+
return this.request(method, params, { ...options, _isRetry: true, forceReconnect: true });
|
|
428
|
+
}
|
|
429
|
+
throw fetchErr;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Si targetUrl devolvió 404 (ej. "Could not find session for ID: <uuid>"),
|
|
433
|
+
// la sesión SSE en mcp-proxy expiró o el servidor se reinició.
|
|
434
|
+
// Invalidamos la sesión, forzamos reconexión limpia a /sse y reintentamos.
|
|
435
|
+
if (res.status === 404 && !options._isRetry && (isSseEndpoint || (targetUrl && targetUrl.includes('session_id')) || this.postUrl)) {
|
|
436
|
+
this.disconnect();
|
|
437
|
+
return this.request(method, params, { ...options, _isRetry: true, forceReconnect: true });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Si targetUrl devolvió 405 y no habíamos probado SSE, intentamos SSE
|
|
441
|
+
if (res.status === 405 && !isSseEndpoint) {
|
|
442
|
+
const resolved = await this.connectSseStream({ ...options, isSse: true });
|
|
443
|
+
if (resolved && resolved !== targetUrl) {
|
|
444
|
+
res = await fetchWithTimeout(resolved, {
|
|
445
|
+
method: 'POST',
|
|
446
|
+
headers: this.buildHeaders(),
|
|
447
|
+
body: JSON.stringify(payload),
|
|
448
|
+
signal: options.signal
|
|
449
|
+
}, timeoutMs);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Si el servidor SSE devuelve 202 Accepted (o 200 sin cuerpo JSON directo), la respuesta llegará vía el stream SSE
|
|
454
|
+
if (res.status === 202) {
|
|
455
|
+
return new Promise((resolve, reject) => {
|
|
456
|
+
const timer = setTimeout(() => {
|
|
457
|
+
if (this.pendingRequests.has(id)) {
|
|
458
|
+
this.pendingRequests.delete(id);
|
|
459
|
+
reject(new Error(`Timeout esperando respuesta para petición #${id} (${method})`));
|
|
460
|
+
}
|
|
461
|
+
}, timeoutMs);
|
|
462
|
+
this.pendingRequests.set(id, { resolve, reject, timer });
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (!res.ok) {
|
|
467
|
+
const errorText = await res.text().catch(() => '');
|
|
468
|
+
throw new Error(`Servidor MCP respondió con HTTP ${res.status}: ${errorText.slice(0, 200)}`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const data = await res.json();
|
|
472
|
+
if (data.error) {
|
|
473
|
+
const code = data.error.code ? ` (${data.error.code})` : '';
|
|
474
|
+
throw new Error(`Error MCP${code}: ${data.error.message || JSON.stringify(data.error)}`);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return data.result;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Negocia el protocolo e inicializa la sesión MCP (initialize).
|
|
482
|
+
*/
|
|
483
|
+
async initialize(options = {}) {
|
|
484
|
+
try {
|
|
485
|
+
const result = await this.request('initialize', {
|
|
486
|
+
protocolVersion: '2024-11-05',
|
|
487
|
+
capabilities: {
|
|
488
|
+
roots: { listChanged: false },
|
|
489
|
+
sampling: {}
|
|
490
|
+
},
|
|
491
|
+
clientInfo: {
|
|
492
|
+
name: 'ZeroChat',
|
|
493
|
+
version: '5.1.0'
|
|
494
|
+
}
|
|
495
|
+
}, options);
|
|
496
|
+
|
|
497
|
+
this.serverCapabilities = result?.capabilities || {};
|
|
498
|
+
this.serverInfo = result?.serverInfo || { name: this.name, version: 'unknown' };
|
|
499
|
+
|
|
500
|
+
// Enviar notificación de inicialización completada si el servidor lo soporta
|
|
501
|
+
try {
|
|
502
|
+
await this.notify('notifications/initialized', {});
|
|
503
|
+
} catch (e) {}
|
|
504
|
+
|
|
505
|
+
return {
|
|
506
|
+
success: true,
|
|
507
|
+
serverInfo: this.serverInfo,
|
|
508
|
+
capabilities: this.serverCapabilities
|
|
509
|
+
};
|
|
510
|
+
} catch (err) {
|
|
511
|
+
// Fallback tolerante: algunos servidores JSON-RPC no requieren initialize estricto
|
|
512
|
+
return {
|
|
513
|
+
success: false,
|
|
514
|
+
error: err.message
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Envía una notificación JSON-RPC (sin esperar resultado).
|
|
521
|
+
*/
|
|
522
|
+
async notify(method, params = {}) {
|
|
523
|
+
const targetUrl = await this.resolvePostUrl().catch(() => this.url);
|
|
524
|
+
if (!targetUrl) return;
|
|
525
|
+
const payload = {
|
|
526
|
+
jsonrpc: '2.0',
|
|
527
|
+
method: method,
|
|
528
|
+
params: params
|
|
529
|
+
};
|
|
530
|
+
try {
|
|
531
|
+
const res = await fetch(targetUrl, {
|
|
532
|
+
method: 'POST',
|
|
533
|
+
headers: this.buildHeaders(),
|
|
534
|
+
body: JSON.stringify(payload)
|
|
535
|
+
});
|
|
536
|
+
if (res.status === 404 && this.postUrl) {
|
|
537
|
+
this.disconnect();
|
|
538
|
+
}
|
|
539
|
+
} catch (e) {}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Descubre las herramientas disponibles en el servidor MCP (tools/list).
|
|
544
|
+
*/
|
|
545
|
+
async listTools(options = {}) {
|
|
546
|
+
const result = await this.request('tools/list', {}, options);
|
|
547
|
+
const tools = result?.tools || [];
|
|
548
|
+
return Array.isArray(tools) ? tools : [];
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* Invoca una herramienta en el servidor MCP (tools/call).
|
|
553
|
+
*/
|
|
554
|
+
async callTool(name, args = {}, options = {}) {
|
|
555
|
+
const startTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
556
|
+
|
|
557
|
+
const result = await this.request('tools/call', {
|
|
558
|
+
name: name,
|
|
559
|
+
arguments: args
|
|
560
|
+
}, options);
|
|
561
|
+
|
|
562
|
+
const endTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
563
|
+
const elapsed = parseFloat((endTime - startTime).toFixed(2));
|
|
564
|
+
|
|
565
|
+
// Extraer y procesar contenido retornado por MCP
|
|
566
|
+
let textOutput = '';
|
|
567
|
+
let isError = result?.isError === true;
|
|
568
|
+
const contentList = result?.content || [];
|
|
569
|
+
|
|
570
|
+
if (Array.isArray(contentList)) {
|
|
571
|
+
textOutput = contentList.map(item => {
|
|
572
|
+
if (item.type === 'text') return item.text || '';
|
|
573
|
+
if (item.type === 'image') return `[Imagen embebida: ${item.mimeType || 'image/png'}]`;
|
|
574
|
+
if (item.type === 'resource') return `[Recurso: ${item.resource?.uri || 'URI'}]\n${item.resource?.text || ''}`;
|
|
575
|
+
return JSON.stringify(item);
|
|
576
|
+
}).join('\n\n').trim();
|
|
577
|
+
} else if (typeof result === 'string') {
|
|
578
|
+
textOutput = result;
|
|
579
|
+
} else if (result) {
|
|
580
|
+
textOutput = JSON.stringify(result, null, 2);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (textOutput.length > MAX_OUTPUT_LENGTH) {
|
|
584
|
+
textOutput = textOutput.slice(0, MAX_OUTPUT_LENGTH) + '\n\n[... Contenido MCP truncado por límite de tamaño ...]';
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return {
|
|
588
|
+
success: !isError,
|
|
589
|
+
isError: isError,
|
|
590
|
+
content: textOutput,
|
|
591
|
+
rawResult: result,
|
|
592
|
+
executionTimeMs: elapsed
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function getMcpIconSvg(size = 14) {
|
|
598
|
+
const Icons = (typeof window !== 'undefined' && window.ChatIcons) || (typeof require !== 'undefined' ? (() => { try { return require('./icons.js'); } catch (e) { return null; } })() : null);
|
|
599
|
+
if (Icons && typeof Icons.get === 'function') {
|
|
600
|
+
return Icons.get('plug', { size });
|
|
601
|
+
}
|
|
602
|
+
return `<svg class="ui-icon" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22v-5"></path><path d="M9 8V2"></path><path d="M15 8V2"></path><path d="M18 8v5a6 6 0 0 1-12 0V8z"></path></svg>`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function formatMcpMarkdown(toolName, args, result, outcome, serverName) {
|
|
606
|
+
const raw = result?.content || (result?.rawResult ? (typeof result.rawResult === 'string' ? result.rawResult : JSON.stringify(result.rawResult, null, 2)) : outcome?.error || result?.error || 'Sin salida');
|
|
607
|
+
const argStr = args && typeof args === 'object' && Object.keys(args).length ? JSON.stringify(args, null, 2) : '';
|
|
608
|
+
const parts = [`> 🔌 **${toolName}** (*${serverName || 'MCP'}*)`];
|
|
609
|
+
if (argStr) parts.push(`> \`\`\`json\n> ${argStr.split('\n').join('\n> ')}\n> \`\`\``);
|
|
610
|
+
parts.push(`> \`\`\`\n> ${String(raw).split('\n').join('\n> ')}\n> \`\`\``);
|
|
611
|
+
return parts.join('\n');
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function createMcpToolView(toolName, serverName) {
|
|
615
|
+
const iconSvg = getMcpIconSvg(14);
|
|
616
|
+
|
|
617
|
+
const renderCard = (args, contentHtml, badgeHtml, ui, isCollapsed = false) => {
|
|
618
|
+
const esc = ui?.markdown?.escapeHtml || getUtils()?.escapeHtml || (() => '');
|
|
619
|
+
const t = ui?.t || (k => k);
|
|
620
|
+
const card = ui?.createCardWrapper ? ui.createCardWrapper('mcp-card') : document.createElement('div');
|
|
621
|
+
card.className = 'tool-card-wrapper mcp-card';
|
|
622
|
+
const argStr = args && typeof args === 'object' && Object.keys(args).length ? esc(JSON.stringify(args, null, 2)) : '';
|
|
623
|
+
const cardClass = isCollapsed ? 'tool-execution-card collapsed' : 'tool-execution-card';
|
|
624
|
+
const btnTitle = isCollapsed ? (t('tool_btn_expand') || 'Expandir') : (t('tool_btn_collapse') || 'Minimizar');
|
|
625
|
+
card.innerHTML = `
|
|
626
|
+
<div class="${cardClass}">
|
|
627
|
+
<div class="tool-card-header">
|
|
628
|
+
<div class="tool-card-title">${iconSvg}<span>${esc(toolName)}</span><span class="mcp-card-server-tag">${esc(serverName || 'MCP')}</span></div>
|
|
629
|
+
<div class="tool-card-header-actions">
|
|
630
|
+
${badgeHtml}
|
|
631
|
+
<button type="button" class="btn-tool-collapse" title="${btnTitle}">${ui?.CHEVRON_SVG || '▼'}</button>
|
|
632
|
+
</div>
|
|
633
|
+
</div>
|
|
634
|
+
<div class="tool-card-collapsible-body">
|
|
635
|
+
${argStr ? `<div class="mcp-input-summary"><code>${argStr}</code></div>` : ''}
|
|
636
|
+
<div class="tool-card-result">${contentHtml}</div>
|
|
637
|
+
</div>
|
|
638
|
+
</div>`;
|
|
639
|
+
return card;
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
displayMode: 'collapsed',
|
|
644
|
+
createLiveCard: (args, ui) => {
|
|
645
|
+
if (typeof document === 'undefined') return null;
|
|
646
|
+
const t = ui?.t || (k => k);
|
|
647
|
+
const spinner = ui?.SPINNER_SVG || '';
|
|
648
|
+
const badge = `<span class="tool-card-badge status-loading">${spinner} <span>${t('tool_badge_executing') || 'Ejecutando...'}</span></span>`;
|
|
649
|
+
const placeholder = `<div class="tool-loading-placeholder">${spinner} <span>${t('tool_badge_executing') || 'Ejecutando...'}</span></div>`;
|
|
650
|
+
return renderCard(args, placeholder, badge, ui, false);
|
|
651
|
+
},
|
|
652
|
+
updateLiveCard: (cardDiv, args, result = {}, elapsedMs = 0, ui) => {
|
|
653
|
+
if (!cardDiv) return;
|
|
654
|
+
const esc = ui?.markdown?.escapeHtml || getUtils()?.escapeHtml || (() => '');
|
|
655
|
+
const t = ui?.t || (k => k);
|
|
656
|
+
const isSuccess = result?.success !== false && !result?.error && !result?.isError;
|
|
657
|
+
const badge = cardDiv.querySelector('.tool-card-badge');
|
|
658
|
+
if (badge) {
|
|
659
|
+
badge.className = `tool-card-badge ${isSuccess ? 'status-success' : 'status-error'}`;
|
|
660
|
+
badge.innerHTML = isSuccess ? `${ui?.CHECK_SVG || ''} <span>${t('tool_status_success') || 'OK'} (${elapsedMs}ms)</span>` : `${ui?.ERROR_SVG || ''} <span>Error (${elapsedMs}ms)</span>`;
|
|
661
|
+
}
|
|
662
|
+
const resEl = cardDiv.querySelector('.tool-card-result');
|
|
663
|
+
if (resEl) {
|
|
664
|
+
const out = result?.content || (result?.rawResult ? (typeof result.rawResult === 'string' ? result.rawResult : JSON.stringify(result.rawResult, null, 2)) : (result?.error || 'Sin salida'));
|
|
665
|
+
resEl.innerHTML = `<pre class="tool-card-code"><code>${esc(out)}</code></pre>`;
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
renderHistoricalCard: (args, message, ui) => {
|
|
669
|
+
if (typeof document === 'undefined') return null;
|
|
670
|
+
const esc = ui?.markdown?.escapeHtml || getUtils()?.escapeHtml || (() => '');
|
|
671
|
+
const t = ui?.t || (k => k);
|
|
672
|
+
const badge = `<span class="tool-card-badge status-success">${ui?.CHECK_SVG || ''} <span>${t('tool_status_success') || 'OK'}</span></span>`;
|
|
673
|
+
const out = typeof message?.content === 'string' ? message.content : (message?.content ? JSON.stringify(message.content, null, 2) : '');
|
|
674
|
+
return renderCard(args, `<pre class="tool-card-code"><code>${esc(out)}</code></pre>`, badge, ui, true);
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Proveedor de herramientas MCP integrado en la arquitectura AgentCore (McpToolProvider).
|
|
681
|
+
*/
|
|
682
|
+
class McpToolProvider {
|
|
683
|
+
constructor(client, options = {}) {
|
|
684
|
+
const AgentCore = getAgentCore();
|
|
685
|
+
this.client = client;
|
|
686
|
+
this.id = options.id || client.id || `mcp_${Date.now()}`;
|
|
687
|
+
this.name = options.name || client.name || 'MCP Tool Provider';
|
|
688
|
+
this.serverName = client.name || 'MCP Server';
|
|
689
|
+
this.serverUrl = client.url || '';
|
|
690
|
+
this.onTransportFailure = typeof options.onTransportFailure === 'function' ? options.onTransportFailure : null;
|
|
691
|
+
this.cachedTools = [];
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Descubre las herramientas remotas del servidor MCP y las transforma en instancias Tool.
|
|
696
|
+
*/
|
|
697
|
+
async discoverTools(options = {}) {
|
|
698
|
+
const AgentCore = getAgentCore();
|
|
699
|
+
if (!AgentCore || !AgentCore.Tool) {
|
|
700
|
+
throw new Error('Módulo ChatAgentCore no disponible.');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Inicializar sesión
|
|
704
|
+
await this.client.initialize(options);
|
|
705
|
+
|
|
706
|
+
// Listar herramientas
|
|
707
|
+
const rawTools = await this.client.listTools(options);
|
|
708
|
+
const toolInstances = [];
|
|
709
|
+
|
|
710
|
+
for (const rt of rawTools) {
|
|
711
|
+
const external = this.client.id === 'mcp_external';
|
|
712
|
+
const sourceServer = external ? rt.metadata?.mcpServerId : this.client.id;
|
|
713
|
+
const toolName = external ? rt.metadata?.originalName : rt.name;
|
|
714
|
+
const namespacedName = publicToolName(this.client.id === 'mcp_proxy' ? null : sourceServer, toolName);
|
|
715
|
+
if (external && rt.name !== namespacedName) throw new Error('Invalid external MCP public name');
|
|
716
|
+
if (toolInstances.some(tool => tool.name === namespacedName)) throw new Error('Duplicate MCP public name');
|
|
717
|
+
|
|
718
|
+
const tool = new AgentCore.Tool({
|
|
719
|
+
id: namespacedName,
|
|
720
|
+
name: namespacedName,
|
|
721
|
+
description: rt.description ? `[MCP: ${this.serverName}] ${rt.description}` : `[MCP: ${this.serverName}] Herramienta ${toolName}`,
|
|
722
|
+
parameters: rt.inputSchema || { type: 'object', properties: {} },
|
|
723
|
+
aliases: [],
|
|
724
|
+
category: 'mcp',
|
|
725
|
+
isAvailable: () => {
|
|
726
|
+
if (this.client.id === 'mcp_proxy') {
|
|
727
|
+
const State = getState();
|
|
728
|
+
return State ? State.get('mcp')?.status === 'connected' : false;
|
|
729
|
+
}
|
|
730
|
+
return true;
|
|
731
|
+
},
|
|
732
|
+
settings: {
|
|
733
|
+
titleFallback: toolName,
|
|
734
|
+
descFallback: rt.description || '',
|
|
735
|
+
icon: 'plug',
|
|
736
|
+
defaultEnabled: true,
|
|
737
|
+
showInSettings: true
|
|
738
|
+
},
|
|
739
|
+
metadata: {
|
|
740
|
+
icon: 'plug',
|
|
741
|
+
iconSvg: getMcpIconSvg(14),
|
|
742
|
+
label: toolName,
|
|
743
|
+
mcpServerName: this.serverName,
|
|
744
|
+
mcpServerUrl: this.serverUrl,
|
|
745
|
+
originalName: toolName,
|
|
746
|
+
mcpServerId: sourceServer,
|
|
747
|
+
description: rt.description || ''
|
|
748
|
+
},
|
|
749
|
+
execute: async (args, context = {}) => {
|
|
750
|
+
try {
|
|
751
|
+
return await this.client.callTool(rt.name, args, {
|
|
752
|
+
signal: context.signal,
|
|
753
|
+
timeoutMs: options.timeoutMs
|
|
754
|
+
});
|
|
755
|
+
} catch (error) {
|
|
756
|
+
if (this.client.id === 'mcp_proxy' && isTransportFailure(error)) {
|
|
757
|
+
await this.onTransportFailure?.(error);
|
|
758
|
+
}
|
|
759
|
+
throw error;
|
|
760
|
+
}
|
|
761
|
+
},
|
|
762
|
+
result: {
|
|
763
|
+
toModel: (args, result, outcome) => {
|
|
764
|
+
if (result?.content) return result.content;
|
|
765
|
+
if (result?.rawResult) {
|
|
766
|
+
if (typeof result.rawResult === 'string') return result.rawResult;
|
|
767
|
+
return JSON.stringify(result.rawResult);
|
|
768
|
+
}
|
|
769
|
+
return outcome?.error || result?.error || 'Sin salida';
|
|
770
|
+
},
|
|
771
|
+
toMarkdown: (args, result, outcome) => {
|
|
772
|
+
return formatMcpMarkdown(toolName, args, result, outcome, this.serverName);
|
|
773
|
+
}
|
|
774
|
+
},
|
|
775
|
+
displayMode: 'collapsed',
|
|
776
|
+
view: createMcpToolView(toolName, this.serverName),
|
|
777
|
+
formatter: (args, result) => {
|
|
778
|
+
return formatMcpMarkdown(toolName, args, result, null, this.serverName);
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
toolInstances.push(tool);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
this.cachedTools = toolInstances;
|
|
786
|
+
return toolInstances;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Devuelve las herramientas descubiertas registradas en este proveedor.
|
|
791
|
+
*/
|
|
792
|
+
getTools() {
|
|
793
|
+
return this.cachedTools;
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Administrador de Servidores MCP y sincronización con ToolRegistry (McpManager).
|
|
799
|
+
*/
|
|
800
|
+
class McpManager {
|
|
801
|
+
constructor() {
|
|
802
|
+
this.servers = [];
|
|
803
|
+
this.clients = new Map();
|
|
804
|
+
this.providers = new Map();
|
|
805
|
+
this.storageKey = 'chat_mcp_servers';
|
|
806
|
+
this.sessionToken = null;
|
|
807
|
+
this.healthCheckPromise = null;
|
|
808
|
+
this.loadConfig();
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
setSessionToken(token) {
|
|
812
|
+
this.sessionToken = token ? String(token).trim() : null;
|
|
813
|
+
for (const client of this.clients.values()) {
|
|
814
|
+
client.token = this.sessionToken;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
getSessionToken() {
|
|
819
|
+
return this.sessionToken;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Carga la lista de servidores MCP configurados desde el almacenamiento local.
|
|
824
|
+
*/
|
|
825
|
+
loadConfig() {
|
|
826
|
+
try {
|
|
827
|
+
const Storage = getStorage();
|
|
828
|
+
let raw = null;
|
|
829
|
+
if (Storage && Storage.getStorageItem) {
|
|
830
|
+
raw = Storage.getStorageItem(this.storageKey);
|
|
831
|
+
if (raw === null && typeof Storage.migrateLegacyStorageItem === 'function') {
|
|
832
|
+
raw = Storage.migrateLegacyStorageItem(this.storageKey);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (raw) {
|
|
837
|
+
this.servers = JSON.parse(raw);
|
|
838
|
+
} else {
|
|
839
|
+
this.servers = [];
|
|
840
|
+
}
|
|
841
|
+
} catch (e) {
|
|
842
|
+
this.servers = [];
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Guarda la configuración de servidores MCP en el almacenamiento local.
|
|
848
|
+
*/
|
|
849
|
+
saveConfig() {
|
|
850
|
+
try {
|
|
851
|
+
const Storage = getStorage();
|
|
852
|
+
const serialized = JSON.stringify(this.servers);
|
|
853
|
+
if (Storage && Storage.setStorageItem) {
|
|
854
|
+
Storage.setStorageItem(this.storageKey, serialized);
|
|
855
|
+
}
|
|
856
|
+
} catch (e) {}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Obtiene la lista de servidores configurados.
|
|
861
|
+
*/
|
|
862
|
+
getServers() {
|
|
863
|
+
return [...this.servers];
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Añade o actualiza un servidor MCP.
|
|
868
|
+
*/
|
|
869
|
+
addServer(serverConfig) {
|
|
870
|
+
if (!serverConfig || !serverConfig.url) {
|
|
871
|
+
throw new Error('La URL del servidor MCP es obligatoria.');
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const id = serverConfig.id || `mcp_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`;
|
|
875
|
+
const existingIdx = this.servers.findIndex(s => s.id === id);
|
|
876
|
+
|
|
877
|
+
const serverData = {
|
|
878
|
+
id: id,
|
|
879
|
+
name: serverConfig.name || 'Servidor MCP',
|
|
880
|
+
url: serverConfig.url.trim(),
|
|
881
|
+
headers: serverConfig.headers || {},
|
|
882
|
+
enabled: serverConfig.enabled !== false,
|
|
883
|
+
lastConnected: null,
|
|
884
|
+
toolCount: 0
|
|
885
|
+
};
|
|
886
|
+
|
|
887
|
+
if (existingIdx !== -1) {
|
|
888
|
+
this.servers[existingIdx] = serverData;
|
|
889
|
+
} else {
|
|
890
|
+
this.servers.push(serverData);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
this.saveConfig();
|
|
894
|
+
return serverData;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Elimina un servidor MCP configurado.
|
|
899
|
+
*/
|
|
900
|
+
removeServer(id) {
|
|
901
|
+
this.servers = this.servers.filter(s => s.id !== id);
|
|
902
|
+
if (this.clients.has(id)) {
|
|
903
|
+
try {
|
|
904
|
+
this.clients.get(id).disconnect();
|
|
905
|
+
} catch (e) {}
|
|
906
|
+
this.clients.delete(id);
|
|
907
|
+
}
|
|
908
|
+
this.providers.delete(id);
|
|
909
|
+
this.saveConfig();
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* Obtiene o crea una instancia de McpClient para un servidor.
|
|
914
|
+
*/
|
|
915
|
+
getClient(serverId) {
|
|
916
|
+
if (this.clients.has(serverId)) {
|
|
917
|
+
return this.clients.get(serverId);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const serverConfig = this.servers.find(s => s.id === serverId);
|
|
921
|
+
if (!serverConfig) return null;
|
|
922
|
+
|
|
923
|
+
const client = new McpClient({ ...serverConfig, token: this.sessionToken });
|
|
924
|
+
this.clients.set(serverId, client);
|
|
925
|
+
return client;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Descubre y registra las herramientas de un servidor MCP en el ToolRegistry de AgentCore.
|
|
930
|
+
*/
|
|
931
|
+
async connectAndRegisterServer(serverId, registry) {
|
|
932
|
+
const AgentCore = getAgentCore();
|
|
933
|
+
const targetRegistry = registry || (AgentCore ? AgentCore.registry : null);
|
|
934
|
+
if (!targetRegistry) return { success: false, error: 'ToolRegistry no disponible' };
|
|
935
|
+
|
|
936
|
+
const serverConfig = this.servers.find(s => s.id === serverId);
|
|
937
|
+
if (!serverConfig || !serverConfig.enabled) {
|
|
938
|
+
return { success: false, error: 'Servidor no encontrado o deshabilitado' };
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const client = this.getClient(serverId);
|
|
942
|
+
const provider = new McpToolProvider(client, {
|
|
943
|
+
id: `mcp_prov_${serverId}`,
|
|
944
|
+
name: serverConfig.name,
|
|
945
|
+
onTransportFailure: error => this.markProxyUnavailable(error, targetRegistry)
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
try {
|
|
949
|
+
const tools = await provider.discoverTools();
|
|
950
|
+
targetRegistry.registerProvider(provider);
|
|
951
|
+
|
|
952
|
+
serverConfig.lastConnected = Date.now();
|
|
953
|
+
serverConfig.toolCount = tools.length;
|
|
954
|
+
this.providers.set(serverId, provider);
|
|
955
|
+
this.saveConfig();
|
|
956
|
+
|
|
957
|
+
return {
|
|
958
|
+
success: true,
|
|
959
|
+
toolCount: tools.length,
|
|
960
|
+
tools: tools.map(t => ({
|
|
961
|
+
id: t.id || t.name,
|
|
962
|
+
name: t.name,
|
|
963
|
+
description: t.metadata?.description || t.settings?.descFallback || t.description || '',
|
|
964
|
+
parameters: t.parameters,
|
|
965
|
+
inputSchema: t.parameters,
|
|
966
|
+
category: t.category,
|
|
967
|
+
titleFallback: t.settings?.titleFallback || t.name,
|
|
968
|
+
descFallback: t.settings?.descFallback || t.metadata?.description || t.description || ''
|
|
969
|
+
}))
|
|
970
|
+
};
|
|
971
|
+
} catch (err) {
|
|
972
|
+
return {
|
|
973
|
+
success: false,
|
|
974
|
+
error: err.message
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Conecta y sincroniza todos los servidores habilitados con el ToolRegistry.
|
|
981
|
+
*/
|
|
982
|
+
async syncAllWithRegistry(registry) {
|
|
983
|
+
const results = [];
|
|
984
|
+
for (const server of this.servers) {
|
|
985
|
+
if (server.enabled) {
|
|
986
|
+
const res = await this.connectAndRegisterServer(server.id, registry);
|
|
987
|
+
results.push({ serverId: server.id, name: server.name, ...res });
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return results;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
/**
|
|
994
|
+
* Sondea la conectividad con un endpoint MCP.
|
|
995
|
+
*/
|
|
996
|
+
async probeConnection(url, options = {}) {
|
|
997
|
+
return probeConnection(url, options);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
async markProxyUnavailable(error, registry = null) {
|
|
1001
|
+
const State = getState();
|
|
1002
|
+
const current = State?.get?.('mcp') || {};
|
|
1003
|
+
const message = error?.message || String(error || 'Conexión MCP no disponible');
|
|
1004
|
+
await this.disconnectProxy(registry);
|
|
1005
|
+
if (State?.set) {
|
|
1006
|
+
State.set('mcp', {
|
|
1007
|
+
status: 'error', host: current.host, port: current.port, endpoint: current.endpoint,
|
|
1008
|
+
serverInfo: null, tools: [], latencyMs: null, error: message,
|
|
1009
|
+
externalServers: [], externalTools: [], lastVerified: Date.now()
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
if (typeof window !== 'undefined' && window.ChatApp?.stopServerHeartbeat) {
|
|
1013
|
+
window.ChatApp.stopServerHeartbeat();
|
|
1014
|
+
}
|
|
1015
|
+
return { success: false, error: message };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
async verifyProxyConnection({ timeoutMs = 1500, registry = null } = {}) {
|
|
1019
|
+
if (this.healthCheckPromise) return this.healthCheckPromise;
|
|
1020
|
+
const State = getState();
|
|
1021
|
+
const current = State?.get?.('mcp') || {};
|
|
1022
|
+
if (current.status !== 'connected') return { success: false, skipped: true };
|
|
1023
|
+
const endpoint = current.endpoint || `http://${current.host || '127.0.0.1'}:${current.port || 6388}/sse`;
|
|
1024
|
+
const run = (async () => {
|
|
1025
|
+
State?.set?.('mcp', { ...current, status: 'checking', error: null });
|
|
1026
|
+
const probe = await probeConnection(endpoint, { timeoutMs, token: this.sessionToken });
|
|
1027
|
+
if (!probe.success) {
|
|
1028
|
+
await this.markProxyUnavailable(new Error(probe.error), registry);
|
|
1029
|
+
return { success: false, probe };
|
|
1030
|
+
}
|
|
1031
|
+
const latest = State?.get?.('mcp') || current;
|
|
1032
|
+
State?.set?.('mcp', {
|
|
1033
|
+
...latest, status: 'connected', serverInfo: probe.serverInfo || latest.serverInfo,
|
|
1034
|
+
latencyMs: probe.latencyMs, error: null, lastVerified: Date.now()
|
|
1035
|
+
});
|
|
1036
|
+
return { success: true, probe };
|
|
1037
|
+
})();
|
|
1038
|
+
this.healthCheckPromise = run;
|
|
1039
|
+
try {
|
|
1040
|
+
return await run;
|
|
1041
|
+
} finally {
|
|
1042
|
+
this.healthCheckPromise = null;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* Conecta con mcp-proxy en el host y puerto configurados, registrando sus herramientas.
|
|
1048
|
+
* Si silentOnFailure es true (p. ej. comprobación inicial de arranque), no marca estado 'error'
|
|
1049
|
+
* si el servidor simplemente no está levantado, sino que registra 'disconnected' limpiamente.
|
|
1050
|
+
*/
|
|
1051
|
+
async connectProxy({ host = '127.0.0.1', port = 6388, endpoint = null, timeoutMs = 1500, silentOnFailure = false, token = null } = {}, registry = null) {
|
|
1052
|
+
const effectiveToken = token || this.sessionToken;
|
|
1053
|
+
if (effectiveToken) {
|
|
1054
|
+
this.setSessionToken(effectiveToken);
|
|
1055
|
+
}
|
|
1056
|
+
const targetEndpoint = endpoint || `http://${host}:${port}/sse`;
|
|
1057
|
+
const State = getState();
|
|
1058
|
+
if (!silentOnFailure && State?.set) {
|
|
1059
|
+
State.set('mcp', { status: 'connecting', host, port, endpoint: targetEndpoint, error: null });
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const probe = await probeConnection(targetEndpoint, { timeoutMs, token: this.sessionToken });
|
|
1063
|
+
if (!probe.success) {
|
|
1064
|
+
if (this.clients.has('mcp_proxy')) {
|
|
1065
|
+
await this.disconnectProxy(registry).catch(() => {});
|
|
1066
|
+
}
|
|
1067
|
+
if (State?.set) {
|
|
1068
|
+
if (silentOnFailure) {
|
|
1069
|
+
State.set('mcp', { status: 'disconnected', host, port, endpoint: targetEndpoint, serverInfo: null, tools: [], latencyMs: null, error: null });
|
|
1070
|
+
} else {
|
|
1071
|
+
State.set('mcp', { status: 'error', host, port, endpoint: targetEndpoint, error: probe.error, latencyMs: probe.latencyMs, serverInfo: null, tools: [] });
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
return { success: false, available: false, probe, error: probe.error };
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (this.clients.has('mcp_proxy')) {
|
|
1078
|
+
try { this.clients.get('mcp_proxy').disconnect(); } catch (e) {}
|
|
1079
|
+
this.clients.delete('mcp_proxy');
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
this.addServer({ id: 'mcp_proxy', name: probe.serverInfo?.name || 'mcp-proxy', url: targetEndpoint, enabled: true });
|
|
1083
|
+
const registerResult = await this.connectAndRegisterServer('mcp_proxy', registry);
|
|
1084
|
+
|
|
1085
|
+
if (State?.set) {
|
|
1086
|
+
const currentMcp = State.get('mcp') || {};
|
|
1087
|
+
State.set('mcp', {
|
|
1088
|
+
...currentMcp,
|
|
1089
|
+
status: registerResult.success ? 'connected' : 'error',
|
|
1090
|
+
host, port, endpoint: targetEndpoint,
|
|
1091
|
+
serverInfo: probe.serverInfo,
|
|
1092
|
+
tools: registerResult.tools || [],
|
|
1093
|
+
lastConnected: Date.now(),
|
|
1094
|
+
latencyMs: probe.latencyMs,
|
|
1095
|
+
error: registerResult.success ? null : registerResult.error
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
let externalSync = null;
|
|
1100
|
+
if (registerResult.success) {
|
|
1101
|
+
externalSync = await this.syncExternalServers(registry).catch(() => null);
|
|
1102
|
+
if (typeof window !== 'undefined' && window.ChatApp?.startServerHeartbeat && this.sessionToken) {
|
|
1103
|
+
window.ChatApp.startServerHeartbeat(host, port, this.sessionToken);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
return {
|
|
1108
|
+
success: registerResult.success,
|
|
1109
|
+
available: true,
|
|
1110
|
+
probe,
|
|
1111
|
+
register: registerResult,
|
|
1112
|
+
tools: registerResult.tools || [],
|
|
1113
|
+
externalSync
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/**
|
|
1118
|
+
* Comprueba si el servidor mcp-proxy está disponible delegando en la función estándar connectProxy.
|
|
1119
|
+
*/
|
|
1120
|
+
async autoConnectIfAvailable(options = {}, registry = null) {
|
|
1121
|
+
return this.connectProxy({ ...options, silentOnFailure: true }, registry);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Desconecta el proxy y actualiza el estado global a desconectado.
|
|
1126
|
+
*/
|
|
1127
|
+
async disconnectProxy(registry = null) {
|
|
1128
|
+
const AgentCore = getAgentCore();
|
|
1129
|
+
const targetRegistry = registry || AgentCore?.registry;
|
|
1130
|
+
if (targetRegistry?.unregisterProvider) targetRegistry.unregisterProvider('mcp_prov_mcp_proxy');
|
|
1131
|
+
this.providers.delete('mcp_proxy');
|
|
1132
|
+
|
|
1133
|
+
if (this.clients.has('mcp_proxy')) {
|
|
1134
|
+
try { this.clients.get('mcp_proxy').disconnect(); } catch (e) {}
|
|
1135
|
+
this.clients.delete('mcp_proxy');
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (targetRegistry?.unregisterProvider) targetRegistry.unregisterProvider('mcp_prov_mcp_external');
|
|
1139
|
+
this.providers.delete('mcp_external');
|
|
1140
|
+
if (this.clients.has('mcp_external')) {
|
|
1141
|
+
try { this.clients.get('mcp_external').disconnect(); } catch (e) {}
|
|
1142
|
+
this.clients.delete('mcp_external');
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
const State = getState();
|
|
1146
|
+
if (State?.set) State.set('mcp', { status: 'disconnected', serverInfo: null, tools: [], latencyMs: null, error: null, externalServers: [], externalTools: [] });
|
|
1147
|
+
return { success: true };
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
getExternalControlClient() {
|
|
1151
|
+
return this.getClient('mcp_proxy');
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
async requestExternalControl(method, params = {}, options = {}) {
|
|
1155
|
+
const client = this.getExternalControlClient();
|
|
1156
|
+
if (!client) throw new Error('Servicio local de herramientas no conectado.');
|
|
1157
|
+
return client.request(method, params, options);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
async refreshExternalProvider(registry = null) {
|
|
1161
|
+
const control = this.getExternalControlClient();
|
|
1162
|
+
if (!control) throw new Error('Servicio local de herramientas no conectado.');
|
|
1163
|
+
const baseUrl = (control.url || 'http://127.0.0.1:6388/sse').replace(/\/sse\/?$/, '');
|
|
1164
|
+
const AgentCore = getAgentCore();
|
|
1165
|
+
const targetRegistry = registry || AgentCore?.registry;
|
|
1166
|
+
if (this.clients.has('mcp_external')) {
|
|
1167
|
+
try { this.clients.get('mcp_external').disconnect(); } catch (_) {}
|
|
1168
|
+
this.clients.delete('mcp_external');
|
|
1169
|
+
}
|
|
1170
|
+
if (targetRegistry?.unregisterProvider) targetRegistry.unregisterProvider('mcp_prov_mcp_external');
|
|
1171
|
+
this.providers.delete('mcp_external');
|
|
1172
|
+
const client = new McpClient({
|
|
1173
|
+
id: 'mcp_external',
|
|
1174
|
+
name: 'ZeroChat External MCP Host',
|
|
1175
|
+
url: `${baseUrl}/mcp/external`,
|
|
1176
|
+
token: this.sessionToken,
|
|
1177
|
+
enabled: true
|
|
1178
|
+
});
|
|
1179
|
+
this.clients.set('mcp_external', client);
|
|
1180
|
+
const provider = new McpToolProvider(client, { id: 'mcp_prov_mcp_external', name: 'ZeroChat External MCP Host' });
|
|
1181
|
+
let result;
|
|
1182
|
+
try {
|
|
1183
|
+
const tools = await provider.discoverTools();
|
|
1184
|
+
if (targetRegistry?.registerProvider) targetRegistry.registerProvider(provider);
|
|
1185
|
+
this.providers.set('mcp_external', provider);
|
|
1186
|
+
result = { success: true, toolCount: tools.length, tools };
|
|
1187
|
+
} catch (error) {
|
|
1188
|
+
this.clients.delete('mcp_external');
|
|
1189
|
+
result = { success: false, error: error.message || String(error), tools: [] };
|
|
1190
|
+
}
|
|
1191
|
+
const State = getState();
|
|
1192
|
+
if (State?.set && result.success) {
|
|
1193
|
+
const current = State.get('mcp') || {};
|
|
1194
|
+
State.set('mcp', { ...current, externalTools: result.tools || [] });
|
|
1195
|
+
}
|
|
1196
|
+
return result;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
async fetchExternalServers(options = {}) {
|
|
1200
|
+
try {
|
|
1201
|
+
return await this.requestExternalControl('zerochat/external/status', {}, options);
|
|
1202
|
+
} catch (_) {
|
|
1203
|
+
return { host: 'running', servers: [] };
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
async syncExternalServers(registry = null) {
|
|
1208
|
+
const State = getState();
|
|
1209
|
+
const status = await this.fetchExternalServers().catch(() => ({ host: 'running', servers: [] }));
|
|
1210
|
+
|
|
1211
|
+
let externalTools = [];
|
|
1212
|
+
const hasRunningServers = (status?.servers || []).some(s => s.status === 'running');
|
|
1213
|
+
if (hasRunningServers) {
|
|
1214
|
+
const refresh = await this.refreshExternalProvider(registry).catch(() => null);
|
|
1215
|
+
if (refresh?.success) {
|
|
1216
|
+
externalTools = refresh.tools || [];
|
|
1217
|
+
}
|
|
1218
|
+
} else {
|
|
1219
|
+
const AgentCore = getAgentCore();
|
|
1220
|
+
const targetRegistry = registry || AgentCore?.registry;
|
|
1221
|
+
if (targetRegistry?.unregisterProvider) targetRegistry.unregisterProvider('mcp_prov_mcp_external');
|
|
1222
|
+
this.providers.delete('mcp_external');
|
|
1223
|
+
if (this.clients.has('mcp_external')) {
|
|
1224
|
+
try { this.clients.get('mcp_external').disconnect(); } catch (_) {}
|
|
1225
|
+
this.clients.delete('mcp_external');
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
if (State?.set) {
|
|
1230
|
+
const current = State.get('mcp') || {};
|
|
1231
|
+
State.set('mcp', {
|
|
1232
|
+
...current,
|
|
1233
|
+
externalHost: status?.host || 'running',
|
|
1234
|
+
externalServers: status?.servers || [],
|
|
1235
|
+
externalTools
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
return { status, externalTools };
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
async startExternalServer(serverId, registry = null) {
|
|
1243
|
+
const result = await this.requestExternalControl('zerochat/external/servers/start', { serverId });
|
|
1244
|
+
await this.syncExternalServers(registry).catch(() => {});
|
|
1245
|
+
return result;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
async stopExternalServer(serverId, registry = null) {
|
|
1249
|
+
const result = await this.requestExternalControl('zerochat/external/servers/stop', { serverId });
|
|
1250
|
+
await this.syncExternalServers(registry).catch(() => {});
|
|
1251
|
+
return result;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
async configureExternalServer(serverId, config = {}) {
|
|
1255
|
+
return this.requestExternalControl('zerochat/external/servers/configure', {
|
|
1256
|
+
serverId,
|
|
1257
|
+
options: config.options || config
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const manager = new McpManager();
|
|
1263
|
+
|
|
1264
|
+
return {
|
|
1265
|
+
McpClient,
|
|
1266
|
+
McpToolProvider,
|
|
1267
|
+
McpManager,
|
|
1268
|
+
probeConnection,
|
|
1269
|
+
manager
|
|
1270
|
+
};
|
|
1271
|
+
});
|