Custom MCP Tools

Custom MCP Tools

Create custom CPG query tools for Claude Code and other MCP clients by defining SQL queries in a YAML file. No code changes required.

Overview

The dynamic tool loader reads .codegraph/tools.yaml from your project root and registers each tool definition as an MCP tool. This lets you create project-specific analysis queries that Claude Code (or any MCP client) can invoke by name.

Key features: - Define custom SQL queries as named MCP tools - Parameters with types and defaults - Read-only (SELECT queries only) — write operations are rejected - No code changes, no server restart (tools loaded at MCP server startup)

Quick Start

1. Create the tools file

mkdir -p .codegraph

Create .codegraph/tools.yaml:

tools:
  - name: find_large_functions
    description: Find functions with more than N lines of code
    sql: |
      SELECT name, filename, (line_number_end - line_number) as lines
      FROM nodes_method
      WHERE (line_number_end - line_number) > {min_lines}
      ORDER BY lines DESC
      LIMIT {limit}
    parameters:
      - name: min_lines
        type: int
        default: 100
      - name: limit
        type: int
        default: 20

2. Configure MCP server

Ensure your config.yaml has:

mcp:
  enabled: true
  dynamic_tools_path: .codegraph/tools.yaml

3. Start MCP server

python -m src.mcp

The tool find_large_functions is now available to any MCP client.

4. Use from Claude Code

Add to your .claude/mcp.json:

{
  "mcpServers": {
    "codegraph": {
      "command": "python",
      "args": ["-m", "src.mcp", "--db", "data/projects/myproject.duckdb"]
    }
  }
}

Then ask Claude: “Find functions longer than 200 lines” — it will call find_large_functions with min_lines=200.

YAML Format

tools:
  - name: tool_name              # Required: unique tool name (snake_case)
    description: Human-readable   # Required: shown to MCP clients
    sql: |                        # Required: SQL template (SELECT only)
      SELECT ...
      WHERE column > {param}
      LIMIT {limit}
    parameters:                   # Optional: tool parameters
      - name: param              # Parameter name (matches {param} in SQL)
        type: int                # Type: int, str, float
        default: 10              # Default value if not provided

Parameter Substitution

Parameters use {name} syntax in SQL templates. They are substituted via Python str.format():

sql: |
  SELECT name, filename
  FROM nodes_method
  WHERE CyclomaticComplexity > {threshold}
    AND filename LIKE '%{path_pattern}%'
  LIMIT {limit}
parameters:
  - name: threshold
    type: int
    default: 10
  - name: path_pattern
    type: str
    default: ""
  - name: limit
    type: int
    default: 20

Example Tools

Find Uncalled Functions

- name: find_uncalled_functions
  description: Find functions that are never called from other code
  sql: |
    SELECT m.name, m.filename, m.line_number
    FROM nodes_method m
    LEFT JOIN edges_call e ON m.id = e.dst
    WHERE e.src IS NULL
    ORDER BY m.filename, m.line_number
    LIMIT {limit}
  parameters:
    - name: limit
      type: int
      default: 50

Find High-Complexity Methods

- name: find_complex_methods
  description: Find methods with high cyclomatic complexity
  sql: |
    SELECT name, filename, CyclomaticComplexity, FanIn, FanOut
    FROM nodes_method
    WHERE CyclomaticComplexity > {threshold}
    ORDER BY CyclomaticComplexity DESC
    LIMIT {limit}
  parameters:
    - name: threshold
      type: int
      default: 15
    - name: limit
      type: int
      default: 30

Find Security Sinks

- name: find_dangerous_calls
  description: Find calls to potentially dangerous functions
  sql: |
    SELECT c.callee_name, c.filename, c.line_number, m.name as caller
    FROM nodes_call c
    JOIN nodes_method m ON c.method_id = m.id
    WHERE c.callee_name IN ('strcpy', 'strcat', 'sprintf', 'gets')
    ORDER BY c.callee_name, c.filename
    LIMIT {limit}
  parameters:
    - name: limit
      type: int
      default: 100

Find TODO/FIXME Comments

- name: find_todos
  description: Find methods with TODO or FIXME markers
  sql: |
    SELECT name, filename, line_number
    FROM nodes_method
    WHERE HasTodoFixme = true
    ORDER BY filename, line_number
    LIMIT {limit}
  parameters:
    - name: limit
      type: int
      default: 50

Security

Read-Only Enforcement

Only SELECT queries are allowed. The loader validates SQL at two points:

  1. At load time — tool definitions with non-SELECT SQL are rejected
  2. At execution time — generated SQL (after parameter substitution) is re-validated

Rejected SQL keywords: INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, TRUNCATE, GRANT, REVOKE.

Row Limits

Results are capped by mcp.max_query_rows (default: 1000) to prevent memory exhaustion.

Configuration Reference

# config.yaml
mcp:
  enabled: true                           # Master on/off for MCP server
  transport: stdio                        # Transport: stdio | http | websocket
  max_query_rows: 1000                    # Maximum rows per query result
  dynamic_tools_path: .codegraph/tools.yaml  # Path to custom tools YAML

Troubleshooting

Tool not appearing? - Check that .codegraph/tools.yaml exists relative to the project root - Check MCP server logs for validation warnings - Verify mcp.enabled: true and mcp.dynamic_tools_path in config.yaml

SQL rejected? - Ensure the query starts with SELECT - Check for accidentally included DDL/DML keywords - Verify parameter names match between {param} placeholders and parameters definitions

Empty results? - Test the SQL directly: python -m src.cli gocpg query "YOUR SQL HERE" - Check table and column names against Schema Reference


Module: src/mcp/dynamic_loader.py Last updated: February 2026