MCP servers / explained, then built

What the f*** is an
MCP server?

A program that gives an AI model a list of functions it's allowed to call. That's pretty much it.

By the end: one you built, on the internet, with a real login screen Claude and Cursor can sign into.
01 / the model on its own

A language model can't do anything

model
text in → [ model ] → text out # can't read your files # can't check your calendar # can't query your database # can't send a Slack message
So you give it tools. A tool is just a function with a name, a description and some parameters.
The model never runs it. It replies "call this one, with these arguments." Your code runs it and sends the result back.
02 / how a tool call works

The whole thing is four steps

1
Model reads the tool listnames, descriptions, parameters
2
Model decides "I need this one"replies with the function name + arguments as JSON
3
Your code runs the functionthe model is not involved here at all
4
Result goes back to the modelit reads it and writes the answer, or calls another tool
what actually goes over the wire
# press Step
03 / the problem MCP solves

Before MCP: everyone rebuilt the same plumbing

Claude
Cursor
ChatGPT
your app
your
GitHub
tools

Four apps, four different integrations, for the exact same functions.

03 / the problem MCP solves

MCP is just a standard for that plug

Claude
Cursor
ChatGPT
your app
MCP
server
list_repos()
create_issue()
search_code()

Model Context Protocol. Write the tools once, every app that speaks MCP can use them. The USB-C of AI tools.

04 / two sides

Client asks, server answers. It's all JSON.

MCP clientClaude, Cursor, VS Code, ChatGPT
tools/list →
← tools/call
MCP serveryour code, your functions
client → server
{ "method": "tools/list" } { "method": "tools/call", "params": { "name": "add_a_note", "arguments": { "text": "buy milk" } } }
server → client
{ "tools": [ { "name": "list_my_notes", "description": "List all saved notes." }, { "name": "add_a_note", "description": "Save a new note." }, { "name": "delete_a_note", ... } ] } { "content": [ { "text": "{\"id\": 1, \"text\": \"buy milk\"}" } ] }
05 / two ways to run one

Local vs remote

local · stdio

The client launches your server as a subprocess and pipes JSON through stdin / stdout.

python my_server.py

Only on your machine. Nobody else can touch it. What 90% of tutorials show.

remote · HTTP

Your server runs somewhere on the internet. It has a URL. Clients connect over HTTP.

https://notes.example.com/mcp

How every real product does it. GitHub, Notion, Stripe. Works from your laptop, phone, work machine.

The moment your server is a URL, you have a problem the local version never had.
06 / access control

Anyone with the URL can call your tools

Your tools read data, write data, delete data. So every request now needs three answers.

01Who is calling this?
02What are they allowed to do?
03Who are they doing it on behalf of?
06 / access control

How it's going out there

~25%

of public MCP servers have no authentication at all

most

of the rest use a static API key. One long-lived secret in a config file, shared by everyone.

few

actually use proper OAuth, which is what the MCP spec says a remote server should do.

2026 audits of public MCP servers. Rough numbers, the shape is what matters.

06 / access control

Why a static key is a problem

agent 1
agent 2
agent 3
agent 4
agent 5
API_KEY=
sk-a83f...
Which one is which? Same key, so you can't tell the agents apart.
Who told it to do that? No user attached to any request.
One gets compromised? Kill the key, break everybody.
Fine for a weekend project. Not fine for anything real.
07 / what the spec says: OAuth 2.1

Your MCP server does not do login

Claude / CursorMCP client
Your MCP serverjust validates tokens
Authorization serverlogin, consent, tokens
1
Claude calls your server with no tokenserver replies 401 + "here is where you go to log in"
2
User logs in at the authorization serverGoogle, email, whatever. Normal login screen.
3
User sees a consent screen"Claude wants to read your notes and create notes. Allow?"
4
Claude gets a token, sends it with every requestAuthorization: Bearer eyJ...
5
Your server checks the token, then runs the toolsignature, meant for this server, has the right scopes
08 / what's inside the token

Scopes say what. sub says who.

access token (decoded)
{ "sub": "U2abc...", ← the user "aud": "https://notes.example.com/mcp", ← YOUR server only "scope": "notes:read notes:write", ← what they approved "azp": "cursor-7f3a", ← which client (agent) "exp": 1757450000 ← short-lived }
Per-tool permissions. list_my_notes needs notes:read, delete_a_note needs notes:write. Only approved read? Write fails cleanly and the client can ask for more.
Per-user data. Every call has a user id, so notes are stored per user with one line of code.
Revoke one thing. One user, one agent, without touching anyone else. That's the difference between a demo and a product.
09 / one more piece

Clients register themselves

Dynamic client registration. Claude, Cursor and VS Code tell the authorization server "hi, I'm a client, here's where to send the user back" and get an id automatically.
You never set up Claude as an app by hand. Paste a URL into the client, the rest just happens.
what Cursor does when you paste the URL
POST /mcp → 401 GET /.well-known/oauth-protected-resource GET /.well-known/oauth-authorization-server POST /register → client_id # browser opens: login → consent POST /token → access_token POST /mcp Authorization: Bearer → 200
10 / so who builds all that

You could build the authorization server yourself

You'd needDo it yourselfPlug one in
Login page + consent screen✗weeks✓config
Token signing, expiry, refresh✗get it wrong once, game over✓done
Dynamic client registration✗spec reading✓toggle
Scopes per tool, per user✗your own policy engine✓dashboard
See who connected, revoke one✗build an admin panel✓dashboard
We're going to plug one in. But first, let's build the server.
let's build it

Three versions, same three tools

v1 · local

FastMCP, stdio, three tools over SQLite. Add it to Cursor. Watch the tool calls.

15 lines
v2 · remote

One line change: it's a URL now. Then hit it from another terminal with zero login.

the problem
v3 · protected

The 401 dance for real. Login, consent, scopes, per-user notes. Claude and Cursor sign in.

the fix
← → to move · F fullscreen