/** * Graphify Client * ---------------- * Talks to the Graphify MCP HTTP service (Docker container) to: * 1. Build/refresh a knowledge graph from a Git repo (per tenant instance) * 2. Query the graph (codebase search, god nodes, shortest path, etc.) * * Graphify is a local-first knowledge-graph builder for code. It parses * 40+ languages with tree-sitter (deterministic, no LLM) and produces a * traversable graph.json. The MCP HTTP service exposes 7 query tools. * * Config (env): * GRAPHIFY_URL - e.g. http://localhost:8080 (Docker container) * GRAPHIFY_API_KEY - shared secret (--api-key on the Graphify server) * * Per-tenant graphs are stored at: /data/graphify/{instanceId}/graph.json */ export interface GraphifyConfig { url: string apiKey?: string } export interface GraphStats { node_count: number edge_count: number community_count: number god_nodes: number extracted_edges: number inferred_edges: number ambiguous_edges: number } export interface GraphNode { id: string label: string type: string file?: string line?: number community?: number is_god_node?: boolean } export interface GraphQueryResult { answer: string nodes: GraphNode[] confidence: 'EXTRACTED' | 'INFERRED' | 'AMBIGUOUS' | 'NONE' path?: string[] } export interface GodNode { id: string label: string type: string in_degree: number out_degree: number file?: string community?: number } class GraphifyError extends Error { constructor(message: string, public status: number, public body?: unknown) { super(message) this.name = 'GraphifyError' } } export function createGraphifyClient(config: GraphifyConfig) { const baseUrl = config.url.replace(/\/$/, '') const headers: Record = { 'Content-Type': 'application/json', } if (config.apiKey) { headers['Authorization'] = `Bearer ${config.apiKey}` } // --- MCP JSON-RPC call --- async function mcpCall(tool: string, args: Record = {}): Promise { const res = await fetch(`${baseUrl}/mcp`, { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/call', params: { name: tool, arguments: args }, id: Date.now(), }), }) const text = await res.text() let json: unknown try { json = text ? JSON.parse(text) : null } catch { json = text } if (!res.ok) { throw new GraphifyError( `Graphify MCP ${tool} failed: HTTP ${res.status}`, res.status, json ) } // MCP response: { result: { content: [{ type: 'text', text: '...' }] } } const result = (json as { result?: { content?: { type: string; text: string }[] } })?.result if (!result) return json as T const textContent = result.content?.find((c) => c.type === 'text')?.text if (!textContent) return json as T try { return JSON.parse(textContent) as T } catch { return textContent as unknown as T } } // --- Graph build (CLI invocation via HTTP trigger if supported) --- // Note: Graphify's MCP server is read-only (query tools). To BUILD a graph, // we either: // a) Shell out to `graphify extract` + `graphify build` in a worker process // b) Call a custom build endpoint (if the Docker image exposes one) // Here we provide a high-level function that the provisioning hook calls. // The actual build is done via a sidecar job (see instance provisioning route). // --- Query tools (read-only, exposed by Graphify MCP) --- async function queryGraph(question: string, graphPath?: string): Promise { const args: Record = { question } if (graphPath) args.graph_path = graphPath return mcpCall('query_graph', args) } async function getNode(nodeId: string, graphPath?: string): Promise { const args: Record = { node_id: nodeId } if (graphPath) args.graph_path = graphPath return mcpCall('get_node', args) } async function getNeighbors(nodeId: string, graphPath?: string): Promise { const args: Record = { node_id: nodeId } if (graphPath) args.graph_path = graphPath return mcpCall('get_neighbors', args) } async function getCommunity(communityId: number, graphPath?: string): Promise<{ id: number label: string nodes: GraphNode[] }> { const args: Record = { community_id: communityId } if (graphPath) args.graph_path = graphPath return mcpCall('get_community', args) } async function getGodNodes(graphPath?: string): Promise { const args: Record = {} if (graphPath) args.graph_path = graphPath return mcpCall('god_nodes', args) } async function getGraphStats(graphPath?: string): Promise { const args: Record = {} if (graphPath) args.graph_path = graphPath return mcpCall('graph_stats', args) } async function shortestPath(sourceId: string, targetId: string, graphPath?: string): Promise<{ path: string[] length: number found: boolean }> { const args: Record = { source: sourceId, target: targetId } if (graphPath) args.graph_path = graphPath return mcpCall('shortest_path', args) } // --- Health check --- async function health(): Promise<{ ok: boolean; version?: string }> { try { const res = await fetch(`${baseUrl}/health`, { headers, signal: AbortSignal.timeout(5000) }) if (!res.ok) return { ok: false } const data = await res.json().catch(() => ({ ok: true })) return { ok: true, version: (data as { version?: string }).version } } catch { return { ok: false } } } return { mcpCall, queryGraph, getNode, getNeighbors, getCommunity, getGodNodes, getGraphStats, shortestPath, health, } } export type GraphifyClient = ReturnType export function createGraphifyClientFromEnv(): GraphifyClient | null { const url = process.env.GRAPHIFY_URL if (!url) return null return createGraphifyClient({ url, apiKey: process.env.GRAPHIFY_API_KEY, }) } // --- Helper: graph path per instance --- // Each tenant's graph is stored at /data/graphify/{instanceId}/graph.json export function graphPathForInstance(instanceId: string): string { return `/data/graphify/${instanceId}/graph.json` } // --- Helper: build graph from a Git repo --- // This is a high-level function that triggers a graph build via the Graphify // CLI. In production, this would be a background job (e.g. via a queue or // Dokploy cron). Here we provide the command that should be executed. export function buildGraphCommand(input: { gitRepo: string gitBranch: string outputPath: string // /data/graphify/{instanceId}/graph.json }): string { return [ `git clone --depth 1 -b ${input.gitBranch} ${input.gitRepo} /tmp/repo`, `&& graphify extract /tmp/repo --output ${input.outputPath}`, `&& graphify build ${input.outputPath} --update`, `&& graphify cluster ${input.outputPath}`, `&& rm -rf /tmp/repo`, ].join(' ') }