Skip to main content

Overview

Nectr acts as an MCP client to pull live context from external tools during PR reviews. This enriches reviews with:
  • Linear issues - Task descriptions and requirements
  • Sentry errors - Production errors related to changed files
  • Slack messages - Team discussions (future)
All MCP integrations are optional. If not configured, Nectr gracefully skips that source and logs an info message. Reviews continue normally.

Architecture

File: app/mcp/client.py
class MCPClientManager:
    """Manages connections to external MCP servers.
    
    All methods are async and return plain Python dicts/lists.
    Failures degrade gracefully (return empty lists).
    """
    
    async def get_linear_issues(self, team_id: str, query: str) -> list[dict]
    async def get_sentry_errors(self, project: str, filename: str) -> list[dict]
    async def get_github_issues(self, repo: str, query: str) -> list[dict]

Generic MCP Query Method

All integrations use the same underlying protocol:
async def query_mcp_server(
    self,
    server_url: str,
    tool_name: str,
    args: dict,
    auth_token: str | None = None,
) -> list[dict] | dict:
    """Generic JSON-RPC 2.0 MCP tool call over HTTP."""
    
    payload = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": {"name": tool_name, "arguments": args},
    }
    
    headers = {"Content-Type": "application/json"}
    if auth_token:
        headers["Authorization"] = f"Bearer {auth_token}"
    
    async with httpx.AsyncClient(timeout=10.0) as client:
        response = await client.post(f"{server_url}/", json=payload, headers=headers)
        response.raise_for_status()
        data = response.json()
    
    # Unwrap MCP content array
    result = data.get("result", data)
    if isinstance(result, dict):
        content = result.get("content", result)
        if isinstance(content, list):
            items = []
            for item in content:
                if item.get("type") == "text":
                    parsed = json.loads(item["text"])
                    if isinstance(parsed, list):
                        items.extend(parsed)
                    else:
                        items.append(parsed)
            return items
    
    return []
MCP context is best-effort. A slow external server should not block the PR review from completing. If a call times out, Nectr logs a warning and continues without that context.

Linear Integration

Configuration

LINEAR_MCP_URL=https://your-linear-mcp-server.com
LINEAR_API_KEY=lin_api_...

Tool: get_linear_issues

Called by: ReviewToolExecutor (get_linked_issues tool)
async def get_linear_issues(self, team_id: str, query: str) -> list[dict]:
    """Pull issues / tasks from the Linear MCP server matching *query*.
    
    Args:
        team_id: Linear team identifier (e.g., "ENG").
        query:   Free-text search query (topic, feature area, keyword).
    
    Returns:
        List of issue dicts: {id, title, state, url, description}.
        Empty list if Linear MCP is not configured or the call fails.
    """
    if not settings.LINEAR_MCP_URL:
        logger.info("LINEAR_MCP_URL not configured — skipping Linear issue fetch")
        return []
    
    return await self.query_mcp_server(
        server_url=settings.LINEAR_MCP_URL,
        tool_name="search_issues",
        args={"team_id": team_id, "query": query},
        auth_token=settings.LINEAR_API_KEY,
    )
[
  {
    "id": "ENG-123",
    "title": "Add JWT token refresh endpoint",
    "state": "In Progress",
    "url": "https://linear.app/team/issue/ENG-123",
    "description": "Users need to refresh their tokens without re-authenticating..."
  }
]

Use Case

When Claude calls get_linked_issues(query="JWT token refresh", source="linear"), Nectr:
  1. Calls Linear MCP server with search_issues tool
  2. Returns list of matching issues
  3. Claude includes issue context in review (e.g., “This PR addresses ENG-123”)
Why not use Linear API directly?MCP provides a standardized protocol for tool calling. If Linear changes their API, only the MCP server needs updating — Nectr’s code stays the same.

Sentry Integration

Configuration

SENTRY_MCP_URL=https://your-sentry-mcp-server.com
SENTRY_AUTH_TOKEN=...

Tool: get_sentry_errors

Called by: ReviewToolExecutor (get_related_errors tool)
async def get_sentry_errors(self, project: str, filename: str) -> list[dict]:
    """Get recent Sentry errors related to a file being reviewed.
    
    Args:
        project:  Sentry project slug (e.g., "backend").
        filename: File path from the PR diff to filter errors by.
    
    Returns:
        List of error dicts: {id, title, culprit, count, last_seen}.
        Empty list if Sentry MCP is not configured or the call fails.
    """
    if not settings.SENTRY_MCP_URL:
        logger.info("SENTRY_MCP_URL not configured — skipping Sentry error fetch")
        return []
    
    return await self.query_mcp_server(
        server_url=settings.SENTRY_MCP_URL,
        tool_name="search_errors",
        args={"project": project, "filename": filename},
        auth_token=settings.SENTRY_AUTH_TOKEN,
    )
[
  {
    "id": "sentry-error-123",
    "title": "JWTDecodeError: Invalid token signature",
    "culprit": "app/auth/jwt_utils.py in verify_token",
    "count": 42,
    "last_seen": "2026-03-10T14:32:00Z"
  }
]

Use Case

When a PR modifies app/auth/jwt_utils.py, Claude calls:
get_related_errors(files=["app/auth/jwt_utils.py"])
Nectr:
  1. Calls Sentry MCP server with search_errors tool
  2. Returns recent errors for that file
  3. Claude includes error context in review (e.g., “This PR might fix the recurring JWTDecodeError”)
Sentry error culprits include stack traces. The MCP server filters errors where the top frame matches the given filename. This surfaces production errors directly related to the code being reviewed.

GitHub Integration (Placeholder)

async def get_github_issues(
    self,
    repo: str,
    labels: list[str] | None = None,
    query: str = "",
) -> list[dict]:
    """Pull open GitHub issues that a PR might be addressing.
    
    Returns an empty list — the native GitHub REST client in
    ReviewToolExecutor covers issue fetching directly.
    """
    return []
GitHub issues are fetched directly via GitHub REST API (not MCP) because Nectr already has GitHub credentials. This method is a placeholder for future use.

How Claude Uses MCP Context

Tool Call Flow

  1. Claude analyzes PR diff
    • Sees changes to app/auth/jwt_utils.py
    • Decides to check for related production errors
  2. Claude calls tool:
    {
      "type": "tool_use",
      "name": "get_related_errors",
      "input": {
        "files": ["app/auth/jwt_utils.py"]
      }
    }
    
  3. ReviewToolExecutor executes:
    async def _get_related_errors(self, files: list[str]) -> str:
        from app.mcp.client import mcp_client
        
        all_errors = []
        for path in files[:5]:
            errors = await mcp_client.get_sentry_errors(
                project=self.repo,
                filename=path,
            )
            all_errors.extend(errors)
        
        if not all_errors:
            return "No related Sentry errors found."
        
        lines = ["Related Sentry errors for modified files:"]
        for err in all_errors[:10]:
            title = err.get("title", "(unknown error)")
            count = err.get("count", "?")
            lines.append(f"  [{count}x] {title}")
        
        return "\n".join(lines)
    
  4. Claude receives result:
    Related Sentry errors for modified files:
      [42x] JWTDecodeError: Invalid token signature
      [18x] TokenExpiredError: Token has expired
    
  5. Claude includes in review:

    Issues

    • 🟡 Moderate: The verify_token() function has 42 production errors in the last 30 days (JWTDecodeError: Invalid token signature). This PR changes signature validation logic — ensure backward compatibility.

Error Handling

All MCP calls are wrapped in try-except blocks that degrade gracefully:
except httpx.TimeoutException:
    logger.warning(
        "MCP call timed out: %s tool=%s (timeout=%ss)",
        server_url, tool_name, _MCP_TIMEOUT,
    )
    return []
User impact: Review continues without MCP context. Claude doesn’t know about Linear issues or Sentry errors for this PR.
except httpx.HTTPStatusError as exc:
    logger.warning(
        "MCP server returned HTTP %s for tool=%s: %s",
        exc.response.status_code, tool_name, exc,
    )
    return []
User impact: Same as timeout — graceful degradation.
except (json.JSONDecodeError, KeyError):
    items.append(item)  # Keep raw item if JSON parse fails
User impact: MCP result is included as-is (might be less useful, but doesn’t break review).

Performance Characteristics

IntegrationTypical LatencyNotes
Linear issues200-500 msDepends on Linear API speed
Sentry errors300-800 msDepends on Sentry query complexity
GitHub issuesN/AUses native GitHub REST API (not MCP)
MCP calls run in parallel with other context fetching (Mem0, Neo4j). Total latency is bound by the slowest call, not the sum.

Configuration Best Practices

Environment Variables

# Required (if using Linear)
LINEAR_MCP_URL=https://your-linear-mcp-server.com
LINEAR_API_KEY=lin_api_...

# Required (if using Sentry)
SENTRY_MCP_URL=https://your-sentry-mcp-server.com
SENTRY_AUTH_TOKEN=...

# Optional (Slack — future)
SLACK_MCP_URL=https://your-slack-mcp-server.com
Option 1: Use Anthropic’s MCP servers (if available)Option 2: Build custom MCP serversOption 3: Skip MCP
  • Leave LINEAR_MCP_URL and SENTRY_MCP_URL unset
  • Nectr reviews still work — just without external context

Monitoring

Check backend logs for MCP failures:
# Railway logs
railway logs

# Look for:
# - "MCP call timed out: ..."
# - "MCP server returned HTTP 500 ..."
# - "LINEAR_MCP_URL not configured — skipping ..."

Future Integrations

Slack

Planned: Pull relevant channel messages during PR reviews.Use case: If a PR mentions “authentication refactor”, search Slack for recent discussions about auth to provide context.Config:
SLACK_MCP_URL=https://your-slack-mcp-server.com

Datadog

Planned: Pull APM traces and metrics for files being modified.Use case: If a PR touches a high-latency endpoint, surface recent performance data.Config:
DATADOG_MCP_URL=https://your-datadog-mcp-server.com
DATADOG_API_KEY=...

Next Steps

Service Layer

Learn how ReviewToolExecutor uses MCP client

Data Flow

See MCP context in the PR review flow

Build docs developers (and LLMs) love