Skip to content

Extract MCP tool dispatcher from app.main#398

Open
ayskobtw-lil wants to merge 2 commits into
ramimbo:mainfrom
ayskobtw-lil:ayskobtw-lil/mergework-390-mcp-tools
Open

Extract MCP tool dispatcher from app.main#398
ayskobtw-lil wants to merge 2 commits into
ramimbo:mainfrom
ayskobtw-lil:ayskobtw-lil/mergework-390-mcp-tools

Conversation

@ayskobtw-lil
Copy link
Copy Markdown
Contributor

@ayskobtw-lil ayskobtw-lil commented May 26, 2026

Summary

  • Extract the MCP tool dispatcher from app/main.py into app/mcp_tools.py.
  • Keep /mcp route wiring in app/main.py, passing the extracted dispatcher into handle_mcp_request.
  • Add direct dispatcher regression tests for list behavior and validation errors.

Evidence

  • Confusion, missing step, stale example, or bug this addresses: app/main.py still owned the full MCP tool dispatch/argument-validation block, making app wiring harder to review separately from MCP tool behavior. This creates a focused MCP boundary without changing public MCP responses.

Test Evidence

  • ruff format --check . -> 67 files already formatted
  • ruff check . -> All checks passed
  • mypy app -> Success: no issues found in 24 source files
  • pytest -> 381 passed
  • python scripts/docs_smoke.py -> docs smoke ok
  • git diff --check -> clean

MRWK

Related bounty or issue (Bounty #N or Refs #N for multi-award bounties): Bounty #390

Summary by CodeRabbit

  • New Features

    • Centralized backend tool API exposing commands to list and search bounties, fetch bounty details, list attempts, query balances and ledger entries, manage wallets, fetch proofs, and submit work with validation and selectable output formats.
  • Refactor

    • Backend dispatch and argument handling reorganized for clearer separation of concerns and more consistent input validation.
  • Tests

    • Added integration tests covering listing, submission behavior, case-insensitive matching, and argument-validation errors.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

📝 Walkthrough

Walkthrough

Extracts MCP tool dispatch into app/mcp_tools.py (validation helpers and multiple tool implementations), updates app/main.py to call the new call_mcp_tool, and adds pytest coverage exercising listing, submission, and argument validation.

Changes

MCP Tool Extraction Refactoring

Layer / File(s) Summary
MCP Tools Module Foundation and Validation Helpers
app/mcp_tools.py
Imports dependencies and defines argument parsing/validation helpers for integers, strings, booleans, repo selectors, limits, and format normalization used by the dispatcher.
Bounty and Account Tool Implementations
app/mcp_tools.py
Implements list_bounties (status/text/issue filtering), get_bounty (with optional awards), list_bounty_attempts (include_expired/limit), get_balance, and wallet register/get/transfer with JSON serialization.
Proof, Ledger, and Work Submission Tools
app/mcp_tools.py
Implements get_ledger_entry, get_proof (parsing proof.public_json), submit_work_proof (bounty_id vs issue_number exclusivity, repo selector constraints, ambiguity detection, JSON/text guidance), and unknown-tool fallback.
Main.py Refactoring to Use Extracted Module
app/main.py
Removes unused imports (list_bounty_attempts, get_balance), imports call_mcp_tool, updates /mcp route to pass the callable, and deletes the local _call_mcp_tool.
Test Coverage for MCP Tools
tests/test_mcp_tools.py
Adds tests: list_bounties end-to-end, case-insensitive repo matching for submit_work_proof, and parametrized invalid-argument validation tests.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • ramimbo/mergework#382: Overlaps on extracting submit_work_proof guidance/tool logic into helper modules.
  • ramimbo/mergework#323: Related work on MCP argument validation and integer parsing for list_bounties/submit paths.
  • ramimbo/mergework#329: Prior refactor of /mcp wiring; this PR completes moving the dispatch into a dedicated module.

Suggested labels

mrwk:accepted, mrwk:paid

Suggested reviewers

  • weilixiong
  • TateLyman
  • tolga-tom-nook
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: extracting the MCP tool dispatcher from app.main into a separate module.
Description check ✅ Passed The pull request description fully addresses the template structure with summary, evidence, test evidence (all checks marked complete), and related bounty information.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/mcp_tools.py`:
- Around line 239-241: submit_work_proof builds issue_query and filters by repo
using Bounty.repo == repo_selector while repo_selector is lowercased; change
this to a case-insensitive comparison to match other code paths by comparing
func.lower(Bounty.repo) to repo_selector (or applying func.lower to both sides)
so the repo filter does not miss bounties when Bounty.repo is not stored
lowercase; update the filter on issue_query (where clause referencing
Bounty.repo and repo_selector) to use func.lower(Bounty.repo) == repo_selector
instead of Bounty.repo == repo_selector.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2903605b-ad7e-411f-8582-b4569b9bfef3

📥 Commits

Reviewing files that changed from the base of the PR and between a1795ed and 1a6484a.

📒 Files selected for processing (3)
  • app/main.py
  • app/mcp_tools.py
  • tests/test_mcp_tools.py

Comment thread app/mcp_tools.py
@ayskobtw-lil
Copy link
Copy Markdown
Contributor Author

Addressed the CodeRabbit repo-selector note in beb4010.

Change:

  • submit_work_proof now filters scoped issue lookups with func.lower(Bounty.repo) == repo_selector, so mixed-case stored repo names still match normalized repo arguments.
  • Added test_submit_work_proof_repo_selector_matches_stored_repo_case covering that path.

Validation after the fix:

  • python -m pytest tests/test_mcp_tools.py tests/test_api_mcp.py -q -> 86 passed
  • python -m pytest -q -> 382 passed
  • python -m ruff check . -> All checks passed
  • python -m ruff format --check . -> 67 files already formatted
  • python -m mypy app -> Success: no issues found in 24 source files
  • python scripts/docs_smoke.py -> docs smoke ok
  • git diff --check -> clean

Copy link
Copy Markdown
Contributor

@tolga-tom-nook tolga-tom-nook left a comment

Choose a reason for hiding this comment

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

No blockers from my review of the MCP tool dispatcher extraction at head beb4010.

Evidence checked:

  • app/main.py now imports call_mcp_tool and keeps the MCP request boundary at handle_mcp_request(request, db_url, call_mcp_tool).
  • app/mcp_tools.py preserves the extracted behavior for bounty listing/search, bounty lookup with awards, attempt listing, balance/wallet/transfer/ledger/proof tools, and submit_work_proof guidance including repo selector validation.
  • tests/test_mcp_tools.py adds direct coverage for the extracted dispatcher and mixed-case stored repo matching.

Validation run locally:

  • /home/ubuntu/tom-agent/work/mergework/.venv/bin/python -m pytest tests/test_mcp_tools.py tests/test_api_mcp.py -q → 86 passed
  • /home/ubuntu/tom-agent/work/mergework/.venv/bin/python -m pytest -q → 382 passed
  • /home/ubuntu/tom-agent/work/mergework/.venv/bin/python -m ruff check app/mcp_tools.py app/main.py tests/test_mcp_tools.py tests/test_api_mcp.py → passed
  • /home/ubuntu/tom-agent/work/mergework/.venv/bin/python -m ruff format --check app/mcp_tools.py app/main.py tests/test_mcp_tools.py tests/test_api_mcp.py → passed
  • /home/ubuntu/tom-agent/work/mergework/.venv/bin/python -m mypy app/mcp_tools.py app/main.py → passed
  • git diff --check origin/main...HEAD → passed

Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_mcp_tools.py`:
- Around line 33-55: The test currently passes even if the repo argument is
ignored because only one bounty exists; create a decoy bounty with the same
issue_number but a different repo and title (using create_bounty) before calling
call_mcp_tool in test_submit_work_proof_repo_selector_matches_stored_repo_case,
then call call_mcp_tool with repo="ramimbo/mergework" and assert the returned
result contains the expected "Code health bounty" and "Bounty `#390`" and does NOT
contain the decoy bounty's title to prove the repo-selector is honored (keep
existing case-variant bounty too to validate case-insensitive matching).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2e64fcc8-8337-41fd-8465-5c817f5e1bd9

📥 Commits

Reviewing files that changed from the base of the PR and between 1a6484a and beb4010.

📒 Files selected for processing (2)
  • app/mcp_tools.py
  • tests/test_mcp_tools.py

Comment thread tests/test_mcp_tools.py
Comment on lines +33 to +55
def test_submit_work_proof_repo_selector_matches_stored_repo_case(sqlite_url: str) -> None:
create_schema(sqlite_url)
with session_scope(sqlite_url) as session:
ensure_genesis(session)
bounty = create_bounty(
session,
repo="ramimbo/mergework",
issue_number=390,
issue_url="https://github.com/ramimbo/mergework/issues/390",
title="Code health bounty",
reward_mrwk="200",
acceptance="Extract a coherent subsystem from app.main.",
)
bounty.repo = "Ramimbo/MergeWork"

result = call_mcp_tool(
sqlite_url,
"submit_work_proof",
{"issue_number": 390, "repo": "ramimbo/mergework"},
)

assert "Code health bounty" in result
assert "Bounty #390" in result
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Strengthen this regression test to actually validate repo-selector behavior.

With only one bounty in the database, this test still passes if the repo argument is ignored and lookup uses only issue_number. Add a decoy bounty sharing the same issue number under a different repo, and assert it is not selected.

Proposed test hardening
 def test_submit_work_proof_repo_selector_matches_stored_repo_case(sqlite_url: str) -> None:
     create_schema(sqlite_url)
     with session_scope(sqlite_url) as session:
         ensure_genesis(session)
-        bounty = create_bounty(
+        bounty = create_bounty(
             session,
             repo="ramimbo/mergework",
             issue_number=390,
             issue_url="https://github.com/ramimbo/mergework/issues/390",
             title="Code health bounty",
             reward_mrwk="200",
             acceptance="Extract a coherent subsystem from app.main.",
         )
+        create_bounty(
+            session,
+            repo="otherorg/otherrepo",
+            issue_number=390,
+            issue_url="https://github.com/otherorg/otherrepo/issues/390",
+            title="Decoy bounty",
+            reward_mrwk="200",
+            acceptance="Decoy acceptance criteria.",
+        )
         bounty.repo = "Ramimbo/MergeWork"
 
     result = call_mcp_tool(
         sqlite_url,
         "submit_work_proof",
         {"issue_number": 390, "repo": "ramimbo/mergework"},
     )
 
     assert "Code health bounty" in result
     assert "Bounty `#390`" in result
+    assert "Decoy bounty" not in result

As per coding guidelines, tests/**/*.py: Add or update tests for changed behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_mcp_tools.py` around lines 33 - 55, The test currently passes even
if the repo argument is ignored because only one bounty exists; create a decoy
bounty with the same issue_number but a different repo and title (using
create_bounty) before calling call_mcp_tool in
test_submit_work_proof_repo_selector_matches_stored_repo_case, then call
call_mcp_tool with repo="ramimbo/mergework" and assert the returned result
contains the expected "Code health bounty" and "Bounty `#390`" and does NOT
contain the decoy bounty's title to prove the repo-selector is honored (keep
existing case-variant bounty too to validate case-insensitive matching).

Copy link
Copy Markdown

@rebel117 rebel117 left a comment

Choose a reason for hiding this comment

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

Reviewed the MCP tool dispatcher extraction. Overall a clean extraction from app/main.py into app/mcp_tools.py.

One observation: the submit_work_proof handler introduces a behavioral change. The original code in main.py does an exact match:

issue_query = issue_query.where(Bounty.repo == repo_selector)

The extracted version uses case-insensitive matching:

issue_query = issue_query.where(func.lower(Bounty.repo) == repo_selector)

Since optional_repo_selector_arg() already lowercases the input, the original code would only match repos stored in lowercase. The new code also matches mixed-case stored repos like Ramimbo/MergeWork. This is actually an improvement (the test test_submit_work_proof_repo_selector_matches_stored_repo_case covers this), but it is a behavioral change beyond a pure extraction. Worth noting in the PR description so reviewers/maintainers are aware.

Checked the rest of the extraction — imports, argument validators, and tool dispatch paths are preserved correctly. The 3 tests cover the main paths (list, case-insensitive repo search, validation errors). The remaining tool handlers (get_bounty, list_bounty_attempts, get_balance, register_wallet, get_wallet, submit_wallet_transfer, get_ledger_entry, get_proof) are untested in this PR but the extraction itself is faithful to the original.

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.

3 participants