Skip to content

feat(mcp/tools): add shell_command, document_analyzer, semantic_search, and ask_agent tools to MCP registry#401

Open
vitali87 wants to merge 1 commit intomainfrom
pr-split/add-cmd-question/pr-4
Open

feat(mcp/tools): add shell_command, document_analyzer, semantic_search, and ask_agent tools to MCP registry#401
vitali87 wants to merge 1 commit intomainfrom
pr-split/add-cmd-question/pr-4

Conversation

@vitali87
Copy link
Owner

@vitali87 vitali87 commented Mar 1, 2026

Extend MCPToolsRegistry with ShellCommander, DocumentAnalyzer, semantic search, and get-function-source tools. Add a lazy-initialized ask_agent method that routes a question through the RAG orchestrator and returns structured output. Uses stderr Console to avoid corrupting JSONRPC on stdout.

Dependency graph

Merge in this order:

pr-1: chore: update .gitignore with popular vibe-coding agent directories
pr-2: fix(llm): add retries to RAG orchestrator to handle output validation issues
├── pr-4: feat(mcp/tools): add shell_command, document_analyzer, semantic_search, and ask_agent tools to MCP registry  <-- this PR
│   └── pr-5: feat(mcp/client): add MCP client CLI for querying the code graph via MCP server
│       └── pr-7: docs: update README with non-interactive mode, new MCP tools, and agent tool list
└── pr-6: feat(main): add non-interactive --ask-agent mode to the CLI
    └── pr-7: docs: update README with non-interactive mode, new MCP tools, and agent tool list
pr-3: feat(mcp/server): improve MCP server logging and suppress output during tool execution
└── pr-4: feat(mcp/tools): add shell_command, document_analyzer, semantic_search, and ask_agent tools to MCP registry  <-- this PR
    └── pr-5: feat(mcp/client): add MCP client CLI for querying the code graph via MCP server
        └── pr-7: docs: update README with non-interactive mode, new MCP tools, and agent tool list

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request enhances the MCP (Multi-Codebase Project) tool registry by integrating several new tools for advanced code interaction and analysis. It introduces functionalities for executing shell commands, analyzing documents, performing semantic searches, and retrieving function source code. A key addition is the ask_agent tool, which leverages a Retrieval-Augmented Generation (RAG) orchestrator to answer codebase-related questions, providing a powerful new way to interact with the code graph. These changes aim to make the MCP system more versatile and intelligent for developers.

Highlights

  • New Tool Additions: Added ShellCommander, DocumentAnalyzer, semantic_search, and get_function_source tools to the MCPToolsRegistry, significantly expanding the system's capabilities.
  • Ask Agent Tool: Introduced an ask_agent tool that allows users to query the codebase via a RAG orchestrator, providing structured answers to complex questions about the codebase.
  • Lazy RAG Agent Initialization: Implemented lazy initialization for the RAG orchestrator agent, optimizing resource usage and improving testability by deferring LLM initialization until first access.
  • Output Redirection for JSONRPC Compatibility: Configured console output for the _query_tool and during ask_agent execution to use stderr, preventing interference with JSONRPC communication on stdout.
Changelog
  • codebase_rag/mcp/tools.py
    • Added new tool imports and initializations for ShellCommander, DocumentAnalyzer, semantic search, and get-function-source.
    • Registered the new tools and the 'ask_agent' tool within the MCP registry.
    • Implemented lazy initialization for the RAG agent to manage resource loading.
    • Added the 'ask_agent' method, including logic for suppressing logging during agent execution to maintain clean JSONRPC output.
Activity
  • No human activity (comments, reviews, etc.) has been recorded for this pull request yet.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 1, 2026

Greptile Summary

This PR extends MCPToolsRegistry with four new tools (shell_command, document_analyzer, semantic_search, get_function_source) and adds a lazy-initialized ask_agent method that routes a question through the RAG orchestrator, routing console output to stderr to avoid corrupting the JSONRPC stream on stdout.

Key concerns:

  • Concurrency bug in ask_agent: logger.disable("codebase_rag"), redirect_stdout, and redirect_stderr all mutate global process state. In an async server handling concurrent requests, one coroutine's suppression directly affects all other in-flight coroutines — output from unrelated requests can be silently dropped, and the logger state can be left in an inconsistent state under a race condition.
  • Silent exception swallowing: The outer except Exception: in ask_agent discards the exception without any logging, even though logging has already been re-enabled by the finally block at that point. Runtime errors will return a generic message with no trace in logs.
  • Any type on _rag_agent: self._rag_agent: Any = None and the rag_agent property should use Agent | None from pydantic_ai, which is what create_rag_orchestrator already returns.
  • Inline comments: Eight inline comments introduced in this PR do not satisfy the project's comment policy (must be top-of-file, contain (H), or be type annotations).
  • Module-level imports deferred to function body: import io and from contextlib import ... are placed inside ask_agent rather than at the module level.

Confidence Score: 2/5

  • Not safe to merge — the ask_agent implementation has a concurrency bug that can corrupt shared global state and silently discard errors in production.
  • The new tool registrations themselves (shell_command, document_analyzer, semantic_search, get_function_source) are straightforward wiring and look correct. However, the ask_agent method contains a global-state mutation pattern that is fundamentally unsafe in an async server: logger.disable and stdout/stderr redirection affect all concurrently running coroutines, not just the current one. Additionally, exceptions are swallowed silently, which will make any production failure extremely hard to diagnose.
  • codebase_rag/mcp/tools.py — specifically the ask_agent method (lines 517–552) and the rag_agent property/setter (lines 266–292).

Important Files Changed

Filename Overview
codebase_rag/mcp/tools.py Extends MCPToolsRegistry with ShellCommander, DocumentAnalyzer, semantic search, and get-function-source tools, and adds a lazy-initialized ask_agent method. Contains a critical concurrency bug: logger.disable/enable and stdout/stderr redirects in ask_agent are global operations unsafe for concurrent async use. All exceptions in ask_agent are silently swallowed after logging is re-enabled, making errors undebuggable. Also introduces several Any-typed fields and inline comments that violate project coding standards.

Sequence Diagram

sequenceDiagram
    participant Client as MCP Client
    participant Registry as MCPToolsRegistry
    participant Agent as RAG Agent (lazy)
    participant Tools as Registered Tools

    Client->>Registry: ask_agent(question)
    Registry->>Registry: redirect stdout/stderr (global!)
    Registry->>Registry: logger.disable("codebase_rag") (global!)
    Registry->>Agent: rag_agent.run(question, message_history=[])
    Agent->>Tools: query_code_graph / get_code_snippet / shell_command / etc.
    Tools-->>Agent: results
    Agent-->>Registry: response.output
    Registry->>Registry: logger.enable("codebase_rag") (finally)
    Registry-->>Client: {"output": response.output}

    note over Registry: If exception thrown, swallowed silently —<br/>no log, generic error message returned
Loading

Last reviewed commit: 665aad3

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 file reviewed, 5 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +537 to +546
with redirect_stdout(io.StringIO()), redirect_stderr(io.StringIO()):
# Temporarily disable loguru logging
logger.disable("codebase_rag")
try:
# Run the query using the RAG agent
response = await self.rag_agent.run(question, message_history=[])
return {"output": response.output}
finally:
# Re-enable logging
logger.enable("codebase_rag")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Global logger suppression is not async-safe

logger.disable("codebase_rag") modifies loguru's global state, which is shared across all coroutines. In a concurrent async server, if two ask_agent calls are in-flight simultaneously, coroutine A's logger.disable(...) will suppress logging for coroutine B mid-execution, and coroutine B's logger.enable(...) in its finally block will re-enable logging while coroutine A still expects it to be disabled.

The same applies to redirect_stdout(io.StringIO()) and redirect_stderr(io.StringIO()): these patch the global sys.stdout/sys.stderr, which are shared across all concurrently running coroutines. Output from unrelated coroutines running while these redirects are active will be silently dropped into the StringIO buffer.

A per-handler loguru sink approach or structlog's contextvars-based filtering would be safe for async; the global mutable approach used here is not.

Prompt To Fix With AI
This is a comment left during a code review.
Path: codebase_rag/mcp/tools.py
Line: 537-546

Comment:
**Global logger suppression is not async-safe**

`logger.disable("codebase_rag")` modifies loguru's global state, which is shared across all coroutines. In a concurrent async server, if two `ask_agent` calls are in-flight simultaneously, coroutine A's `logger.disable(...)` will suppress logging for coroutine B mid-execution, and coroutine B's `logger.enable(...)` in its `finally` block will re-enable logging while coroutine A still expects it to be disabled.

The same applies to `redirect_stdout(io.StringIO())` and `redirect_stderr(io.StringIO())`: these patch the global `sys.stdout`/`sys.stderr`, which are shared across all concurrently running coroutines. Output from unrelated coroutines running while these redirects are active will be silently dropped into the `StringIO` buffer.

A per-handler loguru sink approach or structlog's contextvars-based filtering would be safe for async; the global mutable approach used here is not.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +547 to +552
except Exception:
# Fail silently without logging or printing error details
return {
"output": "There was an error processing your question",
"error": True,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Errors swallowed silently after logging is re-enabled

By the time the outer except Exception: block is reached, the finally clause on line 544 has already re-enabled logging via logger.enable("codebase_rag"). The exception is therefore catchable and loggable, but the block discards it entirely — returning only a generic "There was an error processing your question" string with no trace of what went wrong.

This makes production debugging extremely difficult. The exception should at minimum be logged at logger.error(...) before returning the fallback response:

except Exception as e:
    logger.error(f"[MCP] ask_agent failed: {e}", exc_info=True)
    return {
        "output": "There was an error processing your question",
        "error": True,
    }
Prompt To Fix With AI
This is a comment left during a code review.
Path: codebase_rag/mcp/tools.py
Line: 547-552

Comment:
**Errors swallowed silently after logging is re-enabled**

By the time the outer `except Exception:` block is reached, the `finally` clause on line 544 has already re-enabled logging via `logger.enable("codebase_rag")`. The exception is therefore catchable and loggable, but the block discards it entirely — returning only a generic `"There was an error processing your question"` string with no trace of what went wrong.

This makes production debugging extremely difficult. The exception should at minimum be logged at `logger.error(...)` before returning the fallback response:

```python
except Exception as e:
    logger.error(f"[MCP] ask_agent failed: {e}", exc_info=True)
    return {
        "output": "There was an error processing your question",
        "error": True,
    }
```

How can I resolve this? If you propose a fix, please make it concise.

self._function_source_tool = create_get_function_source_tool()

# Create RAG orchestrator agent (lazy initialization for testing)
self._rag_agent: Any = None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Agent type instead of Any

self._rag_agent: Any = None loses all type information. create_rag_orchestrator returns Agent (from pydantic_ai), so the field and the property should be typed accordingly:

Suggested change
self._rag_agent: Any = None
self._rag_agent: Agent | None = None

The import from pydantic_ai import Agent should be added at the top of the file, and the rag_agent property signature updated to -> Agent: and the setter to value: Agent | None.

This also applies to the property and setter signatures on lines 267 and 290.

Context Used: Rule from dashboard - ## Technical Requirements

Agentic Framework

  • PydanticAI Only: This project uses PydanticAI... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: codebase_rag/mcp/tools.py
Line: 106

Comment:
**Use `Agent` type instead of `Any`**

`self._rag_agent: Any = None` loses all type information. `create_rag_orchestrator` returns `Agent` (from `pydantic_ai`), so the field and the property should be typed accordingly:

```suggestion
        self._rag_agent: Agent | None = None
```

The import `from pydantic_ai import Agent` should be added at the top of the file, and the `rag_agent` property signature updated to `-> Agent:` and the setter to `value: Agent | None`.

This also applies to the property and setter signatures on lines 267 and 290.

**Context Used:** Rule from `dashboard` - ## Technical Requirements

### Agentic Framework
- **PydanticAI Only**: This project uses PydanticAI... ([source](https://app.greptile.com/review/custom-context?memory=d4240b05-b763-467a-a6bf-94f73e8b6859))

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +531 to +532
import io
from contextlib import redirect_stderr, redirect_stdout
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move imports to module level

import io and from contextlib import redirect_stderr, redirect_stdout are placed inside the function body. Per Python convention (and PEP 8), imports belong at the top of the module. Move both to the top-level import block alongside the other stdlib imports.

Suggested change
import io
from contextlib import redirect_stderr, redirect_stdout
# Suppress all logging output during agent execution
try:

The two import lines can be removed from here once they are moved to the module-level import section.

Context Used: Rule from dashboard - ## Technical Requirements

Agentic Framework

  • PydanticAI Only: This project uses PydanticAI... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: codebase_rag/mcp/tools.py
Line: 531-532

Comment:
**Move imports to module level**

`import io` and `from contextlib import redirect_stderr, redirect_stdout` are placed inside the function body. Per Python convention (and PEP 8), imports belong at the top of the module. Move both to the top-level import block alongside the other stdlib imports.

```suggestion
        # Suppress all logging output during agent execution
        try:
```

The two import lines can be removed from here once they are moved to the module-level import section.

**Context Used:** Rule from `dashboard` - ## Technical Requirements

### Agentic Framework
- **PydanticAI Only**: This project uses PydanticAI... ([source](https://app.greptile.com/review/custom-context?memory=d4240b05-b763-467a-a6bf-94f73e8b6859))

How can I resolve this? If you propose a fix, please make it concise.

self.document_analyzer = DocumentAnalyzer(project_root=project_root)

# Create pydantic-ai tools - we'll call the underlying functions directly
# Use a Console that outputs to stderr to avoid corrupting JSONRPC on stdout
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline comments violate the comment policy

The project's comment policy only permits inline comments that are: (1) top-of-file, (2) prefixed with (H), or (3) type annotations. None of the newly added inline comments qualify.

This applies to every inline comment introduced in this PR:

  • codebase_rag/mcp/tools.py:84# Use a Console that outputs to stderr to avoid corrupting JSONRPC on stdout
  • codebase_rag/mcp/tools.py:105# Create RAG orchestrator agent (lazy initialization for testing)
  • codebase_rag/mcp/tools.py:534# Suppress all logging output during agent execution
  • codebase_rag/mcp/tools.py:536# Temporarily redirect stdout and stderr to suppress all output
  • codebase_rag/mcp/tools.py:538# Temporarily disable loguru logging
  • codebase_rag/mcp/tools.py:541# Run the query using the RAG agent
  • codebase_rag/mcp/tools.py:544# Re-enable logging
  • codebase_rag/mcp/tools.py:547# Fail silently without logging or printing error details

Remove or prefix all of these with (H) if the intent is genuinely human-authored explanation.

Context Used: Rule from dashboard - ## Technical Requirements

Agentic Framework

  • PydanticAI Only: This project uses PydanticAI... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: codebase_rag/mcp/tools.py
Line: 84

Comment:
**Inline comments violate the comment policy**

The project's comment policy only permits inline comments that are: (1) top-of-file, (2) prefixed with `(H)`, or (3) type annotations. None of the newly added inline comments qualify.

This applies to every inline comment introduced in this PR:
- `codebase_rag/mcp/tools.py:84``# Use a Console that outputs to stderr to avoid corrupting JSONRPC on stdout`
- `codebase_rag/mcp/tools.py:105``# Create RAG orchestrator agent (lazy initialization for testing)`
- `codebase_rag/mcp/tools.py:534``# Suppress all logging output during agent execution`
- `codebase_rag/mcp/tools.py:536``# Temporarily redirect stdout and stderr to suppress all output`
- `codebase_rag/mcp/tools.py:538``# Temporarily disable loguru logging`
- `codebase_rag/mcp/tools.py:541``# Run the query using the RAG agent`
- `codebase_rag/mcp/tools.py:544``# Re-enable logging`
- `codebase_rag/mcp/tools.py:547``# Fail silently without logging or printing error details`

Remove or prefix all of these with `(H)` if the intent is genuinely human-authored explanation.

**Context Used:** Rule from `dashboard` - ## Technical Requirements

### Agentic Framework
- **PydanticAI Only**: This project uses PydanticAI... ([source](https://app.greptile.com/review/custom-context?memory=d4240b05-b763-467a-a6bf-94f73e8b6859))

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request extends the MCP tool registry by adding several new tools, including shell_command, document_analyzer, semantic_search, and a new ask_agent tool. However, the current implementation introduces significant security flaws. The ask_agent tool allows a nested LLM agent to execute state-changing shell commands and file modifications without user confirmation, leading to potential privilege escalation. Furthermore, the DocumentAnalyzer and ShellCommander tools are vulnerable to path traversal, allowing arbitrary file access on the host system via absolute paths. Beyond these security concerns, it is recommended to improve error handling in the new ask_agent method by adding server-side logging for exceptions and documenting the rationale for broad exception handling, which is currently missing and would make debugging difficult.

Comment on lines +274 to +285
tools=[
self._query_tool,
self._code_tool,
self._file_reader_tool,
self._file_writer_tool,
self._file_editor_tool,
self._shell_command_tool,
self._directory_lister_tool,
self._document_analyzer_tool,
self._semantic_search_tool,
self._function_source_tool,
]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The ask_agent tool uses a nested RAG orchestrator agent that is configured with powerful, state-changing tools like execute_shell_command, write_file, and replace_code_surgically. While ShellCommander has a confirmation mechanism, ask_agent is non-interactive and suppresses logging. A malicious user can craft a question that tricks the nested agent into executing these tools by autonomously setting user_confirmed=True, effectively bypassing intended security controls without the actual user's oversight. It is recommended to restrict the rag_agent to read-only tools for non-interactive execution.

                tools=[
                    self._query_tool,
                    self._code_tool,
                    self._file_reader_tool,
                    self._directory_lister_tool,
                    self._document_analyzer_tool,
                    self._semantic_search_tool,
                    self._function_source_tool,
                ]
References
  1. For security-sensitive features like shell command execution by an LLM agent, prioritize fail-safe behavior. Prefer false positives (blocking a safe command) over false negatives (allowing a dangerous one), especially if the fix for the false positive adds complexity.

self.file_writer = FileWriter(project_root=project_root)
self.directory_lister = DirectoryLister(project_root=project_root)
self.shell_commander = ShellCommander(project_root=project_root)
self.document_analyzer = DocumentAnalyzer(project_root=project_root)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The DocumentAnalyzer tool (initialized here) contains a path traversal vulnerability in its analyze method. It explicitly handles absolute paths by copying the target file to a .tmp directory without verifying if the path is within the project root. This allows an attacker to read any file on the system that the process has access to by providing an absolute path.

self.file_reader = FileReader(project_root=project_root)
self.file_writer = FileWriter(project_root=project_root)
self.directory_lister = DirectoryLister(project_root=project_root)
self.shell_commander = ShellCommander(project_root=project_root)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The ShellCommander tool (initialized here) allows for arbitrary file access. While it checks if the command name is in an allowlist (e.g., cat, ls), it does not validate or sanitize the arguments. An attacker can provide absolute paths as arguments to read or list files outside the project root (e.g., execute_shell_command(command="cat /etc/passwd")).

Comment on lines +547 to +552
except Exception:
# Fail silently without logging or printing error details
return {
"output": "There was an error processing your question",
"error": True,
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

While it's important to return a generic error to the client to avoid leaking implementation details or corrupting the JSON-RPC stream, swallowing the exception without logging it on the server side makes debugging failures nearly impossible. The try...finally block for logger.enable ensures that logging is re-enabled before this except block is reached. Therefore, the exception should be logged here to aid maintainability and troubleshooting. Additionally, if a broad Exception is caught due to unpredictable errors from external libraries, a comment explaining this rationale should be added for maintainability, as per repository guidelines.

        except Exception as e:
            # Log the error for debugging, but return a generic message to the client
            # Catching a broad Exception here is pragmatic due to the wide and unpredictable
            # variety of exceptions that can be raised by external libraries.
            logger.error(f"[MCP] Error in ask_agent: {e}", exc_info=True)
            return {
                "output": "There was an error processing your question",
                "error": True,
            }
References
  1. When handling errors from external libraries that may raise multiple types of exceptions for configuration issues (e.g., ValueError for invalid formats and AssertionError for missing keys), catch a tuple of these specific exceptions, such as (ValueError, AssertionError), instead of a generic Exception.
  2. When interacting with external libraries (like pydantic-ai) that can raise non-standard exceptions (e.g., AssertionError for configuration issues), it may be necessary to catch a broader Exception or a tuple of specific exceptions (ValueError, AssertionError) to ensure all user-facing errors are handled gracefully.
  3. If catching a broad Exception is a pragmatic choice due to a wide and unpredictable variety of exceptions from external libraries, add a comment explaining the rationale to improve code maintainability.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants