* Get tool definitions with dynamic descriptions based on project size. * The codegraph_explore tool description includes a budget recommendation * scaled to the number of indexed files. Honors the CODEGRAPH_MCP_TOOLS * allowlist so a trimmed surface is reflected in ListTools.
()
| 948 | * allowlist so a trimmed surface is reflected in ListTools. |
| 949 | */ |
| 950 | getTools(): ToolDefinition[] { |
| 951 | const allow = this.toolAllowlist(); |
| 952 | // No explicit allowlist → the default 4-tool surface (see |
| 953 | // DEFAULT_MCP_TOOLS for the evidence). An allowlist replaces the |
| 954 | // default entirely, so any defined tool can be re-enabled. |
| 955 | let visible = allow |
| 956 | ? tools.filter(t => allow.has(t.name.replace(/^codegraph_/, ''))) |
| 957 | : tools.filter(t => DEFAULT_MCP_TOOLS.has(t.name.replace(/^codegraph_/, ''))); |
| 958 | // No default project loaded → no-root-index case (#993): a gateway server |
| 959 | // started outside any repo, or a monorepo root whose indexes live in |
| 960 | // sub-projects. With nothing to fall back to, EVERY call needs an explicit |
| 961 | // projectPath, so mark it required in the schema — a high-salience nudge the |
| 962 | // agent acts on, where SERVER_INSTRUCTIONS_NO_ROOT_INDEX's prose alone |
| 963 | // wasn't enough (the reporter had to add an AGENTS.md note). `this.cg` is |
| 964 | // settled by `retryInitIfNeeded()` before `handleToolsList` calls us, so a |
| 965 | // null here means "genuinely no default", not a startup race. When a default |
| 966 | // IS open we leave projectPath optional (below): a bare call falls back to |
| 967 | // it, exactly as in the common single-project launch. |
| 968 | if (!this.cg) return withRequiredProjectPath(visible); |
| 969 | |
| 970 | try { |
| 971 | const stats = this.cg.getStats(); |
| 972 | const budget = getExploreBudget(stats.fileCount); |
| 973 | |
| 974 | // Tiny-repo tool gating: on projects under TINY_REPO_FILE_THRESHOLD |
| 975 | // files, only expose the core trio (search, node, explore) — one |
| 976 | // below even the 4-tool default: at this scale callers, too, reduces |
| 977 | // to one grep. (Historical note: the audit below ran when context and |
| 978 | // trace still existed; its "5 core tools" are today's trio.) |
| 979 | // |
| 980 | // n=2 audits ruled out cutting below 5 tools: |
| 981 | // - 3-tool gate (search + context + trace): cost regressed on |
| 982 | // cobra/ky/sinatra. The agent fell back to raw Reads to cover |
| 983 | // what codegraph_node + codegraph_explore would have answered. |
| 984 | // - 1-tool gate (search only): catastrophic regression — express |
| 985 | // went from -43% WIN to +107% LOSS. With only search, the agent |
| 986 | // can't navigate the call graph structurally and reads everything. |
| 987 | // |
| 988 | // 5 is the empirical lower bound. Tools beyond search/context/ |
| 989 | // node/explore/trace pay overhead that the agent doesn't recoup |
| 990 | // on tiny-repo flow questions. |
| 991 | // ITER4: raise threshold 150 → 500 so single-file frameworks |
| 992 | // (sinatra at 159, slim_framework around 200) also get the |
| 993 | // 5-tool surface. The empirical 5-tool floor was set on <150 |
| 994 | // probes; iter3 measurement showed sinatra is structurally the |
| 995 | // SAME problem as cobra (single-file WITHOUT-arm Read wins), |
| 996 | // so it deserves the same gating. |
| 997 | const TINY_REPO_FILE_THRESHOLD = 500; |
| 998 | const TINY_REPO_CORE_TOOLS = new Set([ |
| 999 | 'codegraph_explore', |
| 1000 | 'codegraph_search', |
| 1001 | 'codegraph_node', |
| 1002 | ]); |
| 1003 | if (stats.fileCount < TINY_REPO_FILE_THRESHOLD) { |
| 1004 | visible = visible.filter(t => TINY_REPO_CORE_TOOLS.has(t.name)); |
| 1005 | } |
| 1006 | |
| 1007 | return visible.map(tool => { |