Architecture¶
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¶
- 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_TOKENin constant time, runs the GitHub OAuth2 authorisation code flow with PKCE, or does both. resolve_tokenreturns the token for the call: the one the request arrived with wherever the transport authenticated it, and the server'sGITHUB_TOKENotherwise. This is the only difference the tools see between the modes.PRIssueAnalyserroutes 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.GitHubIntegrationissues the API request over an async HTTP client bounded byGITHUB_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.