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 - Parameterized queries — values passed via DuckDB ? placeholders (not string interpolation) - Read-only (SELECT queries only) — write operations and set 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.

Full CLI options:

python -m src.mcp --db data/projects/myproject.duckdb        # stdio (default)
python -m src.mcp --transport sse --port 27495                # SSE transport
python -m src.mcp --transport http --port 27495               # Streamable HTTP
python -m src.mcp --transport sse --no-auth                   # SSE without auth
python -m src.mcp --verbose                                   # Debug logging
Argument Description
--db PATH Path to DuckDB CPG database
--transport {stdio,sse,http} Transport protocol (default: from config or stdio)
--host HOST Bind address for SSE/HTTP (default: 0.0.0.0)
--port PORT Listen port for SSE/HTTP (default: 27495)
--no-auth Disable authentication for SSE/HTTP transports
--verbose, -v Enable debug logging

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, string, float
        default: 10              # Default value if not provided

Parameter Types

Type Validation Example
int Converted to integer, raises ValueError on failure 100
float Converted to float, raises ValueError on failure 3.14
string SQL metacharacter check (rejects ', ", ;, --, /*, */) "src/"

Any unrecognized type falls back to string coercion with truncation to 1000 characters (no metacharacter validation).

Parameter Substitution

Parameters use {name} syntax in SQL templates. At execution time, each {name} placeholder is replaced with a ? positional marker, and the sanitized values are passed as a parameter tuple to DuckDB’s parameterized query API. This prevents SQL injection by design.

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: string
    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

Blocked SQL keywords: INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, TRUNCATE, GRANT, REVOKE, UNION, EXCEPT, INTERSECT.

Parameterized Queries

User-supplied parameter values are never interpolated into SQL strings. The loader replaces {name} placeholders with ? markers and passes values as a tuple to DuckDB’s parameterized execution API (execute_query(sql, parameters=...)).

SQL Metacharacter Blocking

String parameters (type string) are checked against a blocklist of SQL metacharacters: ', ", ;, --, /*, */. If any are found, the query is rejected with a ValueError.

Size and Count Limits

Limit Config key Default
SQL query size security.dynamic_sql_max_length_kb 10 KB
Parameter count security.dynamic_sql_max_parameters 10

These limits are enforced at execution time. Exceeding them raises a ValueError.

Row Limits

Results are capped by mcp.max_query_rows (default: 1000) to prevent memory exhaustion. If the SQL template does not contain a LIMIT clause, the server appends one automatically.

Authentication

SSE and HTTP transports are protected by MCPAuthMiddleware. Clients must provide one of:

  • Authorization: Bearer <jwt> header — validated via verify_token()
  • X-API-Key header — validated via ApiKeyRepository

Missing or invalid credentials return HTTP 401. The stdio transport is local and does not require authentication.

Use --no-auth to disable authentication for development:

python -m src.mcp --transport sse --no-auth

Configuration Reference

# config.yaml
mcp:
  enabled: true                           # Master on/off for MCP server
  transport: stdio                        # Transport: stdio | sse | http
  host: 0.0.0.0                           # Bind address for SSE/HTTP
  http_port: 27495                        # Listen port for SSE/HTTP
  max_query_rows: 1000                    # Maximum rows per query result
  dynamic_tools_path: .codegraph/tools.yaml  # Path to custom tools YAML

# Security limits (in security section)
security:
  dynamic_sql_max_length_kb: 10           # Max SQL size in KB
  dynamic_sql_max_parameters: 10          # Max number of parameters per tool

Troubleshooting

Tool not appearing? - Check that .codegraph/tools.yaml exists relative to the project root - Check MCP server logs for validation warnings (--verbose for debug output) - 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 (including UNION, EXCEPT, INTERSECT) - Verify parameter names match between {param} placeholders and parameters definitions - Check that string parameters do not contain SQL metacharacters (', ", ;, --)

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

Authentication errors (SSE/HTTP)? - Verify Bearer JWT or X-API-Key is provided - Use --no-auth for local development

Built-in MCP Tools

CodeGraph ships with 149 built-in MCP tools across code analysis, security, patterns, enterprise features, OpenViking context operations, and platform integrations. These tools are always available without any configuration.

For the complete list of built-in tools, see ACP Integration — Available Tools.


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