Skip to content

Architecture

Architecture of the MCP GitHub PR and Issue Analyser: an MCP client reaches the auth layer over stdio or HTTP, the auth layer selects no auth, a static token verifier or GitHub OAuth2 with a memory, DynamoDB or Redis token store, and the FastMCP server calls the GitHub REST v3 and GraphQL v4 APIs while exposing Prometheus metrics

Layers

Layer Module Responsibility
MCP client - Connects over stdio, or HTTP POST to /mcp when MCP_ENABLE_REMOTE is true
Auth layer auth.py Picks no auth, APIKeyVerifier, GitHubProvider or both, and resolves the token for each call
Token store auth.py MemoryStore by default, DynamoDBStore when DYNAMODB_TABLE_ARN is set, RedisStore when REDIS_HOST_PORT is
Server issues_pr_analyser.py FastMCP server: tool registration, skills provider, metrics middleware, request routing
GitHub integration github_integration.py, activity.py All GitHub API calls, REST v3 and GraphQL v4

Request path

  1. The client opens a session. Over stdio it launches the server as a subprocess and there is no transport auth. Over HTTP the auth layer compares the bearer token against GITHUB_TOKEN in constant time, runs the GitHub OAuth2 authorisation code flow with PKCE, or does both.
  2. resolve_token returns the token for the call: the one the request arrived with wherever the transport authenticated it, and the server's GITHUB_TOKEN otherwise. This is the only difference the tools see between the modes.
  3. PRIssueAnalyser routes the call to a registered tool. Tools are discovered from the MCP annotations on the integration's public methods, so adding an annotated method registers a tool. Every call passes through the metrics middleware.
  4. GitHubIntegration issues the API request over an async HTTP client bounded by GITHUB_API_TIMEOUT. Diffs, comments, merges, issues, labels, tags and releases go over REST v3. User search, activity, PR linked issues, PR status checks and star growth go over GraphQL v4.

Tools

Tools lists every tool the server registers, grouped by what it acts on, and the skill that documents each group.

Auth layer

The verifier is selected at startup, not per request:

Selected when Verifier Identity used
MCP_ENABLE_REMOTE unset or false none Server's GITHUB_TOKEN
MCP_ENABLE_REMOTE true, GITHUB_TOKEN only APIKeyVerifier Server's GITHUB_TOKEN, shared by every caller
MCP_ENABLE_REMOTE true, all GITHUB_OAUTH_* only GitHubProvider Each caller's own GitHub token
MCP_ENABLE_REMOTE true, GITHUB_TOKEN and all GITHUB_OAUTH_* MultiAuth over both Whichever credential the request carried
Neither GITHUB_TOKEN nor GITHUB_OAUTH_* none None. Every call is refused

The server starts holding neither credential. Over HTTP the MCP endpoint answers 401 with [AUTH_FAILED] and the missing configuration named, while / and /metrics keep answering so a readiness probe reports the pod healthy. Over stdio a tool raises the same error when it reaches for a token. See #392.

In OAuth2 mode the server acts as its own authorisation server: it accepts dynamic client registration, proxies GitHub's authorisation code flow, and issues JWTs signed with a key derived from JWT_SIGNING_KEY or the OAuth client secret. Audit trails and rate limits then follow the individual user rather than the server.

Under MultiAuth the GitHubProvider still serves those routes and the protected resource metadata, and APIKeyVerifier is tried after it. The composed provider takes its required scopes from the GitHubProvider, and a static grant reports every scope the flow asks GitHub for, so either credential clears that floor. What a tool needs beyond it is left to the scope gate. A bearer token matching neither is refused with 401 invalid_token, naming neither.

Token store

OAuth client registrations and token state live in the store returned by build_token_store(). The in-process MemoryStore is the default, and clients re-register after every restart. DYNAMODB_TABLE_ARN selects DynamoDB, REDIS_HOST_PORT selects Redis, and setting both picks DynamoDB with a warning. When GITHUB_OAUTH_BASE_URL is set, keys are prefixed with a hash of that URL, so several deployments can share one table or one Redis instance without colliding.

The DynamoDB store is prepared from the lifespan, not on the first request, so a missing table is created and a role that cannot create it stops the server at startup. The server closes the store's client from the lifespan, alongside the GitHub HTTP clients.

Observability

The metrics middleware wraps every tool call and the server serves GET /metrics on the same port as /mcp. Alongside it, GET / returns the server's name, version and registered tool count as JSON. Both routes sit outside the auth layer, so a scraper and a probe need no credentials. See Metrics and Checking the server is up.