Skip to content

Instantly share code, notes, and snippets.

@donbr
Created June 25, 2026 23:36
Show Gist options
  • Select an option

  • Save donbr/649bfc01a3a6c486384b9437dc9f7204 to your computer and use it in GitHub Desktop.

Select an option

Save donbr/649bfc01a3a6c486384b9437dc9f7204 to your computer and use it in GitHub Desktop.
Session 8 Cheat Sheet — Model Context Protocol (MCP)

Session 8 Cheat Sheet — Model Context Protocol (MCP)

A frame to help you reason through the assignment — the concepts, diagrams, and API map. It deliberately does not contain the answers or a filled-in tool. Instead it gives you the questions to ask yourself and the method to get there. The work — and the learning — is in reading the server code, running it, connecting a client, and writing your own conclusions.

Source repo: 08_MCP/ — entry point server.py, server package app/ (server.py, tools.py, oauth.py, routes.py, db.py). There is no notebook this session — Q1/Q2 are answered in README.md, and your code deliverable is a new @mcp.tool() in app/tools.py. Corpus: a seeded SQLite "cat shop" catalog (app/db.pycatshop.db) — 8 demo products, a teaching fixture, not real inventory.


1. Quick Reference

You want to… Reach for One-liner
Stand up an MCP server FastMCP mcp = FastMCP("Cat Shop", auth_server_provider=..., auth=AuthSettings(...)) (app/server.py)
Serve it over the network Streamable HTTP transport mcp.run(transport="streamable-http") (server.py); binds 0.0.0.0:8000
Expose a function as a tool @mcp.tool() @mcp.tool() on an async def with typed args + docstring (app/tools.py)
Read DB inside a tool the provider's connection db = await oauth_provider._get_db()await db.execute(sql, params)await db.commit()
Know who is calling the auth context token = get_access_token()await oauth_provider.get_username_for_token(token.token)
Gate access with OAuth OAuthAuthorizationServerProvider subclass CatShopOAuthProvider implements authorize / exchange_authorization_code / load_access_token … (app/oauth.py)
Add a non-tool web route @mcp.custom_route @mcp.custom_route("/login", methods=["GET","POST"]) renders the login page (app/routes.py)
Make it publicly reachable ngrok + ISSUER_URL ngrok http 8000 then ISSUER_URL=https://… uv run server.py
Drive it from an AI an MCP client Claude Desktop, MCP Inspector, or a custom mcp SDK client over Streamable HTTP
Seed / inspect data aiosqlite app/db.py init_db() creates tables + seeds PRODUCTS; cart_items is per-username

The one sentence that anchors everything: the tool is the contract — a typed, documented async function the AI client calls by name; OAuth decides who may call it, the transport decides how the call arrives, and the DB is the only source of truth the tool may trust.


2. The Big Picture — one server, three planes (auth, transport, tools)

An MCP server publishes tools (typed functions) that an AI client discovers and calls. Three concerns are deliberately separated: OAuth (identity/authorization), the Streamable HTTP transport (how messages travel), and the tools + DB (the actual capability). The assignment adds a fourth tool to the existing six without breaking that separation.

flowchart LR
    AC["AI client<br/>Claude Desktop · MCP Inspector · custom mcp client"]
    OA["CatShopOAuthProvider<br/>+ /login custom route"]
    MCP["FastMCP 'Cat Shop'<br/>mcp.run(transport='streamable-http')"]
    T["@mcp.tool() functions<br/>list_products … checkout (+ yours)"]
    DB[("aiosqlite catshop.db<br/>products · cart_items · tokens")]
    AC -->|"1 · OAuth: authorize → /login → code → token"| OA
    AC -->|"2 · Streamable HTTP + Bearer token"| MCP
    MCP --> T
    T -->|"get_access_token() → username"| OA
    T --> DB
    OA --> DB
Loading

ASCII fallback:

AI client ──OAuth (authorize → /login → code → access token)──► CatShopOAuthProvider ──► catshop.db
   │                                                                                        ▲
   └──Streamable HTTP + Bearer token──► FastMCP "Cat Shop" ──► @mcp.tool() ─────────────────┘
                                                              (get_access_token → username)

Why this shape? The transport carries opaque tool calls; it does not know about cats or carts. OAuth runs beside the tools, not inside them — a tool just asks "who is this token?" and trusts the answer. The DB is shared by both planes (tokens live in the same SQLite file as products). Decoupling these is exactly what Q1/Q2 ask you to reason about.


3. Setup & roles

uv sync                                  # Python 3.13; installs mcp[cli], aiosqlite, uvicorn, …
cp .env.example .env                     # set OPENAI_API_KEY (used by client-side / advanced build)
uv run server.py                         # serves http://localhost:8000 (Streamable HTTP)
# public exposure:
ngrok http 8000                          # in a 2nd terminal → copy the https forwarding URL
ISSUER_URL=https://xxxx.ngrok-free.app uv run server.py   # ISSUER_URL MUST match the public URL
Component File Role
FastMCP("Cat Shop") app/server.py The MCP server object; wires OAuth + tools + transport
mcp.run(transport="streamable-http") server.py Boots the HTTP transport on 0.0.0.0:8000
CatShopOAuthProvider app/oauth.py Full OAuth authorization-server (clients, codes, tokens, refresh, revoke)
@mcp.tool() functions app/tools.py The 6 catalog/cart tools the client can call
@mcp.custom_route("/login") app/routes.py The human login page that mints the auth code
init_db() + PRODUCTS app/db.py Creates tables and seeds the catalog into catshop.db
ISSUER_URL env Public base URL; OAuth metadata + redirects must use it

No ANTHROPIC_API_KEY and no LLM call inside the server. The only key in .env.example is OPENAI_API_KEY (for the advanced custom-client / LangChain build). The server itself is pure tools + OAuth + SQLite.


4. Core concepts

4.1 What MCP actually is

MCP is a client↔server protocol for exposing tools (and resources/prompts) to AI clients in a standard shape, so any compliant client (Claude Desktop, Inspector, your own agent) can discover and call them without bespoke glue. The server declares capabilities; the client lists and invokes them. Docs: https://modelcontextprotocol.io/

4.2 The server object — FastMCP

FastMCP is the high-level server. You give it a name, an auth provider, AuthSettings, and a host/port; decorators register tools and routes onto it.

mcp = FastMCP(
    "Cat Shop",
    auth_server_provider=oauth_provider,
    auth=AuthSettings(issuer_url=ISSUER_URL, resource_server_url=ISSUER_URL, ...),
    host="0.0.0.0", port=8000,
)

4.3 Transport — Streamable HTTP vs stdio

A transport is how MCP messages move between client and server. Two common choices:

stdio:            client ⇄ subprocess pipe   (local only, no network, no auth layer)
Streamable HTTP:  client ⇄ HTTP(S) stream    (remote-capable, SSE/chunked, OAuth-gateable)

This server uses mcp.run(transport="streamable-http") — a long-lived HTTP connection that streams tool events, so a remote client can reach it (via ngrok/OAuth) instead of spawning the server locally. Reasoning about this trade-off is the crux of Q2.

4.4 Defining a tool — @mcp.tool()

The decorator turns an async function into a callable tool. The signature is the schema: type hints become the input schema; the docstring becomes the description the client shows the model.

@mcp.tool()
async def get_product(product_id: int) -> dict:
    """Get full details of a single product by its ID."""
    db = await oauth_provider._get_db()
    cursor = await db.execute("SELECT id, name, price FROM products WHERE id = ?", (product_id,))
    row = await cursor.fetchone()
    return {"id": row[0], "name": row[1], "price": row[2]} if row else {"error": "Not found"}

Conventions every tool in this repo follows: typed args, a one-line docstring, await oauth_provider._get_db() for data, and a JSON-able dict/list return.

4.5 Per-request identity

Cart tools need to know who is calling. The auth middleware exposes the bearer token; the provider maps it to a username (helper _get_username() in tools.py):

token = get_access_token()                                   # from auth_context middleware
username = await oauth_provider.get_username_for_token(token.token)

Catalog tools (list_products, get_product) are identity-free; cart tools (add_to_cart, view_cart, remove_from_cart, checkout) all resolve username first.

4.6 OAuth — the authorization-code flow with PKCE

CatShopOAuthProvider subclasses OAuthAuthorizationServerProvider and persists everything in SQLite. The flow:

client registers ─► /authorize ─► /login (pick username) ─► auth code
       ─► exchange code ─► access_token (+ refresh_token, Bearer, 1h expiry) ─► call tools

Key methods to recognize when reading the code: register_client, authorize, exchange_authorization_code, load_access_token, exchange_refresh_token, revoke_token. The client proves possession via the PKCE code_challenge; tokens carry scopes (read, write). MCP auth background: https://auth0.com/blog/mcp-specs-update-all-about-auth/

4.7 The data layer — aiosqlite

One async SQLite file (catshop.db) holds both OAuth state (clients, codes, access/refresh tokens, token_users) and shop state (products, cart_items, users). init_db() creates the tables and seeds PRODUCTS if empty. cart_items is unique on (username, product_id), so add_to_cart upserts with ON CONFLICT … DO UPDATE.

4.8 Connecting a client + going public

  • Local/dev: point Claude Desktop or MCP Inspector at the Streamable HTTP endpoint, or write a custom mcp SDK client.
  • Public: ngrok http 8000 gives an HTTPS URL; restart with ISSUER_URL=<that url> so OAuth metadata and redirects match. A mismatch is the #1 "auth mysteriously fails" cause (README note).

5. Questions — reason it through (no answers here)

Write your answers in README.md under each question's #### Answer heading. Don't leave the _(insert your answer here)_ placeholder — that grades as not answered.

Q1 — Why is OAuth important for MCP servers, and what security considerations apply when exposing tools to AI clients? Ask yourself: what does the server actually receive from the client — the user's password, or something else? Trace the flow in §4.6: at what point does a token replace a credential, and what does that buy you? Then name the trade-offs you'd worry about in production: how narrow should a token's power be (look at valid_scopes in app/server.py)? What happens when a token is stolen — does expires_at in app/oauth.py help? What must the server still verify about a token before trusting it? A strong answer states the mechanism and at least one concrete consideration.

Q2 — What is Streamable HTTP transport, and why expose a server publicly with OAuth instead of a local stdio connection? Re-read §4.3 and the run line in server.py. Ask: where does the client have to be for stdio to work, versus Streamable HTTP? What can a remote client (Claude Desktop on someone else's machine) do over one that it can't over the other? And once the server is reachable on the public internet, what stops anyone from calling your tools — what role does OAuth play that the transport alone does not? Contrast it with a plain REST API to sharpen the distinction.


6. Activities — what to build + how to check yourself

Activity 1 — Extend the MCP server (graded)

Deliverable: add at least one new @mcp.tool() to app/tools.py, beyond the 6 stubs already there (list_products, get_product, add_to_cart, view_cart, remove_from_cart, checkout). Wire it to the database and demo it through an MCP client in your Loom.

Pick something the shop is missing — e.g. search_products, update_cart_quantity, or get_order_history. Then check your own work against the existing tools as a template:

  • Does your function carry the @mcp.tool() decorator and a genuinely new name (not a renamed stub)?
  • Are the arguments typed the way get_product(product_id: int) is? Would the model know what to pass from your signature + docstring alone?
  • Does it actually touch the DB via await oauth_provider._get_db() and a real execute/commit — not just return a hard-coded list?
  • If it reads or changes the cart, does it resolve the caller with _get_username() first, like the other cart tools?
  • Can you invoke it live through a client (Claude Desktop / MCP Inspector / your own) and show the call + result on screen in the Loom?

Don't copy another tool's body wholesale — open app/tools.py, read how list_products and add_to_cart are shaped, and write yours in that style. The learning is in matching the contract, not in the SQL.

Advanced Activity — custom MCP client (optional)

Build a custom mcp SDK client that authenticates over OAuth + Streamable HTTP and runs a multi-step flow (browse → add to cart → checkout). As you build it, note where MCP made the integration easier or harder than hand-rolling REST calls — that comparison is the point. Include a demo in your Loom. Optional and unscored — skip it freely if you're short on time.


If you want to go deeper: MCP docs https://modelcontextprotocol.io/ · MCP-UI https://mcpui.dev/ · MCP auth deep-dive https://auth0.com/blog/mcp-specs-update-all-about-auth/.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment