Skip to content

feat: add MCP tool orchestration and server config support#2348

Open
universam1 wants to merge 1 commit intoThe-PR-Agent:mainfrom
o11n:mcpsupport
Open

feat: add MCP tool orchestration and server config support#2348
universam1 wants to merge 1 commit intoThe-PR-Agent:mainfrom
o11n:mcpsupport

Conversation

@universam1
Copy link
Copy Markdown

Introduce server-side MCP config loading from /etc/pr-agent/mcp.json or MCP_CONFIG_PATH, including JSONC parsing and VS Code / Claude schema normalization. Add the MCP runtime, HTTP and stdio clients, structured tool-calling orchestration on the base AI handler, and wire /ask, /review, and /improve through the MCP-aware integration helper. Expose MCP runtime status in /config output, document the configuration flow and AWS Knowledge example, and add focused tests for config loading, runtime behavior, tool orchestration, integration, and discovery.

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Add MCP tool orchestration and server configuration support

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add MCP (Model Context Protocol) runtime with stdio and HTTP client support
• Implement structured tool-calling orchestration in base AI handler
• Load MCP server configuration from JSON/JSONC files with schema normalization
• Integrate MCP tools into /ask, /review, and /improve commands
• Expose MCP runtime status in /config output with comprehensive tests
Diagram
flowchart LR
  ConfigLoader["Config Loader<br/>JSONC Parsing"] -->|loads| MCPConfig["MCP Server Config<br/>servers/mcpServers"]
  MCPConfig -->|initializes| MCPRuntime["MCP Runtime<br/>stdio/HTTP clients"]
  MCPRuntime -->|discovers| ToolCatalog["Tool Catalog<br/>schemas"]
  ToolCatalog -->|passed to| ToolOrch["Tool Orchestration<br/>chat_completion_with_tools"]
  ToolOrch -->|executes| Commands["Commands<br/>ask/review/improve"]
  Commands -->|status in| ConfigOutput["/config output"]
Loading

Grey Divider

File Changes

1. pr_agent/algo/ai_handlers/base_ai_handler.py ✨ Enhancement +111/-0

Add structured tool-calling orchestration method

pr_agent/algo/ai_handlers/base_ai_handler.py


2. pr_agent/config_loader.py ✨ Enhancement +135/-1

Add MCP config loading with JSONC parsing

pr_agent/config_loader.py


3. pr_agent/mcp/__init__.py ✨ Enhancement +15/-0

Create MCP module with public exports

pr_agent/mcp/init.py


View more (14)
4. pr_agent/mcp/runtime.py ✨ Enhancement +499/-0

Implement MCP runtime with client support

pr_agent/mcp/runtime.py


5. pr_agent/mcp/integration.py ✨ Enhancement +62/-0

Add MCP integration helper for commands

pr_agent/mcp/integration.py


6. pr_agent/tools/pr_code_suggestions.py ✨ Enhancement +9/-2

Wire improve command through MCP integration

pr_agent/tools/pr_code_suggestions.py


7. pr_agent/tools/pr_questions.py ✨ Enhancement +11/-8

Wire ask command through MCP integration

pr_agent/tools/pr_questions.py


8. pr_agent/tools/pr_reviewer.py ✨ Enhancement +5/-2

Wire review command through MCP integration

pr_agent/tools/pr_reviewer.py


9. pr_agent/tools/pr_config.py ✨ Enhancement +17/-1

Add MCP runtime status to config output

pr_agent/tools/pr_config.py


10. pr_agent/settings/configuration.toml ⚙️ Configuration changes +8/-0

Add MCP configuration settings section

pr_agent/settings/configuration.toml


11. tests/unittest/test_ai_handler_tool_orchestration.py 🧪 Tests +84/-0

Test tool orchestration loop and fallback

tests/unittest/test_ai_handler_tool_orchestration.py


12. tests/unittest/test_config_loader_mcp.py 🧪 Tests +128/-0

Test MCP config loading and schema normalization

tests/unittest/test_config_loader_mcp.py


13. tests/unittest/test_mcp_integration_helper.py 🧪 Tests +119/-0

Test MCP integration with command handlers

tests/unittest/test_mcp_integration_helper.py


14. tests/unittest/test_mcp_runtime.py 🧪 Tests +127/-0

Test MCP runtime client management

tests/unittest/test_mcp_runtime.py


15. tests/unittest/test_mcp_tool_discovery.py 🧪 Tests +38/-0

Test tool schema building and budgeting

tests/unittest/test_mcp_tool_discovery.py


16. docs/docs/usage-guide/additional_configurations.md 📝 Documentation +21/-0

Document MCP configuration and AWS example

docs/docs/usage-guide/additional_configurations.md


17. docs/docs/usage-guide/automations_and_usage.md 📝 Documentation +2/-0

Add MCP setup instructions to usage guide

docs/docs/usage-guide/automations_and_usage.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 23, 2026

Code Review by Qodo

🐞 Bugs (9) 📘 Rule violations (13) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Unused pytest import 📘 Rule violation ⚙ Maintainability ⭐ New
Description
tests/unittest/test_ai_handler_tool_orchestration.py imports pytest but never uses it, which
will be flagged by repository linting and may fail CI. This violates the requirement to keep
modified files compliant with lint/format tooling.
Code

tests/unittest/test_ai_handler_tool_orchestration.py[R1-4]

+import pytest
+
+from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler
+
Evidence
PR Compliance ID 22 requires avoiding lint violations such as unused imports; the added test file
imports pytest but does not reference it anywhere in the file.

tests/unittest/test_ai_handler_tool_orchestration.py[1-4]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new unit test file imports `pytest` but does not use it, which will trigger unused-import lint errors.

## Issue Context
`pytest` is not referenced in this file (no `pytest.mark`, `pytest.raises`, etc.).

## Fix Focus Areas
- tests/unittest/test_ai_handler_tool_orchestration.py[1-4]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Tool prompt invalid JSON 🐞 Bug ≡ Correctness ⭐ New
Description
BaseAiHandler.chat_completion_with_tools tells the model to output JSON “exactly in this shape”, but
the tool-call example is mutated to include {...}, which is not valid JSON and can lead to
unparsable tool calls so the tool loop silently falls back to plain text responses.
Code

pr_agent/algo/ai_handlers/base_ai_handler.py[R64-71]

+        tool_call_example = json.dumps(
+            {
+                "type": "tool_call",
+                "tool": "server.tool",
+                "arguments": {"example": "value"},
+            },
+            separators=(",", ":"),
+        ).replace("{\"example\":\"value\"}", "{...}")
Evidence
The system prompt example is made invalid by replacing the arguments object with {...}. The parser
only accepts json.loads()-parseable dicts with type tool_call/final, so if the model follows the
example literally the orchestration won’t recognize the tool request and will return raw model
output.

/pr_agent/algo/ai_handlers/base_ai_handler.py[63-88]
/pr_agent/algo/ai_handlers/base_ai_handler.py[179-196]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`chat_completion_with_tools()` instructs the model to output JSON exactly matching an example, but the example is not valid JSON because it includes `{...}`. This can cause the model to emit invalid JSON tool calls that `_parse_tool_or_final_response()` cannot parse, disabling tool orchestration.

### Issue Context
The prompt text says “ONLY a JSON object exactly in this shape”, so the example must itself be valid JSON.

### Fix Focus Areas
- pr_agent/algo/ai_handlers/base_ai_handler.py[63-88]
- pr_agent/algo/ai_handlers/base_ai_handler.py[179-196]

### What to change
- Remove the `.replace(..., "{...}")` so the example remains valid JSON (e.g., keep `{"arguments":{"example":"value"}}`).
- If you want to communicate “arbitrary arguments”, adjust the instruction text (in plain English) rather than embedding invalid JSON.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Blocking tool discovery I/O 🐞 Bug ➹ Performance ⭐ New
Description
maybe_chat_completion_with_mcp calls runtime.build_tool_schemas() directly in an async path, but
build_tool_schemas() performs synchronous tool discovery (connect + list_tools) using
requests/subprocess, which can block the event loop and delay webhook/background task processing.
Code

pr_agent/mcp/integration.py[R35-43]

+        max_tools = _get_tool_budget("MCP.MAX_TOOL_CATALOG_TOOLS", 12)
+        max_schema_chars = _get_tool_budget("MCP.MAX_TOOL_CATALOG_SCHEMA_CHARS", 12000)
+
+        tools = runtime.build_tool_schemas(
+            max_tools=max_tools,
+            max_schema_chars=max_schema_chars,
+            include_server_prefix=True,
+        )
+        if not tools:
Evidence
In the async integration helper, tool schemas are built synchronously.
MCPRuntime.build_tool_schemas() calls list_all_tools(), which calls list_server_tools(), which
connects and calls client.list_tools(). For HTTP servers, list_tools ultimately calls
requests.Session.post synchronously. None of this is offloaded to a thread pool during discovery, so
it can block the asyncio loop.

/pr_agent/mcp/integration.py[15-73]
/pr_agent/mcp/runtime.py[672-712]
/pr_agent/mcp/runtime.py[342-409]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Tool discovery (connect + tools/list) is executed synchronously inside `maybe_chat_completion_with_mcp()`’s async flow. Because discovery uses `requests` and (for stdio) `subprocess`, it can block the event loop and stall handling of other requests/background tasks.

### Issue Context
Tool execution is already offloaded via `create_tool_executor()` using `run_in_executor`, but discovery is not.

### Fix Focus Areas
- pr_agent/mcp/integration.py[15-75]
- pr_agent/mcp/runtime.py[672-712]
- pr_agent/mcp/runtime.py[342-409]

### What to change
Choose one:
1) In `maybe_chat_completion_with_mcp`, wrap `runtime.build_tool_schemas(...)` in `await loop.run_in_executor(...)`.
2) Add an async discovery method on `MCPRuntime` (e.g., `async_build_tool_schemas`) that runs `list_all_tools()` / `build_tool_schemas()` in an executor.

Ensure both connect/list_tools and schema-building are included in the offloaded work so no requests/subprocess calls happen on the event loop thread.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (19)
4. Unhandled MCP config types 🐞 Bug ☼ Reliability
Description
MCP client initialization can raise uncaught ValueError/TypeError for malformed config (e.g.,
non-numeric timeout or non-string args elements), which are not wrapped as MCPRuntimeError and
are not caught in tool discovery/orchestration. This can crash MCP tool discovery
(build_tool_schemas) and therefore break /ask, /review, and /improve when MCP is enabled.
Code

pr_agent/mcp/runtime.py[R83-112]

+    def __init__(self, server_name: str, config: dict[str, Any]):
+        super().__init__(server_name, config)
+        self.process: Optional[subprocess.Popen] = None
+        self._request_id = 0
+        self.timeout = float(config.get("timeout", 30))
+
+    def connect(self):
+        command = self.config.get("command")
+        if not command:
+            raise MCPRuntimeError(f"Stdio MCP server '{self.server_name}' is missing 'command'")
+
+        args = self.config.get("args") or []
+        if not isinstance(args, list):
+            raise MCPRuntimeError(f"Stdio MCP server '{self.server_name}' args must be a list")
+
+        env = os.environ.copy()
+        server_env = self.config.get("env") or {}
+        if not isinstance(server_env, dict):
+            raise MCPRuntimeError(f"Stdio MCP server '{self.server_name}' env must be an object")
+        env.update({str(k): str(v) for k, v in server_env.items()})
+
+        cwd = self.config.get("cwd")
+        self.process = subprocess.Popen(
+            [command, *args],
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.DEVNULL,
+            env=env,
+            cwd=cwd,
+        )
Evidence
MCPStdioClient casts timeout with float(...) and passes args directly into
subprocess.Popen([command, *args]) after only checking that args is a list (not that its members
are valid). These exceptions are not converted to MCPRuntimeError. During discovery,
list_all_tools only catches (MCPRuntimeError, OSError), and maybe_chat_completion_with_mcp
doesn’t catch exceptions around build_tool_schemas, so type errors can bubble up and fail the
command.

pr_agent/mcp/runtime.py[82-112]
pr_agent/mcp/runtime.py[645-655]
pr_agent/mcp/integration.py[24-43]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Malformed MCP server config values can raise low-level exceptions (`ValueError`/`TypeError`) during client construction/connection (e.g., `float(timeout)` or invalid `args` element types). These are not consistently wrapped as `MCPRuntimeError` and can escape tool discovery/orchestration, breaking MCP-enabled commands.
### Issue Context
- `MCPStdioClient.__init__` uses `float(config.get("timeout", 30))`.
- `MCPStdioClient.connect` passes `[command, *args]` to `subprocess.Popen` after only validating `args` is a list.
- `list_all_tools` only catches `MCPRuntimeError` and `OSError`.
- `maybe_chat_completion_with_mcp` does not catch exceptions around `runtime.build_tool_schemas(...)`.
### Fix Focus Areas
- pr_agent/mcp/runtime.py[82-112]
- pr_agent/mcp/runtime.py[288-301]
- pr_agent/mcp/runtime.py[645-655]
- pr_agent/mcp/integration.py[24-43]
### Implementation notes
- Wrap timeout parsing in `try/except (TypeError, ValueError)` and raise `MCPRuntimeError` with server context.
- Validate/coerce `args` list items to `str` (or `os.PathLike`) before calling `Popen`; raise `MCPRuntimeError` on invalid types.
- Ensure discovery paths only need to catch `MCPRuntimeError` (i.e., normalize client-side errors into MCPRuntimeError), or broaden the catch in `list_all_tools` to prevent crashes while logging the failure.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Missing requests dependency 🐞 Bug ☼ Reliability
Description
pr_agent/mcp/runtime.py imports and uses the third-party requests library, but
requirements.txt does not declare it. In deployments that install only declared dependencies,
importing MCP integration will raise ImportError, breaking /ask, /review, and /improve even
if MCP is disabled at runtime.
Code

pr_agent/mcp/runtime.py[R1-12]

+import json
+import os
+import subprocess
+import threading
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
+from typing import Any, Optional
+
+import requests
+
+from pr_agent.config_loader import get_settings
+
Evidence
The MCP runtime unconditionally imports requests, MCP integration imports the runtime, and core
tools import the MCP integration helper; meanwhile, the repo dependency list does not include
requests, so a clean install can fail at import time.

pr_agent/mcp/runtime.py[1-12]
pr_agent/mcp/integration.py[1-6]
pr_agent/tools/pr_reviewer.py[19-27]
requirements.txt[1-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pr_agent/mcp/runtime.py` now imports and uses `requests`, but `requests` is not declared in the project dependencies. This can cause `ImportError: No module named 'requests'` in clean/strict installs and break PR-Agent commands that import MCP integration.
### Issue Context
- `pr_agent/mcp/integration.py` imports `MCPRuntime` from `pr_agent/mcp/runtime.py`.
- Core tools (e.g., reviewer/questions/code suggestions) import `maybe_chat_completion_with_mcp`, so the runtime import happens on module import, not only when MCP is enabled.
### Fix Focus Areas
- requirements.txt[1-45]
- pr_agent/mcp/runtime.py[1-12]
- pr_agent/mcp/integration.py[1-6]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Broad except Exception in tool loop 📘 Rule violation ☼ Reliability
Description
chat_completion_with_tools() catches Exception around tool_executor(...), which can mask
unexpected bugs and makes failures harder to debug. The compliance checklist requires targeted
exceptions and proper exception handling patterns.
Code

pr_agent/algo/ai_handlers/base_ai_handler.py[R120-126]

+                try:
+                    tool_result = tool_executor(tool_name, arguments)
+                    if inspect.isawaitable(tool_result):
+                        tool_result = await tool_result
+                except Exception as exc:  # noqa: BLE001
+                    self._logger.warning("MCP tool '%s' raised an exception: %s", tool_name, exc)
+                    tool_result = f"Tool error: {exc}"
Evidence
PR Compliance ID 15 forbids broad except Exception; the added tool execution wrapper uses `except
Exception as exc (suppressed with # noqa: BLE001`).

pr_agent/algo/ai_handlers/base_ai_handler.py[120-126]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`chat_completion_with_tools()` wraps tool execution with a broad `except Exception`, which can hide unexpected errors and violates the targeted-exception requirement.
## Issue Context
The tool executor is an external boundary and should only handle expected failure modes (e.g., runtime/tool errors, validation/type errors), while letting unexpected exceptions surface (or be wrapped with explicit chaining) for debuggability.
## Fix Focus Areas
- pr_agent/algo/ai_handlers/base_ai_handler.py[120-126]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Session leak on connect fail 🐞 Bug ☼ Reliability
Description
MCPRuntime.connect_server() calls client.connect() before registering the client in _clients;
if connect() raises (e.g., unreachable HTTP server), the created client is dropped and its
underlying requests.Session is never closed, leaking sockets/file descriptors over repeated
requests.
Code

pr_agent/mcp/runtime.py[R613-626]

+    def connect_server(self, server_name: str):
+        if not self.enabled:
+            raise MCPRuntimeError("MCP runtime is disabled")
+        if server_name in self._clients:
+            return
+
+        server_config = self._servers_config.get(server_name)
+        if not isinstance(server_config, dict):
+            raise MCPRuntimeError(f"MCP server '{server_name}' config must be an object")
+
+        client = self._build_client(server_name, self._resolve_config_values(server_config))
+        client.connect()
+        self._clients[server_name] = client
+        self._logger.info(f"Connected MCP server '{server_name}'")
Evidence
The runtime builds a client and invokes connect() before storing it; HTTP-based clients allocate a
requests.Session() in __init__ and only release resources in close(). On a connect failure,
the runtime never tracks the client, so disconnect_all() cannot close it, and the session remains
open.

pr_agent/mcp/runtime.py[613-626]
pr_agent/mcp/runtime.py[288-319]
pr_agent/mcp/runtime.py[398-436]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`MCPRuntime.connect_server()` can leak resources when `client.connect()` raises because the client is not yet tracked in `_clients`, so its `close()` method is never called.
### Issue Context
This is particularly impactful for `MCPHttpClient` / `MCPStreamableHttpClient`, which allocate a `requests.Session()` in `__init__` and rely on `close()` to release sockets.
### Fix Focus Areas
- pr_agent/mcp/runtime.py[613-626]
- pr_agent/mcp/runtime.py[288-319]
- pr_agent/mcp/runtime.py[398-436]
### Implementation direction
Wrap `client.connect()` in `try/except`, and call `client.close()` on failure before re-raising. Only add the client to `_clients` after a successful connect.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. resolve_env_vars setting ignored 📘 Rule violation ⚙ Maintainability
Description
The MCP config setting mcp.resolve_env_vars is added but the runtime always expands environment
variables in _resolve_config_values(), making this behavior effectively hardcoded. This violates
the requirement that runtime behavior be configurable via .pr_agent.toml/pr_agent/settings/
rather than embedded in code.
Code

pr_agent/mcp/runtime.py[R730-737]

+    def _resolve_config_values(self, value: Any) -> Any:
+        if isinstance(value, str):
+            return os.path.expanduser(os.path.expandvars(value))
+        if isinstance(value, list):
+            return [self._resolve_config_values(item) for item in value]
+        if isinstance(value, dict):
+            return {key: self._resolve_config_values(item) for key, item in value.items()}
+        return value
Evidence
PR Compliance ID 7 requires configurable behavior to be controlled by settings, not hardcoded. The
PR adds mcp.resolve_env_vars in configuration.toml, but the code always performs
expandvars/expanduser without checking that setting.

AGENTS.md
pr_agent/settings/configuration.toml[73-80]
pr_agent/mcp/runtime.py[730-737]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`mcp.resolve_env_vars` is introduced in settings, but the MCP runtime always expands env vars and `~` paths in `_resolve_config_values()`. This makes the behavior non-configurable and inconsistent with the new setting.
## Issue Context
The setting exists in `pr_agent/settings/configuration.toml` as `resolve_env_vars = true`, implying users can disable it. The runtime should read `get_settings().get("MCP.RESOLVE_ENV_VARS", True)` (or equivalent) and only expand variables when enabled.
## Fix Focus Areas
- pr_agent/settings/configuration.toml[73-80]
- pr_agent/mcp/runtime.py[730-737]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Tool allowlist bypass 🐞 Bug ⛨ Security
Description
BaseAiHandler.chat_completion_with_tools executes whatever tool name the model returns without
validating it against the advertised tool catalog, allowing invocation of tools that were
intentionally omitted due to max_tools/max_schema_chars budgets. This can bypass tool exposure
controls and trigger unexpected tool calls on configured MCP servers.
Code

pr_agent/algo/ai_handlers/base_ai_handler.py[R105-117]

+            tool_name = str(parsed_response.get("tool", "")).strip()
+            arguments = parsed_response.get("arguments") or {}
+            if not tool_name:
+                self._logger.warning("MCP tool orchestration returned an empty tool name; aborting tool loop")
+                return response_text, finish_reason
+            if not isinstance(arguments, dict):
+                self._logger.warning("MCP tool orchestration arguments must be a JSON object; aborting tool loop")
+                return response_text, finish_reason
+
+            try:
+                tool_result = tool_executor(tool_name, arguments)
+                if inspect.isawaitable(tool_result):
+                    tool_result = await tool_result
Evidence
The tool loop extracts tool_name directly from model JSON and calls tool_executor(tool_name,
arguments) with no check that tool_name exists in the provided tools catalog. The MCP integration
builds a tool catalog with explicit budgets (skipping tools when budgets are exceeded) but passes a
runtime executor that can call arbitrary tools by name on configured servers, so omitted tools
remain callable if the model requests them explicitly.

pr_agent/algo/ai_handlers/base_ai_handler.py[105-117]
pr_agent/mcp/integration.py[35-63]
pr_agent/mcp/runtime.py[656-683]
pr_agent/mcp/runtime.py[686-714]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`BaseAiHandler.chat_completion_with_tools()` executes the model-provided `tool` name without validating it against the tool catalog passed in via `tools`. With MCP enabled, `runtime.build_tool_schemas()` may omit tools due to `max_tools` / `max_schema_chars`, but `runtime.create_tool_executor()` can still call any tool on configured servers, so a model can bypass the catalog exposure/budget by naming an omitted tool.
### Issue Context
- The `tools` catalog is used only to generate the system prompt.
- The MCP executor ultimately calls `MCPRuntime.call_tool()` and does not check whether a tool was advertised.
### Fix approach
Implement an allowlist of tool names that were actually advertised and refuse (or return a controlled error result for) calls not in that allowlist.
Recommended implementation (defense-in-depth):
1. In `BaseAiHandler.chat_completion_with_tools`, derive `allowed_tool_names` from the `tools` argument (support both shapes used in repo/tests):
- OpenAI-style: `tool["function"]["name"]`
- Simple style: `tool["name"]`
2. Before calling `tool_executor`, verify `tool_name in allowed_tool_names`.
- If not allowed: log a warning and set `tool_result` to a deterministic error like `"Tool not available: ..."` (and continue the loop / decrement the turn budget), or abort the loop.
3. Optionally, also wrap `runtime.create_tool_executor()` in `maybe_chat_completion_with_mcp` with an allowlist check to ensure enforcement even if other callers bypass the handler-level check.
### Fix Focus Areas
- pr_agent/algo/ai_handlers/base_ai_handler.py[58-129]
- pr_agent/mcp/integration.py[35-63]
- pr_agent/mcp/runtime.py[656-714]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. BaseException caught in stdio reader 📘 Rule violation ☼ Reliability
Description
_read_message_with_timeout() catches BaseException, which can swallow critical control-flow
exceptions (e.g., KeyboardInterrupt, SystemExit) and violates targeted exception handling
guidance. This reduces reliability and observability of failures.
Code

pr_agent/mcp/runtime.py[R210-215]

+        def reader():
+            try:
+                response_holder["message"] = self._read_message()
+            except BaseException as exc:  # noqa: BLE001
+                error_holder["error"] = exc
+
Evidence
The new MCP stdio client uses except BaseException in a background reader thread, which is broader
than permitted by the checklist’s targeted exception handling requirement.

pr_agent/mcp/runtime.py[210-215]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The stdio reader thread catches `BaseException`, which may swallow `SystemExit`/`KeyboardInterrupt` and violates the requirement to catch only expected exception types.
## Issue Context
Prefer catching `Exception` (or narrower) and converting expected failures into `MCPRuntimeError` with chaining where appropriate.
## Fix Focus Areas
- pr_agent/mcp/runtime.py[210-215]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. self.vars['img_path'] may KeyError 📘 Rule violation ☼ Reliability
Description
The new img_path assignment checks membership in variables but indexes into self.vars, which
can raise KeyError when those dicts are out of sync. This violates defensive input validation
expectations and can crash /ask flows.
Code

pr_agent/tools/pr_questions.py[110]

+        img_path = self.vars['img_path'] if 'img_path' in variables else None
Evidence
PR Compliance ID 18 requires guarding external/variable inputs before indexing. The added line
indexes self.vars['img_path'] based on 'img_path' in variables, which does not guarantee the key
exists in self.vars.

pr_agent/tools/pr_questions.py[110-110]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`img_path` is derived by indexing `self.vars['img_path']` after checking `'img_path' in variables`, which can still raise `KeyError`.
## Issue Context
The key presence check is performed on a different dict (`variables`) than the one being indexed (`self.vars`).
## Fix Focus Areas
- pr_agent/tools/pr_questions.py[110-110]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Invalid Content-Length can crash 📘 Rule violation ☼ Reliability
Description
MCPStdioClient._read_message casts Content-Length to int without handling ValueError, which
can raise an unhandled exception on malformed server output. This violates robust error handling for
external inputs.
Code

pr_agent/mcp/runtime.py[R249-254]

+        content_length_value = headers.get("content-length")
+        if not content_length_value:
+            raise MCPRuntimeError(f"MCP server '{self.server_name}' response missing Content-Length")
+
+        content_length = int(content_length_value)
+        body = self.process.stdout.read(content_length)
Evidence
PR Compliance ID 3 requires anticipating and handling error scenarios gracefully. The added code
parses external headers and calls int(content_length_value) without catching conversion errors,
which can crash the MCP runtime instead of raising a controlled MCPRuntimeError.

Rule 3: Robust Error Handling
pr_agent/mcp/runtime.py[249-254]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Parsing `Content-Length` can raise `ValueError` if the header is malformed, causing an unhandled exception.
## Issue Context
This code consumes external server output and should fail with a clear `MCPRuntimeError` using exception chaining.
## Fix Focus Areas
- pr_agent/mcp/runtime.py[249-262]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Streamable HTTP response leak 🐞 Bug ☼ Reliability
Description
MCPStreamableHttpClient._send_request() uses stream=True but never closes the
requests.Response, and _parse_sse_response() can return early on a matching event without
releasing the connection. This can leak connections and exhaust the session pool, breaking
subsequent MCP calls.
Code

pr_agent/mcp/runtime.py[R458-517]

+    def _send_request(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
+        self._request_id += 1
+        request_id = self._request_id
+        payload = {
+            "jsonrpc": "2.0",
+            "id": request_id,
+            "method": method,
+            "params": params,
+        }
+        try:
+            response = self._session.post(
+                self.url,
+                json=payload,
+                headers=self._build_extra_headers(),
+                timeout=self.timeout,
+                stream=True,
+            )
+            response.raise_for_status()
+        except requests.RequestException as exc:
+            raise MCPRuntimeError(
+                f"MCP streamable HTTP request failed for '{self.server_name}': {exc}"
+            ) from exc
+
+        # Capture session ID returned on the initialize response.
+        session_id = response.headers.get("Mcp-Session-Id")
+        if session_id and not self._session_id:
+            self._session_id = session_id
+
+        content_type = response.headers.get("Content-Type", "")
+        if "text/event-stream" in content_type:
+            return self._parse_sse_response(response, request_id, method)
+        return self._parse_json_response(response, method)
+
+    def _parse_json_response(self, response: "requests.Response", method: str) -> dict[str, Any]:
+        try:
+            body = response.json()
+        except ValueError as exc:
+            raise MCPRuntimeError(
+                f"MCP streamable HTTP response is not valid JSON for '{self.server_name}'"
+            ) from exc
+        return self._extract_result(body, method)
+
+    def _parse_sse_response(
+        self, response: "requests.Response", request_id: int, method: str
+    ) -> dict[str, Any]:
+        """Read an SSE stream and return the JSON-RPC result that matches *request_id*."""
+        try:
+            for raw_line in response.iter_lines(decode_unicode=True):
+                if not raw_line or not raw_line.startswith("data:"):
+                    continue
+                data = raw_line[5:].lstrip(" ")
+                if not data:
+                    continue
+                try:
+                    message = json.loads(data)
+                except json.JSONDecodeError:
+                    continue
+                if isinstance(message, dict) and message.get("id") == request_id:
+                    return self._extract_result(message, method)
+        except requests.RequestException as exc:
Evidence
The streamable client enables streaming (stream=True) and then returns directly from
_parse_sse_response()/_parse_json_response() without any response.close()/context manager
usage. For SSE, returning on the first matching event can leave the stream open and the connection
unreleased.

pr_agent/mcp/runtime.py[458-489]
pr_agent/mcp/runtime.py[500-517]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Streamable HTTP MCP requests are made with `stream=True` but the response object is never closed, which can leak connections.
### Issue Context
- `_send_request()` returns from parsing methods without ensuring the response is closed.
- `_parse_sse_response()` returns early on a match, which is especially likely to leak.
### Fix Focus Areas
- pr_agent/mcp/runtime.py[458-489]
- pr_agent/mcp/runtime.py[500-517]
### Suggested fix approach
- Ensure `response.close()` is called in a `finally` block in `_send_request()` after parsing completes.
- For SSE, close the response before returning a matching result (or structure `_send_request()` as:
- create response
- try parse
- finally close response).
- Consider also explicitly consuming/closing SSE streams when returning early.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


14. apply_mcp_server_config broad except 📘 Rule violation ☼ Reliability
Description
apply_mcp_server_config() and _get_logger() catch Exception, which can mask unexpected bugs
and make failures harder to debug. The compliance rules require targeted exceptions and preserving
error context rather than broad catches.
Code

pr_agent/config_loader.py[R189-192]

+    except Exception as exc:
+        logger.error(f"Failed to load MCP server configuration from {config_path}: {exc}")
+        if get_settings().get("MCP.FAIL_ON_INVALID_CONFIG", False):
+            raise
Evidence
The new code introduces broad except Exception handlers in pr_agent/config_loader.py, which
violates the rule to use targeted exceptions and avoid except Exception.

pr_agent/config_loader.py[67-77]
pr_agent/config_loader.py[176-192]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`pr_agent/config_loader.py` uses broad `except Exception` in `_get_logger()` and `apply_mcp_server_config()`, which can hide real bugs and complicate debugging.
## Issue Context
Compliance requires catching only expected exception types and avoiding broad exception handlers.
## Fix Focus Areas
- pr_agent/config_loader.py[67-77]
- pr_agent/config_loader.py[176-192]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


15. DummyLogger violates Ruff E701 📘 Rule violation ⚙ Maintainability
Description
DummyLogger defines methods as single-line def ...: pass, which is flagged by Ruff (E701) and
can fail CI linting. Repository lint/format compliance requires new code to pass Ruff checks.
Code

pr_agent/config_loader.py[R72-77]

+        class DummyLogger:
+            def debug(self, *args, **kwargs): pass
+            def info(self, *args, **kwargs): pass
+            def warning(self, *args, **kwargs): pass
+            def error(self, *args, **kwargs): pass
+        return DummyLogger()
Evidence
The added DummyLogger methods use one-line function bodies (def ...: pass), which violates Ruff
formatting rules selected in this repository and is a likely CI failure.

pr_agent/config_loader.py[72-77]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `DummyLogger` methods are written as `def ...: pass` on one line, which Ruff flags (E701).
## Issue Context
Ruff linting is enforced via `pyproject.toml` and must pass in CI.
## Fix Focus Areas
- pr_agent/config_loader.py[72-77]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


16. list_tools() assumes dict tools 📘 Rule violation ☼ Reliability
Description
MCP client list_tools() implementations assume each tool entry is a dict and call tool.get(...)
without validating shape, which can raise AttributeError on malformed/unexpected server responses.
This violates defensive validation for external inputs and can crash tool discovery at runtime.
Code

pr_agent/mcp/runtime.py[R134-147]

+    def list_tools(self) -> list[MCPToolDefinition]:
+        result = self._send_request("tools/list", {})
+        tools = result.get("tools") or []
+        parsed: list[MCPToolDefinition] = []
+        for tool in tools:
+            parsed.append(
+                MCPToolDefinition(
+                    server_name=self.server_name,
+                    name=tool.get("name", ""),
+                    description=tool.get("description", ""),
+                    input_schema=tool.get("inputSchema", {}),
+                )
+            )
+        return parsed
Evidence
The checklist requires validating external input shapes before indexing/calling methods. The MCP
server responses are external inputs, yet the new code iterates tools and calls tool.get(...)
without checking isinstance(tool, dict) (same pattern in multiple transports).

pr_agent/mcp/runtime.py[134-147]
pr_agent/mcp/runtime.py[289-302]
pr_agent/mcp/runtime.py[396-409]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
MCP tool discovery assumes each `tools` element is a dict and calls `.get()` directly, which can crash on unexpected server responses.
## Issue Context
MCP server responses are external/untrusted input; code must defensively validate types before calling methods.
## Fix Focus Areas
- pr_agent/mcp/runtime.py[134-147]
- pr_agent/mcp/runtime.py[289-302]
- pr_agent/mcp/runtime.py[396-409]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


17. Ruff style violations in tests 📘 Rule violation ⚙ Maintainability
Description
New test code introduces Ruff/formatting violations: very long lines and inconsistent quoting, plus
several new files are missing a trailing newline. These can fail repository lint/format tooling and
CI checks.
Code

tests/unittest/test_ai_handler_tool_orchestration.py[R75-81]

+        handler = FakeToolHandler(
+            [
+                'Ask❓\n\nwhat are the latest AWS services released\nAnswer:\n'
+                '{"type":"tool_call","tool":"AWS Knowledge.aws___read_documentation","arguments":{"requests":[{"url":"https://aws.amazon.com/about-aws/whats-new/","max_length":8000}]}}\n'
+                '{"type":"final","content":"should be ignored in the first turn"}',
+                '{"type": "final", "content": "done"}',
+            ]
Evidence
The checklist requires Python changes to follow Ruff style (line length and quoting) and to fix
lint/format issues. The added test includes an extremely long single-quoted JSON string line, and
multiple newly added files show No newline at end of file, which commonly violates formatting
hooks.

AGENTS.md
tests/unittest/test_ai_handler_tool_orchestration.py[75-81]
pr_agent/mcp/init.py[1-15]
pr_agent/mcp/integration.py[1-65]
pr_agent/mcp/runtime.py[1-708]
tests/unittest/test_mcp_tool_discovery.py[1-38]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New tests/files likely violate Ruff/format tooling (long lines, quote style) and several new files are missing a trailing newline.
## Issue Context
Repo compliance requires keeping code compatible with Ruff/isort/format and common whitespace hooks.
## Fix Focus Areas
- tests/unittest/test_ai_handler_tool_orchestration.py[75-81]
- pr_agent/mcp/__init__.py[1-15]
- pr_agent/mcp/integration.py[1-65]
- pr_agent/mcp/runtime.py[1-708]
- tests/unittest/test_mcp_tool_discovery.py[1-38]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


18. No response type-check before .get() 📘 Rule violation ☼ Reliability
Description
MCP client code assumes JSON-RPC responses are dicts and calls .get()/membership checks without
validating the decoded JSON shape, which can raise runtime exceptions on malformed or unexpected
responses.
Code

pr_agent/mcp/runtime.py[R192-197]

+    def _read_response(self, request_id: int) -> dict[str, Any]:
+        while True:
+            message = self._read_message_with_timeout()
+            if message.get("id") == request_id:
+                return message
+
Evidence
The checklist requires validating external input shapes before indexing/calling methods; both stdio
and HTTP paths use JSON decoded from external servers and then call .get() / check keys without
confirming the object is a dict.

pr_agent/mcp/runtime.py[192-197]
pr_agent/mcp/runtime.py[341-347]
Best Practice: Learned patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
JSON decoded from MCP servers can be non-dict (or invalid shape). The code currently assumes a dict and calls `.get()` / checks keys, which can crash.
## Issue Context
This is external input from MCP servers (stdio subprocess or HTTP endpoint). Defensive validation should happen immediately after `json.loads()` / `response.json()`.
## Fix Focus Areas
- pr_agent/mcp/runtime.py[192-197]
- pr_agent/mcp/runtime.py[231-255]
- pr_agent/mcp/runtime.py[332-347]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


19. Single quotes and long lines 📘 Rule violation ⚙ Maintainability
Description
New Python code introduces single-quoted string literals and very long inline JSON strings that are
likely to violate the repository’s Ruff style expectations (double quotes, 120-char lines).
Code

pr_agent/algo/ai_handlers/base_ai_handler.py[R61-71]

+        tool_catalog_text = json.dumps(tools, indent=2, sort_keys=True)
+        structured_system = (
+            f"{system}\n\n"
+            f"Available MCP tools (JSON schema):\n{tool_catalog_text}\n\n"
+            "Always inspect the available tools first and use them before responding whenever they can help answer the user's request.\n"
+            "When you need a tool, respond with ONLY a JSON object exactly in this shape:\n"
+            '{"type":"tool_call","tool":"server.tool","arguments":{...}}\n'
+            "Do not include a final answer in the same message as a tool call.\n"
+            "When you are finished, respond with ONLY a JSON object exactly in this shape:\n"
+            '{"type":"final","content":"..."}\n'
+            "Do not wrap the JSON in markdown fences."
Evidence
The checklist requires Ruff-aligned formatting (double quotes, line length). The added orchestration
prompt embeds JSON examples using single-quoted Python strings, and tests include very long inline
JSON tool-call strings.

AGENTS.md
pr_agent/algo/ai_handlers/base_ai_handler.py[61-71]
tests/unittest/test_ai_handler_tool_orchestration.py[75-81]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Some newly added Python strings likely violate enforced formatting: single quotes where the project prefers double quotes and long inline JSON strings that likely exceed 120 characters.
## Issue Context
Ruff/pre-commit commonly enforces quote style and line length; keeping long JSON examples as concatenated strings or multiline literals improves compliance.
## Fix Focus Areas
- pr_agent/algo/ai_handlers/base_ai_handler.py[61-71]
- tests/unittest/test_ai_handler_tool_orchestration.py[75-81]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


20. Untrusted MCP command execution 🐞 Bug ⛨ Security
Description
Repository-local pyproject.toml is loaded into settings and can influence MCP enablement/config, and
MCP stdio servers are executed via subprocess.Popen from that configuration. This enables untrusted
PR content to drive arbitrary command execution (stdio) or outbound requests (HTTP) on the PR-Agent
host when MCP is enabled.
Code

pr_agent/mcp/runtime.py[R89-112]

+    def connect(self):
+        command = self.config.get("command")
+        if not command:
+            raise MCPRuntimeError(f"Stdio MCP server '{self.server_name}' is missing 'command'")
+
+        args = self.config.get("args") or []
+        if not isinstance(args, list):
+            raise MCPRuntimeError(f"Stdio MCP server '{self.server_name}' args must be a list")
+
+        env = os.environ.copy()
+        server_env = self.config.get("env") or {}
+        if not isinstance(server_env, dict):
+            raise MCPRuntimeError(f"Stdio MCP server '{self.server_name}' env must be an object")
+        env.update({str(k): str(v) for k, v in server_env.items()})
+
+        cwd = self.config.get("cwd")
+        self.process = subprocess.Popen(
+            [command, *args],
+            stdin=subprocess.PIPE,
+            stdout=subprocess.PIPE,
+            stderr=subprocess.DEVNULL,
+            env=env,
+            cwd=cwd,
+        )
Evidence
The agent loads configuration from the repository being reviewed (pyproject.toml) into settings,
then applies MCP server configuration from a settings-derived path. MCPRuntime reads MCP.SERVERS and
MCP enablement from settings, and MCPStdioClient.connect spawns the configured command with
subprocess.Popen, meaning settings-controlled server definitions can result in command execution.

pr_agent/config_loader.py[195-226]
pr_agent/config_loader.py[143-149]
pr_agent/config_loader.py[176-192]
pr_agent/mcp/runtime.py[349-369]
pr_agent/mcp/runtime.py[82-127]
pr_agent/mcp/runtime.py[257-347]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Untrusted repository configuration (loaded from `pyproject.toml`) can influence MCP settings, and MCP stdio servers are launched via `subprocess.Popen`, creating an RCE/SSRF risk when running PR-Agent on untrusted PR content.
### Issue Context
`config_loader` loads `pyproject.toml` from the repo being reviewed into settings, and MCP runtime/server config is driven from those settings. MCP stdio transport executes OS commands from the server definitions.
### Fix Focus Areas
- pr_agent/config_loader.py[143-149]
- pr_agent/config_loader.py[176-192]
- pr_agent/config_loader.py[195-226]
- pr_agent/mcp/runtime.py[349-395]
- pr_agent/mcp/runtime.py[82-127]
### What to change
- Ensure MCP configuration (enabled flag, config path, and servers) can only come from trusted server-side sources (e.g., environment variables and/or a fixed server config file), not from repository-loaded config.
- Consider disabling `stdio` transport by default (or require an explicit server-side allowlist) in hosted/untrusted modes.
- If you keep repo-level configuration, add a hard trust gate (e.g., only allow MCP when the repo/PR author is trusted) before loading/connecting any MCP servers.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


21....

Comment thread pr_agent/mcp/runtime.py Outdated

raise MCPRuntimeError(
f"MCP server '{server_name}' uses unsupported transport type '{server_type}'"
) No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. Files missing final newline 📘 Rule violation ⚙ Maintainability

Several newly added files end without a trailing newline, which commonly fails pre-commit hooks
(end-of-file-fixer) and can break CI. Add a final newline to each affected file.
Agent Prompt
## Issue description
Multiple newly added files are missing a final newline, which can cause pre-commit/CI failures.

## Issue Context
The diff marks these files with `No newline at end of file`.

## Fix Focus Areas
- pr_agent/mcp/__init__.py[15-15]
- pr_agent/mcp/integration.py[62-62]
- pr_agent/mcp/runtime.py[499-499]
- tests/unittest/test_mcp_runtime.py[127-127]
- tests/unittest/test_mcp_tool_discovery.py[38-38]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread pr_agent/mcp/integration.py Outdated
Comment on lines +24 to +62
runtime = MCPRuntime()
if not runtime.enabled:
return await ai_handler.chat_completion(
model=model,
system=system,
user=user,
temperature=temperature,
img_path=img_path,
)

max_tools = _get_tool_budget("MCP.MAX_TOOL_CATALOG_TOOLS", 12)
max_schema_chars = _get_tool_budget("MCP.MAX_TOOL_CATALOG_SCHEMA_CHARS", 12000)

tools = runtime.build_tool_schemas(
max_tools=max_tools,
max_schema_chars=max_schema_chars,
include_server_prefix=True,
)
if not tools:
return await ai_handler.chat_completion(
model=model,
system=system,
user=user,
temperature=temperature,
img_path=img_path,
)

if command_name:
system = f"{system}\n\nCommand context: {command_name}"

return await ai_handler.chat_completion_with_tools(
model=model,
system=system,
user=user,
tools=tools,
tool_executor=runtime.create_tool_executor(),
temperature=temperature,
img_path=img_path,
) No newline at end of file
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

2. Mcp clients never closed 🐞 Bug ☼ Reliability

maybe_chat_completion_with_mcp creates a new MCPRuntime, connects to servers during tool
discovery/calls, and returns without disconnecting; this leaks requests sessions and leaves stdio
subprocesses running across requests. In a long-running server, repeated /ask,/review,/improve calls
can exhaust file descriptors/process slots and degrade performance or crash the service.
Agent Prompt
### Issue description
`maybe_chat_completion_with_mcp()` creates an `MCPRuntime()` and implicitly connects servers during tool discovery/execution, but it never calls `disconnect_all()`. This leaks `requests.Session` objects and leaves stdio subprocesses running.

### Issue Context
- `build_tool_schemas()` -> `list_all_tools()` -> `list_server_tools()` can connect servers.
- `MCPRuntime` provides `disconnect_all()` to close sessions/terminate processes.

### Fix Focus Areas
- pr_agent/mcp/integration.py[15-62]
- pr_agent/mcp/runtime.py[344-375]

### Suggested fix
Wrap the MCP-enabled execution path in `try/finally` and ensure `runtime.disconnect_all()` runs on *all* return paths (disabled, no tools, successful tool loop, exceptions). If you want to keep connections for reuse, refactor to a shared runtime with explicit lifecycle management (startup/shutdown) instead of per-request instantiation.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread pr_agent/mcp/runtime.py Outdated
Comment on lines +195 to +224
def _read_response(self, request_id: int) -> dict[str, Any]:
while True:
message = self._read_message()
if message.get("id") == request_id:
return message

def _read_message(self) -> dict[str, Any]:
if not self.process or not self.process.stdout:
raise MCPRuntimeError(f"Stdio MCP server '{self.server_name}' is not connected")

headers: dict[str, str] = {}
while True:
line = self.process.stdout.readline()
if line == b"":
raise MCPRuntimeError(f"MCP server '{self.server_name}' closed stdout unexpectedly")
if line in (b"\r\n", b"\n"):
break
key, _, value = line.decode("utf-8").partition(":")
headers[key.strip().lower()] = value.strip()

content_length_value = headers.get("content-length")
if not content_length_value:
raise MCPRuntimeError(f"MCP server '{self.server_name}' response missing Content-Length")

content_length = int(content_length_value)
body = self.process.stdout.read(content_length)
if not body:
raise MCPRuntimeError(f"MCP server '{self.server_name}' returned an empty response body")

return json.loads(body.decode("utf-8"))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

3. Stdio tool calls can hang 🐞 Bug ☼ Reliability

MCPStdioClient performs fully blocking reads (readline/read) and loops until a matching request id
is seen, with no timeout or process-liveness checks. If a server stalls, emits partial frames, or
never responds, PR-Agent can hang indefinitely while building the tool catalog or executing a tool
call.
Agent Prompt
### Issue description
`MCPStdioClient` can block forever waiting for a response frame (`stdout.readline()` / `stdout.read()`), causing PR-Agent requests to hang indefinitely.

### Issue Context
Tool discovery (`tools/list`) happens before model calls, so a hang here can break /ask,/review,/improve even before the LLM is invoked.

### Fix Focus Areas
- pr_agent/mcp/runtime.py[169-225]

### Suggested fix
- Add a configurable timeout for stdio request/response (e.g., default 30s, overridable via server config).
- Implement non-blocking/timeout-aware IO (e.g., `selectors` on `self.process.stdout`), and raise `MCPRuntimeError` on timeout.
- In the wait loop, periodically check `self.process.poll()` and fail fast if the process exited.
- Consider handling notifications (messages without `id`) explicitly so they don’t starve the request loop.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 23, 2026

Persistent review updated to latest commit 8008c02

Comment thread pr_agent/mcp/runtime.py
Comment on lines +231 to +255
def _read_message(self) -> dict[str, Any]:
if not self.process or not self.process.stdout:
raise MCPRuntimeError(f"Stdio MCP server '{self.server_name}' is not connected")

headers: dict[str, str] = {}
while True:
line = self.process.stdout.readline()
if line == b"":
raise MCPRuntimeError(f"MCP server '{self.server_name}' closed stdout unexpectedly")
if line in (b"\r\n", b"\n"):
break
key, _, value = line.decode("utf-8").partition(":")
headers[key.strip().lower()] = value.strip()

content_length_value = headers.get("content-length")
if not content_length_value:
raise MCPRuntimeError(f"MCP server '{self.server_name}' response missing Content-Length")

content_length = int(content_length_value)
body = self.process.stdout.read(content_length)
if not body:
raise MCPRuntimeError(f"MCP server '{self.server_name}' returned an empty response body")

return json.loads(body.decode("utf-8"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. Stdio json errors uncaught 🐞 Bug ☼ Reliability

MCPStdioClient._read_message() can raise JSONDecodeError/ValueError when parsing a server response,
but MCPRuntime.list_all_tools() only catches MCPRuntimeError/OSError, so a malformed stdio response
can crash MCP tool discovery and break MCP-enabled /ask,/review,/improve flows.
Agent Prompt
### Issue description
`MCPStdioClient._read_message()` can raise `json.JSONDecodeError` (and `ValueError` from header parsing) that are not wrapped as `MCPRuntimeError`. Since `MCPRuntime.list_all_tools()` only catches `(MCPRuntimeError, OSError)`, these exceptions can bubble out and crash MCP-enabled commands.

### Issue Context
The runtime tries to be resilient in `list_all_tools()` by catching per-server failures, but that only works if client failures are surfaced as `MCPRuntimeError` (or are explicitly caught).

### Fix Focus Areas
- pr_agent/mcp/runtime.py[231-255]
- pr_agent/mcp/runtime.py[413-423]

### Implementation notes
- In `_read_message()`, wrap `int(content_length_value)` and `json.loads(...)` in `try/except` and raise `MCPRuntimeError` with context (server name, operation) on failure.
- Alternatively (or additionally), broaden `list_all_tools()` to catch `Exception` (or at least `ValueError`) so one misbehaving server can’t break tool discovery.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 23, 2026

Persistent review updated to latest commit 7fdc2d8

Comment on lines +95 to +107
if remaining_turns <= 0:
raise ValueError("MCP tool orchestration exceeded the configured turn budget")

tool_name = str(parsed_response.get("tool", "")).strip()
arguments = parsed_response.get("arguments") or {}
if not tool_name:
raise ValueError("MCP tool orchestration returned an empty tool name")
if not isinstance(arguments, dict):
raise ValueError("MCP tool orchestration arguments must be a JSON object")

tool_result = tool_executor(tool_name, arguments)
if inspect.isawaitable(tool_result):
tool_result = await tool_result
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. Tool loop raises uncaught valueerror 📘 Rule violation ☼ Reliability

chat_completion_with_tools() raises ValueError for common model/tool-loop failure modes (turn
budget, empty tool name, non-dict arguments) and does not catch exceptions from tool_executor.
This can crash /ask, /review, or /improve instead of failing gracefully.
Agent Prompt
## Issue description
`BaseAiHandler.chat_completion_with_tools()` raises uncaught exceptions (e.g., turn budget exceeded, invalid tool call shape) and does not handle `tool_executor` failures, which can terminate the command instead of returning a safe fallback.

## Issue Context
This method is used as part of MCP tool orchestration and can be triggered by imperfect model outputs or transient tool failures.

## Fix Focus Areas
- pr_agent/algo/ai_handlers/base_ai_handler.py[84-112]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread pr_agent/mcp/runtime.py Outdated
Comment on lines +451 to +456
def create_tool_executor(self):
async def executor(tool_name: str, arguments: Optional[dict[str, Any]] = None):
server_name, server_tool_name = self._split_tool_name(tool_name)
return self.call_tool(server_name, server_tool_name, arguments)

return executor
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

2. Blocking mcp tool calls 🐞 Bug ➹ Performance

MCP tool execution performs synchronous HTTP/subprocess I/O inside an async tool loop, which can
block the event loop and stall concurrent /ask,/review,/improve requests under load. This occurs
because the async tool executor calls a synchronous call path that uses requests.Session.post().
Agent Prompt
### Issue description
MCP tool calls run blocking I/O (requests + subprocess) on the async execution path, which can block the event loop.

### Issue Context
`BaseAiHandler.chat_completion_with_tools()` awaits `tool_executor()`. `MCPRuntime.create_tool_executor()` returns an `async def` that calls the synchronous `call_tool()`, and HTTP tool calls use `requests.Session.post()`.

### Fix Focus Areas
- pr_agent/mcp/runtime.py[451-470]
- pr_agent/mcp/runtime.py[313-346]
- pr_agent/mcp/runtime.py[458-470]

### Implementation direction
- Option A (quick, minimal): wrap synchronous tool execution in `asyncio.to_thread(...)` inside the executor (and similarly for tool discovery if needed).
- Option B (better): replace `requests` with an async HTTP client (e.g., `httpx.AsyncClient`) and consider an asyncio-native stdio transport, so tool listing/calls don’t block the loop.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 23, 2026

Persistent review updated to latest commit 7fdc2d8

Comment on lines +88 to +91
def _prepare_mcp_status_block(self) -> str:
status = MCPRuntime().get_status()
if not status["enabled"] and not status["configured_servers"]:
return ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. /config mcp status may crash 🐞 Bug ☼ Reliability

PRConfig._prepare_mcp_status_block() calls MCPRuntime().get_status() without handling
MCPRuntimeError, so a malformed MCP.SERVERS value can cause the /config command to fail. This makes
it harder to debug config problems because /config itself becomes unavailable.
Agent Prompt
### Issue description
`/config` can crash if MCP settings are malformed because MCPRuntime construction errors aren’t handled.

### Issue Context
Users often run `/config` specifically to debug configuration problems; it should degrade gracefully.

### Fix Focus Areas
- pr_agent/tools/pr_config.py[88-100]
- pr_agent/mcp/runtime.py[350-357]

### Suggested fix approach
- Wrap `MCPRuntime().get_status()` in try/except for `Exception` or `MCPRuntimeError`.
- On failure, emit a small status block like:
  - `mcp.enabled = unknown`
  - `mcp.error = "..."`
- Ensure the rest of the config output still renders.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 23, 2026

Persistent review updated to latest commit 75a838c

Comment thread pr_agent/mcp/runtime.py
Comment on lines +192 to +197
def _read_response(self, request_id: int) -> dict[str, Any]:
while True:
message = self._read_message_with_timeout()
if message.get("id") == request_id:
return message

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. No response type-check before .get() 📘 Rule violation ☼ Reliability

MCP client code assumes JSON-RPC responses are dicts and calls .get()/membership checks without
validating the decoded JSON shape, which can raise runtime exceptions on malformed or unexpected
responses.
Agent Prompt
## Issue description
JSON decoded from MCP servers can be non-dict (or invalid shape). The code currently assumes a dict and calls `.get()` / checks keys, which can crash.

## Issue Context
This is external input from MCP servers (stdio subprocess or HTTP endpoint). Defensive validation should happen immediately after `json.loads()` / `response.json()`.

## Fix Focus Areas
- pr_agent/mcp/runtime.py[192-197]
- pr_agent/mcp/runtime.py[231-255]
- pr_agent/mcp/runtime.py[332-347]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +61 to +71
tool_catalog_text = json.dumps(tools, indent=2, sort_keys=True)
structured_system = (
f"{system}\n\n"
f"Available MCP tools (JSON schema):\n{tool_catalog_text}\n\n"
"Always inspect the available tools first and use them before responding whenever they can help answer the user's request.\n"
"When you need a tool, respond with ONLY a JSON object exactly in this shape:\n"
'{"type":"tool_call","tool":"server.tool","arguments":{...}}\n'
"Do not include a final answer in the same message as a tool call.\n"
"When you are finished, respond with ONLY a JSON object exactly in this shape:\n"
'{"type":"final","content":"..."}\n'
"Do not wrap the JSON in markdown fences."
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

2. Single quotes and long lines 📘 Rule violation ⚙ Maintainability

New Python code introduces single-quoted string literals and very long inline JSON strings that are
likely to violate the repository’s Ruff style expectations (double quotes, 120-char lines).
Agent Prompt
## Issue description
Some newly added Python strings likely violate enforced formatting: single quotes where the project prefers double quotes and long inline JSON strings that likely exceed 120 characters.

## Issue Context
Ruff/pre-commit commonly enforces quote style and line length; keeping long JSON examples as concatenated strings or multiline literals improves compliance.

## Fix Focus Areas
- pr_agent/algo/ai_handlers/base_ai_handler.py[61-71]
- tests/unittest/test_ai_handler_tool_orchestration.py[75-81]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread pr_agent/mcp/runtime.py
Comment on lines +89 to +112
def connect(self):
command = self.config.get("command")
if not command:
raise MCPRuntimeError(f"Stdio MCP server '{self.server_name}' is missing 'command'")

args = self.config.get("args") or []
if not isinstance(args, list):
raise MCPRuntimeError(f"Stdio MCP server '{self.server_name}' args must be a list")

env = os.environ.copy()
server_env = self.config.get("env") or {}
if not isinstance(server_env, dict):
raise MCPRuntimeError(f"Stdio MCP server '{self.server_name}' env must be an object")
env.update({str(k): str(v) for k, v in server_env.items()})

cwd = self.config.get("cwd")
self.process = subprocess.Popen(
[command, *args],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
env=env,
cwd=cwd,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

3. Untrusted mcp command execution 🐞 Bug ⛨ Security

Repository-local pyproject.toml is loaded into settings and can influence MCP enablement/config, and
MCP stdio servers are executed via subprocess.Popen from that configuration. This enables untrusted
PR content to drive arbitrary command execution (stdio) or outbound requests (HTTP) on the PR-Agent
host when MCP is enabled.
Agent Prompt
### Issue description
Untrusted repository configuration (loaded from `pyproject.toml`) can influence MCP settings, and MCP stdio servers are launched via `subprocess.Popen`, creating an RCE/SSRF risk when running PR-Agent on untrusted PR content.

### Issue Context
`config_loader` loads `pyproject.toml` from the repo being reviewed into settings, and MCP runtime/server config is driven from those settings. MCP stdio transport executes OS commands from the server definitions.

### Fix Focus Areas
- pr_agent/config_loader.py[143-149]
- pr_agent/config_loader.py[176-192]
- pr_agent/config_loader.py[195-226]
- pr_agent/mcp/runtime.py[349-395]
- pr_agent/mcp/runtime.py[82-127]

### What to change
- Ensure MCP configuration (enabled flag, config path, and servers) can only come from trusted server-side sources (e.g., environment variables and/or a fixed server config file), not from repository-loaded config.
- Consider disabling `stdio` transport by default (or require an explicit server-side allowlist) in hosted/untrusted modes.
- If you keep repo-level configuration, add a hard trust gate (e.g., only allow MCP when the repo/PR author is trusted) before loading/connecting any MCP servers.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 23, 2026

Persistent review updated to latest commit fda158e

Comment thread pr_agent/mcp/runtime.py
Comment on lines +134 to +147
def list_tools(self) -> list[MCPToolDefinition]:
result = self._send_request("tools/list", {})
tools = result.get("tools") or []
parsed: list[MCPToolDefinition] = []
for tool in tools:
parsed.append(
MCPToolDefinition(
server_name=self.server_name,
name=tool.get("name", ""),
description=tool.get("description", ""),
input_schema=tool.get("inputSchema", {}),
)
)
return parsed
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. list_tools() assumes dict tools 📘 Rule violation ☼ Reliability

MCP client list_tools() implementations assume each tool entry is a dict and call tool.get(...)
without validating shape, which can raise AttributeError on malformed/unexpected server responses.
This violates defensive validation for external inputs and can crash tool discovery at runtime.
Agent Prompt
## Issue description
MCP tool discovery assumes each `tools` element is a dict and calls `.get()` directly, which can crash on unexpected server responses.

## Issue Context
MCP server responses are external/untrusted input; code must defensively validate types before calling methods.

## Fix Focus Areas
- pr_agent/mcp/runtime.py[134-147]
- pr_agent/mcp/runtime.py[289-302]
- pr_agent/mcp/runtime.py[396-409]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +75 to +81
handler = FakeToolHandler(
[
'Ask❓\n\nwhat are the latest AWS services released\nAnswer:\n'
'{"type":"tool_call","tool":"AWS Knowledge.aws___read_documentation","arguments":{"requests":[{"url":"https://aws.amazon.com/about-aws/whats-new/","max_length":8000}]}}\n'
'{"type":"final","content":"should be ignored in the first turn"}',
'{"type": "final", "content": "done"}',
]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

2. Ruff style violations in tests 📘 Rule violation ⚙ Maintainability

New test code introduces Ruff/formatting violations: very long lines and inconsistent quoting, plus
several new files are missing a trailing newline. These can fail repository lint/format tooling and
CI checks.
Agent Prompt
## Issue description
New tests/files likely violate Ruff/format tooling (long lines, quote style) and several new files are missing a trailing newline.

## Issue Context
Repo compliance requires keeping code compatible with Ruff/isort/format and common whitespace hooks.

## Fix Focus Areas
- tests/unittest/test_ai_handler_tool_orchestration.py[75-81]
- pr_agent/mcp/__init__.py[1-15]
- pr_agent/mcp/integration.py[1-65]
- pr_agent/mcp/runtime.py[1-708]
- tests/unittest/test_mcp_tool_discovery.py[1-38]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 23, 2026

Persistent review updated to latest commit 74b69d1

Comment thread pr_agent/config_loader.py Outdated
Comment on lines +189 to +192
except Exception as exc:
logger.error(f"Failed to load MCP server configuration from {config_path}: {exc}")
if get_settings().get("MCP.FAIL_ON_INVALID_CONFIG", False):
raise
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. apply_mcp_server_config broad except 📘 Rule violation ☼ Reliability

apply_mcp_server_config() and _get_logger() catch Exception, which can mask unexpected bugs
and make failures harder to debug. The compliance rules require targeted exceptions and preserving
error context rather than broad catches.
Agent Prompt
## Issue description
`pr_agent/config_loader.py` uses broad `except Exception` in `_get_logger()` and `apply_mcp_server_config()`, which can hide real bugs and complicate debugging.

## Issue Context
Compliance requires catching only expected exception types and avoiding broad exception handlers.

## Fix Focus Areas
- pr_agent/config_loader.py[67-77]
- pr_agent/config_loader.py[176-192]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread pr_agent/config_loader.py
Comment on lines +72 to +77
class DummyLogger:
def debug(self, *args, **kwargs): pass
def info(self, *args, **kwargs): pass
def warning(self, *args, **kwargs): pass
def error(self, *args, **kwargs): pass
return DummyLogger()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

2. dummylogger violates ruff e701 📘 Rule violation ⚙ Maintainability

DummyLogger defines methods as single-line def ...: pass, which is flagged by Ruff (E701) and
can fail CI linting. Repository lint/format compliance requires new code to pass Ruff checks.
Agent Prompt
## Issue description
The new `DummyLogger` methods are written as `def ...: pass` on one line, which Ruff flags (E701).

## Issue Context
Ruff linting is enforced via `pyproject.toml` and must pass in CI.

## Fix Focus Areas
- pr_agent/config_loader.py[72-77]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 23, 2026

Persistent review updated to latest commit 4faffbe

Comment thread pr_agent/tools/pr_questions.py Outdated
else:
response, finish_reason = await self.ai_handler.chat_completion(
model=model, temperature=get_settings().config.temperature, system=system_prompt, user=user_prompt)
img_path = self.vars['img_path'] if 'img_path' in variables else None
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. self.vars['img_path'] may keyerror 📘 Rule violation ☼ Reliability

The new img_path assignment checks membership in variables but indexes into self.vars, which
can raise KeyError when those dicts are out of sync. This violates defensive input validation
expectations and can crash /ask flows.
Agent Prompt
## Issue description
`img_path` is derived by indexing `self.vars['img_path']` after checking `'img_path' in variables`, which can still raise `KeyError`.

## Issue Context
The key presence check is performed on a different dict (`variables`) than the one being indexed (`self.vars`).

## Fix Focus Areas
- pr_agent/tools/pr_questions.py[110-110]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread pr_agent/mcp/runtime.py
Comment on lines +249 to +254
content_length_value = headers.get("content-length")
if not content_length_value:
raise MCPRuntimeError(f"MCP server '{self.server_name}' response missing Content-Length")

content_length = int(content_length_value)
body = self.process.stdout.read(content_length)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

2. Invalid content-length can crash 📘 Rule violation ☼ Reliability

MCPStdioClient._read_message casts Content-Length to int without handling ValueError, which
can raise an unhandled exception on malformed server output. This violates robust error handling for
external inputs.
Agent Prompt
## Issue description
Parsing `Content-Length` can raise `ValueError` if the header is malformed, causing an unhandled exception.

## Issue Context
This code consumes external server output and should fail with a clear `MCPRuntimeError` using exception chaining.

## Fix Focus Areas
- pr_agent/mcp/runtime.py[249-262]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread pr_agent/mcp/runtime.py
Comment on lines +458 to +517
def _send_request(self, method: str, params: dict[str, Any]) -> dict[str, Any]:
self._request_id += 1
request_id = self._request_id
payload = {
"jsonrpc": "2.0",
"id": request_id,
"method": method,
"params": params,
}
try:
response = self._session.post(
self.url,
json=payload,
headers=self._build_extra_headers(),
timeout=self.timeout,
stream=True,
)
response.raise_for_status()
except requests.RequestException as exc:
raise MCPRuntimeError(
f"MCP streamable HTTP request failed for '{self.server_name}': {exc}"
) from exc

# Capture session ID returned on the initialize response.
session_id = response.headers.get("Mcp-Session-Id")
if session_id and not self._session_id:
self._session_id = session_id

content_type = response.headers.get("Content-Type", "")
if "text/event-stream" in content_type:
return self._parse_sse_response(response, request_id, method)
return self._parse_json_response(response, method)

def _parse_json_response(self, response: "requests.Response", method: str) -> dict[str, Any]:
try:
body = response.json()
except ValueError as exc:
raise MCPRuntimeError(
f"MCP streamable HTTP response is not valid JSON for '{self.server_name}'"
) from exc
return self._extract_result(body, method)

def _parse_sse_response(
self, response: "requests.Response", request_id: int, method: str
) -> dict[str, Any]:
"""Read an SSE stream and return the JSON-RPC result that matches *request_id*."""
try:
for raw_line in response.iter_lines(decode_unicode=True):
if not raw_line or not raw_line.startswith("data:"):
continue
data = raw_line[5:].lstrip(" ")
if not data:
continue
try:
message = json.loads(data)
except json.JSONDecodeError:
continue
if isinstance(message, dict) and message.get("id") == request_id:
return self._extract_result(message, method)
except requests.RequestException as exc:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

3. Streamable http response leak 🐞 Bug ☼ Reliability

MCPStreamableHttpClient._send_request() uses stream=True but never closes the
requests.Response, and _parse_sse_response() can return early on a matching event without
releasing the connection. This can leak connections and exhaust the session pool, breaking
subsequent MCP calls.
Agent Prompt
### Issue description
Streamable HTTP MCP requests are made with `stream=True` but the response object is never closed, which can leak connections.

### Issue Context
- `_send_request()` returns from parsing methods without ensuring the response is closed.
- `_parse_sse_response()` returns early on a match, which is especially likely to leak.

### Fix Focus Areas
- pr_agent/mcp/runtime.py[458-489]
- pr_agent/mcp/runtime.py[500-517]

### Suggested fix approach
- Ensure `response.close()` is called in a `finally` block in `_send_request()` after parsing completes.
- For SSE, close the response before returning a matching result (or structure `_send_request()` as:
  - create response
  - try parse
  - finally close response).
- Consider also explicitly consuming/closing SSE streams when returning early.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 23, 2026

Persistent review updated to latest commit ee26abf

Comment thread pr_agent/mcp/runtime.py
Comment on lines +210 to +215
def reader():
try:
response_holder["message"] = self._read_message()
except BaseException as exc: # noqa: BLE001
error_holder["error"] = exc

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. baseexception caught in stdio reader 📘 Rule violation ☼ Reliability

_read_message_with_timeout() catches BaseException, which can swallow critical control-flow
exceptions (e.g., KeyboardInterrupt, SystemExit) and violates targeted exception handling
guidance. This reduces reliability and observability of failures.
Agent Prompt
## Issue description
The stdio reader thread catches `BaseException`, which may swallow `SystemExit`/`KeyboardInterrupt` and violates the requirement to catch only expected exception types.

## Issue Context
Prefer catching `Exception` (or narrower) and converting expected failures into `MCPRuntimeError` with chaining where appropriate.

## Fix Focus Areas
- pr_agent/mcp/runtime.py[210-215]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 24, 2026

Persistent review updated to latest commit 5df9224

Comment thread pr_agent/mcp/runtime.py
Comment on lines +730 to +737
def _resolve_config_values(self, value: Any) -> Any:
if isinstance(value, str):
return os.path.expanduser(os.path.expandvars(value))
if isinstance(value, list):
return [self._resolve_config_values(item) for item in value]
if isinstance(value, dict):
return {key: self._resolve_config_values(item) for key, item in value.items()}
return value
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. resolve_env_vars setting ignored 📘 Rule violation ⚙ Maintainability

The MCP config setting mcp.resolve_env_vars is added but the runtime always expands environment
variables in _resolve_config_values(), making this behavior effectively hardcoded. This violates
the requirement that runtime behavior be configurable via .pr_agent.toml/pr_agent/settings/
rather than embedded in code.
Agent Prompt
## Issue description
`mcp.resolve_env_vars` is introduced in settings, but the MCP runtime always expands env vars and `~` paths in `_resolve_config_values()`. This makes the behavior non-configurable and inconsistent with the new setting.

## Issue Context
The setting exists in `pr_agent/settings/configuration.toml` as `resolve_env_vars = true`, implying users can disable it. The runtime should read `get_settings().get("MCP.RESOLVE_ENV_VARS", True)` (or equivalent) and only expand variables when enabled.

## Fix Focus Areas
- pr_agent/settings/configuration.toml[73-80]
- pr_agent/mcp/runtime.py[730-737]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +105 to +117
tool_name = str(parsed_response.get("tool", "")).strip()
arguments = parsed_response.get("arguments") or {}
if not tool_name:
self._logger.warning("MCP tool orchestration returned an empty tool name; aborting tool loop")
return response_text, finish_reason
if not isinstance(arguments, dict):
self._logger.warning("MCP tool orchestration arguments must be a JSON object; aborting tool loop")
return response_text, finish_reason

try:
tool_result = tool_executor(tool_name, arguments)
if inspect.isawaitable(tool_result):
tool_result = await tool_result
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

2. Tool allowlist bypass 🐞 Bug ⛨ Security

BaseAiHandler.chat_completion_with_tools executes whatever tool name the model returns without
validating it against the advertised tool catalog, allowing invocation of tools that were
intentionally omitted due to max_tools/max_schema_chars budgets.
This can bypass tool exposure controls and trigger unexpected tool calls on configured MCP servers.
Agent Prompt
### Issue description
`BaseAiHandler.chat_completion_with_tools()` executes the model-provided `tool` name without validating it against the tool catalog passed in via `tools`. With MCP enabled, `runtime.build_tool_schemas()` may omit tools due to `max_tools` / `max_schema_chars`, but `runtime.create_tool_executor()` can still call any tool on configured servers, so a model can bypass the catalog exposure/budget by naming an omitted tool.

### Issue Context
- The `tools` catalog is used only to generate the system prompt.
- The MCP executor ultimately calls `MCPRuntime.call_tool()` and does not check whether a tool was advertised.

### Fix approach
Implement an allowlist of tool names that were actually advertised and refuse (or return a controlled error result for) calls not in that allowlist.

Recommended implementation (defense-in-depth):
1. In `BaseAiHandler.chat_completion_with_tools`, derive `allowed_tool_names` from the `tools` argument (support both shapes used in repo/tests):
   - OpenAI-style: `tool["function"]["name"]`
   - Simple style: `tool["name"]`
2. Before calling `tool_executor`, verify `tool_name in allowed_tool_names`.
   - If not allowed: log a warning and set `tool_result` to a deterministic error like `"Tool not available: ..."` (and continue the loop / decrement the turn budget), or abort the loop.
3. Optionally, also wrap `runtime.create_tool_executor()` in `maybe_chat_completion_with_mcp` with an allowlist check to ensure enforcement even if other callers bypass the handler-level check.

### Fix Focus Areas
- pr_agent/algo/ai_handlers/base_ai_handler.py[58-129]
- pr_agent/mcp/integration.py[35-63]
- pr_agent/mcp/runtime.py[656-714]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 24, 2026

Persistent review updated to latest commit 7a7991a

Comment on lines +120 to +126
try:
tool_result = tool_executor(tool_name, arguments)
if inspect.isawaitable(tool_result):
tool_result = await tool_result
except Exception as exc: # noqa: BLE001
self._logger.warning("MCP tool '%s' raised an exception: %s", tool_name, exc)
tool_result = f"Tool error: {exc}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. Broad except exception in tool loop 📘 Rule violation ☼ Reliability

chat_completion_with_tools() catches Exception around tool_executor(...), which can mask
unexpected bugs and makes failures harder to debug. The compliance checklist requires targeted
exceptions and proper exception handling patterns.
Agent Prompt
## Issue description
`chat_completion_with_tools()` wraps tool execution with a broad `except Exception`, which can hide unexpected errors and violates the targeted-exception requirement.

## Issue Context
The tool executor is an external boundary and should only handle expected failure modes (e.g., runtime/tool errors, validation/type errors), while letting unexpected exceptions surface (or be wrapped with explicit chaining) for debuggability.

## Fix Focus Areas
- pr_agent/algo/ai_handlers/base_ai_handler.py[120-126]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread pr_agent/mcp/runtime.py
Comment on lines +613 to +626
def connect_server(self, server_name: str):
if not self.enabled:
raise MCPRuntimeError("MCP runtime is disabled")
if server_name in self._clients:
return

server_config = self._servers_config.get(server_name)
if not isinstance(server_config, dict):
raise MCPRuntimeError(f"MCP server '{server_name}' config must be an object")

client = self._build_client(server_name, self._resolve_config_values(server_config))
client.connect()
self._clients[server_name] = client
self._logger.info(f"Connected MCP server '{server_name}'")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

2. Session leak on connect fail 🐞 Bug ☼ Reliability

MCPRuntime.connect_server() calls client.connect() before registering the client in _clients;
if connect() raises (e.g., unreachable HTTP server), the created client is dropped and its
underlying requests.Session is never closed, leaking sockets/file descriptors over repeated
requests.
Agent Prompt
### Issue description
`MCPRuntime.connect_server()` can leak resources when `client.connect()` raises because the client is not yet tracked in `_clients`, so its `close()` method is never called.

### Issue Context
This is particularly impactful for `MCPHttpClient` / `MCPStreamableHttpClient`, which allocate a `requests.Session()` in `__init__` and rely on `close()` to release sockets.

### Fix Focus Areas
- pr_agent/mcp/runtime.py[613-626]
- pr_agent/mcp/runtime.py[288-319]
- pr_agent/mcp/runtime.py[398-436]

### Implementation direction
Wrap `client.connect()` in `try/except`, and call `client.close()` on failure before re-raising. Only add the client to `_clients` after a successful connect.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 24, 2026

Persistent review updated to latest commit 288e518

Comment thread pr_agent/mcp/runtime.py
Comment on lines +1 to +12
import json
import os
import subprocess
import threading
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Optional

import requests

from pr_agent.config_loader import get_settings

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. Missing requests dependency 🐞 Bug ☼ Reliability

pr_agent/mcp/runtime.py imports and uses the third-party requests library, but
requirements.txt does not declare it. In deployments that install only declared dependencies,
importing MCP integration will raise ImportError, breaking /ask, /review, and /improve even
if MCP is disabled at runtime.
Agent Prompt
### Issue description
`pr_agent/mcp/runtime.py` now imports and uses `requests`, but `requests` is not declared in the project dependencies. This can cause `ImportError: No module named 'requests'` in clean/strict installs and break PR-Agent commands that import MCP integration.

### Issue Context
- `pr_agent/mcp/integration.py` imports `MCPRuntime` from `pr_agent/mcp/runtime.py`.
- Core tools (e.g., reviewer/questions/code suggestions) import `maybe_chat_completion_with_mcp`, so the runtime import happens on module import, not only when MCP is enabled.

### Fix Focus Areas
- requirements.txt[1-45]
- pr_agent/mcp/runtime.py[1-12]
- pr_agent/mcp/integration.py[1-6]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 24, 2026

Persistent review updated to latest commit d00b274

Comment thread pr_agent/mcp/runtime.py
Comment on lines +83 to +112
def __init__(self, server_name: str, config: dict[str, Any]):
super().__init__(server_name, config)
self.process: Optional[subprocess.Popen] = None
self._request_id = 0
self.timeout = float(config.get("timeout", 30))

def connect(self):
command = self.config.get("command")
if not command:
raise MCPRuntimeError(f"Stdio MCP server '{self.server_name}' is missing 'command'")

args = self.config.get("args") or []
if not isinstance(args, list):
raise MCPRuntimeError(f"Stdio MCP server '{self.server_name}' args must be a list")

env = os.environ.copy()
server_env = self.config.get("env") or {}
if not isinstance(server_env, dict):
raise MCPRuntimeError(f"Stdio MCP server '{self.server_name}' env must be an object")
env.update({str(k): str(v) for k, v in server_env.items()})

cwd = self.config.get("cwd")
self.process = subprocess.Popen(
[command, *args],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
env=env,
cwd=cwd,
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. Unhandled mcp config types 🐞 Bug ☼ Reliability

MCP client initialization can raise uncaught ValueError/TypeError for malformed config (e.g.,
non-numeric timeout or non-string args elements), which are not wrapped as MCPRuntimeError and
are not caught in tool discovery/orchestration. This can crash MCP tool discovery
(build_tool_schemas) and therefore break /ask, /review, and /improve when MCP is enabled.
Agent Prompt
### Issue description
Malformed MCP server config values can raise low-level exceptions (`ValueError`/`TypeError`) during client construction/connection (e.g., `float(timeout)` or invalid `args` element types). These are not consistently wrapped as `MCPRuntimeError` and can escape tool discovery/orchestration, breaking MCP-enabled commands.

### Issue Context
- `MCPStdioClient.__init__` uses `float(config.get("timeout", 30))`.
- `MCPStdioClient.connect` passes `[command, *args]` to `subprocess.Popen` after only validating `args` is a list.
- `list_all_tools` only catches `MCPRuntimeError` and `OSError`.
- `maybe_chat_completion_with_mcp` does not catch exceptions around `runtime.build_tool_schemas(...)`.

### Fix Focus Areas
- pr_agent/mcp/runtime.py[82-112]
- pr_agent/mcp/runtime.py[288-301]
- pr_agent/mcp/runtime.py[645-655]
- pr_agent/mcp/integration.py[24-43]

### Implementation notes
- Wrap timeout parsing in `try/except (TypeError, ValueError)` and raise `MCPRuntimeError` with server context.
- Validate/coerce `args` list items to `str` (or `os.PathLike`) before calling `Popen`; raise `MCPRuntimeError` on invalid types.
- Ensure discovery paths only need to catch `MCPRuntimeError` (i.e., normalize client-side errors into MCPRuntimeError), or broaden the catch in `list_all_tools` to prevent crashes while logging the failure.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Introduce server-side MCP config loading from /etc/pr-agent/mcp.json or MCP_CONFIG_PATH, including JSONC parsing and VS Code / Claude schema normalization. Add the MCP runtime, HTTP and stdio clients, structured tool-calling orchestration on the base AI handler, and wire /ask, /review, and /improve through the MCP-aware integration helper. Expose MCP runtime status in /config output, document the configuration flow and AWS Knowledge example, and add focused tests for config loading, runtime behavior, tool orchestration, integration, and discovery.
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented Apr 24, 2026

Persistent review updated to latest commit f7fa86b

Comment on lines +1 to +4
import pytest

from pr_agent.algo.ai_handlers.base_ai_handler import BaseAiHandler

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

1. Unused pytest import 📘 Rule violation ⚙ Maintainability

tests/unittest/test_ai_handler_tool_orchestration.py imports pytest but never uses it, which
will be flagged by repository linting and may fail CI. This violates the requirement to keep
modified files compliant with lint/format tooling.
Agent Prompt
## Issue description
The new unit test file imports `pytest` but does not use it, which will trigger unused-import lint errors.

## Issue Context
`pytest` is not referenced in this file (no `pytest.mark`, `pytest.raises`, etc.).

## Fix Focus Areas
- tests/unittest/test_ai_handler_tool_orchestration.py[1-4]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +64 to +71
tool_call_example = json.dumps(
{
"type": "tool_call",
"tool": "server.tool",
"arguments": {"example": "value"},
},
separators=(",", ":"),
).replace("{\"example\":\"value\"}", "{...}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

2. Tool prompt invalid json 🐞 Bug ≡ Correctness

BaseAiHandler.chat_completion_with_tools tells the model to output JSON “exactly in this shape”, but
the tool-call example is mutated to include {...}, which is not valid JSON and can lead to
unparsable tool calls so the tool loop silently falls back to plain text responses.
Agent Prompt
### Issue description
`chat_completion_with_tools()` instructs the model to output JSON exactly matching an example, but the example is not valid JSON because it includes `{...}`. This can cause the model to emit invalid JSON tool calls that `_parse_tool_or_final_response()` cannot parse, disabling tool orchestration.

### Issue Context
The prompt text says “ONLY a JSON object exactly in this shape”, so the example must itself be valid JSON.

### Fix Focus Areas
- pr_agent/algo/ai_handlers/base_ai_handler.py[63-88]
- pr_agent/algo/ai_handlers/base_ai_handler.py[179-196]

### What to change
- Remove the `.replace(..., "{...}")` so the example remains valid JSON (e.g., keep `{"arguments":{"example":"value"}}`).
- If you want to communicate “arbitrary arguments”, adjust the instruction text (in plain English) rather than embedding invalid JSON.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +35 to +43
max_tools = _get_tool_budget("MCP.MAX_TOOL_CATALOG_TOOLS", 12)
max_schema_chars = _get_tool_budget("MCP.MAX_TOOL_CATALOG_SCHEMA_CHARS", 12000)

tools = runtime.build_tool_schemas(
max_tools=max_tools,
max_schema_chars=max_schema_chars,
include_server_prefix=True,
)
if not tools:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Action required

3. Blocking tool discovery i/o 🐞 Bug ➹ Performance

maybe_chat_completion_with_mcp calls runtime.build_tool_schemas() directly in an async path, but
build_tool_schemas() performs synchronous tool discovery (connect + list_tools) using
requests/subprocess, which can block the event loop and delay webhook/background task processing.
Agent Prompt
### Issue description
Tool discovery (connect + tools/list) is executed synchronously inside `maybe_chat_completion_with_mcp()`’s async flow. Because discovery uses `requests` and (for stdio) `subprocess`, it can block the event loop and stall handling of other requests/background tasks.

### Issue Context
Tool execution is already offloaded via `create_tool_executor()` using `run_in_executor`, but discovery is not.

### Fix Focus Areas
- pr_agent/mcp/integration.py[15-75]
- pr_agent/mcp/runtime.py[672-712]
- pr_agent/mcp/runtime.py[342-409]

### What to change
Choose one:
1) In `maybe_chat_completion_with_mcp`, wrap `runtime.build_tool_schemas(...)` in `await loop.run_in_executor(...)`.
2) Add an async discovery method on `MCPRuntime` (e.g., `async_build_tool_schemas`) that runs `list_all_tools()` / `build_tool_schemas()` in an executor.

Ensure both connect/list_tools and schema-building are included in the offloaded work so no requests/subprocess calls happen on the event loop thread.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant