fix: harden clinical assistant MCP connectivity
This commit is contained in:
parent
c8e911cb64
commit
33ca4e65d9
1 changed files with 95 additions and 10 deletions
|
|
@ -16,7 +16,8 @@ var logger = require('../utils/logger');
|
||||||
|
|
||||||
router.use(authMiddleware);
|
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 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 GREETING_RE = /^(hi|hello|hey|yo|good\s+(morning|afternoon|evening)|thanks|thank you|ok|okay|sup)[\s.!?]*$/i;
|
||||||
var TOPIC_SUGGESTIONS = {
|
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 imageModel = await getSetting('clinical_assistant.image_model', '');
|
||||||
var searchLimit = clampInt(await getSetting('clinical_assistant.search_limit', '8'), 3, 20, 8);
|
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);
|
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) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: 'Request failed' });
|
res.status(500).json({ error: 'Request failed' });
|
||||||
}
|
}
|
||||||
|
|
@ -209,28 +218,104 @@ async function semanticSearch(query, opts) {
|
||||||
context_chars: opts.contextChars
|
context_chars: opts.contextChars
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, session.sessionId);
|
}, session.sessionId, session.mcpUrl);
|
||||||
return search.result || search;
|
return search.result || search;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mcpRequest(payload, sessionId) {
|
async function mcpRequest(payload, sessionId, preferredUrl) {
|
||||||
var headers = {
|
var headers = {
|
||||||
'Accept': 'application/json, text/event-stream',
|
'Accept': 'application/json, text/event-stream',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
};
|
};
|
||||||
if (sessionId) headers['mcp-session-id'] = sessionId;
|
if (sessionId) headers['mcp-session-id'] = sessionId;
|
||||||
var resp = await axios.post(MCP_URL, payload, {
|
var resp = await postMcpWithRetry(payload, headers, preferredUrl);
|
||||||
headers: headers,
|
|
||||||
timeout: 90000,
|
|
||||||
responseType: 'text',
|
|
||||||
transformResponse: [function(data) { return data; }]
|
|
||||||
});
|
|
||||||
var parsed = parseMcpResponse(resp.data);
|
var parsed = parseMcpResponse(resp.data);
|
||||||
if (parsed.error) throw new Error(parsed.error.message || JSON.stringify(parsed.error));
|
if (parsed.error) throw new Error(parsed.error.message || JSON.stringify(parsed.error));
|
||||||
parsed.sessionId = resp.headers['mcp-session-id'] || sessionId || null;
|
parsed.sessionId = resp.headers['mcp-session-id'] || sessionId || null;
|
||||||
|
parsed.mcpUrl = resp.config && resp.config.url ? resp.config.url : preferredUrl || _lastGoodMcpUrl;
|
||||||
return parsed;
|
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) {
|
function parseMcpResponse(body) {
|
||||||
var text = String(body || '').trim();
|
var text = String(body || '').trim();
|
||||||
if (!text) return {};
|
if (!text) return {};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue