diff --git a/README.md b/README.md index f8f39e97..9712bca6 100644 --- a/README.md +++ b/README.md @@ -358,6 +358,8 @@ When running as an MCP server, CodeGraph exposes these tools to Claude Code: | `codegraph_files` | Get indexed file structure (faster than filesystem scanning) | | `codegraph_status` | Check index health and statistics | +You can hide specific MCP tools per-project via `.codegraph/config.json` (see Configuration below). Hidden tools are removed from `tools/list` and are treated as unknown if called directly. + --- ## Library Usage @@ -396,7 +398,10 @@ The `.codegraph/config.json` file controls indexing: "frameworks": [], "maxFileSize": 1048576, "extractDocstrings": true, - "trackCallSites": true + "trackCallSites": true, + "mcp": { + "disabledTools": ["codegraph_explore"] + } } ``` @@ -408,6 +413,13 @@ The `.codegraph/config.json` file controls indexing: | `maxFileSize` | Skip files larger than this (bytes) | `1048576` (1MB) | | `extractDocstrings` | Extract docstrings from code | `true` | | `trackCallSites` | Track call site locations | `true` | +| `mcp.disabledTools` | MCP tool names to hide/disable for this project | `[]` | + +Example disabled tool names: `codegraph_explore`, `codegraph_context`, `codegraph_files`. + +Notes: +- This setting is project-local only (no global override). +- Unknown tool names are ignored (lenient behavior). ## Supported Languages diff --git a/__tests__/foundation.test.ts b/__tests__/foundation.test.ts index 4e8f204a..0fcd234d 100644 --- a/__tests__/foundation.test.ts +++ b/__tests__/foundation.test.ts @@ -205,6 +205,31 @@ describe('CodeGraph Foundation', () => { const config = loadConfig(tempDir); expect(config.maxFileSize).toBe(999999); }); + + it('should persist MCP disabled tools config', () => { + const cg = CodeGraph.initSync(tempDir); + + cg.updateConfig({ + mcp: { disabledTools: ['codegraph_explore'] }, + }); + + cg.close(); + + const config = loadConfig(tempDir); + expect(config.mcp?.disabledTools).toEqual(['codegraph_explore']); + }); + + it('should reject invalid mcp.disabledTools format', () => { + const cg = CodeGraph.initSync(tempDir); + cg.close(); + + const configPath = path.join(getCodeGraphDir(tempDir), 'config.json'); + const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8')) as Record; + raw.mcp = { disabledTools: [123] }; + fs.writeFileSync(configPath, JSON.stringify(raw, null, 2)); + + expect(() => loadConfig(tempDir)).toThrow(/Invalid configuration format/i); + }); }); describe('Directory Management', () => { diff --git a/__tests__/security.test.ts b/__tests__/security.test.ts index 53441d58..eaf8655d 100644 --- a/__tests__/security.test.ts +++ b/__tests__/security.test.ts @@ -14,7 +14,7 @@ import * as path from 'path'; import * as os from 'os'; import { FileLock } from '../src/utils'; import CodeGraph from '../src/index'; -import { ToolHandler, tools } from '../src/mcp/tools'; +import { ToolHandler, tools, filterToolsByConfig } from '../src/mcp/tools'; import { shouldIncludeFile, scanDirectory } from '../src/extraction'; import { shouldIncludeFile as configShouldInclude } from '../src/config'; import { CodeGraphConfig, DEFAULT_CONFIG } from '../src/types'; @@ -265,6 +265,37 @@ describe('MCP Input Validation', () => { const result = await handler.execute('codegraph_search', { query: 'example', limit: -5 }); expect(result.isError).toBeFalsy(); }); + + it('should hide disabled tools from tool list', async () => { + const projectWithDisabled = createTempDir(); + const srcDir = path.join(projectWithDisabled, 'src'); + fs.mkdirSync(srcDir); + fs.writeFileSync(path.join(srcDir, 'x.ts'), 'export const x = 1;\n'); + + const disabledCg = CodeGraph.initSync(projectWithDisabled, { + config: { + include: ['**/*.ts'], + exclude: [], + mcp: { disabledTools: ['codegraph_explore'] }, + }, + }); + + try { + const disabledHandler = new ToolHandler(disabledCg); + const toolNames = disabledHandler.getTools().map(t => t.name); + expect(toolNames).not.toContain('codegraph_explore'); + } finally { + disabledCg.close(); + cleanupTempDir(projectWithDisabled); + } + }); + + it('should ignore unknown disabled tool names (lenient)', () => { + const filtered = filterToolsByConfig(tools, { + mcp: { disabledTools: ['not_a_real_tool_name'] }, + }); + expect(filtered).toHaveLength(tools.length); + }); }); describe('Atomic Writes', () => { diff --git a/package-lock.json b/package-lock.json index 3cd20819..0196118e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1903,7 +1903,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/src/config.ts b/src/config.ts index 9ab1032a..a6244797 100644 --- a/src/config.ts +++ b/src/config.ts @@ -68,6 +68,16 @@ export function validateConfig(config: unknown): config is CodeGraphConfig { if (typeof c.extractDocstrings !== 'boolean') return false; if (typeof c.trackCallSites !== 'boolean') return false; + // Validate MCP config if present + if (c.mcp !== undefined) { + if (typeof c.mcp !== 'object' || c.mcp === null) return false; + const mcp = c.mcp as Record; + if (mcp.disabledTools !== undefined) { + if (!Array.isArray(mcp.disabledTools)) return false; + if (!mcp.disabledTools.every((t) => typeof t === 'string')) return false; + } + } + // Validate include/exclude are string arrays if (!c.include.every((p) => typeof p === 'string')) return false; if (!c.exclude.every((p) => typeof p === 'string')) return false; @@ -127,6 +137,9 @@ function mergeConfig( maxFileSize: overrides.maxFileSize ?? defaults.maxFileSize, extractDocstrings: overrides.extractDocstrings ?? defaults.extractDocstrings, trackCallSites: overrides.trackCallSites ?? defaults.trackCallSites, + mcp: { + disabledTools: overrides.mcp?.disabledTools ?? defaults.mcp?.disabledTools ?? [], + }, customPatterns: overrides.customPatterns ?? defaults.customPatterns, }; } diff --git a/src/mcp/index.ts b/src/mcp/index.ts index e516631a..cb557599 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -18,7 +18,7 @@ import * as path from 'path'; import CodeGraph, { findNearestCodeGraphRoot } from '../index'; import { StdioTransport, JsonRpcRequest, JsonRpcNotification, ErrorCodes } from './transport'; -import { tools, ToolHandler } from './tools'; +import { ToolHandler } from './tools'; import { SERVER_INSTRUCTIONS } from './server-instructions'; /** @@ -314,9 +314,11 @@ export class MCPServer { const toolName = params.name; const toolArgs = params.arguments || {}; - // Validate tool exists - const tool = tools.find(t => t.name === toolName); - if (!tool) { + // Validate tool is enabled for this project (disabled tools are hidden) + const projectPath = typeof toolArgs.projectPath === 'string' + ? toolArgs.projectPath + : undefined; + if (!this.toolHandler.isToolEnabled(toolName, projectPath)) { this.transport.sendError( request.id, ErrorCodes.InvalidParams, diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index e796cfc7..bd4f83b3 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -5,7 +5,7 @@ */ import CodeGraph, { findNearestCodeGraphRoot } from '../index'; -import type { Node, Edge, SearchResult, Subgraph, TaskContext, NodeKind } from '../types'; +import type { Node, Edge, SearchResult, Subgraph, TaskContext, NodeKind, CodeGraphConfig } from '../types'; import { createHash } from 'crypto'; import { writeFileSync, readFileSync, existsSync } from 'fs'; import { clamp, validatePathWithinRoot } from '../utils'; @@ -286,6 +286,19 @@ export const tools: ToolDefinition[] = [ }, ]; +/** + * Filter tool definitions by project MCP config. + * Unknown disabled tool names are ignored (lenient behavior). + */ +export function filterToolsByConfig( + allTools: ToolDefinition[], + config?: Pick | null +): ToolDefinition[] { + const disabled = new Set(config?.mcp?.disabledTools ?? []); + if (disabled.size === 0) return allTools; + return allTools.filter(tool => !disabled.has(tool.name)); +} + /** * Tool handler that executes tools against a CodeGraph instance * @@ -317,14 +330,25 @@ export class ToolHandler { * The codegraph_explore tool description includes a budget recommendation * scaled to the number of indexed files. */ - getTools(): ToolDefinition[] { - if (!this.cg) return tools; + getTools(projectPath?: string): ToolDefinition[] { + let baseTools = tools; + + try { + const cg = projectPath ? this.getCodeGraph(projectPath) : this.cg; + if (cg) { + baseTools = filterToolsByConfig(baseTools, cg.getConfig()); + } + } catch { + // If project resolution fails here, keep fallback behavior and expose all tools. + } + + if (!this.cg) return baseTools; try { const stats = this.cg.getStats(); const budget = getExploreBudget(stats.fileCount); - return tools.map(tool => { + return baseTools.map(tool => { if (tool.name === 'codegraph_explore') { return { ...tool, @@ -334,10 +358,17 @@ export class ToolHandler { return tool; }); } catch { - return tools; + return baseTools; } } + /** + * Check whether a tool is enabled for the target project. + */ + isToolEnabled(toolName: string, projectPath?: string): boolean { + return this.getTools(projectPath).some(tool => tool.name === toolName); + } + /** * Get CodeGraph instance for a project * diff --git a/src/types.ts b/src/types.ts index 328f7432..0968f2e5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -478,6 +478,12 @@ export interface CodeGraphConfig { /** Whether to track call sites */ trackCallSites: boolean; + /** MCP server configuration */ + mcp?: { + /** Tool names to hide/disable from MCP clients */ + disabledTools?: string[]; + }; + /** Custom symbol patterns to extract */ customPatterns?: { /** Name for this pattern group */ @@ -693,6 +699,9 @@ export const DEFAULT_CONFIG: CodeGraphConfig = { maxFileSize: 1024 * 1024, // 1MB extractDocstrings: true, trackCallSites: true, + mcp: { + disabledTools: [], + }, }; // =============================================================================