Skip to main content

Overview

Nectr exposes 4 MCP tools that external agents can call to fetch review data, contributor stats, and repository health metrics. Each tool is an async function decorated with @mcp.tool() in app/mcp/server.py. Implementation: app/mcp/server.py:106-237

get_recent_reviews

Fetch recent PR reviews for a repository with verdicts and summaries.

Parameters

repo
string
required
Full repository name (e.g., "acme/backend", "owner/repo")
limit
integer
default:10
Maximum number of reviews to return. Capped at 50.

Response

Returns a list of review objects:
id
integer
Database ID of the workflow run
repo
string
Repository name (e.g., "acme/backend")
pr_number
integer
GitHub PR number
verdict
string
AI verdict: "APPROVE", "REQUEST_CHANGES", "COMMENT", or "UNKNOWN"
summary
string
Review summary text (markdown)
created_at
string
ISO 8601 timestamp (e.g., "2026-03-10T10:00:00Z")
status
string
Workflow status: "completed", "failed", or "running"

Example

from app.mcp.client import mcp_client

reviews = await mcp_client.call_tool(
    "get_recent_reviews",
    {"repo": "acme/backend", "limit": 5}
)
print(reviews)

Implementation

File: app/mcp/server.py:106
@mcp.tool()
async def get_recent_reviews(repo: str, limit: int = 10) -> list[dict]:
    """Get recent PR reviews for a repository with verdicts and summaries.

    Args:
        repo:  Full repository name, e.g. ``owner/repo``.
        limit: Maximum number of reviews to return (default 10, capped at 50).

    Returns:
        List of review dicts:
        ``{id, repo, pr_number, verdict, summary, created_at, status}``.
    """
    limit = min(max(1, limit), 50)
    reviews = await _query_db_reviews(repo, limit)
    if not reviews:
        logger.info("MCP get_recent_reviews: no reviews found for %s", repo)
    return reviews

get_contributor_stats

Get top contributors for a repository with PR-touch counts from the Neo4j knowledge graph.

Parameters

repo
string
required
Full repository name (e.g., "acme/backend", "owner/repo")

Response

Returns a list of contributor objects:
login
string
GitHub username (e.g., "alice")
pr_count
integer
Number of PRs where this contributor touched files (from Neo4j graph)

Example

stats = await mcp_client.call_tool(
    "get_contributor_stats",
    {"repo": "acme/backend"}
)
print(stats)

Implementation

File: app/mcp/server.py:125
@mcp.tool()
async def get_contributor_stats(repo: str) -> list[dict]:
    """Get top contributors for a repository with PR-touch counts.

    Args:
        repo: Full repository name, e.g. ``owner/repo``.

    Returns:
        List of contributor dicts: ``{login, pr_count}``.
    """
    return await _query_contributor_stats(repo)
Helper: app/mcp/server.py:83
async def _query_contributor_stats(repo: str) -> list[dict]:
    """Pull contributor stats from the Neo4j knowledge graph.

    Falls back to an empty list if Neo4j is not configured or the query fails.
    """
    try:
        from app.services import graph_builder

        stats = await graph_builder.get_file_experts(repo, paths=[], top_k=20)
        return [
            {"login": s.get("login", ""), "pr_count": s.get("touch_count", 0)}
            for s in (stats or [])
        ]
    except Exception as exc:
        logger.warning("MCP _query_contributor_stats failed: %s", exc)
        return []
Requires Neo4j: This tool returns [] if NEO4J_URI is not configured. Neo4j stores the knowledge graph built from historical PR diffs.

get_pr_verdict

Retrieve Nectr’s AI verdict for a specific pull request.

Parameters

repo
string
required
Full repository name (e.g., "acme/backend")
pr_number
integer
required
GitHub PR number

Response

repo
string
Repository name (echoed from input)
pr_number
integer
PR number (echoed from input)
verdict
string
AI verdict: "APPROVE", "REQUEST_CHANGES", "COMMENT", or "UNKNOWN"
summary
string
Review summary text (markdown)
created_at
string
ISO 8601 timestamp
error
string
Error message if review not found (e.g., "No review found for acme/backend#123")

Example

verdict = await mcp_client.call_tool(
    "get_pr_verdict",
    {"repo": "acme/backend", "pr_number": 123}
)
print(verdict)

Implementation

File: app/mcp/server.py:138
@mcp.tool()
async def get_pr_verdict(repo: str, pr_number: int) -> dict:
    """Get Nectr's AI verdict for a specific pull request.

    Args:
        repo:      Full repository name, e.g. ``owner/repo``.
        pr_number: GitHub PR number.

    Returns:
        ``{repo, pr_number, verdict, summary, created_at}``
        or ``{error}`` when the review is not found.
    """
    try:
        from app.core.database import async_session
        from app.models.workflow import WorkflowRun
        from sqlalchemy import select, desc

        async with async_session() as db:
            stmt = (
                select(WorkflowRun)
                .where(WorkflowRun.workflow_type == "pr_review")
                .order_by(desc(WorkflowRun.created_at))
                .limit(100)
            )
            result = await db.execute(stmt)
            rows = result.scalars().all()

        repo_name = repo.split("/")[-1]
        for row in rows:
            meta: dict = {}
            if row.result_data:
                try:
                    meta = (
                        json.loads(row.result_data)
                        if isinstance(row.result_data, str)
                        else row.result_data
                    )
                except (ValueError, TypeError):
                    pass
            stored_repo = meta.get("repo", "")
            if meta.get("pr_number") == pr_number and stored_repo.endswith(repo_name):
                return {
                    "repo": repo,
                    "pr_number": pr_number,
                    "verdict": meta.get("verdict", "UNKNOWN"),
                    "summary": meta.get("summary", ""),
                    "created_at": row.created_at.isoformat() if row.created_at else None,
                }
        return {"error": f"No review found for {repo}#{pr_number}"}
    except Exception as exc:
        logger.warning("MCP get_pr_verdict failed: %s", exc)
        return {"error": str(exc)}

get_repo_health

Calculate overall repository health score (0-100) based on review activity.

Parameters

repo
string
required
Full repository name (e.g., "acme/backend")

Response

repo
string
Repository name (echoed from input)
review_coverage_pct
integer
Percentage of PRs reviewed (0-100)
avg_merge_time_hours
float
Average time from PR open to merge (hours). Currently null — requires merge-event tracking.
open_prs_indexed
integer
Number of PRs reviewed and indexed
health_score
integer
Overall health score (0-100). Calculated as min(100, reviewed_count / 20 * 100).
note
string
Informational note about missing features (e.g., avg_merge_time_hours not yet implemented)
error
string
Error message if database query fails

Example

health = await mcp_client.call_tool(
    "get_repo_health",
    {"repo": "acme/backend"}
)
print(health)

Implementation

File: app/mcp/server.py:192
@mcp.tool()
async def get_repo_health(repo: str) -> dict:
    """Get overall repository health metrics.

    Derives a 0-100 health score from review activity, and reports review
    coverage percentage and indexed PR count.

    Args:
        repo: Full repository name, e.g. ``owner/repo``.

    Returns:
        ``{repo, review_coverage_pct, avg_merge_time_hours,
          open_prs_indexed, health_score, note}``.
    """
    try:
        from app.core.database import async_session
        from app.models.workflow import WorkflowRun
        from sqlalchemy import select, func

        async with async_session() as db:
            stmt = select(func.count()).select_from(WorkflowRun).where(
                WorkflowRun.workflow_type == "pr_review",
                WorkflowRun.status == "completed",
            )
            result = await db.execute(stmt)
            reviewed_count: int = result.scalar() or 0

        health_score = min(100, int((reviewed_count / 20) * 100))
        return {
            "repo": repo,
            "review_coverage_pct": min(100, reviewed_count * 5),
            "avg_merge_time_hours": None,   # requires merge-event timestamp tracking
            "open_prs_indexed": reviewed_count,
            "health_score": health_score,
            "note": "avg_merge_time_hours requires merge-event tracking (roadmap item)",
        }
    except Exception as exc:
        logger.warning("MCP get_repo_health failed: %s", exc)
        return {
            "repo": repo,
            "review_coverage_pct": 0,
            "avg_merge_time_hours": None,
            "open_prs_indexed": 0,
            "health_score": 0,
            "error": str(exc),
        }

Error Handling

All MCP tools follow graceful degradation principles:
  1. Database unavailable → Return empty list [] or error dict {"error": "..."}
  2. Neo4j unavailable → Return empty list [] (contributor stats)
  3. No matching data → Return empty list or error dict
  4. Log warnings → All exceptions are logged with logger.warning(...)
Never crashes — external MCP clients can always call tools without worrying about 500 errors.

Next Steps

MCP Resources

Learn about MCP resources for streaming data

MCP Server Setup

Configure and deploy Nectr’s MCP server

MCP Protocol

Understand how MCP works in Nectr

Build docs developers (and LLMs) love