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 (15)

Grey Divider


Action required

1. Unused io import 📘 Rule violation ⚙ Maintainability ⭐ New
Description
io is imported but never used, which will be flagged by Ruff and can fail CI pre-commit checks.
This adds unnecessary noise and can mask other lint issues.
Code

tests/unittest/test_mcp_runtime.py[1]

+import io
Evidence
The compliance checklist requires new/updated code to remain lint-clean (no unused imports). The
file imports io but does not reference it anywhere in the added test module.

tests/unittest/test_mcp_runtime.py[1-1]
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
`tests/unittest/test_mcp_runtime.py` imports `io` but never uses it, which violates lint/pre-commit hygiene.

## Issue Context
Ruff will flag this as an unused import and may fail CI.

## Fix Focus Areas
- tests/unittest/test_mcp_runtime.py[1-7]

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


2. MCPStdioClient.connect catches Exception 📘 Rule violation ☼ Reliability ⭐ New
Description
MCPStdioClient.connect() uses a broad except Exception which can mask unexpected failures and
violates the targeted-exception requirement. This reduces debuggability and can hide programming
errors.
Code

pr_agent/mcp/runtime.py[R135-152]

+        try:
+            init_result = self._send_request(
+                "initialize",
+                {
+                    "protocolVersion": self.config.get("protocol_version", "2024-11-05"),
+                    "capabilities": self.config.get("client_capabilities", {}),
+                    "clientInfo": self.config.get(
+                        "client_info",
+                        {"name": "pr-agent", "version": "mcp-runtime"},
+                    ),
+                },
+            )
+            self.server_capabilities = init_result.get("capabilities", {})
+            self._send_notification("notifications/initialized", {})
+        except Exception:
+            self._terminate_process()
+            self.process = None
+            raise
Evidence
The checklist requires targeted exception handling and discourages broad except Exception. The new
MCP stdio connection code catches Exception broadly during initialization.

pr_agent/mcp/runtime.py[135-152]
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
`MCPStdioClient.connect()` catches `Exception` broadly during initialization, which can swallow unexpected errors.

## Issue Context
This code path is a recoverable flow where specific exception types are expected (I/O, subprocess, MCP protocol/runtime errors). The current pattern reduces debuggability and violates the compliance rule.

## Fix Focus Areas
- pr_agent/mcp/runtime.py[135-152]

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


3. connect_server catches Exception 📘 Rule violation ☼ Reliability ⭐ New
Description
MCPRuntime.connect_server() catches a broad Exception when connecting a client, which can hide
unexpected errors and violates the targeted-exception requirement. It should catch only expected
connection-related exception types (and still perform cleanup).
Code

pr_agent/mcp/runtime.py[R648-653]

+        client = self._build_client(server_name, self._resolve_config_values(server_config))
+        try:
+            client.connect()
+        except Exception:
+            client.close()
+            raise
Evidence
The compliance checklist requires targeted exception handling. The new MCP runtime connection code
uses except Exception for cleanup, which is overly broad for a recoverable connect flow.

pr_agent/mcp/runtime.py[648-653]
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
`MCPRuntime.connect_server()` uses `except Exception` around `client.connect()`, which is too broad and can mask bugs.

## Issue Context
Cleanup (`client.close()`) is important, but the handler should only catch expected exception types (e.g., MCPRuntimeError, OSError / transport errors) and preserve context.

## Fix Focus Areas
- pr_agent/mcp/runtime.py[648-653]

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


View more (21)
4. Bool flags misparsed 🐞 Bug ≡ Correctness ⭐ New
Description
apply_mcp_server_config() and MCPRuntime convert settings with bool(...), so string values like
"false" become True and can unintentionally enable MCP or force FAIL_ON_INVALID_CONFIG behavior.
Since the repo explicitly disables Dynaconf auto-casting, environment-provided overrides can flip
MCP behavior and cause unexpected load failures.
Code

pr_agent/config_loader.py[R233-248]

+    config_path = _resolve_mcp_config_path()
+    fail_on_invalid = bool(get_settings().get("MCP.FAIL_ON_INVALID_CONFIG", False))
+    try:
+        if not config_path.exists():
+            logger.debug(f"MCP config file not found, skipping load: {config_path}")
+            return
+        config_data = load_mcp_server_config(config_path)
+        settings = get_settings()
+        settings.set("MCP.SERVERS", config_data["servers"], merge=False)
+        settings.set("MCP.SERVER_CONFIG", config_data, merge=False)
+        settings.set("MCP.ACTIVE_CONFIG_PATH", str(config_path), merge=False)
+        logger.info(f"Loaded MCP server configuration from {config_path}")
+    except (ValueError, OSError, FileNotFoundError) as exc:
+        logger.error(f"Failed to load MCP server configuration from {config_path}: {exc}")
+        if fail_on_invalid:
+            raise
Evidence
The project sets AUTO_CAST_FOR_DYNACONF=false, which means Dynaconf may surface env overrides as
strings; bool("false") evaluates to True. The PR adds MCP logic that uses
bool(get_settings().get(...)) for MCP flags, so an env override like
MCP_FAIL_ON_INVALID_CONFIG="false" can be treated as True, and MCP_ENABLED="false" can be treated as
enabled.

pr_agent/log/init.py[1-2]
pr_agent/git_providers/utils.py[14-17]
pr_agent/config_loader.py[231-248]
pr_agent/mcp/runtime.py[604-626]

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 flag settings are parsed via `bool(...)`, which treats any non-empty string (including `'false'`) as `True`. Because this repo disables Dynaconf auto-casting, environment overrides can be strings and will be misinterpreted.

### Issue Context
This can unexpectedly enable MCP, expand env vars, or force `FAIL_ON_INVALID_CONFIG` behavior.

### Fix Focus Areas
- pr_agent/config_loader.py[231-248]
- pr_agent/mcp/runtime.py[604-626]

### Suggested fix
- Introduce a small helper like `_coerce_bool(value, default)` that:
 - returns the value if it is already a `bool`
 - if `str`, maps common values (`"true"/"1"/"yes"/"on"` => True; `"false"/"0"/"no"/"off"/""` => False)
 - otherwise falls back to `default` or `bool(value)` as appropriate
- Use it for `MCP.FAIL_ON_INVALID_CONFIG`, `MCP.RESOLVE_ENV_VARS`, and `MCP.ENABLED`.

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


5. _resolve_mcp_config_path() accepts None 📘 Rule violation ☼ Reliability
Description
_resolve_mcp_config_path() converts an unset MCP.CONFIG_PATH setting to Path('None'), which
can cause confusing runtime behavior and prevent MCP config loading from working as intended.
Settings-derived values should be defaulted/validated before use.
Code

pr_agent/config_loader.py[R194-200]

+def _resolve_mcp_config_path() -> Path:
+    env_path = os.getenv(MCP_CONFIG_ENV_VAR)
+    if env_path:
+        return Path(env_path).expanduser()
+    configured_path = get_settings().get("MCP.CONFIG_PATH")
+    return Path(str(configured_path)).expanduser()
+
Evidence
PR Compliance ID 15 requires validating/normalizing settings values before use. The added
_resolve_mcp_config_path() reads MCP.CONFIG_PATH without a default and then does
Path(str(configured_path)), so a missing setting becomes the literal path None rather than a
valid config path.

pr_agent/config_loader.py[194-200]
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
`_resolve_mcp_config_path()` can return `Path('None')` when `MCP.CONFIG_PATH` is unset/None, instead of a sensible default path.
## Issue Context
This path is used by `apply_mcp_server_config()` to decide whether to load MCP servers; a `None`-derived path can silently disable expected behavior.
## Fix Focus Areas
- pr_agent/config_loader.py[194-200]

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


6. Dot server name ambiguity 🐞 Bug ≡ Correctness
Description
MCP tool names are constructed as "<server>.<tool>", but _split_tool_name splits on the first '.',
so servers whose names contain '.' will be parsed incorrectly and tool calls can be routed to the
wrong server/tool or fail when multiple servers are configured.
Code

pr_agent/mcp/runtime.py[R775-787]

+    def _split_tool_name(self, tool_name: str) -> tuple[str, str]:
+        if "." in tool_name:
+            server_name, tool_short_name = tool_name.split(".", 1)
+            if server_name and tool_short_name:
+                return server_name, tool_short_name
+
+        if len(self._servers_config) == 1:
+            server_name = next(iter(self._servers_config.keys()))
+            return server_name, tool_name
+
+        raise MCPRuntimeError(
+            f"Tool name '{tool_name}' must use the '<server>.<tool>' form when multiple MCP servers are configured"
+        )
Evidence
Tool names embed the raw server name using a dot separator, while parsing assumes the first dot
separates server/tool. This makes names ambiguous when the server name itself contains dots.

pr_agent/mcp/runtime.py[47-56]
pr_agent/mcp/runtime.py[775-787]

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 naming uses `f"{server_name}.{tool_name}"` and later parses tool calls via `split(".", 1)`. If a configured server name contains a dot (e.g., `"my.server"`), tool calls like `"my.server.echo"` are misparsed as server=`"my"`, tool=`"server.echo"`, causing wrong routing or failures.
## Issue Context
This affects both schema advertisement (`MCPToolDefinition.to_openai_tool`) and execution (`MCPRuntime._split_tool_name`).
## Fix Focus Areas
- pr_agent/mcp/runtime.py[47-56]
- pr_agent/mcp/runtime.py[775-787]
## Implementation direction
- Introduce an unambiguous encoding or mapping from advertised tool name -> (server_name, tool_name).
- Example approaches: maintain a dict built during `build_tool_schemas()` mapping the exact advertised function name to server/tool; or escape/encode server names before concatenation and reverse it in `_split_tool_name`.
- Add a unit test covering a server name containing '.' to prevent regressions.

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


7. Unused pytest import 📘 Rule violation ⚙ Maintainability
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


8. Tool prompt invalid JSON 🐞 Bug ≡ Correctness
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


9. Blocking tool discovery I/O 🐞 Bug ➹ Performance
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


10. 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


11. 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


12. 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


13. 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


14. 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


15. 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


16. 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


17. 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


18. 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


19. 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


20. 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


21. 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


22. 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 d...

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

@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

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

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

Persistent review updated to latest commit 04c884d

Comment thread pr_agent/config_loader.py
Comment on lines +194 to +200
def _resolve_mcp_config_path() -> Path:
env_path = os.getenv(MCP_CONFIG_ENV_VAR)
if env_path:
return Path(env_path).expanduser()
configured_path = get_settings().get("MCP.CONFIG_PATH")
return Path(str(configured_path)).expanduser()

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_mcp_config_path() accepts none 📘 Rule violation ☼ Reliability

_resolve_mcp_config_path() converts an unset MCP.CONFIG_PATH setting to Path('None'), which
can cause confusing runtime behavior and prevent MCP config loading from working as intended.
Settings-derived values should be defaulted/validated before use.
Agent Prompt
## Issue description
`_resolve_mcp_config_path()` can return `Path('None')` when `MCP.CONFIG_PATH` is unset/None, instead of a sensible default path.

## Issue Context
This path is used by `apply_mcp_server_config()` to decide whether to load MCP servers; a `None`-derived path can silently disable expected behavior.

## Fix Focus Areas
- pr_agent/config_loader.py[194-200]

ⓘ 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 +775 to +787
def _split_tool_name(self, tool_name: str) -> tuple[str, str]:
if "." in tool_name:
server_name, tool_short_name = tool_name.split(".", 1)
if server_name and tool_short_name:
return server_name, tool_short_name

if len(self._servers_config) == 1:
server_name = next(iter(self._servers_config.keys()))
return server_name, tool_name

raise MCPRuntimeError(
f"Tool name '{tool_name}' must use the '<server>.<tool>' form when multiple MCP servers are configured"
)
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. Dot server name ambiguity 🐞 Bug ≡ Correctness

MCP tool names are constructed as "<server>.<tool>", but _split_tool_name splits on the first '.',
so servers whose names contain '.' will be parsed incorrectly and tool calls can be routed to the
wrong server/tool or fail when multiple servers are configured.
Agent Prompt
## Issue description
MCP tool naming uses `f"{server_name}.{tool_name}"` and later parses tool calls via `split(".", 1)`. If a configured server name contains a dot (e.g., `"my.server"`), tool calls like `"my.server.echo"` are misparsed as server=`"my"`, tool=`"server.echo"`, causing wrong routing or failures.

## Issue Context
This affects both schema advertisement (`MCPToolDefinition.to_openai_tool`) and execution (`MCPRuntime._split_tool_name`).

## Fix Focus Areas
- pr_agent/mcp/runtime.py[47-56]
- pr_agent/mcp/runtime.py[775-787]

## Implementation direction
- Introduce an unambiguous encoding or mapping from advertised tool name -> (server_name, tool_name).
  - Example approaches: maintain a dict built during `build_tool_schemas()` mapping the exact advertised function name to server/tool; or escape/encode server names before concatenation and reverse it in `_split_tool_name`.
- Add a unit test covering a server name containing '.' to prevent regressions.

ⓘ 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 28, 2026

Persistent review updated to latest commit 6f0b13d

@@ -0,0 +1,388 @@
import io
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 io import 📘 Rule violation ⚙ Maintainability

io is imported but never used, which will be flagged by Ruff and can fail CI pre-commit checks.
This adds unnecessary noise and can mask other lint issues.
Agent Prompt
## Issue description
`tests/unittest/test_mcp_runtime.py` imports `io` but never uses it, which violates lint/pre-commit hygiene.

## Issue Context
Ruff will flag this as an unused import and may fail CI.

## Fix Focus Areas
- tests/unittest/test_mcp_runtime.py[1-7]

ⓘ 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 +135 to +152
try:
init_result = self._send_request(
"initialize",
{
"protocolVersion": self.config.get("protocol_version", "2024-11-05"),
"capabilities": self.config.get("client_capabilities", {}),
"clientInfo": self.config.get(
"client_info",
{"name": "pr-agent", "version": "mcp-runtime"},
),
},
)
self.server_capabilities = init_result.get("capabilities", {})
self._send_notification("notifications/initialized", {})
except Exception:
self._terminate_process()
self.process = None
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

2. mcpstdioclient.connect catches exception 📘 Rule violation ☼ Reliability

MCPStdioClient.connect() uses a broad except Exception which can mask unexpected failures and
violates the targeted-exception requirement. This reduces debuggability and can hide programming
errors.
Agent Prompt
## Issue description
`MCPStdioClient.connect()` catches `Exception` broadly during initialization, which can swallow unexpected errors.

## Issue Context
This code path is a recoverable flow where specific exception types are expected (I/O, subprocess, MCP protocol/runtime errors). The current pattern reduces debuggability and violates the compliance rule.

## Fix Focus Areas
- pr_agent/mcp/runtime.py[135-152]

ⓘ 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 +648 to +653
client = self._build_client(server_name, self._resolve_config_values(server_config))
try:
client.connect()
except Exception:
client.close()
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

3. connect_server catches exception 📘 Rule violation ☼ Reliability

MCPRuntime.connect_server() catches a broad Exception when connecting a client, which can hide
unexpected errors and violates the targeted-exception requirement. It should catch only expected
connection-related exception types (and still perform cleanup).
Agent Prompt
## Issue description
`MCPRuntime.connect_server()` uses `except Exception` around `client.connect()`, which is too broad and can mask bugs.

## Issue Context
Cleanup (`client.close()`) is important, but the handler should only catch expected exception types (e.g., MCPRuntimeError, OSError / transport errors) and preserve context.

## Fix Focus Areas
- pr_agent/mcp/runtime.py[648-653]

ⓘ 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 +233 to +248
config_path = _resolve_mcp_config_path()
fail_on_invalid = bool(get_settings().get("MCP.FAIL_ON_INVALID_CONFIG", False))
try:
if not config_path.exists():
logger.debug(f"MCP config file not found, skipping load: {config_path}")
return
config_data = load_mcp_server_config(config_path)
settings = get_settings()
settings.set("MCP.SERVERS", config_data["servers"], merge=False)
settings.set("MCP.SERVER_CONFIG", config_data, merge=False)
settings.set("MCP.ACTIVE_CONFIG_PATH", str(config_path), merge=False)
logger.info(f"Loaded MCP server configuration from {config_path}")
except (ValueError, OSError, FileNotFoundError) as exc:
logger.error(f"Failed to load MCP server configuration from {config_path}: {exc}")
if fail_on_invalid:
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

4. Bool flags misparsed 🐞 Bug ≡ Correctness

apply_mcp_server_config() and MCPRuntime convert settings with bool(...), so string values like
"false" become True and can unintentionally enable MCP or force FAIL_ON_INVALID_CONFIG behavior.
Since the repo explicitly disables Dynaconf auto-casting, environment-provided overrides can flip
MCP behavior and cause unexpected load failures.
Agent Prompt
### Issue description
MCP flag settings are parsed via `bool(...)`, which treats any non-empty string (including `'false'`) as `True`. Because this repo disables Dynaconf auto-casting, environment overrides can be strings and will be misinterpreted.

### Issue Context
This can unexpectedly enable MCP, expand env vars, or force `FAIL_ON_INVALID_CONFIG` behavior.

### Fix Focus Areas
- pr_agent/config_loader.py[231-248]
- pr_agent/mcp/runtime.py[604-626]

### Suggested fix
- Introduce a small helper like `_coerce_bool(value, default)` that:
  - returns the value if it is already a `bool`
  - if `str`, maps common values (`"true"/"1"/"yes"/"on"` => True; `"false"/"0"/"no"/"off"/""` => False)
  - otherwise falls back to `default` or `bool(value)` as appropriate
- Use it for `MCP.FAIL_ON_INVALID_CONFIG`, `MCP.RESOLVE_ENV_VARS`, and `MCP.ENABLED`.

ⓘ 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