Adapter Pattern for GitHub, n8n & MCP
so1 intermediate 5 min read
ELI5
Each external service is a different appliance with a different plug. Adapters are the wall sockets that make them all look the same to the route handlers — same shape going in, same shape coming out, regardless of whether the appliance is GitHub, n8n, or an MCP server.
Technical Deep Dive
Module Layout
Per ADR-002, each integration owns one module: lib/adapters/{service}.ts. Each adapter is responsible for:
- Calling the upstream API.
- Transforming responses into
@so1/sharedtypes. - Translating upstream errors into so1 error codes (so1-011).
Adapters do not read sessions, generate requestIds, or set HTTP statuses — those belong to middleware and route handlers.
Class Shape
classDiagram class Adapter { +call(ctx, params) Promise~Result~ -secrets: SecretsManager -httpClient: Fetch } class GitHubAdapter class N8nAdapter class McpBroker class SecretsManager { +get(orgId, key) Promise~string~ } Adapter <|-- GitHubAdapter Adapter <|-- N8nAdapter Adapter <|-- McpBroker Adapter --> SecretsManager : reads token per orgRequest Flow
flowchart LR route[route handler] --> ad[adapter.call ctx,params] ad --> sec[SecretsManager.get orgId,key] ad --> http[fetch upstream] http --> norm[normalise body] norm --> route http -.error.-> trans[map to so1 code] trans --> routeToken Brokering
Per ADR-001 and ADR-002, the adapter is the only place a third-party token is dereferenced. Tokens come from the per-org secrets manager (deferred choice: KMS / Vault / Google Secret Manager) via a lib/secrets.ts abstraction. Token rotation is invisible above the adapter.
Error Translation
| Upstream | Adapter Output Code |
|---|---|
| GitHub 4xx/5xx | GITHUB_ERROR (HTTP 502 — see so1-011) |
| n8n 4xx/5xx | N8N_ERROR (502) |
| MCP tool failure | MCP_ERROR (502) |
Key Terms
- Adapter → integration-specific façade hiding upstream quirks.
- Broker → adapter variant for protocol-multiplexed transports (MCP stdio/SSE).
- Secrets manager → server-side store yielding per-org tokens on demand.
Q&A
Q: Can a route handler call fetch('https://api.github.com/...') directly?
A: No — that bypasses error normalisation, secrets brokering, and request-id propagation. Always go through the GitHub adapter.
Q: How does the adapter know which org’s token to use?
A: It reads orgId from the Hono context (set by auth middleware) and asks SecretsManager.get(orgId, key).
Q: What if an upstream returns an unknown error shape?
A: It maps to {service}_ERROR (502) with the raw payload tucked into error.details.upstream, preserving debuggability without leaking secrets.
Examples
A GET /api/catalog route calls githubAdapter.listRepos(ctx, { orgId }). The adapter resolves the org’s PAT from secrets, hits https://api.github.com/orgs/.../repos, and returns a normalised RepoList validated against a @so1/shared Zod schema before the route hands it to the response.
neighbors on the map
- Factory Equipment Services deciding which factory service should own a new pipeline step
- purr-api Layered Architecture adding a new feature to purr-api
- Two-Service Architecture onboarding to the chronicle-hq monorepo