TAI BUI
Back to curriculum
CapstonePython (server, via FastMCP) or TypeScript (@modelcontextprotocol/sdk), Go (registry service)

Capstone 13 — MCP Server with Registry and Governance

The Model Context Protocol became the default tool-use spec in 2026. Anthropic, OpenAI, Google, and every major IDE ship MCP clients. Pinterest published its internal ecosystem of MCP servers, the AAIF Registry formalized capability metadata at `.well-known`, AWS ECS published the reference stateless deployment, and Block's goose-agent put the same protocol inside a hosted assistant. The production shape is StreamableHTTP transport, OAuth 2.1 scopes, OPA policy gating, and a registry where platform teams discover, validate, and enable servers. Build all of it.

TIME: 25 hoursPREREQUISITES: Phase 11 (LLM engineering), Phase 13 (tools and MCP), Phase 14 (agents), Phase 17 (infrastructure), Phase 18 (safety)

Learning Objectives

  • Design a StreamableHTTP MCP server that stays stateless behind a load balancer, and prove it with a multi-replica load test that needs no session stickiness.
  • Enforce OAuth 2.1 per-tool scopes and OPA policy on every tool call, covering scope checks, PII redaction, and payload-size caps.
  • Separate destructive tools onto a second MCP server gated by an approved:by:human scope elevated through a Slack card within 15 minutes.
  • Publish a .well-known/mcp-capabilities manifest, index it with a registry service, and write a redacted, append-only per-tenant audit log for every call.

Problem

A production MCP server is a network service that exposes internal tools to any MCP client over StreamableHTTP, paired with a registry, a separate service that lets a platform team discover, validate, and enable those servers. MCP is now the lingua franca of tool use: Claude Code, Cursor 3, Amp, OpenCode, Gemini CLI, and every managed agent consume MCP servers.

Writing the server is the easy part, since FastMCP makes it quick. The hard part is deploying it at scale under enterprise requirements: per-tenant OAuth scopes, OPA policy on destructive tools, StreamableHTTP stateless scaling, a registry for discovery, and an audit log per tool call. Pinterest's internal MCP ecosystem and the AAIF Registry spec set the 2026 bar.

You will build an MCP server exposing 10 internal tools (Postgres read-only, S3 listing, Jira, Linear, Datadog, etc.), a registry UI for platform discovery, and a human-approval gate for destructive tools. The load test shows StreamableHTTP scaling horizontally, and the audit trail is what gets you through an enterprise security review.

Warm-up0/4
1. A service behind a load balancer keeps each user's session data in its own process memory. What goes wrong when you add a second replica?Warm-up
2. An agent must read tickets but never delete them. What is the standard way to express that in an OAuth token?Warm-up
3. Why should personal data such as emails or phone numbers be masked before application logs are stored?Warm-up
4. In an MCP setup, what role does the server play for an agent client such as an IDE assistant?Warm-up

Concept

StreamableHTTP is the MCP transport that serves JSON-RPC over a single HTTP endpoint: it accepts requests, streams responses, and supports long-lived connections for notifications. The 2026 revision of MCP mandates it as the default, replacing the earlier stdio-and-SSE shape for networked servers. It is stateless by default, which means you can scale it horizontally behind a load balancer.

Authorization is OAuth 2.1 with per-tool scopes. A token carries scopes like jira:read, s3:list, and postgres:query:readonly, and the server checks them at tool-call time, not just at session start. For high-risk tools the server rejects any call whose scope has not been elevated to approved:by:human within the last N minutes. That elevation comes from a Slack review card.

The registry is a separate service. Every MCP server exposes a .well-known/mcp-capabilities document listing its tool manifest, transport URL, and auth requirements. The registry polls each one, validates it, and indexes it. Platform teams use the registry UI to see which tools exist, which scopes they need, and which teams own them.

Check: Concept0/10
1. What is StreamableHTTP in the MCP design?Define
2. What is the role of the .well-known/mcp-capabilities document?Define
3. What is scope elevation in this design?Define
4. What is the registry in this architecture?Define
5. Why does a stateless StreamableHTTP server make horizontal scaling straightforward?Explain
6. How does StreamableHTTP differ from the stdio-and-SSE shape that it replaced for networked servers?Explain
7. Why does the server check scopes at tool-call time rather than only when a session starts?Explain
8. Why is the approved:by:human scope valid only for a limited time instead of staying on the token?Explain
9. A user's jira:read scope is removed, yet an open connection keeps returning Jira results for another hour. Which design flaw explains it?Analyze
10. You are planning the build and the load balancer is already in place. Which early check best protects the stateless design?Design

Architecture

MCP client (Claude Code, Cursor 3, ...)
          |
          v
StreamableHTTP over HTTPS (JSON-RPC + streaming)
          |
          v
MCP server (FastMCP) behind load balancer
          |
   +------+------+---------+----------+------------+
   v             v         v          v            v
Postgres    S3 listing  Jira       Linear     Datadog
(read-only) (paged)     (read)     (read)     (query)
          |
   +------+-------------+
   v                    v
 OPA policy gate   destructive tool MCP (separate server)
                        |
                        v
                   human approval via Slack
                        |
                        v
                   audit log (append-only, per-tenant)

  registry service
     |
     v  GET /.well-known/mcp-capabilities from each server
     v
     UI: search / validate / enable-disable / ownership

Stack

  • Server framework: FastMCP (Python) or @modelcontextprotocol/sdk (TypeScript)
  • Transport: StreamableHTTP over HTTPS (stateless)
  • Auth: OAuth 2.1 with workload identity via SPIFFE / SPIRE
  • Policy: OPA / Rego rules per tool; policy decision service per request
  • Registry: self-hosted, consumes .well-known/mcp-capabilities manifests
  • Human approval: Slack interactive message for destructive tools
  • Deployment: AWS ECS Fargate or Fly.io, one server per tenant or shared with tenant scoping
  • Audit: structured JSONL per-tenant bucket with per-call lineage
Check: Stack0/1
1. What does SPIFFE / SPIRE provide in this architecture?Define

Build It

  1. Tool surface. Expose 10 internal tools: Postgres read-only query, S3 list objects, Jira search/fetch, Linear search/fetch, Datadog metric query, PagerDuty on-call lookup, GitHub read-only, Notion search, Slack search, Salesforce read. Each tool gets a typed schema and a scope label.

  2. FastMCP server. Mount the tools and configure the StreamableHTTP transport. Add middleware for OAuth token introspection and scope enforcement.

  3. OPA policy. Write a Rego policy per tool covering which scopes permit invocation, what PII redaction applies, and what payload-size caps apply. The decision service is called on every tool call.

  4. Registry service. Build a separate Go or TS service that polls .well-known/mcp-capabilities from registered servers and validates each manifest with JSON Schema. It exposes a list / search / validate / enable-disable UI.

  5. Capability manifest. Each server exposes .well-known/mcp-capabilities with the tool list, auth requirements, transport URL, owner team, and SLO.

  6. Destructive tool separation. Tools that mutate state (Jira create, Linear create, Postgres write) live on a second MCP server with a stricter auth flow. Tokens need the approved:by:human scope, elevated via Slack card within 15 minutes.

  7. Audit log. Write append-only JSONL per tenant: {timestamp, user, tool, args_redacted, response_redacted, outcome}. Presidio redacts PII before anything is written.

  8. Load test. Run 100 concurrent clients on StreamableHTTP. Add a second replica and show the load balancer redistributing traffic without session stickiness.

  9. Conformance tests. Run the official MCP conformance suite against both servers and pass all mandatory sections.

Check: Build It0/26
1. What does the OPA / Rego policy decide on each tool call?Define
2. What is the conformance suite used for in the build?Define
3. Which evidence shows StreamableHTTP scaling horizontally in the load test?Explain
4. Why do destructive tools live on a separate MCP server in this design?Explain
5. Why does the audit log redact PII with Presidio before persisting each per-tenant record?Explain
6. Why does the registry validate each manifest with JSON Schema before listing a server?Explain
7. Why does Build It step 9 run the conformance suite against both servers instead of only the read-only one?Explain
8. Why is the destructive server a better first build than the registry UI?Explain
9. A caller's token holds jira:write and approved:by:human, but the Slack approval happened 40 minutes ago. What should the destructive server do with a jira.create call?Apply
10. In main.py, both demo servers are registered and registry.search('jira') runs. What does it return?Apply
11. In main.py, a token with scopes {'jira:read'} calls s3.list on the read-only server. What does dispatch return?Apply
12. In main.py, Token(user='u7', scopes={'jira:write', 'approved:by:human'}) has no approved_at value and calls jira.create. What happens?Apply
13. In main.py, a caller with the right scope sends jira.search arguments whose JSON is about 9,000 characters. What does policy_decide return?Apply
14. In main.py, redact() is applied to {'note': 'mail [email protected], ssn 123-45-6789'}. What is returned?Apply
15. In main.py, jira.create is called with a token that has no fresh approval. What does the audit list receive?Apply
16. In main.py, the destructive server receives a call to jira.delete, which is not registered. What does dispatch return?Apply
17. In the TypeScript server, a tools/call to incidents_list passes severity 'p0'. Which incidents does it return?Apply
18. In the TypeScript server, a tools/call names no_such_tool. What does the reply look like?Apply
19. In your load test, a client's next call fails or loses its context whenever the load balancer routes it to the other replica, and only pinning clients to one replica fixes it. What is the most likely cause?Analyze
20. A mutating Jira tool runs for a token that has only jira:write, with no approval step. In main.py terms, what is the most likely misconfiguration?Analyze
21. A security review finds that 9 of 10 tools have a Rego policy and one tool was added with only a scope check. What is the real gap?Analyze
22. An audit file is written with raw emails and a nightly job then scrubs them. What principle of the design does this violate?Analyze
23. In main.py, the audit args for {'contact': 'call 415-555-0100'} still show the full phone number. Why?Analyze
24. You must cap each tenant at N calls per minute per tool. Where does the lesson's design put this rule?Design
25. You add Linear issue creation, which mutates state. Which placement fits the design?Design
26. A new team wants its server discoverable, with owner team, auth needs, and SLO visible without reading its code. What should it do?Design

Use It

$ curl -H "Authorization: Bearer eyJhbGc..." \
       -X POST https://mcp.internal.example.com/ \
       -d '{"jsonrpc":"2.0","method":"tools/call",
            "params":{"name":"postgres.readonly","arguments":{"sql":"SELECT 1"}}}'
[registry]   capability validated: postgres.readonly v1.2
[policy]    scope postgres:query:readonly present; allowed
[audit]     logged: user=u42 tool=postgres.readonly outcome=ok
response:    { "result": { "rows": [[1]] } }

Ship It

outputs/skill-mcp-server.md describes the deliverable: a production-grade MCP server, registry, and audit layer for internal tools, with OAuth 2.1 scopes and OPA gating.

WeightCriterionHow it is measured
25Spec conformanceStreamableHTTP + capability manifest passes MCP conformance tests
20SecurityScope enforcement, OPA coverage across every tool, secret hygiene
20ObservabilityPer-tool-call audit log with PII redaction
20Scale100-client load test horizontal scale demonstration
15Registry UXDiscover / validate / enable-disable workflow
100
Author's Note

I'd build the destructive-tool server from step 6 before I polish the registry UI. The approval flow is where the Slack elevation, the 15-minute window, and the scope check all have to agree, and a gap there lets a mutating call through. A registry bug costs you a bad listing; this one costs you a bad write. The rubric gives Security 20 points and Registry UX 15, which points the same way.

I'd also run the step 8 load test early, with the second replica added and no session stickiness. The point of a stateless StreamableHTTP server is that any replica can serve any request, and a server that keeps per-session state in memory would force stickiness and break that. My bet is that it's cheaper to find that out before the registry and audit layers are built on top.

Exercises

  1. Add a new tool (Confluence search). Ship it through the registry validation flow without touching the core server.

  2. Write an OPA policy that redacts Postgres query results containing columns named email, ssn, or phone. Exercise with a probe query.

  3. Benchmark StreamableHTTP vs stdio on local latency. Report per-call p50/p95.

  4. Implement per-tenant quota: maximum N calls per minute per tool per tenant. Enforce via a second OPA rule.

  5. Run the MCP conformance suite from mcp-conformance-tests and fix every failure.

Key Terms

TermWhat people sayWhat it actually means
StreamableHTTP"2026 MCP transport"Stateless HTTP + streaming; replaces SSE + stdio for networked servers
Capability manifest"Well-known doc".well-known/mcp-capabilities with tool list, auth, transport URL
OPA / Rego"Policy engine"Open Policy Agent for authorizing tool calls against external rules
Scope elevation"Approved-by-human"Short-lived scope granted via Slack approval, required for destructive tools
Registry"Tool discovery"Service that indexes MCP servers from their capability manifests
Workload identity"SPIFFE / SPIRE"Cryptographic service identity for OAuth token issuance
Conformance suite"Spec tests"Official MCP test battery for StreamableHTTP + tool manifest correctness

Further Reading

Code

Ships With