Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save matthewbusel-basis/fea8e5527a70ac2bd40fbc20cafd2065 to your computer and use it in GitHub Desktop.

Select an option

Save matthewbusel-basis/fea8e5527a70ac2bd40fbc20cafd2065 to your computer and use it in GitHub Desktop.
Recipe: MCP Gateway
Hand this document to Claude Code or Codex and let it walk you through building a working unified MCP gateway in roughly a day. The output is a git repo, one provider mounted end-to-end, a usage skill paired with that provider, and a provider-addition skill so the next provider lands in an hour. The shape mirrors what we run at Basis as Satellite, with most of the names changed and the heavyweight pieces simplified.
What you're about to build
A single endpoint that fronts many provider integrations, authenticates the caller, holds the right credential per user, exposes provider tools through one MCP surface, and ships with the procedural-knowledge layer that makes those tools actually useful inside an agent. The post that links this recipe argues that the under-appreciated cost of running your own gateway is skills, not security or maintenance. The recipe takes that seriously: it pairs every provider with a usage skill, treats the skill as part of the provider contract, and refuses to call the build done until an agent has used the provider through the local gateway and the skill that teaches it.
Goal
When you finish, you will have:
a git repository
a deployable gateway system
a local development path that any MCP client can connect to and validate against
one provider mounted and exercised end-to-end through the gateway
a usage skill for that provider, paired with the wrapper, in the agent's voice
a provider-addition skill so the next provider does not require tribal knowledge
a completion checklist a fresh agent can re-run
Core
Models
Gateway: one MCP surface for callers; one namespace-aware discovery surface for provider tools and resources.
Provider: stable identity, namespace, connection requirement, tool set, optional resource set.
UserIdentity: one canonical user id used for auth, connections, filtering, and auditability.
ServicePrincipal: optional second identity class for service-to-service callers; carries a service name and an allowed_provider_prefixes allowlist.
ProviderConnection: one connection record per user per provider for user-scoped providers; enough stored state to support connect, disconnect, and runtime use.
CredentialMaterial: secret material stored only when needed, isolated from provider business logic.
Tool: provider-owned callable MCP capability.
Resource: provider-owned MCP-readable resource.
Three load-bearing patterns
These are easy to design in and expensive to retrofit. Internalize them before you write code.
Two-endpoint design. If the gateway needs to serve both human-attributed callers (Claude Code, Codex, Cursor, Cowork, your status UI) and service-to-service callers (cron jobs, internal services, eval pipelines), expose two endpoints that share the same provider mounts but use entirely different identity machinery. Humans get OAuth at /mcp. Services get signed tokens or service-account credentials at /mcp/internal/, plus a per-service allowlist of which provider prefixes they may see. Do not multiplex the two through one auth path; you will end up bypassing scope checks under pressure.
Multi-axis tool visibility filtering. A unified gateway only adds value if it can hide tools that the caller should not see. Filter on every list-tools and call-tool request along at least these axes: server-wide mount status (a provider that failed to mount cannot leak tools), per-user connection state (Slack tools do not appear until the user has connected Slack), per-user preferences (a user can hide a provider from their own session), and an internal-only prefix gate (some prefixes are reachable only through the service endpoint, never the user endpoint). Add an agent-environment self-management block once any of your providers can manage agent runtimes.
Tool-prefix invariant. Pick a single tool prefix per provider and align it across the four places it lives: the registry entry (servers.json in our case), the in-code namespace constant, the supported-apps list used for connection checks, and the credential-provider mapping used by the filter middleware. Add a startup check that fails loudly if any of the four diverge. The most common cause of "the tool mounted but never appears" is a one-character drift in this invariant. Failing closed at startup is dramatically cheaper than debugging a missing tool live.
Security rules
Persisted secret material stays encrypted at rest. The encryption key is itself secret material, managed through your chosen secret-management system; do not check it into the repo even in dev defaults.
Plaintext credentials stay out of logs, debug output, and fixtures. Add a redaction filter on the logger before you write the first log line.
Secret-handling code stays isolated enough to audit or replace later. One module, one surface, one set of tests.
Provider access stays scoped through the gateway rather than bypassing the gateway contract. The first provider verification includes one real call through the MCP gateway, not a provider-only shortcut.
For any provider that ships write capability, add a fail-closed allowlist or blocklist applied server-side before the call leaves the gateway, configured outside the agent's reach. Once write tools are on the gateway, you cannot un-write a destructive call.
Ask these questions first
Ask these questions in order, one at a time, and wait for the user's answer before moving on. The order matters; later answers depend on earlier ones.
Caller classes. Who calls this gateway? Some options: human callers only, services only, or both. If both, plan for the two-endpoint design above.
Agent harnesses. Which MCP clients must this gateway serve? Some options: Claude Code, Codex CLI/App, Cursor, Cowork, custom internal clients, or all of the above. The gateway must remain harness-agnostic; this question is mainly to inform the verification step.
Persistent data store. Where should gateway data live? Some options: SQLite (lowest friction, good for prototype and small teams), Postgres, MySQL, DynamoDB, or another store. Encrypted-at-rest credential storage works on any of these; SQLite plus a Fernet-encrypted column is the fastest path to a real persistent system.
Backend and MCP runtime. Some options: Python + FastAPI + FastMCP, TypeScript + Express + MCP SDK, Go + HTTP + MCP library, or another stack.
Auth. How should human callers authenticate? Some options: Google OAuth, Auth0, work-email SSO, or another auth system. If you also have service callers, how should they authenticate? Some options: Google service-account ID tokens, signed JWTs from your own issuer, or mTLS.
Secret management. Where do secrets live in production? Some options: environment variables, AWS Secrets Manager, GCP Secret Manager, HashiCorp Vault, or another system.
Deployment target. Where will the gateway run? Some options: Docker on a VM, Render, Fly.io, AWS ECS, Kubernetes, or another platform.
Local development and validation workflow. Some options: Docker Compose plus a Makefile, devcontainer, native local processes, Tilt, or another workflow.
Starter provider. Which provider should ship first? Strong recommendation: pick a shared-API-key provider for milestone one (Better Stack, Grafana, Braintrust, PostHog) so the first day is about building the gateway, not debugging an OAuth flow. Save per-user OAuth providers (Linear, Slack, GitHub) for milestone two when the gateway is already working.
Status surface. Does the gateway need a human-readable status page beyond a JSON endpoint? Some options: server-rendered HTML, static HTML plus JavaScript, React, Next.js, or no frontend at all. This is intentionally last; defer if unsure.
Turn the answers into a build plan
After you collect the answers, summarize them in one short implementation plan before you start coding. Record: repository name, persistent data store, backend and MCP runtime, auth system or systems, secret-management system, deployment target, local startup and validation workflow, first provider, second-provider target, and which agent harnesses you will validate against.
If anything required for implementation is still unclear, ask one more focused question before writing code. After that, move directly into implementation. Do not stop for another approval step between the plan and the build unless there is a real blocker.
Use sub-agents deliberately
Use sub-agents only for bounded tasks. Good uses: one agent for repo skeleton and startup flow; one for the provider registry and provider contract; one for the first-provider implementation; one for validation and deployment docs. Keep the parent agent responsible for final architecture decisions, integration, conflict resolution, and final verification. Do not hand the entire repository build to one sub-agent. Use sub-agents for slices, then return to the parent flow.
Build steps
1. Create the repository
Initialize git. Suggested layout: backend/ (gateway app, provider registry, provider implementations, auth and identity code, credential and encryption code), frontend/ or static/ (minimal status surface), tests/ (smoke and provider tests), skills/ (provider usage skills and the provider-addition meta-skill, see steps 6 and 8), plus Dockerfile, docker-compose.yml, Makefile, README.md, and .env.example.
2. Implement the gateway first
Build the shared MCP gateway before any provider-specific logic. The gateway should include: one MCP surface; provider namespace routing; provider registration; discovery for tools and resources; the chosen auth and identity path or paths; the chosen secret-management path; the minimal status surface; and the multi-axis filter middleware described in the load-bearing patterns above. If you are building both endpoints, build the user endpoint first and stub the service endpoint with a 501; the service path is easier to add once the user path is verifiable.
3. Implement secret management correctly
Treat the gateway as a credential-bearing system from line one. Persisted provider secrets stay encrypted at rest. Plaintext credentials stay out of logs, debug output, and fixtures. Secret-handling code stays in one isolated module with its own tests. The encryption key is itself secret material and is managed through the chosen secret-management system. If your first provider does not need persisted secrets in milestone one, keep the secret-management boundary in place anyway so the second provider does not require a redesign.
4. Implement observability
Do not ship blind. At minimum: structured JSON logs on every MCP request with user identifier, client identifier, JSON-RPC method, tool name, latency, status code, and a request id; a token-redaction filter on the logger applied before the first call; and a slow-call warning threshold that fires per request and per tool call. Add a small bounded ring buffer of high-signal events per session if your data store supports it. The cost of this layer is one afternoon. The cost of not having it, when something starts misbehaving in production, is many afternoons.
5. Implement the first provider end to end
Build the first provider all the way through the gateway. The implementation should include: provider registration; connection requirements (whether the user must connect anything, and through what flow); at least one real callable capability; the real credential or config path needed; and discovery through the gateway. Confirm the tool-prefix invariant alignment as a startup check while you are there.
6. Pair the first provider with a usage skill
This is the step that distinguishes a useful gateway from a generic one. Write a usage skill, in the agent's voice, that teaches future agents how to use the first provider for real work. The skill should be markdown, scoped to the provider, paired with the wrapper in the same repo (under skills/<provider>/), and treated as part of the provider contract: provider PRs that ship without skill updates get sent back for revision. Cover at least: when the agent should reach for this provider, the most useful workflow patterns, common pitfalls and how to recognize them, the project's domain shorthand that maps to provider concepts, and the failure modes the agent should retry versus the failure modes it should escalate. Keep it under 1,000 words. Have your coding agent draft it, then read it yourself.
7. Shape the repo for provider two
Even if only one provider ships in milestone one, shape the repo so adding the second is a registry update, an OAuth flow stub, and a usage skill, not a refactor. The MCP gateway surface, the provider contract, the secret-management boundary, and the auth and identity boundary should not move when the second provider lands.
8. Create the provider-addition meta-skill
Write a reusable skill that teaches future developers and agents how to add new providers, separate from the per-provider usage skills. At minimum, cover: the supported integration paths (public MCP provider, direct API provider); how to research the chosen public MCP or API before implementing it; the key facts that research should capture (auth model, tool surface, resource surface, rate limits, expected secret material, local testing requirements); the required file touchpoints; how provider registration works; how namespaces and the prefix invariant are defined; how auth and credential wiring work for user-specific providers and how to handle exceptions when a provider needs a different credential model; how secret management applies to a new provider; how provider tools and resources are exposed through the gateway; the common failure modes when a provider mounts incorrectly or does not appear; and the minimum validation checklist for a newly added provider. Optimize for boring, repeatable provider onboarding rather than clever abstractions.
9. Verify
Verification is the bar; do not treat a successful first-call smoke check as the end. Run all of the following:
docker compose up --build (or your equivalent local startup).
make validate (or your equivalent local validation).
One gateway discovery call.
One real call through the gateway to the first provider.
Two different MCP clients (for example Claude Code and Codex) connecting to the same local gateway and seeing the same tool list. This proves the harness-agnostic claim.
A second user with a different connection state seeing a different filtered tool list. This proves the per-user filter actually works.
One full round-trip through the encrypted credential storage path. Connect a credential, restart the gateway, reconnect a client, confirm the credential decrypts and the tool call succeeds. This proves the at-rest encryption is wired through, not stubbed.
Completion bar
The build is done when all of these are true:
The repository exists and is under git.
The system starts locally.
The gateway surface is reachable at the chosen URL.
The first provider is discoverable and callable.
The usage skill for the first provider is paired with the wrapper in the repo.
The provider-addition meta-skill exists.
Two different MCP clients can connect to the local gateway and use the first provider.
A second user with different connection state sees a different filtered tool list.
Persisted secret material is encrypted at rest end-to-end (verified by restart-and-decrypt).
Logs are structured, redacted, and include a slow-call threshold.
The deployment path is documented.
Another agent could clone the repo, follow the local instructions, and reach this same bar.
Hand off
At the end, give the user: the repository, the chosen technical decisions, the local run and validation commands, the deployment path, the exact verification steps that confirmed provider one worked, the usage skill paired with provider one, the provider-addition meta-skill for milestone two, and a one-line next step naming the second provider. Keep the handoff terse; the completion bar is the artifact, not the handoff.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment