| 1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js'; |
| 2 | import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js'; |
| 3 | |
| 4 | const RAG_URL = process.env.ZPM_RAG_SSE || 'http://REDACTED-IP:9876/sse'; |
| 5 | const CBM_URL = process.env.ZPM_CBM_SSE || 'http://REDACTED-IP:9877/sse'; |
| 6 | |
| 7 | function withTimeout<T>(p: Promise<T>, ms: number, label: string): Promise<T> { |
| 8 | return Promise.race([ |
| 9 | p, |
| 10 | new Promise<T>((_, rej) => setTimeout(() => rej(new Error(`${label} timeout ${ms}ms`)), ms)), |
| 11 | ]); |
| 12 | } |
| 13 | |
| 14 | async function mcpCall(url: string, name: string, args: Record<string, unknown>, timeoutMs = 60_000): Promise<string> { |
| 15 | const c = new Client({ name: 'zion-pm', version: '1.0.0' }, { capabilities: {} }); |
| 16 | try { |
| 17 | await withTimeout(c.connect(new SSEClientTransport(new URL(url))), 15_000, 'mcp connect'); |
| 18 | const r = (await withTimeout(c.callTool({ name, arguments: args }), timeoutMs, 'mcp call')) as { content?: Array<{ text?: string }> }; |
| 19 | return (r?.content || []).map((b) => b.text || '').join('\n'); |
| 20 | } finally { |
| 21 | try { await c.close(); } catch { } |
| 22 | } |
| 23 | } |
| 24 | |
| 25 | export async function queryMemory(query: string, limit = 6): Promise<string> { |
| 26 | try { |
| 27 | return await mcpCall(RAG_URL, 'homelab_context', { query, limit }); |
| 28 | } catch (e) { |
| 29 | return `queryMemory unavailable: ${(e as Error).message}`; |
| 30 | } |
| 31 | } |
| 32 | |
| 33 | const looksLikeError = (s: string) => !s || /not found|not indexed|"error"|validation|required property|must have/i.test(s); |
| 34 | |
| 35 | export async function queryGraph(query: string): Promise<string> { |
| 36 | try { |
| 37 | const arch = await mcpCall(CBM_URL, 'get_architecture', { project: query }); |
| 38 | if (!looksLikeError(arch)) return arch; |
| 39 | } catch { } |
| 40 | try { |
| 41 | const s = await mcpCall(CBM_URL, 'search_graph', { query }); |
| 42 | if (!looksLikeError(s)) return s; |
| 43 | } catch { } |
| 44 | try { |
| 45 | const projects = await mcpCall(CBM_URL, 'list_projects', {}); |
| 46 | if (!looksLikeError(projects)) return `No single code-graph project matched the query. Indexed projects available:\n${projects}`; |
| 47 | } catch { } |
| 48 | return 'No matching code-graph project (use queryMemory for prose/docs).'; |
| 49 | } |