Published: 2026-09-24
Deep dive

Build an MCP server three ways: local stdio, over HTTP, then with OAuth and per-user scopes

Chapters / key moments (click to jump — plays here on the page)

The first half is a clear explanation of what an MCP server is. The reason to watch — and read this — is the second half, which most tutorials skip: taking the server off your laptop and securing it properly, so different people and agents can use it without sharing one long-lived secret.

Source video

"MCP Servers Explained & Built" by Tech With Tim — Watch on YouTube →

The model in one paragraph

An MCP server is just a program that tells a model which functions it can call — a name, a description and parameters for each. The client asks the server what tools it has, the model asks for a call in text, the server runs it and returns the result. MCP standardises that handshake so one server works with every compatible client instead of one integration per model per tool.

Two transports matter: stdio, where the client launches the server as a local subprocess (fine for personal tools), and HTTP, where the server runs somewhere others can reach (what every real product, like GitHub's MCP server, does).

Step 1 — a local server with FastMCP

Add fastmcp as a dependency, write ordinary Python functions and decorate them with @mcp.tool. FastMCP builds the tool schema for you. His example is a small notes database with add, list and delete tools backed by Postgres. The shape:

from fastmcp import FastMCP

mcp = FastMCP("notes")

@mcp.tool
def add_note(title: str, body: str) -> dict:
    """Create a note and return it."""
    ...

if __name__ == "__main__":
    mcp.run()          # stdio by default
Two details that change answer quality

FastMCP uses the function's docstring as the tool description and its type hints as the parameter and return schema. Both are what the model reads when deciding whether and how to call a tool, so write them as carefully as a prompt.

To use it locally, add it to your client's MCP config — in Cursor, Settings → Tools & MCP — under mcpServers with the command and args that start it. The client then talks to it over stdio and lists the three tools.

Step 2 — the same server over HTTP

Switch the transport to HTTP with a host and port, run the server yourself, and point the client at the URL instead of a command — in his demo http://localhost:8000/mcp. Include the /mcp path; leaving it off is the easy mistake. The same three tools appear.

This is also where the danger starts: anyone who can reach that URL can call every tool, and nothing is scoped to a user.

Why a static API key is the wrong fix

The common shortcut is one long-lived key that every agent sends. With five agents on one key you cannot tell them apart, cannot limit what each may do, and if one leaks you have to kill the key and break all of them at once. For every request the server should be able to answer who is this, what are they allowed to do, and did the user actually grant it — which is what OAuth gives you.

Step 3 — OAuth and per-user scopes

  • A request without a valid token gets 401, and the server advertises where to authorise via a well-known URL.
  • The client registers with the authorisation server, the user sees an ordinary login and consent screen, and the client then sends the resulting token with every request.
  • Each tool reads the current user and checks the token's scopes (he defines custom ones such as reading and writing notes) before acting. Notes become owned by a user ID, so one person's agent cannot list another's.
  • Signing out and back in as a different user demonstrates the point: the new user sees no notes.

Sponsor note: the authorisation server in the demo is a hosted identity product from the video's sponsor, which handles client registration, the login flow and token refresh. The pattern — protected resource, well-known discovery, scoped tokens checked per tool — is standard and works with any OAuth 2.1 authorisation server; you don't need that vendor to apply it.

Key Takeaways

  • Start with FastMCP and @mcp.tool; write docstrings and type hints like prompts.
  • stdio for personal tools, HTTP for anything someone else uses — and remember the /mcp path.
  • An HTTP MCP server with no auth lets anyone who finds the URL run every tool.
  • Don't secure it with one shared static key; use OAuth so each user and agent has its own revocable, scoped token.
  • Check scopes inside each tool and scope data to the user, not just the connection.

Before you connect third-party MCP servers: MCP supply-chain risks. Adding tools to an agent: Hermes MCP tools.

What you can actually set up from this

Extracted from the video's own transcript — the specifics the original summary left out.

Configuration shown

Shape of the local stdio entry described in the video (placeholders, not copied from Cursor docs)

{
  "mcpServers": {
    "notes-local": {
      "command": "uv",
      "args": ["run", "<directory on your computer>", "<server file>"]
    }
  }
}

Remote server URL

http://localhost:8000/mcp

Template shown in the video. Adapt to your project — do not copy verbatim without reviewing each line.

Errors & fixes covered

Error: Requests to the remote server fail with errors after adding it to Cursor

Why it happens: The URL was entered without the /mcp path

Fix: Add /mcp to the end of the server URL, in the client and in Descope. Without it the connection does not work.

Reproducible steps

  1. Write tools as plain Python functions

    With FastMCP installed via pip, decorate each function with the tool decorator and FastMCP adds it to the server's tool list automatically. The example is three note tools (list, add, delete) backed by a Postgres database.

  2. Write docstrings and type hints

    The function docstring becomes the tool description the model sees, and the type hints on parameters and return values give the model more context about how to call it. The speaker says this gives better results.

  3. Run it locally over stdio

    In Cursor, go to Settings, then Tools and MCP, then add a custom MCP server. The entry has a server name, a command (uv for a Python server, node for a Node.js one) and arguments that run the server file. Cursor launches the server as a subprocess and no HTTP requests are sent.

  4. Switch to remote streamable HTTP

    Change the run call at the bottom of the script to set the transport, host and port. Run it with uv run, keep that terminal open, and add the server to Cursor by name and URL.

  5. Add authorization with Descope

    In the Descope console (sponsor of the video, free tier), create a project (the demo used the name tutorial, non-production), open the Agentic Identity Hub, then MCP servers, add a server named notes, set its URL to http://localhost:8000/mcp, and add a scope such as notes read.

Gotchas

  • An unauthenticated remote MCP server lets anyone with the URL list, add and delete notes, and notes are not scoped to a user. The server in the first two demos has no authentication.
  • Security claims in the video, not verified: audits found about a quarter of public MCP servers with no authentication, and most others use one shared static API key, which you cannot revoke per agent.
  • The flow the speaker describes follows the MCP specification: the server answers 401 and points to an authorization server, the user signs in and consents, and the client sends the token on each request. The spec references OAuth 2.1 with PKCE and dynamic client registration; check the MCP authorization spec for the exact requirements.
  • Scopes (for example a notes read and a notes write scope) live inside the token, and each tool can require one.