Skip to content

refactor: composable MCP tool registry (ROADMAP 3.5)#426

Merged
carlos-alm merged 12 commits intomainfrom
refactor/queries-analysis-modules
Mar 12, 2026
Merged

refactor: composable MCP tool registry (ROADMAP 3.5)#426
carlos-alm merged 12 commits intomainfrom
refactor/queries-analysis-modules

Conversation

@carlos-alm
Copy link
Contributor

Summary

  • Split the monolithic 1,470-line src/mcp.js (31 tools in one switch dispatch) into self-contained tool modules under src/mcp/
  • Each tool is now a single file exporting { name, handler } — adding a new MCP tool = adding a file + one barrel line
  • src/mcp.js becomes a 2-line re-export shim preserving the public API (TOOLS, buildToolList, startMCPServer), so zero consumer/test changes
  • New structure: server.js (MCP lifecycle + dispatch), tool-registry.js (31 tool schemas), middleware.js (pagination helpers), tools/ (31 handler files + barrel)

Test plan

  • All 37 MCP unit tests pass unchanged (import paths preserved by shim)
  • Lint clean (biome check — 0 errors, 0 warnings)
  • No circular dependencies (codegraph cycles)
  • Self-analysis works (codegraph build && codegraph check)
  • ROADMAP 3.5 marked complete

carlos-alm and others added 9 commits March 11, 2026 00:44
…shared/ modules

queries.js (2289 lines) decomposed into focused modules per roadmap 3.4:

src/shared/normalize.js       — normalizeSymbol, kindIcon
src/shared/generators.js      — iterListFunctions, iterRoles, iterWhere
src/analysis/symbol-lookup.js — findMatchingNodes, queryNameData, whereData,
                                listFunctionsData, childrenData
src/analysis/impact.js        — impactAnalysisData, fnImpactData, diffImpactData,
                                diffImpactMermaid
src/analysis/dependencies.js  — fileDepsData, fnDepsData, pathData
src/analysis/module-map.js    — moduleMapData, statsData
src/analysis/context.js       — contextData, explainData
src/analysis/exports.js       — exportsData
src/analysis/roles.js         — rolesData

queries.js is now a ~36-line barrel re-export for backward compatibility.
All 85 test files pass.
Resolve merge conflicts with main (v3.1.3 version bumps, changelog).

Extract duplicated helpers into shared modules per Greptile review:
- resolveMethodViaHierarchy → src/shared/hierarchy.js (was duplicated
  in context.js and dependencies.js)
- safePath, readSourceRange, extractSummary, extractSignature,
  isFileLikeTarget → src/shared/file-utils.js (were in context.js,
  imported cross-module by exports.js)

Impact: 61 functions changed, 138 affected
Address Greptile review: _getFileHash in symbol-lookup.js was identical
to the private getFileHash in normalize.js. Export the original and
import it instead of maintaining a duplicate.

Impact: 2 functions changed, 6 affected
…s.js

Deduplicate three identical getFileLines closures (two in context.js, one
in exports.js) into a single createFileLinesReader factory in
src/shared/file-utils.js. Remove now-unused fs/debug/safePath imports.

Impact: 4 functions changed, 3 affected
No consumer imports ALL_SYMBOL_KINDS from this module — all callers
import directly from kinds.js. The re-export was dead code that
muddied normalize.js's responsibility.
…MAP 3.5)

Replace the 1,470-line src/mcp.js (31 tools in one switch dispatch) with
self-contained tool modules under src/mcp/. Each tool is now a single file
exporting { name, handler }. The original src/mcp.js becomes a 2-line
re-export shim preserving the public API (TOOLS, buildToolList, startMCPServer).

Structure: server.js (MCP lifecycle + dispatch), tool-registry.js (schemas),
middleware.js (pagination helpers), tools/ (31 handler files + barrel).

Adding a new MCP tool = adding a file + one barrel line.

Impact: 38 functions changed, 26 affected
@claude
Copy link

claude bot commented Mar 12, 2026

Claude finished @carlos-alm's task —— View job


PR Review: Composable MCP Tool Registry Refactor

Todo List

  • Phase 1: Root Cause Analysis - Understand the problem and approach
  • Phase 2: Configuration Change Detection - Check for rule relaxations
  • Phase 3: Test Integrity - Verify test preservation claims
  • Phase 4: Code Quality - Analyze refactored code structure
  • Final Assessment - Provide rating and recommendation

Root Cause Analysis ✅

Problem Being Solved: The PR addresses a legitimate architectural debt issue identified in ROADMAP 3.5. The monolithic 1,470-line src/mcp.js contained 31 MCP tools handled via a single switch dispatch, making it difficult to add new tools and violating single responsibility principle.

Why This Approach: The refactor splits the monolith into self-contained modules following a clean pattern:

  • Each tool becomes { name, handler } in a separate file
  • New src/mcp/server.js handles MCP lifecycle + dispatch
  • src/mcp/tool-registry.js contains schemas
  • Barrel exports preserve the exact public API (TOOLS, buildToolList, startMCPServer)
  • Adding a new tool now requires: create file + add one line to barrel export

This is a well-executed vertical slice refactor that improves maintainability without breaking changes.

Configuration Change Detection ✅

Findings: No configuration rules relaxed or tests weakened

  • No changes to biome.json, package.json, or other config files
  • No test files modified - tests preserved unchanged
  • No linting rules disabled or relaxed

Test Integrity ✅

API Preservation: The refactor maintains perfect backward compatibility:

  • src/mcp.js becomes a 2-line shim: export { startMCPServer } from './mcp/server.js'; export { buildToolList, TOOLS } from './mcp/tool-registry.js';
  • All existing imports work unchanged
  • MCP tests in tests/unit/mcp.test.js verify all 31+1 tools are present
  • No test modifications required - excellent indicator of true API preservation

Code Quality ✅

Excellent Refactor Execution:

Clean Modular Structure

  • 31 tool handlers split into src/mcp/tools/*.js files, each exporting { name, handler }
  • Single-purpose modules: Each tool file ~10-65 lines, focused on one capability
  • Consistent patterns: All handlers follow async function handler(args, ctx) signature
  • Dynamic imports: Lazy-load heavy modules (await import('../../embedder.js')) - performance benefit

Proper Abstraction Layers

  • Server layer (server.js): MCP protocol, transport, error handling
  • Registry layer (tool-registry.js): Schema definitions, tool enumeration
  • Middleware layer (middleware.js): Pagination helpers
  • Tool handlers: Pure business logic

Context Injection Pattern

Smart context injection avoiding circular dependencies:

const ctx = { dbPath, getQueries, getDatabase, findDbPath, allowedRepos, MCP_MAX_LIMIT };
const result = await toolEntry.handler(args, ctx);

Smart Error Handling

  • Tools return structured MCP responses ({ content: [{ type: 'text', text }], isError: true })
  • Graceful fallbacks for missing optional dependencies
  • Proper database connection management

Future-Proof Design

  • Adding new MCP tool = create file + add to barrel
  • No central dispatch modification needed
  • Each tool independently testable

Critical Concerns

None. This is a textbook example of clean refactoring:

  • Legitimate problem (monolithic 1.5K line file)
  • Well-executed solution (modular, maintainable)
  • Zero breaking changes (perfect API preservation)
  • No shortcuts or rule relaxations
  • Consistent, readable code patterns

Final Recommendation

## Root Cause Analysis
**Problem Being Solved**: Legitimate architectural debt - 1,470-line monolithic MCP file with 31 tools in single switch dispatch, hindering maintainability and violating SRP
**Why This Approach**: Vertical slice refactor splitting into self-contained tool modules with preserved public API via shim
**Risk Assessment**: Minimal - no breaking changes, no configuration relaxations, API perfectly preserved

## Critical Concerns (if any)
None identified. This represents excellent refactoring practices.

## Final Recommendation
- Rating: ⭐⭐⭐⭐⭐ (5/5)
- Action: APPROVE
- Reasoning: Exemplary refactoring that solves a real architectural problem without breaking changes. Clean modular structure, consistent patterns, proper abstractions, and future-proof design. The 2-line shim preserving the exact API demonstrates thoughtful backward compatibility. This is how monolith refactoring should be done.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 12, 2026

Greptile Summary

This PR successfully completes ROADMAP 3.5: the 1,470-line monolithic src/mcp.js is split into a clean, composable architecture under src/mcp/ — a server, a schema registry, pagination middleware, and 31 self-contained tool handler modules behind a barrel. The public API is preserved via a 2-line re-export shim, and the dispatch switch is replaced by a Map lookup driven by each tool module's own name export, eliminating the need to synchronise string literals between two files.

Key observations:

  • The try/finally guard added to export-graph.js (and already present in find-cycles.js) correctly addresses the prior database-leak review thread.
  • server.js correctly blocks args.repo usage and list_repos calls in single-repo mode before reaching the handler, and the TOOL_HANDLERS Map contains all 32 entries (31 BASE_TOOLS + list_repos) as expected.
  • Two tools that manually open a SQLite connection — export-graph.js and find-cycles.js — import findDbPath directly from ../../db.js and MCP_MAX_LIMIT directly from middleware, rather than consuming them via ctx. Every other tool module in the barrel uses ctx.* for injected dependencies, so these two files are inconsistent with the established pattern and harder to test in isolation without module-level mocking.

Confidence Score: 5/5

  • This PR is safe to merge — it is a pure structural refactoring with no logic changes, all existing tests pass, and the public API is fully preserved.
  • The change is a mechanical extraction of 31 inline handlers from a switch dispatch into individual modules with no altered logic. The public API shim, schema registry, and dispatch Map all align correctly. The one flagged concern (direct findDbPath import in two tool files) is a style inconsistency that carries no runtime risk.
  • src/mcp/tools/export-graph.js and src/mcp/tools/find-cycles.js — minor context-bypass inconsistency worth fixing for test isolation parity with all other tool modules.

Important Files Changed

Filename Overview
src/mcp/server.js New MCP lifecycle + dispatch hub. Replaces the monolithic switch with a TOOL_HANDLERS.get(name) lookup. Multi-repo guards, context construction, and error formatting are clean and correct.
src/mcp/tool-registry.js Owns all 31 tool schemas and buildToolList(). Schema content is a verbatim port of the original mcp.js; logic is correct and backward-compatible TOOLS export is preserved.
src/mcp/tools/index.js Barrel that builds the TOOL_HANDLERS Map from 32 static imports (31 BASE_TOOLS + list_repos). Tool names are used as Map keys via each module's own name export, keeping registry and handler in sync by construction.
src/mcp/middleware.js Thin re-export + two helper functions (effectiveLimit, effectiveOffset). Clean and minimal; effectiveLimit provides a safe ?? 100 fallback that raw MCP_DEFAULTS[key] lookups in some tools lack.
src/mcp/tools/export-graph.js Correctly wraps the DB session in try/finally (resolving the prior review thread). Minor inconsistency: imports findDbPath from ../../db.js and MCP_MAX_LIMIT from middleware directly instead of using ctx.findDbPath / ctx.MCP_MAX_LIMIT.
src/mcp/tools/find-cycles.js Correctly uses try/finally for DB cleanup. Same inconsistency as export-graph.js: findDbPath is imported directly from ../../db.js instead of using ctx.findDbPath.
src/mcp.js Reduced to a 2-line re-export shim. Correctly preserves the public API (startMCPServer, buildToolList, TOOLS) so no consumer or test changes are needed.
src/mcp/index.js Thin re-export barrel for the src/mcp/ sub-package. Correct and minimal.
src/mcp/tools/batch-query.js Minimal wrapper delegating to batchData. Correctly passes ctx.dbPath (already repo-resolved by the server before handler invocation).

Sequence Diagram

sequenceDiagram
    participant Client as MCP Client
    participant Server as server.js (CallToolRequestSchema)
    participant Registry as tool-registry.js (buildToolList)
    participant Barrel as tools/index.js (TOOL_HANDLERS Map)
    participant Tool as tools/<name>.js (handler)
    participant Ctx as ctx (dbPath, getQueries, getDatabase, findDbPath, MCP_MAX_LIMIT)
    participant Upstream as src/*.js (queries, export, flow…)

    Client->>Server: ListTools request
    Server->>Registry: buildToolList(multiRepo)
    Registry-->>Server: BASE_TOOLS [+ repo prop + list_repos]
    Server-->>Client: { tools: [...] }

    Client->>Server: CallTool { name, arguments }
    Server->>Server: guard: multiRepo / allowedRepos / list_repos check
    Server->>Server: resolve dbPath (registry.js if args.repo)
    Server->>Barrel: TOOL_HANDLERS.get(name)
    Barrel-->>Server: { name, handler }
    Server->>Ctx: build ctx object
    Server->>Tool: handler(args, ctx)
    Tool->>Upstream: await import('../../<module>.js') [lazy]
    Upstream-->>Tool: result
    Tool-->>Server: result (plain object) or { content, isError }
    Server-->>Client: { content: [{ type:'text', text: JSON }] }
Loading

Last reviewed commit: 919185e

Comment on lines +15 to +53
switch (args.format) {
case 'dot':
result = exportDOT(db, { fileLevel, limit: exportLimit });
break;
case 'mermaid':
result = exportMermaid(db, { fileLevel, limit: exportLimit });
break;
case 'json':
result = exportJSON(db, {
limit: exportLimit,
offset: effectiveOffset(args),
});
break;
case 'graphml':
result = exportGraphML(db, { fileLevel, limit: exportLimit });
break;
case 'graphson':
result = exportGraphSON(db, {
fileLevel,
limit: exportLimit,
offset: effectiveOffset(args),
});
break;
case 'neo4j':
result = exportNeo4jCSV(db, { fileLevel, limit: exportLimit });
break;
default:
db.close();
return {
content: [
{
type: 'text',
text: `Unknown format: ${args.format}. Use dot, mermaid, json, graphml, graphson, or neo4j.`,
},
],
isError: true,
};
}
db.close();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Database connection leak on thrown exceptions

The db handle is opened manually and closed only on the happy path (db.close() on line 53) and on the default branch (line 42). If any of the export functions (exportDOT, exportMermaid, exportJSON, etc.) throws an unexpected exception, execution escapes to the server-level catch block in server.js and db.close() is never called. Over many calls in a long-running MCP session, SQLite connections accumulate and are never released.

A try/finally guard is needed:

Suggested change
switch (args.format) {
case 'dot':
result = exportDOT(db, { fileLevel, limit: exportLimit });
break;
case 'mermaid':
result = exportMermaid(db, { fileLevel, limit: exportLimit });
break;
case 'json':
result = exportJSON(db, {
limit: exportLimit,
offset: effectiveOffset(args),
});
break;
case 'graphml':
result = exportGraphML(db, { fileLevel, limit: exportLimit });
break;
case 'graphson':
result = exportGraphSON(db, {
fileLevel,
limit: exportLimit,
offset: effectiveOffset(args),
});
break;
case 'neo4j':
result = exportNeo4jCSV(db, { fileLevel, limit: exportLimit });
break;
default:
db.close();
return {
content: [
{
type: 'text',
text: `Unknown format: ${args.format}. Use dot, mermaid, json, graphml, graphson, or neo4j.`,
},
],
isError: true,
};
}
db.close();
let result;
try {
switch (args.format) {
case 'dot':
result = exportDOT(db, { fileLevel, limit: exportLimit });
break;
case 'mermaid':
result = exportMermaid(db, { fileLevel, limit: exportLimit });
break;
case 'json':
result = exportJSON(db, {
limit: exportLimit,
offset: effectiveOffset(args),
});
break;
case 'graphml':
result = exportGraphML(db, { fileLevel, limit: exportLimit });
break;
case 'graphson':
result = exportGraphSON(db, {
fileLevel,
limit: exportLimit,
offset: effectiveOffset(args),
});
break;
case 'neo4j':
result = exportNeo4jCSV(db, { fileLevel, limit: exportLimit });
break;
default:
return {
content: [
{
type: 'text',
text: `Unknown format: ${args.format}. Use dot, mermaid, json, graphml, graphson, or neo4j.`,
},
],
isError: true,
};
}
} finally {
db.close();
}

@carlos-alm
Copy link
Contributor Author

Fixed the db connection leak in export-graph.js and find-cycles.js — both now use try/finally to ensure db.close() is always called even if an export function throws (7d1a25c).

@greptileai

@carlos-alm carlos-alm merged commit cc991ab into main Mar 12, 2026
14 checks passed
@carlos-alm carlos-alm deleted the refactor/queries-analysis-modules branch March 12, 2026 10:51
@github-actions github-actions bot locked and limited conversation to collaborators Mar 12, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant