fix: harden clinical assistant MCP connectivity

This commit is contained in:
Daniel 2026-05-06 07:42:03 +02:00
parent c8e911cb64
commit 33ca4e65d9

View file

@ -16,7 +16,8 @@ var logger = require('../utils/logger');
router.use(authMiddleware);
var MCP_URL = process.env.CLINICAL_ASSISTANT_MCP_URL || process.env.MCP_SERVER_URL || 'http://127.0.0.1:8100/mcp';
var MCP_URLS = buildMcpUrls();
var _lastGoodMcpUrl = MCP_URLS[0];
var DEFAULT_BEHAVIOR = 'You are a concise pediatric clinical assistant. Use retrieved context only for factual claims. If the user input is a greeting or too vague, answer briefly and ask what they want to look up. Synthesize across sources, cite claims with numbered citations like [1], and list sources at the bottom by title/resource and page. Do not invent citations.';
var GREETING_RE = /^(hi|hello|hey|yo|good\s+(morning|afternoon|evening)|thanks|thank you|ok|okay|sup)[\s.!?]*$/i;
var TOPIC_SUGGESTIONS = {
@ -70,7 +71,15 @@ router.get('/clinical-assistant/status', async function(req, res) {
var imageModel = await getSetting('clinical_assistant.image_model', '');
var searchLimit = clampInt(await getSetting('clinical_assistant.search_limit', '8'), 3, 20, 8);
var contextChars = clampInt(await getSetting('clinical_assistant.context_chars', '1400'), 300, 4000, 1400);
res.json({ success: true, chatModel: chatModel, imageModel: imageModel, searchLimit: searchLimit, contextChars: contextChars, mcpUrl: MCP_URL.replace(/\/mcp$/, '/mcp') });
var mcpHealth = await getMcpHealth();
res.json({
success: true,
chatModel: chatModel,
imageModel: imageModel,
searchLimit: searchLimit,
contextChars: contextChars,
mcp: mcpHealth
});
} catch (e) {
res.status(500).json({ error: 'Request failed' });
}
@ -209,28 +218,104 @@ async function semanticSearch(query, opts) {
context_chars: opts.contextChars
}
}
}, session.sessionId);
}, session.sessionId, session.mcpUrl);
return search.result || search;
}
async function mcpRequest(payload, sessionId) {
async function mcpRequest(payload, sessionId, preferredUrl) {
var headers = {
'Accept': 'application/json, text/event-stream',
'Content-Type': 'application/json'
};
if (sessionId) headers['mcp-session-id'] = sessionId;
var resp = await axios.post(MCP_URL, payload, {
headers: headers,
timeout: 90000,
responseType: 'text',
transformResponse: [function(data) { return data; }]
});
var resp = await postMcpWithRetry(payload, headers, preferredUrl);
var parsed = parseMcpResponse(resp.data);
if (parsed.error) throw new Error(parsed.error.message || JSON.stringify(parsed.error));
parsed.sessionId = resp.headers['mcp-session-id'] || sessionId || null;
parsed.mcpUrl = resp.config && resp.config.url ? resp.config.url : preferredUrl || _lastGoodMcpUrl;
return parsed;
}
async function postMcpWithRetry(payload, headers, preferredUrl) {
var lastErr = null;
var urls = orderedMcpUrls(preferredUrl);
var isInitialize = payload && payload.method === 'initialize';
for (var round = 0; round < 2; round++) {
for (var i = 0; i < urls.length; i++) {
var url = urls[i];
try {
var resp = await axios.post(url, payload, {
headers: headers,
timeout: isInitialize ? 8000 : 90000,
responseType: 'text',
transformResponse: [function(data) { return data; }]
});
_lastGoodMcpUrl = url;
return resp;
} catch (e) {
lastErr = e;
if (!isTransientMcpError(e)) throw e;
console.warn('[clinical-assistant] MCP endpoint unavailable:', url, e.code || e.message);
}
}
await sleep(750 * (round + 1));
}
throw lastErr;
}
function orderedMcpUrls(preferredUrl) {
var candidates = [];
[preferredUrl, _lastGoodMcpUrl].concat(MCP_URLS).forEach(function(url) {
if (url && candidates.indexOf(url) === -1) candidates.push(url);
});
return candidates;
}
function isTransientMcpError(e) {
var code = e && (e.code || (e.cause && e.cause.code)) || '';
var status = e && e.response && e.response.status;
return code === 'ECONNREFUSED' || code === 'ECONNRESET' || code === 'ETIMEDOUT' || status === 502 || status === 503 || status === 504;
}
async function getMcpHealth() {
var results = [];
var urls = orderedMcpUrls();
for (var i = 0; i < urls.length; i++) {
var mcpUrl = urls[i];
var healthUrl = mcpUrl.replace(/\/mcp\/?$/, '/health/live');
try {
var resp = await axios.get(healthUrl, { timeout: 3000 });
results.push({ url: mcpUrl, healthUrl: healthUrl, ok: true, status: resp.status, data: resp.data });
_lastGoodMcpUrl = mcpUrl;
return { ok: true, activeUrl: mcpUrl, checked: results, candidates: MCP_URLS };
} catch (e) {
results.push({ url: mcpUrl, healthUrl: healthUrl, ok: false, error: e.code || e.message });
}
}
return { ok: false, activeUrl: _lastGoodMcpUrl || null, checked: results, candidates: MCP_URLS };
}
function buildMcpUrls() {
var raw = [];
if (process.env.CLINICAL_ASSISTANT_MCP_URLS) raw = raw.concat(process.env.CLINICAL_ASSISTANT_MCP_URLS.split(','));
raw.push(process.env.CLINICAL_ASSISTANT_MCP_URL || process.env.MCP_SERVER_URL || '');
raw.push('http://mcp:8000/mcp');
raw.push('http://mcp-server-mcp-1:8000/mcp');
raw.push('http://127.0.0.1:8100/mcp');
var out = [];
raw.forEach(function(url) {
url = String(url || '').trim();
if (!url) return;
if (!/\/mcp\/?$/.test(url)) url = url.replace(/\/$/, '') + '/mcp';
if (out.indexOf(url) === -1) out.push(url);
});
return out;
}
function sleep(ms) {
return new Promise(function(resolve) { setTimeout(resolve, ms); });
}
function parseMcpResponse(body) {
var text = String(body || '').trim();
if (!text) return {};