Skip to content

feat: implement MCP architecture review spec#66

Open
petems wants to merge 5 commits intomasterfrom
petems/mcp-arch-audit
Open

feat: implement MCP architecture review spec#66
petems wants to merge 5 commits intomasterfrom
petems/mcp-arch-audit

Conversation

@petems
Copy link
Copy Markdown
Owner

@petems petems commented Mar 4, 2026

Implements the MCP architecture review spec by adding canonical read-only MCP tools with strict schemas, annotations, structured outputs, and normalized actionable error envelopes while preserving legacy tool compatibility.
The server now supports cursor-based single-page comment listing (github_list_pr_review_comments) and structured PR URL resolution (github_resolve_open_pr_url), backed by new pagination and error helper modules.
The changes also add strict Pydantic models for tool I/O, list response envelope fields (items, nextCursor, total), and filtering support (author, path_prefix, include_resolved).
Coverage includes a new contract-focused test suite plus full quality validation (ruff format, ruff check --fix, mypy, compile-check, pytest) passing in this branch.

Summary by CodeRabbit

  • New Features

    • List and filter PR review comments with cursor-based pagination
    • Resolve open GitHub PR URLs via a new tool
    • Improved structured error responses with actionable next steps
  • Documentation

    • Added a comprehensive MCP architecture verification plan
  • Tests

    • Added integration tests covering the new tools and architecture verification workflow

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 4, 2026

📝 Walkthrough

Walkthrough

Adds an MCP Architecture Review verification plan and implements structured error handling, cursor-based pagination, new Pydantic models, two GitHub PR review tools (list_pr_review_comments, resolve_open_pr_url), pagination helpers, server-side fetch logic, and tests validating schemas and error normalization (≤50 words).

Changes

Cohort / File(s) Summary
Verification Plan
specs/MCP-ARCH-REVIEW.md
New architecture review verification plan with scope, preconditions, verification phases, evidence collection, report template, fail conditions, and execution order.
Error Handling
src/mcp_github_pr_review/errors.py
New frozen dataclass ToolExecutionError with fields code, message, next_steps and to_model() conversion to ToolErrorDetailsModel.
Data Models
src/mcp_github_pr_review/models.py
Adds Pydantic v2 models: GithubListPrReviewCommentsArgs (StrictInt limit, normalization), GithubResolveOpenPrUrlResult, PaginatedReviewCommentsResult (nextCursor alias), ToolErrorDetailsModel, ToolErrorEnvelopeModel.
Pagination Utilities
src/mcp_github_pr_review/pagination.py
Adds encode_cursor and decode_cursor for base64-url-safe cursor encoding/decoding with HTTP URL validation and error wrapping.
Server Implementation
src/mcp_github_pr_review/server.py
Extends server: new tools (github_list_pr_review_comments, github_resolve_open_pr_url), fetch_pr_comments_page, _extract_next_link, cursor-based pagination, structured error envelopes, ToolCallResult union, improved arg validation, and retry/auth fallback logic.
Tests
tests/test_mcp_arch_review.py
New integration tests covering tool metadata, argument normalization/validation, structured error envelopes, pagination outputs, and mocked internal calls.
Docs / CI / Misc
.github/*, README.md, CLAUDE.md, SECURITY.md, TEST_COVERAGE_SUMMARY.md, UV_COMMANDS.md, Makefile, .gitignore
Minor documentation updates, pre-commit notes, formatting/whitespace tweaks, and workflow whitespace fixes.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Server as PRReviewServer
    participant GitHub as GitHub API

    Client->>Server: handle_call_tool(github_list_pr_review_comments, args)
    Server->>Server: validate & normalize args (limit, author, path_prefix, cursor)
    alt invalid args
        Server->>Client: ToolErrorEnvelopeModel (code, message, next_steps)
    else
        Server->>Server: if PR URL needed -> call github_resolve_open_pr_url
        Server->>GitHub: fetch_pr_comments_page(owner, repo, pull_number, page_size)
        GitHub-->>Server: comments + Link header (next URL)
        Server->>Server: parse Link header -> encode_cursor(next_url)
        Server->>Client: PaginatedReviewCommentsResult(items, nextCursor, total)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 Hopping through the code with a cheerful whirl,
Cursors tucked like secrets in a tiny curl.
Errors neatly boxed, models tidy and bright,
Tests clap their paws — the review tools take flight! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: implement MCP architecture review spec' clearly and specifically summarizes the main change—implementing a new MCP architecture review specification with new tools, models, and validation infrastructure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch petems/mcp-arch-audit

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
src/mcp_github_pr_review/models.py (1)

301-305: Redundant serialization_alias when alias is already set.

In Pydantic v2, alias applies to both validation and serialization by default. Since both are set to "nextCursor", the serialization_alias is redundant.

♻️ Simplified field definition
     next_cursor: str | None = Field(
         default=None,
         alias="nextCursor",
-        serialization_alias="nextCursor",
     )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_github_pr_review/models.py` around lines 301 - 305, The Field for
next_cursor (next_cursor: str | None = Field(..., alias="nextCursor",
serialization_alias="nextCursor")) redundantly sets serialization_alias
identical to alias; remove the serialization_alias argument and rely on alias
(Pydantic v2 uses alias for both validation and serialization) so update the
Field call for next_cursor to only include alias="nextCursor".
src/mcp_github_pr_review/errors.py (1)

14-14: Consider typing code as a Literal to match ToolErrorDetailsModel.

The code field is typed as str, but ToolErrorDetailsModel.code only accepts specific literal values ("invalid_arguments", "auth", "not_found", "rate_limited", "upstream", "internal_error"). Passing an invalid code will cause a Pydantic ValidationError at runtime in to_model(). Aligning the type here would catch mismatches statically.

♻️ Suggested improvement
+from typing import Literal
+
+ErrorCode = Literal[
+    "invalid_arguments",
+    "auth",
+    "not_found",
+    "rate_limited",
+    "upstream",
+    "internal_error",
+]
+
 `@dataclass`(frozen=True)
 class ToolExecutionError(Exception):
     """Actionable error information for canonical MCP tool responses."""

-    code: str
+    code: ErrorCode
     message: str
     next_steps: list[str] = field(default_factory=list)

     def to_model(self) -> ToolErrorDetailsModel:
         """Convert error details to the normalized response model."""
         return ToolErrorDetailsModel(
-            code=self.code,  # type: ignore[arg-type]
+            code=self.code,
             message=self.message,
             next_steps=self.next_steps,
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_github_pr_review/errors.py` at line 14, The field "code" is currently
annotated as str but must be restricted to the exact set of values used by
ToolErrorDetailsModel to avoid runtime Pydantic ValidationError; change the
annotation for "code" to a Literal containing "invalid_arguments", "auth",
"not_found", "rate_limited", "upstream", and "internal_error" (import Literal
from typing), update any constructors/usages that build this error to use those
literal values, and ensure the to_model() conversion still maps directly to
ToolErrorDetailsModel without casting.
tests/test_mcp_arch_review.py (1)

129-129: Misleading return type annotation on test helper.

The fail function declares -> PaginatedReviewCommentsResult but always raises ToolExecutionError. While this works because AsyncMock uses it correctly, consider using NoReturn for clarity.

♻️ Suggested fix
+from typing import Any, NoReturn
...
-    async def fail(*args: Any, **kwargs: Any) -> PaginatedReviewCommentsResult:  # noqa: ARG001
+    async def fail(*args: Any, **kwargs: Any) -> NoReturn:  # noqa: ARG001
         raise ToolExecutionError(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_mcp_arch_review.py` at line 129, The test helper function fail
currently declares a misleading return type PaginatedReviewCommentsResult but
always raises ToolExecutionError; update the signature of fail (the async def
fail helper) to return typing.NoReturn instead, add an import for NoReturn from
typing at the top of the test module, and keep the body raising
ToolExecutionError unchanged so static type checkers and readers correctly
understand it never returns.
src/mcp_github_pr_review/server.py (1)

1922-1930: Placeholder values used when cursor is provided.

When a cursor is supplied, the code passes placeholder strings ("placeholder-owner", "placeholder-repo", pull_number=0) to fetch_pr_comments_page. While these are ignored when cursor is valid (since the URL is decoded from the cursor), they would appear in error messages or logs if the cursor is malformed.

Consider preserving actual values from the initial request or extracting them from the cursor URL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_github_pr_review/server.py` around lines 1922 - 1930, The code calls
fetch_pr_comments_page with placeholder values ("placeholder-owner",
"placeholder-repo", pull_number=0) when a cursor is provided; replace those
placeholders so real context is preserved by passing request_owner,
request_repo, and pull_number (the original request values) instead of
placeholders, and add a small fallback that attempts to extract
owner/repo/pull_number from the decoded cursor URL only if request_* values are
empty or None and handle malformed/invalid cursor parsing errors so they don't
produce misleading logs; update the call site for fetch_pr_comments_page and the
cursor-parsing logic accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@specs/MCP-ARCH-REVIEW.md`:
- Around line 155-166: The spec's module layout (client/, tools/, schemas/,
errors/, pagination/, types/) does not match the actual package layout (e.g.,
modules errors.py, pagination.py, models.py, server.py under
src/mcp_github_pr_review); update the MCP-ARCH-REVIEW.md to either reflect the
current flat structure (listing errors.py, pagination.py, models.py, server.py
and their responsibilities) or explicitly mark the spec as a future-refactor
target and state the intended target layout; while editing, call out two
concrete expectations to preserve: ensure runtime config validation happens once
at startup (reference server.py bootstrap/startup path) and remove/centralize
any duplicate HTTP request logic (search for duplicated request wrappers across
modules such as client-like logic in server.py or models.py) so verifiers know
what to check.

In `@src/mcp_github_pr_review/pagination.py`:
- Around line 16-19: The exception handler around base64.urlsafe_b64decode
should also catch binascii.Error so invalid base64 input is normalized to
ValueError("Invalid pagination cursor"); update the except tuple to include
binascii.Error (e.g., except (ValueError, UnicodeDecodeError, binascii.Error) as
exc) and add an import for binascii at the top of the module so the symbol is
available; keep the raised ValueError from the same block and use the existing
exc variable for exception chaining.

In `@src/mcp_github_pr_review/server.py`:
- Around line 1943-1969: The current code applies
include_resolved/author/path_prefix filtering after a single fetch which breaks
pagination guarantees (variables: comments, filtered_comments, include_resolved,
author, path_prefix, next_cursor, limit, ReviewCommentModel,
PaginatedReviewCommentsResult); change the logic to perform server-side
accumulation: keep fetching subsequent GitHub pages (using the same fetch loop
that produced comments and next_cursor) and apply the filters as you accumulate
until you have limit matching items or a max_fetch_pages cap to avoid unbounded
fetching, then convert those accumulated filtered items via
ReviewCommentModel.model_validate and return them with next_cursor set to the
actual next page token and total reflecting the count of matching items (or a
consistent sentinel if you cannot compute full total). Ensure the fetch loop and
pagination token handling are updated to reference the same next_cursor used in
the external API call.

---

Nitpick comments:
In `@src/mcp_github_pr_review/errors.py`:
- Line 14: The field "code" is currently annotated as str but must be restricted
to the exact set of values used by ToolErrorDetailsModel to avoid runtime
Pydantic ValidationError; change the annotation for "code" to a Literal
containing "invalid_arguments", "auth", "not_found", "rate_limited", "upstream",
and "internal_error" (import Literal from typing), update any
constructors/usages that build this error to use those literal values, and
ensure the to_model() conversion still maps directly to ToolErrorDetailsModel
without casting.

In `@src/mcp_github_pr_review/models.py`:
- Around line 301-305: The Field for next_cursor (next_cursor: str | None =
Field(..., alias="nextCursor", serialization_alias="nextCursor")) redundantly
sets serialization_alias identical to alias; remove the serialization_alias
argument and rely on alias (Pydantic v2 uses alias for both validation and
serialization) so update the Field call for next_cursor to only include
alias="nextCursor".

In `@src/mcp_github_pr_review/server.py`:
- Around line 1922-1930: The code calls fetch_pr_comments_page with placeholder
values ("placeholder-owner", "placeholder-repo", pull_number=0) when a cursor is
provided; replace those placeholders so real context is preserved by passing
request_owner, request_repo, and pull_number (the original request values)
instead of placeholders, and add a small fallback that attempts to extract
owner/repo/pull_number from the decoded cursor URL only if request_* values are
empty or None and handle malformed/invalid cursor parsing errors so they don't
produce misleading logs; update the call site for fetch_pr_comments_page and the
cursor-parsing logic accordingly.

In `@tests/test_mcp_arch_review.py`:
- Line 129: The test helper function fail currently declares a misleading return
type PaginatedReviewCommentsResult but always raises ToolExecutionError; update
the signature of fail (the async def fail helper) to return typing.NoReturn
instead, add an import for NoReturn from typing at the top of the test module,
and keep the body raising ToolExecutionError unchanged so static type checkers
and readers correctly understand it never returns.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 86456c05-736e-4278-a94b-81ae36f8eb79

📥 Commits

Reviewing files that changed from the base of the PR and between 524f5bf and f281f92.

📒 Files selected for processing (6)
  • specs/MCP-ARCH-REVIEW.md
  • src/mcp_github_pr_review/errors.py
  • src/mcp_github_pr_review/models.py
  • src/mcp_github_pr_review/pagination.py
  • src/mcp_github_pr_review/server.py
  • tests/test_mcp_arch_review.py

Comment thread specs/MCP-ARCH-REVIEW.md Outdated
Comment thread src/mcp_github_pr_review/pagination.py
Comment thread src/mcp_github_pr_review/server.py Outdated
petems added 4 commits March 4, 2026 12:30
* docs(readme): document pre-commit hook install with global core.hooksPath

* docs(claude): add pre-commit hook installation notes

* chore: normalize formatting across docs workflows and tests

* docs(readme): replace absolute hook paths with repo-relative examples

* docs(claude): replace absolute hook paths with placeholders

* style(tests): apply ruff formatting in git PR resolver tests

* style(tests): apply ruff formatting in MCP tool schema tests

* style(tests): apply ruff formatting in pagination limit tests

* style(tests): apply ruff formatting in performance tests
…ms/mcp-arch-audit

# Conflicts:
#	specs/MCP-ARCH-REVIEW.md
#	src/mcp_github_pr_review/errors.py
#	src/mcp_github_pr_review/models.py
#	src/mcp_github_pr_review/pagination.py
#	src/mcp_github_pr_review/server.py
#	tests/test_mcp_arch_review.py
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/mcp_github_pr_review/server.py (1)

1655-1701: ⚠️ Potential issue | 🟠 Major

Do not force branch/git detection for latest or first strategies.

Current logic requires branch (or git context) even when strategy does not need it. This makes github_resolve_open_pr_url/list flows fail unnecessarily outside a local git checkout when owner+repo are already provided.

✅ Strategy-aware fix pattern
- if not (owner and repo and branch):
+ needs_branch = select_strategy in {"branch", "error"}
+ if not (owner and repo and (branch or not needs_branch)):
     ctx = git_detect_repo_branch()
     owner = owner or ctx.owner
     repo = repo or ctx.repo
-    branch = branch or ctx.branch
+    if needs_branch:
+        branch = branch or ctx.branch
     host = host or ctx.host

Also applies to: 1894-1912

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_github_pr_review/server.py` around lines 1655 - 1701, The code forces
git_detect_repo_branch() to fill owner/repo/branch even when select_strategy
does not require branch (e.g., "latest" or "first"), causing
github_resolve_open_pr_url/list flows to fail outside a repo; update the logic
after ResolveOpenPrUrlArgs.model_validate so that you only call
git_detect_repo_branch() or require branch when select_strategy is one that
needs it (reference the select_strategy variable and ResolveOpenPrUrlArgs
validation), e.g., check select_strategy value before invoking
git_detect_repo_branch() and only populate branch from git context when
necessary; apply the same conditional fix in the analogous block around the code
handling select_strategy later (the other resolve/list handler).
🧹 Nitpick comments (1)
tests/test_mcp_arch_review.py (1)

208-260: Add a hostile-cursor regression test.

Please add a case where cursor decodes to a non-GitHub/non-API URL and assert the tool returns invalid_arguments (and does not perform fetch). This will lock in cursor safety behavior.

As per coding guidelines, "Test error conditions and edge cases in addition to happy path scenarios".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_mcp_arch_review.py` around lines 208 - 260, Add a hostile-cursor
regression test alongside
test_list_pr_review_comments_uses_cursor_context_without_placeholders that calls
mcp_server.list_pr_review_comments with a cursor which
_extract_cursor_pr_context is monkeypatched to decode into a non-GitHub host
(e.g., "evil.com", owner/repo/pull_number values). Also monkeypatch
fetch_pr_comments_page to a function that raises if invoked to ensure no fetch
is attempted; then assert the RPC result indicates invalid_arguments (or
equivalent error) and that fetch_pr_comments_page was not called. Use the same
symbols: mcp_server.list_pr_review_comments, _extract_cursor_pr_context, and
fetch_pr_comments_page to locate and implement the test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/mcp_github_pr_review/server.py`:
- Around line 1019-1038: The code trusts decode_cursor(cursor) and then issues
an authenticated fetch; instead validate any decoded URL before using it in
fetch_pr_comments_page: after calling decode_cursor (and in the similar decode
usage at the other spot), parse the URL and ensure its scheme is https, the
hostname equals api_base_for_host(host) (or the expected host derived from
host), and the path matches the expected prefix for this endpoint (e.g., starts
with /repos/{owner}/{repo}/pulls/{pull_number}/comments) and contains the
per_page parameter; if validation fails, raise the same ToolExecutionError for
invalid_arguments. Use decode_cursor, fetch_pr_comments_page and
api_base_for_host as anchors to find where to add the checks.

---

Outside diff comments:
In `@src/mcp_github_pr_review/server.py`:
- Around line 1655-1701: The code forces git_detect_repo_branch() to fill
owner/repo/branch even when select_strategy does not require branch (e.g.,
"latest" or "first"), causing github_resolve_open_pr_url/list flows to fail
outside a repo; update the logic after ResolveOpenPrUrlArgs.model_validate so
that you only call git_detect_repo_branch() or require branch when
select_strategy is one that needs it (reference the select_strategy variable and
ResolveOpenPrUrlArgs validation), e.g., check select_strategy value before
invoking git_detect_repo_branch() and only populate branch from git context when
necessary; apply the same conditional fix in the analogous block around the code
handling select_strategy later (the other resolve/list handler).

---

Nitpick comments:
In `@tests/test_mcp_arch_review.py`:
- Around line 208-260: Add a hostile-cursor regression test alongside
test_list_pr_review_comments_uses_cursor_context_without_placeholders that calls
mcp_server.list_pr_review_comments with a cursor which
_extract_cursor_pr_context is monkeypatched to decode into a non-GitHub host
(e.g., "evil.com", owner/repo/pull_number values). Also monkeypatch
fetch_pr_comments_page to a function that raises if invoked to ensure no fetch
is attempted; then assert the RPC result indicates invalid_arguments (or
equivalent error) and that fetch_pr_comments_page was not called. Use the same
symbols: mcp_server.list_pr_review_comments, _extract_cursor_pr_context, and
fetch_pr_comments_page to locate and implement the test.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 82809ae0-5600-4cb6-a268-81ffc802d9c4

📥 Commits

Reviewing files that changed from the base of the PR and between f281f92 and e5b9239.

📒 Files selected for processing (17)
  • .github/dependabot.yml
  • .github/workflows/lint.yml
  • .github/workflows/tests.yml
  • .gitignore
  • CLAUDE.md
  • Makefile
  • README.md
  • SECURITY.md
  • TEST_COVERAGE_SUMMARY.md
  • UV_COMMANDS.md
  • specs/MCP-ARCH-REVIEW.md
  • specs/PYDANTIC_MODEL_INTEGRATION.md
  • src/mcp_github_pr_review/errors.py
  • src/mcp_github_pr_review/models.py
  • src/mcp_github_pr_review/pagination.py
  • src/mcp_github_pr_review/server.py
  • tests/test_mcp_arch_review.py
💤 Files with no reviewable changes (2)
  • Makefile
  • specs/PYDANTIC_MODEL_INTEGRATION.md
✅ Files skipped from review due to trivial changes (8)
  • README.md
  • .github/workflows/tests.yml
  • TEST_COVERAGE_SUMMARY.md
  • .gitignore
  • .github/dependabot.yml
  • SECURITY.md
  • UV_COMMANDS.md
  • .github/workflows/lint.yml

Comment on lines +1019 to +1038
if cursor:
try:
url = decode_cursor(cursor)
except ValueError as exc:
raise ToolExecutionError(
code="invalid_arguments",
message="The provided cursor is invalid.",
next_steps=[
"Use the nextCursor value returned by a previous response.",
"Restart pagination with cursor omitted.",
],
) from exc
else:
safe_owner = quote(owner, safe="")
safe_repo = quote(repo, safe="")
api_base = api_base_for_host(host)
url = (
f"{api_base}/repos/{safe_owner}/{safe_repo}/pulls/{pull_number}"
f"/comments?per_page={per_page}"
)
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.

⚠️ Potential issue | 🔴 Critical

Validate decoded cursor URLs before authenticated fetches.

fetch_pr_comments_page currently trusts decode_cursor(cursor) and then sends an authenticated request to that URL. A forged cursor can redirect requests (with Authorization) to attacker-controlled hosts.

🔒 Suggested hardening
@@
     if cursor:
         try:
-            url = decode_cursor(cursor)
+            decoded_url = decode_cursor(cursor)
         except ValueError as exc:
             raise ToolExecutionError(
                 code="invalid_arguments",
                 message="The provided cursor is invalid.",
@@
-            ) from exc
+            ) from exc
+
+        parsed = urlparse(decoded_url)
+        expected_host = (urlparse(api_base_for_host(host)).netloc or "").lower()
+        path_match = re.match(
+            r"^/repos/([^/]+)/([^/]+)/pulls/(\d+)/comments$",
+            parsed.path,
+        )
+        if (
+            parsed.scheme != "https"
+            or parsed.netloc.lower() != expected_host
+            or not path_match
+        ):
+            raise ToolExecutionError(
+                code="invalid_arguments",
+                message="The provided cursor is invalid.",
+                next_steps=[
+                    "Use only nextCursor values returned by this tool.",
+                    "Restart pagination with cursor omitted.",
+                ],
+            )
+        url = decoded_url
     else:

Also applies to: 1065-1067

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/mcp_github_pr_review/server.py` around lines 1019 - 1038, The code trusts
decode_cursor(cursor) and then issues an authenticated fetch; instead validate
any decoded URL before using it in fetch_pr_comments_page: after calling
decode_cursor (and in the similar decode usage at the other spot), parse the URL
and ensure its scheme is https, the hostname equals api_base_for_host(host) (or
the expected host derived from host), and the path matches the expected prefix
for this endpoint (e.g., starts with
/repos/{owner}/{repo}/pulls/{pull_number}/comments) and contains the per_page
parameter; if validation fails, raise the same ToolExecutionError for
invalid_arguments. Use decode_cursor, fetch_pr_comments_page and
api_base_for_host as anchors to find where to add the checks.

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