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);
|
||||
|
||||
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 {};
|
||||
|
|
|
|||
Loading…
Reference in a new issue