CRUMB a card from devarno-cloud

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:

  1. Calling the upstream API.
  2. Transforming responses into @so1/shared types.
  3. 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 org

Request 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 --> route

Token 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

UpstreamAdapter Output Code
GitHub 4xx/5xxGITHUB_ERROR (HTTP 502 — see so1-011)
n8n 4xx/5xxN8N_ERROR (502)
MCP tool failureMCP_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