Skip to content

Feature: Expand MCP tool surface (3→9 tools) -- map, callers, callees, references, implementations, impact analysis #502

@buger

Description

@buger

Problem Statement

Probe's MCP server currently exposes only 3 tools (search_code, extract_code, grep), while competitors expose significantly more:

  • Stakgraph MCP: 8 tools including get_edges, shortest_path, explore, get_map
  • grepai MCP: 10 tools including trace_callers, trace_callees, trace_graph, rpg_search
  • ABCoder MCP: 6 tools including hierarchical get_repo_structure, get_package_structure, get_file_structure, get_ast_node with dependency/reference data
  • Octocode MCP: bridges to external LSP servers, exposing hover, go-to-definition, find-references

When Probe is used as an MCP server for Claude Code, Cursor, Windsurf, or other AI editors, it cannot answer:

  • "Who calls this function?" (call graph)
  • "What does this function depend on?" (dependencies)
  • "What implements this trait/interface?" (implementations)
  • "What's the structure of this repo?" (map)
  • "What would break if I change this?" (impact analysis)

PR #103 adds the LSP daemon with call hierarchy, references, and implementations persisted in SQLite. This investment should be exposed through MCP.

Current State

File: npm/src/mcp/index.ts

  • ProbeServer wraps @modelcontextprotocol/sdk Server
  • Transport: StdioServerTransport
  • Each tool calls the probe Rust binary via child process

Current tools (3):

Tool Description
search_code Semantic code search with ElasticSearch queries
extract_code Extract code blocks using tree-sitter AST
grep Standard grep-style search

Proposed New Tools (6)

Tool 1: map_code -- Repository Structure Overview

Depends on: #501 (probe map command)
LSP required: No

{
  name: "map_code",
  description: "Get a structural overview of a codebase showing directory tree and symbol signatures. Use this FIRST when exploring an unfamiliar project to understand its architecture before searching for specific code.",
  inputSchema: {
    type: "object",
    properties: {
      path: {
        type: "string",
        description: "Root directory to map (absolute or relative path)"
      },
      depth: {
        type: "number",
        description: "Maximum directory depth to traverse. Default: unlimited. Use 1-2 for large repos."
      },
      detail: {
        type: "string",
        enum: ["files", "signatures", "full"],
        description: "Level of detail. 'files': filenames + line counts. 'signatures': + function/class/struct signatures (default). 'full': + first doc comment line.",
        default: "signatures"
      },
      language: {
        type: "string",
        description: "Filter to specific language: rust, typescript, python, go, java, etc."
      },
      maxTokens: {
        type: "number",
        description: "Maximum output tokens. Output truncated gracefully within budget.",
        default: 4000
      }
    },
    required: ["path"]
  }
}

Example call:

{ "path": "./src", "depth": 2, "detail": "signatures", "maxTokens": 3000 }

Example response:

src/
  search/
    search_runner.rs
      pub fn perform_probe(options: &SearchOptions) -> Result<Vec<SearchResult>>
      pub fn search_with_structured_patterns(...) -> Result<HashMap<PathBuf, ...>>
    result_ranking.rs
      pub fn rank_search_results(results: &mut Vec<SearchResult>, ...) -> Result<()>
    elastic_query.rs
      pub enum Expr
      pub fn parse_query(query: &str) -> Result<Expr>
  language/
    language_trait.rs
      pub trait LanguageImpl
    factory.rs
      pub fn get_language_for_file(path: &Path) -> Option<Box<dyn LanguageImpl>>
  models.rs
    pub struct SearchResult
    pub struct ParentContext
... (15 more files)

[42 files, 187 symbols, 2,950 tokens]

Tool 2: find_callers -- Who Calls This Function?

Depends on: PR #103 (LSP daemon)
LSP required: Yes (graceful fallback to text search)

{
  name: "find_callers",
  description: "Find all functions that call a given function/method. Requires the LSP daemon to be running. Returns caller function signatures with file locations.",
  inputSchema: {
    type: "object",
    properties: {
      file: {
        type: "string",
        description: "File containing the target function (absolute or relative path)"
      },
      line: {
        type: "number",
        description: "Line number of the function definition"
      },
      symbol: {
        type: "string",
        description: "Symbol name (alternative to file+line). Will be searched in the codebase."
      },
      depth: {
        type: "number",
        description: "How many levels of callers to traverse. 1 = direct callers only (default). 2+ = callers of callers.",
        default: 1
      },
      maxResults: {
        type: "number",
        description: "Maximum number of callers to return.",
        default: 20
      }
    },
    required: []
  }
}

Example call:

{ "file": "src/search/search_runner.rs", "line": 225, "depth": 1 }

Example response:

Callers of `perform_probe` (src/search/search_runner.rs:225):

1. src/main.rs:42
   pub fn handle_search(args: SearchArgs) -> Result<()>

2. src/search/mod.rs:18
   pub fn search(options: SearchOptions) -> Result<LimitedSearchResults>

3. tests/integration_test.rs:55
   fn test_basic_search()

[3 callers found, depth: 1]

Tool 3: find_callees -- What Does This Function Call?

Depends on: PR #103
LSP required: Yes

{
  name: "find_callees",
  description: "Find all functions/methods called by a given function. Requires the LSP daemon. Returns callee signatures with file locations.",
  inputSchema: {
    type: "object",
    properties: {
      file: { type: "string", description: "File containing the source function" },
      line: { type: "number", description: "Line number of the function definition" },
      symbol: { type: "string", description: "Symbol name (alternative to file+line)" },
      depth: { type: "number", description: "Levels of callees to traverse. 1 = direct (default).", default: 1 },
      maxResults: { type: "number", description: "Maximum callees to return.", default: 30 }
    },
    required: []
  }
}

Example call:

{ "symbol": "perform_probe", "depth": 1 }

Example response:

Callees of `perform_probe` (src/search/search_runner.rs:225):

1. src/search/elastic_query.rs:19
   pub fn parse_query(query: &str) -> Result<Expr>

2. src/search/filters.rs:7
   pub fn extract_and_simplify_with_autodetect(...) -> SearchFilters

3. src/search/file_list_cache.rs:45
   pub fn get_files(path: &Path, ...) -> Result<Vec<PathBuf>>

4. src/search/search_runner.rs:1621
   pub fn search_with_structured_patterns(...) -> Result<...>

5. src/search/result_ranking.rs:12
   pub fn rank_search_results(...) -> Result<()>

6. src/search/block_merging.rs:8
   pub fn merge_ranked_blocks(...) -> Vec<SearchResult>

7. src/search/search_output.rs:54
   pub fn format_and_print_search_results(...) -> Result<String>

[7 callees found, depth: 1]

Tool 4: find_references -- Where Is This Symbol Used?

Depends on: PR #103
LSP required: Yes

{
  name: "find_references",
  description: "Find all references (usages) of a symbol across the codebase. Requires the LSP daemon. More precise than text search -- only finds actual code references, not string matches in comments or docs.",
  inputSchema: {
    type: "object",
    properties: {
      file: { type: "string", description: "File containing the symbol definition" },
      line: { type: "number", description: "Line number of the symbol" },
      symbol: { type: "string", description: "Symbol name (alternative to file+line)" },
      includeDeclaration: { type: "boolean", description: "Include the declaration itself.", default: false },
      maxResults: { type: "number", default: 30 }
    },
    required: []
  }
}

Example response:

References to `SearchResult` (src/models.rs:35):

1. src/search/search_runner.rs:225 -- return type of perform_probe()
2. src/search/search_runner.rs:1621 -- return type of search_with_structured_patterns()
3. src/search/result_ranking.rs:12 -- parameter of rank_search_results()
4. src/search/block_merging.rs:8 -- parameter/return of merge_ranked_blocks()
5. src/search/search_output.rs:54 -- parameter of format_and_print_search_results()
6. src/extract/processor.rs:27 -- return type of process_file_for_extraction()
7. src/search/file_processing.rs:42 -- return type of process_file_with_results()
8. tests/cli_tests.rs:120 -- test assertion

[8 of 23 references shown]

Tool 5: find_implementations -- What Implements This Trait/Interface?

Depends on: PR #103
LSP required: Yes

{
  name: "find_implementations",
  description: "Find all implementations of a trait, interface, or abstract class. Requires the LSP daemon. Works with Rust traits, TypeScript/Java interfaces, Go interfaces, Python abstract classes.",
  inputSchema: {
    type: "object",
    properties: {
      file: { type: "string", description: "File containing the trait/interface definition" },
      line: { type: "number", description: "Line number of the trait/interface" },
      symbol: { type: "string", description: "Symbol name (alternative to file+line)" },
      maxResults: { type: "number", default: 20 }
    },
    required: []
  }
}

Example response:

Implementations of trait `LanguageImpl` (src/language/language_trait.rs:8):

1. src/language/rust.rs:12       -- pub struct RustLanguage
2. src/language/typescript.rs:10 -- pub struct TypeScriptLanguage
3. src/language/javascript.rs:8  -- pub struct JavaScriptLanguage
4. src/language/python.rs:7      -- pub struct PythonLanguage
5. src/language/go.rs:10         -- pub struct GoLanguage
6. src/language/java.rs:8        -- pub struct JavaLanguage
7. src/language/c.rs:7           -- pub struct CLanguage
8. src/language/cpp.rs:7         -- pub struct CppLanguage

[8 implementations found]

Tool 6: impact_analysis -- What Would Break If I Change This?

Depends on: Tools 2-5 above
LSP required: Yes
This is the highest-value compound tool.

{
  name: "impact_analysis",
  description: "Analyze the impact of changing a function, type, or method. Shows all code that directly depends on this symbol: callers, type references, trait implementations, and test coverage. Use this before refactoring to understand the blast radius.",
  inputSchema: {
    type: "object",
    properties: {
      file: { type: "string", description: "File containing the symbol" },
      line: { type: "number", description: "Line number of the symbol" },
      symbol: { type: "string", description: "Symbol name (alternative to file+line)" },
      depth: { type: "number", description: "Depth of transitive analysis. 1 = direct dependents (default).", default: 1 },
      maxTokens: { type: "number", description: "Token budget for the response.", default: 4000 }
    },
    required: []
  }
}

Implementation: Compound tool that internally:

  1. Calls find_callers (depth N) for call hierarchy
  2. Calls find_references for all usages
  3. Calls find_implementations if the symbol is a trait/interface
  4. Categorizes results: Direct callers, Type references, Test coverage, Downstream dependents
  5. Formats as a structured report within token budget

Example response:

Impact Analysis: struct `SearchResult` (src/models.rs:35)

## Direct Usage (18 locations in 12 files)

### Core Pipeline (would break search):
  src/search/search_runner.rs:225 -- perform_probe() return type
  src/search/search_runner.rs:1621 -- search_with_structured_patterns() return type
  src/search/result_ranking.rs:12 -- rank_search_results() parameter
  src/search/block_merging.rs:8 -- merge_ranked_blocks() parameter + return
  src/search/search_output.rs:54 -- format_and_print_search_results() parameter

### Extract Pipeline (would break extract):
  src/extract/processor.rs:27 -- process_file_for_extraction() return type
  src/extract/processor.rs:884 -- extract_all_symbols_from_file() return type

### Serialization (would break JSON/XML output):
  src/search/search_output.rs:120 -- JSON serialization
  src/search/search_output.rs:180 -- XML serialization

## Test Coverage
  tests/cli_tests.rs:120 -- test_basic_search
  tests/cli_tests.rs:245 -- test_search_ranking
  tests/integration_test.rs:55 -- test_end_to_end

## Summary
  Files affected: 12
  Functions affected: 18
  Tests covering this symbol: 3
  Risk: HIGH (core data structure used across search + extract pipelines)

Implementation Plan

Phase 1: map_code (No LSP dependency)

  1. Implement probe map command in Rust (see Feature: probe map -- Repository Structure Overview Command #501)
  2. Add map_code tool to MCP server in npm/src/mcp/index.ts
  3. Wire up: MCP tool calls probe map via child process
  4. Add to ProbeAgent system prompt as recommended first step

Files to modify:

  • npm/src/mcp/index.ts -- add map_code tool registration + handler

Estimated scope: ~60 lines TypeScript (after #501 is done)

Phase 2: LSP-Backed Tools (Depends on PR #103)

After PR #103 lands and the LSP daemon + IPC client are available:

  1. Add a new Rust CLI subcommand: probe lsp query <type> <file> <line> that:

    • Connects to the LSP daemon via IPC
    • Sends the appropriate DaemonRequest
    • Formats the response as outline/json
    • Types: callers, callees, references, implementations
  2. Add 4 MCP tools that call probe lsp query ...

  3. Add impact_analysis as a compound MCP tool

Files to modify:

  • src/cli.rs -- add Lsp command with Query subcommand
  • src/lsp_integration/query.rs -- new: CLI query handler
  • src/lsp_integration/output.rs -- new: format LSP results
  • npm/src/mcp/index.ts -- add 5 new tool registrations + handlers

Estimated scope: ~500 lines Rust, ~200 lines TypeScript

Phase 3: Agent Prompt Updates

Update ProbeAgent system prompt in npm/src/agent/ProbeAgent.js (~line 2420):

## Available Tools

### Exploration
- map_code: Get repo structure overview. Use FIRST on unfamiliar codebases.
- search_code: Keyword search with ElasticSearch syntax.
- extract_code: Read specific files or symbols.

### Code Intelligence (requires LSP daemon)
- find_callers: Who calls this function?
- find_callees: What does this function call?
- find_references: Where is this symbol used?
- find_implementations: What implements this trait/interface?
- impact_analysis: What would break if I change this?

### Workflow
When exploring a codebase:
1. map_code to understand structure
2. search_code to find relevant code
3. extract_code to read specific symbols
4. find_callers/find_callees to understand relationships
5. impact_analysis before making changes

Phase 4: Graceful Degradation

All LSP-backed tools must handle the daemon not running:

try {
  const result = await executeProbe(['lsp', 'query', type, file, line]);
  return { content: [{ type: 'text', text: result }] };
} catch (error) {
  if (error.message.includes('daemon not running')) {
    return {
      content: [{
        type: 'text',
        text: `LSP daemon is not running. To enable code intelligence:\n` +
              `  probe lsp start\n\n` +
              `Falling back to text-based search...\n\n` +
              await fallbackToSearch(symbol)
      }]
    };
  }
  throw error;
}

The fallback uses probe search with the symbol name as a best-effort alternative.

Tool Priority Ranking

Priority Tool Value Effort LSP Required
1 map_code Very High -- every agent session starts with orientation Medium No
2 find_callers High -- most common structural question Low Yes
3 find_references High -- essential for refactoring Low Yes
4 find_callees Medium -- useful for understanding code flow Low Yes
5 find_implementations Medium -- essential for polymorphic code Low Yes
6 impact_analysis Very High -- but depends on 2-5 Medium Yes

Success Criteria

  1. MCP server exposes 9 tools (3 existing + 6 new) instead of 3
  2. map_code works instantly without LSP daemon (same zero-setup as probe search)
  3. LSP tools gracefully degrade to text search when daemon is not running
  4. All tools respect token budgets
  5. Agent prompt guides LLM to use tools in the right order (map → search → extract → analyze)
  6. Response times: map_code <1s, LSP tools <2s, impact_analysis <5s
  7. All tools have JSON output format option for programmatic consumption

Competitive Context

This feature set was identified by comparing probe's MCP surface with:

  • Stakgraph -- 8 MCP tools including get_edges, shortest_path, explore
  • grepai -- 10 MCP tools including trace_callers, trace_callees, trace_graph
  • ABCoder -- 6 MCP tools with hierarchical drill-down and dependency data
  • Octocode -- MCP bridges to external LSP for hover, go-to-def, find-refs

Probe's advantage: tools 2-6 leverage the LSP daemon investment from PR #103 with minimal additional code, and map_code requires zero setup.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions