PyCon 2026 Tutorial

Build your first MCP server in Python

Pamela Fox

pamelafox.github.io/pycon2026-mcp-tutorial

Introductions

About me

Photo of Pamela smiling with an Olaf statue

Python Cloud Advocate at Microsoft

Formerly: UC Berkeley, Coursera, Khan Academy, Google

Find me online at:

Mastodon @pamelafox@fosstodon.org
BlueSky @pamelafox.bsky.social
Twitter @pamelafox
LinkedIn www.linkedin.com/in/pamela-s-fox/
GitHub www.github.com/pamelafox
Website pamelafox.org

About you

Turn to your neighbor and introduce yourself:

  • Name
  • Location
  • Is this your first PyCon, or have you attended before?
  • What are you excited for at PyCon 2026?
  • Make up a new meaning for "MCP" (e.g. "More Coffee Please") and write it on a sticky note.

Today's agenda

PartTime
Welcome10 min
MCP 10115 min
Building agents10 min
Exercise: Use existing MCP servers30 min
Building an MCP server15 min
Exercise: Build your own server40 min
☕️ break20 min
Advanced server features10 min
Exercise: Add visual UI and confirmation20 min
Authenticated servers15 min
Exercise: Add authentication20 min
Next steps5 min

MCP 101

AI agents

Agent loop
User query LLM Goal Tools

An AI agent uses an LLM to run tools in a loop to achieve a goal.

Agents are often augmented by:

  • Context from files, docs, or databases
  • Memory across a session or project
  • Human approvals and feedback

You've probably used an AI agent...

Coding agents like GitHub Copilot and Claude Code are AI agents.
Chatbots like ChatGPT also have agentic loops now.
You can also build your own agents in Python.

The agentic loop in action

Agent loop
User query LLM Goal Tools
User
Can I reorder my last bakery order for pickup today?
LLM
find_customer("Pamela")
Tool result
customer_id = "cust_42"
LLM
get_last_order("cust_42")
Tool result
items = [croissant, baguette]
LLM
check_inventory(items)
Tool result
croissant yes; baguette sold out
LLM
find_substitute("baguette")
Tool result
substitute = sourdough
Answer
I can reorder it with sourdough instead. Want me to place it?

Agents rely on LLM tool-calling

LLMs have been trained to know how to "call tools".

  • LLM sees the available tools and their descriptions.
  • LLM suggests a tool name and arguments.
  • Agent runtime runs the actual code.
  • Tool result is sent back to the LLM as context.
  • LLM decides whether to call another tool or answer.

The model does not execute the tool.
The runtime does.

User request LLM suggested tool call get_last_order("cust_42") Agent runtime executes tool tool result items = [...]

MCP: Model Context Protocol

MCP is the open standard that tells agent runtimes how to discover and connect to the tools they can call.

🤖 AI agent 🗄️ Database 💬 Slack 🐙 GitHub MCP MCP MCP

🔗 MCP specification: modelcontextprotocol.io

MCP architecture

MCP Host AI agents and apps MCP Client A MCP Client B MCP Server A MCP Server B Tools Prompts Resources Tools Prompts Resources MCP MCP

MCP client/server request flow

MCP Client MCP Server ① Discover tools {"method": "tools/list"} {"tools": [{"name": "get_product", "description": "...", "inputSchema": {...}}, ...]} ② Call a tool {"method": "tools/call", "params": {"name": "get_product", "arguments": {"id": 1}}} {"content": [{"type": "text", "text": "Product: Widget (id=1), $9.99"}]}

MCP clients

Clients with the strongest support:

🤖
Claude Code
🖥️
Claude Desktop
💻
VS Code + GitHub Copilot
🪿
goose
🔬
MCPJam Inspector
🔍
MCP Inspector

Support across 100+ clients varies:

CapabilitySupport
ToolsAll
Prompts43/113
Resources47/113
OAuth with DCR16/113
Elicitation16/113
Apps10/113

Coding agents + MCP

Coding agent: VS Code + GitHub Copilot

1. Add MCP servers

Install from Extensions:

VS Code extensions marketplace search results for MCP servers

Or configure .vscode/mcp.json file:

{
  "servers": {
    "github": {
      "type": "http",
      "url": "https://api.githubcopilot.com/mcp/"
    }
  }
}

2. Use from Copilot

Once added, tools appear in chat with approvals and structured results.

GitHub Copilot chat using a GitHub MCP server tool in VS Code

Coding agent: Claude Code

Claude Code connects terminal-first coding workflows to MCP servers.

1. Add MCP servers

Add remote servers from the CLI:

claude mcp add \
	--transport http huggingface \
	'https://huggingface.co/mcp?login'

Then confirm what Claude Code can see:

claude mcp list

For authenticated servers, you will need to start up claude to go through the auth flow.

2. Use from Claude Code

Ask in the terminal, and Claude Code can call Hugging Face tools through MCP.

> what research papers are in 2026
	about vector retrieval?

⏺ huggingface - Paper Search (MCP)
	query: "vector retrieval 2026"
	concise_only: true

120 papers matched the query.
Here are the first 12 results.

## Foundations of Vector Retrieval

Python agents + MCP

Building agents with frameworks

An agent framework coordinates model calls, tool schemas, tool execution, memory or state, and final responses.

FrameworkDescription
langchain v1An agent-centric framework built on top of LangGraph, with optional LangSmith monitoring.
pydantic-aiA flexible framework designed for type safety and observability, from the creators of Pydantic.
agent-frameworkA framework from Microsoft with support for agentic patterns and full integration with Azure offerings. Successor to AutoGen and Semantic Kernel.

Pydantic AI + MCP


from pydantic_ai import Agent
from pydantic_ai.mcp import MCPServerStreamableHTTP
from pydantic_ai.models.openai import OpenAIChatModel

model = OpenAIChatModel("gpt-5.5")
server = MCPServerStreamableHTTP(url="https://learn.microsoft.com/api/mcp")

agent = Agent(model, toolsets=[server],
  system_prompt="Use available tools to answer questions.")

result = await agent.run("How do I use DefaultAzureCredential in Python?")
print(result.output)
					
⏳ 30 min

Exercise time

Exercise 1:
Connect your coding agent to public MCP servers
tinyurl.com/pyconmcp-ex1

Bonus: Exercise 2
Build a Python agent that uses MCP servers
tinyurl.com/pyconmcp-ex2


🙋🏽‍♀️ 🙋🏻‍♂️ 🙋🏿 🙋🏼‍♀️ 🙋🏾‍♂️ Got a question? Raise your hand, we'll come around!

Building an MCP server

Python MCP frameworks

FrameworkDescription
python-sdkOfficial Python SDK for MCP. Lets you build both clients and servers fully compatible with the protocol specification. Handles transport (stdio, SSE, HTTP), message cycles, tools, resources, and prompts.
fastmcpFramework built on top of the official SDK. Adds production-focused features like server composition, proxying, OpenAPI/FastAPI generation, enterprise authentication (Google, GitHub, Azure, Auth0, etc.), deployment utilities, and client libraries.

FastMCP server skeleton


from fastmcp import FastMCP

mcp = FastMCP("Expenses tracker")

# Define tools, prompts, and resources here...

if __name__ == "__main__":
    mcp.run(transport="streamable-http", host="0.0.0.0", port=8420)
					

🔗 Docs: gofastmcp.com

MCP transports: STDIO vs. HTTP

STDIOHTTP (Streamable)
Client configCommand to run:
uv run mcp_server.py
Server URL:
http://localhost:8420/mcp
StartupClient launches the server processServer runs before clients connect
Best forLocal tools, quick tests, single-user appsRemote access, web apps, multiple clients
TradeoffSimple, but tied to one local processMore flexible, but needs host/port setup

Learn more: gofastmcp.com/deployment/running-server

Tool with FastMCP


@mcp.tool
async def add_expense(
    date: Annotated[date, "Date of the expense in YYYY-MM-DD format"],
    amount: Annotated[float, "Positive numeric amount of the expense"],
    category: Annotated[Category, "Category label"],
    description: Annotated[str, "Human-readable description of the expense"],
    payment_method: Annotated[PaymentMethod, "Payment method used"],
) -> str:
    """Add a new expense to the expenses.csv file."""
    # Implementation ...

    return f"Added expense: ${amount} for {description} on {date_iso}"
					

🔗 Full example: expenses_tracker.py

Tool signature → JSON schema

This tool signature:

@mcp.tool
async def add_expense(
    date: Annotated[date, "YYYY-MM-DD"],
    amount: Annotated[float, "Amount"],
    category: Category,
    description: str,
    payment_method: PaymentMethod,
) -> str:
    """Add a new expense."""
    ...

becomes this schema:

{
	"name": "add_expense",
	"description": "Add a new expense.",
	"inputSchema": {
		"type": "object",
		"properties": {
			"date": {
				"type": "string",
				"format": "date"
			},
			"amount": {"type": "number"},
			"category": {"type": "string"},
			"description": {"type": "string"},
			"payment_method": {"type": "string"}
		},
		"required": ["date", "amount", "category",
			"description", "payment_method"]
	}
}

Resource with FastMCP


@mcp.resource("resource://expenses")
async def get_expenses_data():
    csv_content = f"Expense data ({len(expenses_data)} entries):\n\n"
    for expense in expenses_data:
        csv_content += (
            f"Date: {expense['date']}, "
            f"Amount: ${expense['amount']}, "
            f"Category: {expense['category']}, "
            f"Description: {expense['description']}, "
            f"Payment: {expense['payment_method']}\n"
        )
    return csv_content
					

🔗 Full example: expenses_tracker.py

Prompt with FastMCP


@mcp.prompt
def analyze_spending_prompt(
    category: str | None = None,
    start_date: str | None = None,
    end_date: str | None = None,
) -> str:
    # Build filters ...
    return f"""Please analyze my spending patterns {filter_text} and provide:
    1. Total spending breakdown by category
    2. Average daily/weekly spending
    3. Most expensive single transaction
    4. Payment method distribution"""
					

🔗 Full example: expenses_tracker.py

Run the server

Start the server:

uv run servers/expenses_tracker.py
Terminal running the FastMCP server with log output

Point agent at the local URL:

http://localhost:8000/mcp

Test with MCP Inspector or MCPJam

MCP Inspector

npx @modelcontextprotocol/inspector

Official local tester for inspecting tools, resources, prompts, and raw MCP messages.

MCPJam

npx @mcpjam/inspector@latest

A friendly web UI for trying MCP servers during development. mcpjam.com

Useful debugging loop: direct server logs first, Inspector or MCPJam second, LLM client last.

⏳ 40 min

Exercise time

Exercise 3:
Build your own product-store MCP server
tinyurl.com/pyconmcp-ex3

🙋🏽‍♀️ 🙋🏻‍♂️ 🙋🏿 🙋🏼‍♀️ 🙋🏾‍♂️ Got a question? Raise your hand, we'll come around!

Advanced server features

MCP as a collaboration protocol

New MCP features turn agents from executors into collaborators.

💬

Elicitations
Servers request structured input from the user mid-flow, so agents can ask instead of guess.

🖥️

MCP Apps
Render interactive UI inside the client, so users can inspect and manipulate results.

Tasks
Run long work without blocking, with progress updates and partial failure handling.

Elicitations

Elicitations let servers request user input through the client.

Form mode

Structured data collection in the client, optionally schema-validated.

Example uses: Clarification of parameters, confirmation (especially before write ops)

URL mode

Out-of-band flow for sensitive info that must not pass through the MCP client.

Example uses: API keys, OAuth, payment

purchase-product FORM ELICITATION

Buy 2x Raspberry Pi Pico for $13.98?

Cancel Confirm
connect-payments URL ELICITATION

Open a secure page to connect your payment provider.

https://payments.example.com/connect
Cancel Open URL

Form mode elicitation flow

User MCP Client MCP Server tools/call id: 1 Server needs more info InputRequiredResult: elicitation/create mode: "form" Client presents elicitation UI User provides requested information Client retries request with new information tools/call id: 2 + user response

Elicitations in FastMCP


@mcp.tool
async def remove_expenses_for_date(
  expense_date: Annotated[date, "Date to remove expenses for"],
  ctx: Context,
) -> str:
  class DeleteExpensesConfirmation(BaseModel):
    confirm: bool = Field(title="Delete expenses")

  response = await ctx.elicit(
    message=f"Delete expenses from {expense_date}?",
    response_type=DeleteExpensesConfirmation,
  )

  if response.action != "accept" or not response.data.confirm:
    return "Deletion cancelled."

  # Delete matching rows from expenses.csv ...
					

🔗 Full example: expenses_tracker.py
📖 Learn more: gofastmcp.com/servers/elicitation

MCP Apps

MCP Apps let tools return rich, interactive UI that renders inside chat.

Example uses

  • Drag-and-drop ordering
  • Visual profiling and flame graphs
  • Form-based selection
  • Data visualization dashboards
🤖

Here's the performance profile for handle_request():

🖥️ MCP App flame-graph
main() - 420ms
handle_request() - 302ms
db.query()
serialize()

Examples of MCP servers using MCP Apps

📕

Storybook

Interactive component previews from design-system tasks.

✏️

Excalidraw

Live diagrams in chat for visual planning and edits.

🎨

Figma

Design context and assets rendered where agents work.

MCP Apps flow

User Agent MCP App iframe MCP Server "show me analytics" Interactive app rendered in chat tools/call tool input/result tool result pushed to app user interacts tools/call request forwarded call + fresh data

Making MCP Apps with FastMCP

Four paths, from Python components to custom UI.

Prefab Apps

Quickest path: add app=True to a tool and return Prefab components.

🧩

FastMCPApp

For multi-tool UIs: separate model-visible entry points from UI-only backend tools.

🌐

Custom HTML

Use your own HTML, CSS, JavaScript, or framework for maps, 3D, video, and bespoke UI.

Generative AI

Let the model design Prefab UI at runtime, execute it in a sandbox, and stream the result.

Learn more: gofastmcp.com/apps/overview

Prefab app with FastMCP


from prefab_ui.app import PrefabApp
from prefab_ui.components import Column, Heading
from prefab_ui.components.table import Table, TableBody, TableCell, TableHead, TableHeader, TableRow

@mcp.tool(app=True)
def render_expenses() -> PrefabApp:
  """Render the expenses as an interactive table."""
  expenses = load_expenses_from_csv()

  with Column(gap=4) as view:
    Heading("Expenses")
    with Table():
      with TableHeader():
        with TableRow():
          TableHead("Date")
	      TableHead("Amount")
        with TableBody():
          for expense in expenses:
            with TableRow():
              TableCell(expense["date"])
              TableCell(f"${float(expense['amount']):.2f}")

  return PrefabApp(view=view)
					

🔗 Full example: expenses_tracker.py

⏳ 20 min

Exercise time

Exercise 4:
Add visual UI and purchase confirmation
tinyurl.com/pyconmcp-ex4

🙋🏽‍♀️ 🙋🏻‍♂️ 🙋🏿 🙋🏼‍♀️ 🙋🏾‍♂️ Got a question? Raise your hand, we'll come around!

Authenticated MCP servers

Three approaches to restricting access

Pick the access model that matches the server boundary and the data risk.

Private network Client MCP server

Private network

Deploy inside a virtual network and disable public ingress.

Best for: internal services where every allowed client is already on the network.

Client key header MCP server

Key-based access

Clients send a key in a header, and the server verifies it before responding.

Watch out: keys can be copied, leaked, or shared outside the intended user.

Client Auth server MCP server login token bearer token

OAuth-based access

Clients obtain a user access token and send it as bearer authorization.

Best for: user-specific data and servers that need identity-aware authorization.

OAuth request/response

MCP client makes requests to MCP server on behalf of a signed-in user:

MCP Client MCP Server Verifies the access token and returns results scoped to that signed-in user. MCP Authorization: Bearer <access_token> { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "get_expenses", "user_id": "abc123" } } MCP { "jsonrpc": "2.0", "result": { "content": [{ "amount": 200 }, ...] } }

OAuth 2.1 overview

OAuth 2.1 is a standard for allowing resource owners to make authorized requests. MCP auth builds on top of OAuth 2.1.

OAuth client MCP client Requests access Authorization server Authenticates and issues tokens Resource server MCP server Hosts protected tools/data Resource owner User Owns the data and grants consent authorization request access token

OAuth flow for MCP (Simplified)

User MCP Client Authorization Server MCP Server Initiates action requiring MCP server Redirects to AS with authorization request Presents authentication prompt Enters credentials Authenticates user and validates client registration Displays consent page for client Grants consent Issues authorization code Exchanges code for access token Returns access token MCP request + access token Authenticated result or error

How the authorization server validates the client

Does authorization server know the MCP client? Pre-registration Yes Do authorization server and MCP client support CIMD? x No CIMD: Client Identity Metadata Document Yes DCR: Dynamic Client Registration x No

DCR flow

Only the initial client registration step differs.

User MCP client Authorization server (AS) Standard OAuth flow, with new client ID Sends Dynamic Client Registration request to /register ?redirect_uris=...&grant_types=...&client_name=... Stores in client database Returns client_id Initiates action requiring MCP server Redirects to AS with authorization request ?client_id=NEW_CLIENT_ID Presents authentication prompt Enters credentials Authenticates user and validates client registration Displays consent page for client Grants consent Issues authorization code Exchanges code for access token Returns access token

Support for MCP Auth across auth providers

Provider Description CIMD DCR
Okta Auth0Hosted identity provider
Microsoft EntraHosted identity provider
ScaleKitIdentity provider
(+ wrapper of hosted IdPs)
DescopeIdentity provider
(+ wrapper of hosted IdPs)
WorkOS AuthKitIdentity provider
(+ wrapper of hosted IdPs)
KeycloakOSS identity provider

Keycloak: open-source identity server

Keycloak website screenshot
Runs from a Docker image Admin portal Realms separate tenants Supports OAuth flows and DCR

Implementing MCP auth in FastMCP

Two approaches:

  • RemoteAuthProvider for providers with DCR support.
  • OAuthProxy for providers that need the MCP server to implement DCR.
OAuthProvider RemoteAuthProvider Provider supports DCR OAuthProxy Server implements DCR Descope, Supabase, ScaleKit, AuthKit, Keycloak Azure, Auth0, GitHub, Google, Cognito, Discord, WorkOS

Integrating Keycloak with FastMCP


from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider

auth = KeycloakAuthProvider(
    realm_url="https://example.com/realms/pycon",
    base_url="http://localhost:8420",
    required_scopes=["openid", "mcp:access"],
    audience="product-store",
)

mcp = FastMCP("Pamela's Bakery", auth=auth)
					
⏳ 35 min

Exercise time

Exercise 5:
Add Keycloak authentication to your MCP server
tinyurl.com/pyconmcp-ex5

🙋🏽‍♀️ 🙋🏻‍♂️ 🙋🏿 🙋🏼‍♀️ 🙋🏾‍♂️ Got a question? Raise your hand, we'll come around!

Next steps

Productionizing MCP servers

  • Testing: Test with both direct MCP tools and LLM-driven clients.
  • Evaluation: Optimize tool descriptions for user scenarios across agents.
  • Usage controls: Rate-limit expensive or state-changing tools.
  • Observability: Log tool calls without logging sensitive payloads.
  • Security: Continuously audit tools for security risks and logs for abuse.

Security questions to ask every server

  • Who is allowed to call this tool?
  • What data can the tool read or mutate?
  • Does the user understand when state will change?
  • Can prompt injection alter the server's authority?
  • What happens when the model passes surprising arguments?

Thank you!

Repository:
github.com/pamelafox/pycon2026-mcp-tutorial

Check out more of my MCP talks:

Before you go

Please take the tutorial survey.

Your feedback helps improve future PyCon tutorials and tells us what to build next.