Skip to main content
Glama

NervusDB MCP Server

Official
by nervusdb
db.ts11.5 kB
import path from 'node:path'; import { stat } from 'node:fs/promises'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { NervusDB } from '@nervusdb/core'; import { FingerprintService } from '../domain/fingerprint/fingerprintService.js'; import { IndexMaintenanceService } from '../services/indexMaintenanceService.js'; import type { IndexMetadata } from '../domain/types/indexMetadata.js'; import { QueryExecutor } from '../domain/query/queryExecutor.js'; import { QueryRequestSchema } from '../domain/query/queryBuilder.js'; export interface DbToolDependencies { fingerprint: FingerprintService; openDatabase?: typeof NervusDB.open; indexMaintenance?: IndexMaintenanceService; queryExecutor?: QueryExecutor; } async function requireValidIndex( fingerprint: FingerprintService, projectPath: string, schema?: 'complete', ) { const meta = await fingerprint.validate(projectPath); if (schema && meta.state !== schema) { throw new Error(`索引状态异常:${meta.state}`); } return meta; } export function registerDbTools(server: McpServer, deps: DbToolDependencies): void { const openDatabase = deps.openDatabase ?? NervusDB.open.bind(NervusDB); const indexMaintenanceService = deps.indexMaintenance ?? new IndexMaintenanceService(); const queryExecutor = deps.queryExecutor ?? new QueryExecutor(); server.registerTool( 'db.getStats', { title: 'Get graph index stats', description: 'Read metadata for indexed project', inputSchema: { projectPath: z.string() }, outputSchema: { fileCount: z.number(), fingerprint: z.string(), projectHash: z.string(), indexedAt: z.string(), versions: z.record(z.string()).optional(), dbFile: z.string(), dbSizeBytes: z.number().optional(), }, }, async ({ projectPath }) => { const meta = await requireValidIndex(deps.fingerprint, projectPath, 'complete'); const dbPath = path.resolve(meta.output.dbFile); const size = await safeStat(dbPath); const output = { fileCount: meta.fileCount, fingerprint: meta.fingerprint.value, projectHash: meta.projectHash, indexedAt: meta.indexedAt, versions: meta.versions, dbFile: dbPath, dbSizeBytes: size?.size, }; return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output as unknown as { [x: string]: unknown }, }; }, ); server.registerTool( 'db.query', { title: 'Run graph query', description: 'Execute typed or raw queries against the knowledge graph', inputSchema: { projectPath: z.string(), query: QueryRequestSchema, }, outputSchema: { facts: z.array( z.object({ subject: z.string(), predicate: z.string(), object: z.string(), properties: z.record(z.unknown()).optional(), }), ), count: z.number(), hasMore: z.boolean(), }, }, async ({ projectPath, query }) => { const meta = await requireValidIndex(deps.fingerprint, projectPath, 'complete'); const dbPath = path.resolve(meta.output.dbFile); const db = await openDatabase(dbPath, { enableLock: false, registerReader: false, experimental: { cypher: true }, }); try { const result = await queryExecutor.execute(db, query); // Monitor response size to detect potential token limit issues const resultJson = JSON.stringify(result); const responseSizeBytes = resultJson.length; const SIZE_WARNING_THRESHOLD = 1024 * 1024; // 1MB if (responseSizeBytes > SIZE_WARNING_THRESHOLD) { const sizeMB = (responseSizeBytes / (1024 * 1024)).toFixed(2); console.warn( `[db.query] Response size (${sizeMB}MB) exceeds 1MB threshold. ` + `This may cause MCP client token limit errors. Consider using LIMIT clause or db.getNodeDetails for large result sets.`, ); } return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], structuredContent: result as unknown as { [x: string]: unknown }, }; } finally { await db.close(); } }, ); server.registerTool( 'db.rebuildIndex', { title: 'Rebuild project index', description: 'Trigger indexing service to rebuild shadow index for the project with telemetry.', inputSchema: { projectPath: z.string() }, outputSchema: { processedFiles: z.number(), metadata: z.object({ projectHash: z.string(), indexedAt: z.string(), fingerprint: z.object({ value: z.string(), commit: z.string().optional(), branch: z.string().optional(), dirty: z.boolean().optional(), }), }), telemetry: z.object({ duration: z.number(), startTime: z.string(), endTime: z.string(), success: z.boolean(), }), }, }, async ({ projectPath }) => { const result = await indexMaintenanceService.rebuildIndex(projectPath); const output = { processedFiles: result.processedFiles, metadata: { projectHash: result.metadata.projectHash, indexedAt: result.metadata.indexedAt, fingerprint: result.metadata.fingerprint, }, telemetry: { duration: result.telemetry.duration, startTime: result.telemetry.startTime.toISOString(), endTime: result.telemetry.endTime.toISOString(), success: result.telemetry.success, }, }; return { content: [{ type: 'text', text: JSON.stringify(output) }], structuredContent: output as unknown as { [x: string]: unknown }, }; }, ); server.registerTool( 'db.getNodeDetails', { title: 'Get complete node details', description: 'Retrieve complete information for a specific node by ID. Use this after db.query to get full node details (AST, source code, etc.) that are omitted from query result summaries.', inputSchema: { projectPath: z.string().describe('Project root path'), nodeId: z.string().describe('Node ID from query results'), }, outputSchema: { found: z.boolean().describe('Whether the node was found'), node: z.record(z.unknown()).optional().describe('Complete node object if found'), }, }, async ({ projectPath, nodeId }) => { const meta = await requireValidIndex(deps.fingerprint, projectPath, 'complete'); const dbPath = path.resolve(meta.output.dbFile); const db = await openDatabase(dbPath, { enableLock: false, registerReader: false, experimental: { cypher: true }, }); try { // Execute Cypher query to find node by ID const cypherResult = await db.cypher( 'MATCH (n) WHERE n.id = $nodeId RETURN n', { nodeId }, { readonly: true }, ); if (cypherResult.records.length === 0) { const output = { found: false }; return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], structuredContent: output as unknown as { [x: string]: unknown }, }; } // Extract the node from the first record const record = cypherResult.records[0]; const node = record.n as Record<string, unknown>; const output = { found: true, node, }; return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }], structuredContent: output as unknown as { [x: string]: unknown }, }; } finally { await db.close(); } }, ); server.registerTool( 'db.getHealth', { title: 'Check index health', description: 'Validate index presence, fingerprint freshness, and WAL size limits.', inputSchema: { projectPath: z.string(), walSizeWarningBytes: z .number() .int() .default(50 * 1024 * 1024), }, outputSchema: { status: z.enum(['healthy', 'warning', 'error']), checks: z.array( z.object({ name: z.string(), status: z.enum(['pass', 'warn', 'fail']), message: z.string(), details: z.record(z.unknown()).optional(), }), ), metadata: z.any().optional(), }, }, async ({ projectPath, walSizeWarningBytes }) => { const checks: Array<{ name: string; status: 'pass' | 'warn' | 'fail'; message: string; details?: Record<string, unknown>; }> = []; let metadata: IndexMetadata | undefined; try { metadata = await deps.fingerprint.validate(projectPath); checks.push({ name: 'fingerprint', status: 'pass', message: 'Fingerprint matches current working tree.', }); } catch (error) { const err = error as Error; checks.push({ name: 'fingerprint', status: 'fail', message: err.message }); return buildHealthResponse(checks, metadata); } const dbPath = path.resolve(metadata.output.dbFile); const dbStats = await safeStat(dbPath); if (!dbStats) { checks.push({ name: 'dbFile', status: 'fail', message: 'Index database file is missing.' }); } else { checks.push({ name: 'dbFile', status: 'pass', message: 'Index database file exists.', details: { sizeBytes: dbStats.size }, }); } const walPath = `${dbPath}-wal`; const walStats = await safeStat(walPath); if (walStats) { const status = walStats.size > walSizeWarningBytes ? 'warn' : 'pass'; checks.push({ name: 'walSize', status, message: status === 'warn' ? 'WAL size exceeds configured warning threshold.' : 'WAL size within acceptable range.', details: { sizeBytes: walStats.size, thresholdBytes: walSizeWarningBytes }, }); } else { checks.push({ name: 'walSize', status: 'pass', message: 'No WAL file detected.' }); } return buildHealthResponse(checks, metadata); }, ); } async function safeStat(targetPath: string): Promise<{ size: number } | undefined> { try { const stats = await stat(targetPath); return { size: stats.size }; } catch (error) { if ((error as NodeJS.ErrnoException).code === 'ENOENT') { return undefined; } throw error; } } function buildHealthResponse( checks: Array<{ name: string; status: 'pass' | 'warn' | 'fail'; message: string; details?: Record<string, unknown>; }>, metadata?: IndexMetadata, ) { const worstStatus = checks.some((check) => check.status === 'fail') ? 'error' : checks.some((check) => check.status === 'warn') ? 'warning' : 'healthy'; const output = { status: worstStatus, checks, metadata, } as const; return { content: [{ type: 'text' as const, text: JSON.stringify(output, null, 2) }], structuredContent: output as unknown as { [x: string]: unknown }, }; }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nervusdb/nervusdb-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server