VOOZH about

URL: https://pypi.org/project/mcp/2.0.0a3/

โ‡ฑ mcp ยท PyPI


Skip to main content

mcp 2.0.0a3

pip install mcp==2.0.0a3

Stable version available (1.28.1)

Released:

Model Context Protocol SDK

Navigation

Unverified details

These details have not been verified by PyPI
Project links
Meta
  • License: MIT License (MIT)
  • Author: Model Context Protocol a Series of LF Projects, LLC.
  • Maintainer: David Soria Parra
  • Tags automation , llm , mcp
  • Requires: Python >=3.10
  • Provides-Extra: cli , rich

Project description

MCP Python SDK

Important: this documents v2 of the SDK, which is in alpha. Pre-releases are published to PyPI as 2.0.0aN, and each alpha may contain breaking changes from the previous one.

v2 is a major rework of the SDK, both to support the 2026-07-28 MCP specification release and to fix long-standing architectural issues. See the migration guide for what's changed. We're targeting a beta on 2026-06-30 and a stable v2 on 2026-07-27, alongside the spec release. Before stable, we plan to add a significant set of backwards compatibility shims so the final upgrade is much smaller than today's diff.

v1.x is the only stable release line and remains recommended for production. It is in maintenance mode and continues to receive critical bug fixes and security patches. Installers never select a pre-release unless you opt in (for example pip install mcp==2.0.0aN), so existing installs are unaffected. If your package depends on mcp, add a <2 upper bound to your version constraint (for example mcp>=1.27,<2) before the stable release lands.

Try the alpha and tell us what breaks: #python-sdk-dev on the MCP Contributors Discord. For v1 documentation, see the v1.x README.

Table of Contents

Overview

The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This Python SDK implements the full MCP specification, making it easy to:

  • Build MCP clients that can connect to any MCP server
  • Create MCP servers that expose resources, prompts and tools
  • Use standard transports like stdio, SSE, and Streamable HTTP
  • Handle all MCP protocol messages and lifecycle events

Installation

Adding MCP to your python project

We recommend using uv to manage your Python projects.

If you haven't created a uv-managed project yet, create one:

uvinitmcp-server-demo
cdmcp-server-demo

Then add MCP to your project dependencies:

uvadd"mcp[cli]==2.0.0a1"

Alternatively, for projects using pip for dependencies:

pipinstall"mcp[cli]==2.0.0a1"

While v2 is in pre-release, you must pin the version explicitly: unpinned installs resolve to the latest stable v1.x release, which these docs do not describe. Check the release history for the newest pre-release. The same applies to ad-hoc commands: use uv run --with "mcp==2.0.0a1" rather than uv run --with mcp.

Running the standalone MCP development tools

To run the mcp command with uv:

uvrunmcp

Quickstart

Let's create a simple MCP server that exposes a calculator tool and some data:

"""MCPServer quickstart example.

Run from the repository root:
 uv run examples/snippets/servers/mcpserver_quickstart.py
"""

frommcp.server.mcpserverimport MCPServer

# Create an MCP server
mcp = MCPServer("Demo")


# Add an addition tool
@mcp.tool()
defadd(a: int, b: int) -> int:
"""Add two numbers"""
 return a + b


# Add a dynamic greeting resource
@mcp.resource("greeting://{name}")
defget_greeting(name: str) -> str:
"""Get a personalized greeting"""
 return f"Hello, {name}!"


# Add a prompt
@mcp.prompt()
defgreet_user(name: str, style: str = "friendly") -> str:
"""Generate a greeting prompt"""
 styles = {
 "friendly": "Please write a warm, friendly greeting",
 "formal": "Please write a formal, professional greeting",
 "casual": "Please write a casual, relaxed greeting",
 }

 return f"{styles.get(style,styles['friendly'])} for someone named {name}."


# Run with streamable HTTP transport
if __name__ == "__main__":
 mcp.run(transport="streamable-http", json_response=True)

Full example: examples/snippets/servers/mcpserver_quickstart.py

You can install this server in Claude Code and interact with it right away. First, run the server:

uvrun--with"mcp==2.0.0a1"examples/snippets/servers/mcpserver_quickstart.py

Then add it to Claude Code:

claudemcpadd--transporthttpmy-serverhttp://localhost:8000/mcp

Alternatively, you can test it with the MCP Inspector. Start the server as above, then in a separate terminal:

npx-y@modelcontextprotocol/inspector

In the inspector UI, connect to http://localhost:8000/mcp.

What is MCP?

The Model Context Protocol (MCP) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions.

MCP follows a client-server model, where LLM applications act as clients and connect to MCP servers to access capabilities such as data retrieval and tool execution in a consistent format.

MCP servers can:

  • Expose data through Resources (think of these sort of like GET endpoints; they are used to load information into the LLM's context)
  • Provide functionality through Tools (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)
  • Define interaction patterns through Prompts (reusable templates for LLM interactions)
  • And more!

Core Concepts

Server

The MCPServer server is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing:

"""Example showing lifespan support for startup/shutdown with strong typing."""

fromcollections.abcimport AsyncIterator
fromcontextlibimport asynccontextmanager
fromdataclassesimport dataclass

frommcp.server.mcpserverimport Context, MCPServer


# Mock database class for example
classDatabase:
"""Mock database class for example."""

 @classmethod
 async defconnect(cls) -> "Database":
"""Connect to database."""
 return cls()

 async defdisconnect(self) -> None:
"""Disconnect from database."""
 pass

 defquery(self) -> str:
"""Execute a query."""
 return "Query result"


@dataclass
classAppContext:
"""Application context with typed dependencies."""

 db: Database


@asynccontextmanager
async defapp_lifespan(server: MCPServer) -> AsyncIterator[AppContext]:
"""Manage application lifecycle with type-safe context."""
 # Initialize on startup
 db = await Database.connect()
 try:
 yield AppContext(db=db)
 finally:
 # Cleanup on shutdown
 await db.disconnect()


# Pass lifespan to server
mcp = MCPServer("My App", lifespan=app_lifespan)


# Access type-safe lifespan context in tools
@mcp.tool()
defquery_db(ctx: Context[AppContext]) -> str:
"""Tool that uses initialized resources."""
 db = ctx.request_context.lifespan_context.db
 return db.query()

Full example: examples/snippets/servers/lifespan_example.py

Resources

Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects:

frommcp.server.mcpserverimport MCPServer

mcp = MCPServer(name="Resource Example")


@mcp.resource("file://documents/{name}")
defread_document(name: str) -> str:
"""Read a document by name."""
 # This would normally read from disk
 return f"Content of {name}"


@mcp.resource("config://settings")
defget_settings() -> str:
"""Get application settings."""
 return """{
 "theme": "dark",
 "language": "en",
 "debug": false
}"""

Full example: examples/snippets/servers/basic_resource.py

Tools

Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects:

frommcp.server.mcpserverimport MCPServer

mcp = MCPServer(name="Tool Example")


@mcp.tool()
defsum(a: int, b: int) -> int:
"""Add two numbers together."""
 return a + b


@mcp.tool()
defget_weather(city: str, unit: str = "celsius") -> str:
"""Get weather for a city."""
 # This would normally call a weather API
 return f"Weather in {city}: 22degrees{unit[0].upper()}"

Full example: examples/snippets/servers/basic_tool.py

Tools can optionally receive a Context object by including a parameter with the Context type annotation. This context is automatically injected by the MCPServer framework and provides access to MCP capabilities:

frommcp.server.mcpserverimport Context, MCPServer

mcp = MCPServer(name="Progress Example")


@mcp.tool()
async deflong_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:
"""Execute a task with progress updates."""
 await ctx.info(f"Starting: {task_name}") # pyright: ignore[reportDeprecated]

 for i in range(steps):
 progress = (i + 1) / steps
 await ctx.report_progress(
 progress=progress,
 total=1.0,
 message=f"Step {i+1}/{steps}",
 )
 await ctx.debug(f"Completed step {i+1}") # pyright: ignore[reportDeprecated]

 return f"Task '{task_name}' completed"

Full example: examples/snippets/servers/tool_progress.py

Structured Output

Tools will return structured results by default, if their return type annotation is compatible. Otherwise, they will return unstructured results.

Structured output supports these return types:

  • Pydantic models (BaseModel subclasses)
  • TypedDicts
  • Dataclasses and other classes with type hints
  • dict[str, T] (where T is any JSON-serializable type)
  • Primitive types (str, int, float, bool, bytes, None) - wrapped in {"result": value}
  • Generic types (list, tuple, Union, Optional, etc.) - wrapped in {"result": value}

Classes without type hints cannot be serialized for structured output. Only classes with properly annotated attributes will be converted to Pydantic models for schema generation and validation.

Structured results are automatically validated against the output schema generated from the annotation. This ensures the tool returns well-typed, validated data that clients can easily process.

Note: For backward compatibility, unstructured results are also returned. Unstructured results are provided for backward compatibility with previous versions of the MCP specification, and are quirks-compatible with previous versions of MCPServer in the current version of the SDK.

Note: In cases where a tool function's return type annotation causes the tool to be classified as structured and this is undesirable, the classification can be suppressed by passing structured_output=False to the @tool decorator.

Advanced: Direct CallToolResult

For full control over tool responses including the _meta field (for passing data to client applications without exposing it to the model), you can return CallToolResult directly:

"""Example showing direct CallToolResult return for advanced control."""

fromtypingimport Annotated

frommcp_typesimport CallToolResult, TextContent
frompydanticimport BaseModel

frommcp.server.mcpserverimport MCPServer

mcp = MCPServer("CallToolResult Example")


classValidationModel(BaseModel):
"""Model for validating structured output."""

 status: str
 data: dict[str, int]


@mcp.tool()
defadvanced_tool() -> CallToolResult:
"""Return CallToolResult directly for full control including _meta field."""
 return CallToolResult(
 content=[TextContent(type="text", text="Response visible to the model")],
 _meta={"hidden": "data for client applications only"},
 )


@mcp.tool()
defvalidated_tool() -> Annotated[CallToolResult, ValidationModel]:
"""Return CallToolResult with structured output validation."""
 return CallToolResult(
 content=[TextContent(type="text", text="Validated response")],
 structured_content={"status": "success", "data": {"result": 42}},
 _meta={"internal": "metadata"},
 )


@mcp.tool()
defempty_result_tool() -> CallToolResult:
"""For empty results, return CallToolResult with empty content."""
 return CallToolResult(content=[])

Full example: examples/snippets/servers/direct_call_tool_result.py

Important: CallToolResult must always be returned (no Optional or Union). For empty results, use CallToolResult(content=[]). For optional simple types, use str | None without CallToolResult.

"""Example showing structured output with tools."""

fromtypingimport TypedDict

frompydanticimport BaseModel, Field

frommcp.server.mcpserverimport MCPServer

mcp = MCPServer("Structured Output Example")


# Using Pydantic models for rich structured data
classWeatherData(BaseModel):
"""Weather information structure."""

 temperature: float = Field(description="Temperature in Celsius")
 humidity: float = Field(description="Humidity percentage")
 condition: str
 wind_speed: float


@mcp.tool()
defget_weather(city: str) -> WeatherData:
"""Get weather for a city - returns structured data."""
 # Simulated weather data
 return WeatherData(
 temperature=22.5,
 humidity=45.0,
 condition="sunny",
 wind_speed=5.2,
 )


# Using TypedDict for simpler structures
classLocationInfo(TypedDict):
 latitude: float
 longitude: float
 name: str


@mcp.tool()
defget_location(address: str) -> LocationInfo:
"""Get location coordinates"""
 return LocationInfo(latitude=51.5074, longitude=-0.1278, name="London, UK")


# Using dict[str, Any] for flexible schemas
@mcp.tool()
defget_statistics(data_type: str) -> dict[str, float]:
"""Get various statistics"""
 return {"mean": 42.5, "median": 40.0, "std_dev": 5.2}


# Ordinary classes with type hints work for structured output
classUserProfile:
 name: str
 age: int
 email: str | None = None

 def__init__(self, name: str, age: int, email: str | None = None):
 self.name = name
 self.age = age
 self.email = email


@mcp.tool()
defget_user(user_id: str) -> UserProfile:
"""Get user profile - returns structured data"""
 return UserProfile(name="Alice", age=30, email="alice@example.com")


# Classes WITHOUT type hints cannot be used for structured output
classUntypedConfig:
 def__init__(self, setting1, setting2): # type: ignore[reportMissingParameterType]
 self.setting1 = setting1
 self.setting2 = setting2


@mcp.tool()
defget_config() -> UntypedConfig:
"""This returns unstructured output - no schema generated"""
 return UntypedConfig("value1", "value2")


# Lists and other types are wrapped automatically
@mcp.tool()
deflist_cities() -> list[str]:
"""Get a list of cities"""
 return ["London", "Paris", "Tokyo"]
 # Returns: {"result": ["London", "Paris", "Tokyo"]}


@mcp.tool()
defget_temperature(city: str) -> float:
"""Get temperature as a simple float"""
 return 22.5
 # Returns: {"result": 22.5}

Full example: examples/snippets/servers/structured_output.py

Prompts

Prompts are reusable templates that help LLMs interact with your server effectively:

frommcp.server.mcpserverimport MCPServer
frommcp.server.mcpserver.promptsimport base

mcp = MCPServer(name="Prompt Example")


@mcp.prompt(title="Code Review")
defreview_code(code: str) -> str:
 return f"Please review this code:\n\n{code}"


@mcp.prompt(title="Debug Assistant")
defdebug_error(error: str) -> list[base.Message]:
 return [
 base.UserMessage("I'm seeing this error:"),
 base.UserMessage(error),
 base.AssistantMessage("I'll help debug that. What have you tried so far?"),
 ]

Full example: examples/snippets/servers/basic_prompt.py

Icons

MCP servers can provide icons for UI display. Icons can be added to the server implementation, tools, resources, and prompts:

frommcp.server.mcpserverimport MCPServer, Icon

# Create an icon from a file path or URL
icon = Icon(
 src="icon.png",
 mime_type="image/png",
 sizes=["64x64"]
)

# Add icons to server
mcp = MCPServer(
 "My Server",
 website_url="https://example.com",
 icons=[icon]
)

# Add icons to tools, resources, and prompts
@mcp.tool(icons=[icon])
defmy_tool():
"""Tool with an icon."""
 return "result"

@mcp.resource("demo://resource", icons=[icon])
defmy_resource():
"""Resource with an icon."""
 return "content"

Full example: examples/mcpserver/icons_demo.py

Images

MCPServer provides an Image class that automatically handles image data:

"""Example showing image handling with MCPServer."""

fromPILimport Image as PILImage

frommcp.server.mcpserverimport Image, MCPServer

mcp = MCPServer("Image Example")


@mcp.tool()
defcreate_thumbnail(image_path: str) -> Image:
"""Create a thumbnail from an image"""
 img = PILImage.open(image_path)
 img.thumbnail((100, 100))
 return Image(data=img.tobytes(), format="png")

Full example: examples/snippets/servers/images.py

Context

The Context object is automatically injected into tool and resource functions that request it via type hints. It provides access to MCP capabilities like logging, progress reporting, resource reading, user interaction, and request metadata.

Getting Context in Functions

To use context in a tool or resource function, add a parameter with the Context type annotation:

frommcp.server.mcpserverimport Context, MCPServer

mcp = MCPServer(name="Context Example")


@mcp.tool()
async defmy_tool(x: int, ctx: Context) -> str:
"""Tool that uses context capabilities."""
 # The context parameter can have any name as long as it's type-annotated
 return await process_with_context(x, ctx)

Context Properties and Methods

The Context object provides the following capabilities:

  • ctx.request_id - Unique ID for the current request
  • ctx.client_id - Client ID if available
  • ctx.mcp_server - Access to the MCPServer server instance (see MCPServer Properties)
  • ctx.session - Access to the underlying session for advanced communication (see Session Properties and Methods)
  • ctx.request_context - Access to request-specific data and lifespan resources (see Request Context Properties)
  • await ctx.debug(data) - Send debug log message
  • await ctx.info(data) - Send info log message
  • await ctx.warning(data) - Send warning log message
  • await ctx.error(data) - Send error log message
  • await ctx.log(level, data, logger_name=None) - Send log with custom level
  • await ctx.report_progress(progress, total=None, message=None) - Report operation progress
  • await ctx.read_resource(uri) - Read a resource by URI
  • await ctx.elicit(message, schema) - Request additional information from user with validation
frommcp.server.mcpserverimport Context, MCPServer

mcp = MCPServer(name="Progress Example")


@mcp.tool()
async deflong_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:
"""Execute a task with progress updates."""
 await ctx.info(f"Starting: {task_name}") # pyright: ignore[reportDeprecated]

 for i in range(steps):
 progress = (i + 1) / steps
 await ctx.report_progress(
 progress=progress,
 total=1.0,
 message=f"Step {i+1}/{steps}",
 )
 await ctx.debug(f"Completed step {i+1}") # pyright: ignore[reportDeprecated]

 return f"Task '{task_name}' completed"

Full example: examples/snippets/servers/tool_progress.py

Completions

MCP supports providing completion suggestions for prompt arguments and resource template parameters. With the context parameter, servers can provide completions based on previously resolved values:

Client usage:

"""cd to the `examples/snippets` directory and run:
uv run completion-client
"""

importasyncio
importos

frommcp_typesimport PromptReference, ResourceTemplateReference

frommcpimport ClientSession, StdioServerParameters
frommcp.client.stdioimport stdio_client

# Create server parameters for stdio connection
server_params = StdioServerParameters(
 command="uv", # Using uv to run the server
 args=["run", "server", "completion", "stdio"], # Server with completion support
 env={"UV_INDEX": os.environ.get("UV_INDEX", "")},
)


async defrun():
"""Run the completion client example."""
 async with stdio_client(server_params) as (read, write):
 async with ClientSession(read, write) as session:
 # Initialize the connection
 await session.initialize()

 # List available resource templates
 templates = await session.list_resource_templates()
 print("Available resource templates:")
 for template in templates.resource_templates:
 print(f" - {template.uri_template}")

 # List available prompts
 prompts = await session.list_prompts()
 print("\nAvailable prompts:")
 for prompt in prompts.prompts:
 print(f" - {prompt.name}")

 # Complete resource template arguments
 if templates.resource_templates:
 template = templates.resource_templates[0]
 print(f"\nCompleting arguments for resource template: {template.uri_template}")

 # Complete without context
 result = await session.complete(
 ref=ResourceTemplateReference(type="ref/resource", uri=template.uri_template),
 argument={"name": "owner", "value": "model"},
 )
 print(f"Completions for 'owner' starting with 'model': {result.completion.values}")

 # Complete with context - repo suggestions based on owner
 result = await session.complete(
 ref=ResourceTemplateReference(type="ref/resource", uri=template.uri_template),
 argument={"name": "repo", "value": ""},
 context_arguments={"owner": "modelcontextprotocol"},
 )
 print(f"Completions for 'repo' with owner='modelcontextprotocol': {result.completion.values}")

 # Complete prompt arguments
 if prompts.prompts:
 prompt_name = prompts.prompts[0].name
 print(f"\nCompleting arguments for prompt: {prompt_name}")

 result = await session.complete(
 ref=PromptReference(type="ref/prompt", name=prompt_name),
 argument={"name": "style", "value": ""},
 )
 print(f"Completions for 'style' argument: {result.completion.values}")


defmain():
"""Entry point for the completion client."""
 asyncio.run(run())


if __name__ == "__main__":
 main()

Full example: examples/snippets/clients/completion_client.py

Elicitation

Request additional information from users. This example shows an Elicitation during a Tool Call:

"""Elicitation examples demonstrating form and URL mode elicitation.

Form mode elicitation collects structured, non-sensitive data through a schema.
URL mode elicitation directs users to external URLs for sensitive operations
like OAuth flows, credential collection, or payment processing.
"""

importuuid

frommcp_typesimport ElicitRequestURLParams
frompydanticimport BaseModel, Field

frommcp.server.mcpserverimport Context, MCPServer
frommcp.shared.exceptionsimport UrlElicitationRequiredError

mcp = MCPServer(name="Elicitation Example")


classBookingPreferences(BaseModel):
"""Schema for collecting user preferences."""

 checkAlternative: bool = Field(description="Would you like to check another date?")
 alternativeDate: str = Field(
 default="2024-12-26",
 description="Alternative date (YYYY-MM-DD)",
 )


@mcp.tool()
async defbook_table(date: str, time: str, party_size: int, ctx: Context) -> str:
"""Book a table with date availability check.

 This demonstrates form mode elicitation for collecting non-sensitive user input.
 """
 # Check if date is available
 if date == "2024-12-25":
 # Date unavailable - ask user for alternative
 result = await ctx.elicit(
 message=(f"No tables available for {party_size} on {date}. Would you like to try another date?"),
 schema=BookingPreferences,
 )

 if result.action == "accept" and result.data:
 if result.data.checkAlternative:
 return f"[SUCCESS] Booked for {result.data.alternativeDate}"
 return "[CANCELLED] No booking made"
 return "[CANCELLED] Booking cancelled"

 # Date available
 return f"[SUCCESS] Booked for {date} at {time}"


@mcp.tool()
async defsecure_payment(amount: float, ctx: Context) -> str:
"""Process a secure payment requiring URL confirmation.

 This demonstrates URL mode elicitation using ctx.elicit_url() for
 operations that require out-of-band user interaction.
 """
 elicitation_id = str(uuid.uuid4())

 result = await ctx.elicit_url(
 message=f"Please confirm payment of ${amount:.2f}",
 url=f"https://payments.example.com/confirm?amount={amount}&id={elicitation_id}",
 elicitation_id=elicitation_id,
 )

 if result.action == "accept":
 # In a real app, the payment confirmation would happen out-of-band
 # and you'd verify the payment status from your backend
 return f"Payment of ${amount:.2f} initiated - check your browser to complete"
 elif result.action == "decline":
 return "Payment declined by user"
 return "Payment cancelled"


@mcp.tool()
async defconnect_service(service_name: str, ctx: Context) -> str:
"""Connect to a third-party service requiring OAuth authorization.

 This demonstrates the "throw error" pattern using UrlElicitationRequiredError.
 Use this pattern when the tool cannot proceed without user authorization.
 """
 elicitation_id = str(uuid.uuid4())

 # Raise UrlElicitationRequiredError to signal that the client must complete
 # a URL elicitation before this request can be processed.
 # The MCP framework will convert this to a -32042 error response.
 raise UrlElicitationRequiredError(
 [
 ElicitRequestURLParams(
 mode="url",
 message=f"Authorization required to connect to {service_name}",
 url=f"https://{service_name}.example.com/oauth/authorize?elicit={elicitation_id}",
 elicitation_id=elicitation_id,
 )
 ]
 )

Full example: examples/snippets/servers/elicitation.py

Elicitation schemas support default values for all field types. Default values are automatically included in the JSON schema sent to clients, allowing them to pre-populate forms.

The elicit() method returns an ElicitationResult with:

  • action: "accept", "decline", or "cancel"
  • data: The validated response (only when accepted)

If the client returns data that doesn't match the schema, elicit() raises a pydantic.ValidationError.

Sampling

Tools can interact with LLMs through sampling (generating text):

frommcp_typesimport SamplingMessage, TextContent

frommcp.server.mcpserverimport Context, MCPServer

mcp = MCPServer(name="Sampling Example")


@mcp.tool()
async defgenerate_poem(topic: str, ctx: Context) -> str:
"""Generate a poem using LLM sampling."""
 prompt = f"Write a short poem about {topic}"

 result = await ctx.session.create_message( # pyright: ignore[reportDeprecated]
 messages=[
 SamplingMessage(
 role="user",
 content=TextContent(type="text", text=prompt),
 )
 ],
 max_tokens=100,
 )

 # Since we're not passing tools param, result.content is single content
 if result.content.type == "text":
 return result.content.text
 return str(result.content)

Full example: examples/snippets/servers/sampling.py

Logging and Notifications

Tools can send logs and notifications through the context:

frommcp.server.mcpserverimport Context, MCPServer

mcp = MCPServer(name="Notifications Example")


@mcp.tool()
async defprocess_data(data: str, ctx: Context) -> str:
"""Process data with logging."""
 # Different log levels
 await ctx.debug(f"Debug: Processing '{data}'") # pyright: ignore[reportDeprecated]
 await ctx.info("Info: Starting processing") # pyright: ignore[reportDeprecated]
 await ctx.warning("Warning: This is experimental") # pyright: ignore[reportDeprecated]
 await ctx.error("Error: (This is just a demo)") # pyright: ignore[reportDeprecated]

 # Notify about resource changes
 await ctx.session.send_resource_list_changed()

 return f"Processed: {data}"

Full example: examples/snippets/servers/notifications.py

Authentication

Authentication can be used by servers that want to expose tools accessing protected resources.

mcp.server.auth implements OAuth 2.1 resource server functionality, where MCP servers act as Resource Servers (RS) that validate tokens issued by separate Authorization Servers (AS). This follows the MCP authorization specification and implements RFC 9728 (Protected Resource Metadata) for AS discovery.

MCP servers can use authentication by providing an implementation of the TokenVerifier protocol:

"""Run from the repository root:
uv run examples/snippets/servers/oauth_server.py
"""

frompydanticimport AnyHttpUrl

frommcp.server.auth.providerimport AccessToken, TokenVerifier
frommcp.server.auth.settingsimport AuthSettings
frommcp.server.mcpserverimport MCPServer


classSimpleTokenVerifier(TokenVerifier):
"""Simple token verifier for demonstration."""

 async defverify_token(self, token: str) -> AccessToken | None:
 pass # This is where you would implement actual token validation


# Create MCPServer instance as a Resource Server
mcp = MCPServer(
 "Weather Service",
 # Token verifier for authentication
 token_verifier=SimpleTokenVerifier(),
 # Auth settings for RFC 9728 Protected Resource Metadata
 auth=AuthSettings(
 issuer_url=AnyHttpUrl("https://auth.example.com"), # Authorization Server URL
 resource_server_url=AnyHttpUrl("http://localhost:3001"), # This server's URL
 required_scopes=["user"],
 ),
)


@mcp.tool()
async defget_weather(city: str = "London") -> dict[str, str]:
"""Get weather data for a city"""
 return {
 "city": city,
 "temperature": "22",
 "condition": "Partly cloudy",
 "humidity": "65%",
 }


if __name__ == "__main__":
 mcp.run(transport="streamable-http", json_response=True)

Full example: examples/snippets/servers/oauth_server.py

For a complete example with separate Authorization Server and Resource Server implementations, see examples/servers/simple-auth/.

Architecture:

  • Authorization Server (AS): Handles OAuth flows, user authentication, and token issuance
  • Resource Server (RS): Your MCP server that validates tokens and serves protected resources
  • Client: Discovers AS through RFC 9728, obtains tokens, and uses them with the MCP server

See TokenVerifier for more details on implementing token validation.

MCPServer Properties

The MCPServer server instance accessible via ctx.mcp_server provides access to server configuration and metadata:

  • ctx.mcp_server.name - The server's name as defined during initialization
  • ctx.mcp_server.instructions - Server instructions/description provided to clients
  • ctx.mcp_server.website_url - Optional website URL for the server
  • ctx.mcp_server.icons - Optional list of icons for UI display
  • ctx.mcp_server.settings - Complete server configuration object containing:
    • debug - Debug mode flag
    • log_level - Current logging level
    • host and port - Server network configuration
    • sse_path, streamable_http_path - Transport paths
    • stateless_http - Whether the server operates in stateless mode
    • And other configuration options
@mcp.tool()
defserver_info(ctx: Context) -> dict:
"""Get information about the current server."""
 return {
 "name": ctx.mcp_server.name,
 "instructions": ctx.mcp_server.instructions,
 "debug_mode": ctx.mcp_server.settings.debug,
 "log_level": ctx.mcp_server.settings.log_level,
 "host": ctx.mcp_server.settings.host,
 "port": ctx.mcp_server.settings.port,
 }

Session Properties and Methods

The session object accessible via ctx.session provides advanced control over client communication:

  • ctx.session.client_params - Client initialization parameters and declared capabilities
  • await ctx.session.send_log_message(level, data, logger) - Send log messages with full control
  • await ctx.session.create_message(messages, max_tokens=...) - Request LLM sampling/completion (max_tokens is keyword-only)
  • await ctx.session.send_progress_notification(token, progress, total, message) - Direct progress updates
  • await ctx.session.send_resource_updated(uri) - Notify clients that a specific resource changed
  • await ctx.session.send_resource_list_changed() - Notify clients that the resource list changed
  • await ctx.session.send_tool_list_changed() - Notify clients that the tool list changed
  • await ctx.session.send_prompt_list_changed() - Notify clients that the prompt list changed
@mcp.tool()
async defnotify_data_update(resource_uri: str, ctx: Context) -> str:
"""Update data and notify clients of the change."""
 # Perform data update logic here

 # Notify clients that this specific resource changed
 await ctx.session.send_resource_updated(AnyUrl(resource_uri))

 # If this affects the overall resource list, notify about that too
 await ctx.session.send_resource_list_changed()

 return f"Updated {resource_uri} and notified clients"

Request Context Properties

The request context accessible via ctx.request_context contains request-specific information and resources:

  • ctx.request_context.lifespan_context - Access to resources initialized during server startup
    • Database connections, configuration objects, shared services
    • Type-safe access to resources defined in your server's lifespan function
  • ctx.request_context.meta - Request metadata from the client including:
    • progress_token - Token for progress notifications
    • Other client-provided metadata
  • ctx.request_context.request - Data the transport attached to this message (for example the HTTP request object on HTTP transports; None on stdio)
  • ctx.request_context.request_id - Unique identifier for this request
# Example with typed lifespan context
@dataclass
classAppContext:
 db: Database
 config: AppConfig

@mcp.tool()
defquery_with_config(query: str, ctx: Context) -> str:
"""Execute a query using shared database and configuration."""
 # Access typed lifespan context
 app_ctx: AppContext = ctx.request_context.lifespan_context

 # Use shared resources
 connection = app_ctx.db
 settings = app_ctx.config

 # Execute query with configuration
 result = connection.execute(query, timeout=settings.query_timeout)
 return str(result)

Full lifespan example: examples/snippets/servers/lifespan_example.py

Running Your Server

Development Mode

The fastest way to test and debug your server is with the MCP Inspector:

uvrunmcpdevserver.py

# Add dependencies
uvrunmcpdevserver.py--withpandas--withnumpy

# Mount local code
uvrunmcpdevserver.py--with-editable.

Claude Desktop Integration

Once your server is ready, install it in Claude Desktop:

uvrunmcpinstallserver.py

# Custom name
uvrunmcpinstallserver.py--name"My Analytics Server"

# Environment variables
uvrunmcpinstallserver.py-vAPI_KEY=abc123-vDB_URL=postgres://...
uvrunmcpinstallserver.py-f.env

Direct Execution

For advanced scenarios like custom deployments:

"""Example showing direct execution of an MCP server.

This is the simplest way to run an MCP server directly.
cd to the `examples/snippets` directory and run:
 uv run direct-execution-server
 or
 python servers/direct_execution.py
"""

frommcp.server.mcpserverimport MCPServer

mcp = MCPServer("My App")


@mcp.tool()
defhello(name: str = "World") -> str:
"""Say hello to someone."""
 return f"Hello, {name}!"


defmain():
"""Entry point for the direct execution server."""
 mcp.run()


if __name__ == "__main__":
 main()

Full example: examples/snippets/servers/direct_execution.py

Run it with:

pythonservers/direct_execution.py
# or
uvrunmcprunservers/direct_execution.py

Note that uv run mcp run or uv run mcp dev only supports server using MCPServer and not the low-level server variant.

Streamable HTTP Transport

Note: Streamable HTTP transport is the recommended transport for production deployments. Use stateless_http=True and json_response=True for optimal scalability.

"""Run from the repository root:
uv run examples/snippets/servers/streamable_config.py
"""

frommcp.server.mcpserverimport MCPServer

mcp = MCPServer("StatelessServer")


# Add a simple tool to demonstrate the server
@mcp.tool()
defgreet(name: str = "World") -> str:
"""Greet someone by name."""
 return f"Hello, {name}!"


# Run server with streamable_http transport
# Transport-specific options (stateless_http, json_response) are passed to run()
if __name__ == "__main__":
 # Stateless server with JSON responses (recommended)
 mcp.run(transport="streamable-http", stateless_http=True, json_response=True)

 # Other configuration options:
 # Stateless server with SSE streaming responses
 # mcp.run(transport="streamable-http", stateless_http=True)

 # Stateful server with session persistence
 # mcp.run(transport="streamable-http")

Full example: examples/snippets/servers/streamable_config.py

You can mount multiple MCPServer servers in a Starlette application:

"""Run from the repository root:
uvicorn examples.snippets.servers.streamable_starlette_mount:app --reload
"""

importcontextlib

fromstarlette.applicationsimport Starlette
fromstarlette.routingimport Mount

frommcp.server.mcpserverimport MCPServer

# Create the Echo server
echo_mcp = MCPServer(name="EchoServer")


@echo_mcp.tool()
defecho(message: str) -> str:
"""A simple echo tool"""
 return f"Echo: {message}"


# Create the Math server
math_mcp = MCPServer(name="MathServer")


@math_mcp.tool()
defadd_two(n: int) -> int:
"""Tool to add two to the input"""
 return n + 2


# Create a combined lifespan to manage both session managers
@contextlib.asynccontextmanager
async deflifespan(app: Starlette):
 async with contextlib.AsyncExitStack() as stack:
 await stack.enter_async_context(echo_mcp.session_manager.run())
 await stack.enter_async_context(math_mcp.session_manager.run())
 yield


# Create the Starlette app and mount the MCP servers
app = Starlette(
 routes=[
 Mount("/echo", echo_mcp.streamable_http_app(stateless_http=True, json_response=True)),
 Mount("/math", math_mcp.streamable_http_app(stateless_http=True, json_response=True)),
 ],
 lifespan=lifespan,
)

# Note: Clients connect to http://localhost:8000/echo/mcp and http://localhost:8000/math/mcp
# To mount at the root of each path (e.g., /echo instead of /echo/mcp):
# echo_mcp.streamable_http_app(streamable_http_path="/", stateless_http=True, json_response=True)
# math_mcp.streamable_http_app(streamable_http_path="/", stateless_http=True, json_response=True)

Full example: examples/snippets/servers/streamable_starlette_mount.py

For low level server with Streamable HTTP implementations, see:

The streamable HTTP transport supports:

  • Stateful and stateless operation modes
  • Resumability with event stores
  • JSON or SSE response formats
  • Better scalability for multi-node deployments

CORS Configuration for Browser-Based Clients

If you'd like your server to be accessible by browser-based MCP clients, you'll need to configure CORS headers. The Mcp-Session-Id header must be exposed for browser clients to access it:

fromstarlette.applicationsimport Starlette
fromstarlette.middleware.corsimport CORSMiddleware

# Create your Starlette app first
starlette_app = Starlette(routes=[...])

# Then wrap it with CORS middleware
starlette_app = CORSMiddleware(
 starlette_app,
 allow_origins=["*"], # Configure appropriately for production
 allow_methods=["GET", "POST", "DELETE"], # MCP streamable HTTP methods
 expose_headers=["Mcp-Session-Id"],
)

This configuration is necessary because:

  • The MCP streamable HTTP transport uses the Mcp-Session-Id header for session management
  • Browsers restrict access to response headers unless explicitly exposed via CORS
  • Without this configuration, browser-based clients won't be able to read the session ID from initialization responses

Mounting to an Existing ASGI Server

By default, SSE servers are mounted at /sse and Streamable HTTP servers are mounted at /mcp. You can customize these paths using the methods described below.

For more information on mounting applications in Starlette, see the Starlette documentation.

StreamableHTTP servers

You can mount the StreamableHTTP server to an existing ASGI server using the streamable_http_app method. This allows you to integrate the StreamableHTTP server with other ASGI applications.

Basic mounting
"""Basic example showing how to mount StreamableHTTP server in Starlette.

Run from the repository root:
 uvicorn examples.snippets.servers.streamable_http_basic_mounting:app --reload
"""

importcontextlib

fromstarlette.applicationsimport Starlette
fromstarlette.routingimport Mount

frommcp.server.mcpserverimport MCPServer

# Create MCP server
mcp = MCPServer("My App")


@mcp.tool()
defhello() -> str:
"""A simple hello tool"""
 return "Hello from MCP!"


# Create a lifespan context manager to run the session manager
@contextlib.asynccontextmanager
async deflifespan(app: Starlette):
 async with mcp.session_manager.run():
 yield


# Mount the StreamableHTTP server to the existing ASGI server
# Transport-specific options are passed to streamable_http_app()
app = Starlette(
 routes=[
 Mount("/", app=mcp.streamable_http_app(json_response=True)),
 ],
 lifespan=lifespan,
)

Full example: examples/snippets/servers/streamable_http_basic_mounting.py

Host-based routing
"""Example showing how to mount StreamableHTTP server using Host-based routing.

Run from the repository root:
 uvicorn examples.snippets.servers.streamable_http_host_mounting:app --reload
"""

importcontextlib

fromstarlette.applicationsimport Starlette
fromstarlette.routingimport Host

frommcp.server.mcpserverimport MCPServer

# Create MCP server
mcp = MCPServer("MCP Host App")


@mcp.tool()
defdomain_info() -> str:
"""Get domain-specific information"""
 return "This is served from mcp.acme.corp"


# Create a lifespan context manager to run the session manager
@contextlib.asynccontextmanager
async deflifespan(app: Starlette):
 async with mcp.session_manager.run():
 yield


# Mount using Host-based routing
# Transport-specific options are passed to streamable_http_app()
app = Starlette(
 routes=[
 Host("mcp.acme.corp", app=mcp.streamable_http_app(json_response=True)),
 ],
 lifespan=lifespan,
)

Full example: examples/snippets/servers/streamable_http_host_mounting.py

Multiple servers with path configuration
"""Example showing how to mount multiple StreamableHTTP servers with path configuration.

Run from the repository root:
 uvicorn examples.snippets.servers.streamable_http_multiple_servers:app --reload
"""

importcontextlib

fromstarlette.applicationsimport Starlette
fromstarlette.routingimport Mount

frommcp.server.mcpserverimport MCPServer

# Create multiple MCP servers
api_mcp = MCPServer("API Server")
chat_mcp = MCPServer("Chat Server")


@api_mcp.tool()
defapi_status() -> str:
"""Get API status"""
 return "API is running"


@chat_mcp.tool()
defsend_message(message: str) -> str:
"""Send a chat message"""
 return f"Message sent: {message}"


# Create a combined lifespan to manage both session managers
@contextlib.asynccontextmanager
async deflifespan(app: Starlette):
 async with contextlib.AsyncExitStack() as stack:
 await stack.enter_async_context(api_mcp.session_manager.run())
 await stack.enter_async_context(chat_mcp.session_manager.run())
 yield


# Mount the servers with transport-specific options passed to streamable_http_app()
# streamable_http_path="/" means endpoints will be at /api and /chat instead of /api/mcp and /chat/mcp
app = Starlette(
 routes=[
 Mount("/api", app=api_mcp.streamable_http_app(json_response=True, streamable_http_path="/")),
 Mount("/chat", app=chat_mcp.streamable_http_app(json_response=True, streamable_http_path="/")),
 ],
 lifespan=lifespan,
)

Full example: examples/snippets/servers/streamable_http_multiple_servers.py

Path configuration at initialization
"""Example showing path configuration when mounting MCPServer.

Run from the repository root:
 uvicorn examples.snippets.servers.streamable_http_path_config:app --reload
"""

fromstarlette.applicationsimport Starlette
fromstarlette.routingimport Mount

frommcp.server.mcpserverimport MCPServer

# Create a simple MCPServer server
mcp_at_root = MCPServer("My Server")


@mcp_at_root.tool()
defprocess_data(data: str) -> str:
"""Process some data"""
 return f"Processed: {data}"


# Mount at /process with streamable_http_path="/" so the endpoint is /process (not /process/mcp)
# Transport-specific options like json_response are passed to streamable_http_app()
app = Starlette(
 routes=[
 Mount(
 "/process",
 app=mcp_at_root.streamable_http_app(json_response=True, streamable_http_path="/"),
 ),
 ]
)

Full example: examples/snippets/servers/streamable_http_path_config.py

SSE servers

Note: SSE transport is being superseded by Streamable HTTP transport.

You can mount the SSE server to an existing ASGI server using the sse_app method. This allows you to integrate the SSE server with other ASGI applications.

fromstarlette.applicationsimport Starlette
fromstarlette.routingimport Mount, Host
frommcp.server.mcpserverimport MCPServer


mcp = MCPServer("My App")

# Mount the SSE server to the existing ASGI server
app = Starlette(
 routes=[
 Mount('/', app=mcp.sse_app()),
 ]
)

# or dynamically mount as host
app.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app()))

You can also mount multiple MCP servers at different sub-paths. The SSE transport automatically detects the mount path via ASGI's root_path mechanism, so message endpoints are correctly routed:

fromstarlette.applicationsimport Starlette
fromstarlette.routingimport Mount
frommcp.server.mcpserverimport MCPServer

# Create multiple MCP servers
github_mcp = MCPServer("GitHub API")
browser_mcp = MCPServer("Browser")
search_mcp = MCPServer("Search")

# Mount each server at its own sub-path
# The SSE transport automatically uses ASGI's root_path to construct
# the correct message endpoint (e.g., /github/messages/, /browser/messages/)
app = Starlette(
 routes=[
 Mount("/github", app=github_mcp.sse_app()),
 Mount("/browser", app=browser_mcp.sse_app()),
 Mount("/search", app=search_mcp.sse_app()),
 ]
)

For more information on mounting applications in Starlette, see the Starlette documentation.

Advanced Usage

Low-Level Server

For more control, you can use the low-level server implementation directly. This gives you full access to the protocol and allows you to customize every aspect of your server, including lifecycle management through the lifespan API:

"""Run from the repository root:
uv run examples/snippets/servers/lowlevel/lifespan.py
"""

fromcollections.abcimport AsyncIterator
fromcontextlibimport asynccontextmanager
fromtypingimport TypedDict

importmcp_typesastypes

importmcp.server.stdio
frommcp.serverimport Server, ServerRequestContext


# Mock database class for example
classDatabase:
"""Mock database class for example."""

 @classmethod
 async defconnect(cls) -> "Database":
"""Connect to database."""
 print("Database connected")
 return cls()

 async defdisconnect(self) -> None:
"""Disconnect from database."""
 print("Database disconnected")

 async defquery(self, query_str: str) -> list[dict[str, str]]:
"""Execute a query."""
 # Simulate database query
 return [{"id": "1", "name": "Example", "query": query_str}]


classAppContext(TypedDict):
 db: Database


@asynccontextmanager
async defserver_lifespan(_server: Server[AppContext]) -> AsyncIterator[AppContext]:
"""Manage server startup and shutdown lifecycle."""
 db = await Database.connect()
 try:
 yield {"db": db}
 finally:
 await db.disconnect()


async defhandle_list_tools(
 ctx: ServerRequestContext[AppContext], params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
"""List available tools."""
 return types.ListToolsResult(
 tools=[
 types.Tool(
 name="query_db",
 description="Query the database",
 input_schema={
 "type": "object",
 "properties": {"query": {"type": "string", "description": "SQL query to execute"}},
 "required": ["query"],
 },
 )
 ]
 )


async defhandle_call_tool(
 ctx: ServerRequestContext[AppContext], params: types.CallToolRequestParams
) -> types.CallToolResult:
"""Handle database query tool call."""
 if params.name != "query_db":
 raise ValueError(f"Unknown tool: {params.name}")

 db = ctx.lifespan_context["db"]
 results = await db.query((params.arguments or {})["query"])

 return types.CallToolResult(content=[types.TextContent(type="text", text=f"Query results: {results}")])


server = Server(
 "example-server",
 lifespan=server_lifespan,
 on_list_tools=handle_list_tools,
 on_call_tool=handle_call_tool,
)


async defrun():
"""Run the server with lifespan management."""
 async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
 await server.run(
 read_stream,
 write_stream,
 server.create_initialization_options(),
 )


if __name__ == "__main__":
 importasyncio

 asyncio.run(run())

Full example: examples/snippets/servers/lowlevel/lifespan.py

The lifespan API provides:

  • A way to initialize resources when the server starts and clean them up when it stops
  • Access to initialized resources through the request context in handlers
  • Type-safe context passing between lifespan and request handlers
"""Run from the repository root:
uv run examples/snippets/servers/lowlevel/basic.py
"""

importasyncio

importmcp_typesastypes

importmcp.server.stdio
frommcp.serverimport Server, ServerRequestContext


async defhandle_list_prompts(
 ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListPromptsResult:
"""List available prompts."""
 return types.ListPromptsResult(
 prompts=[
 types.Prompt(
 name="example-prompt",
 description="An example prompt template",
 arguments=[types.PromptArgument(name="arg1", description="Example argument", required=True)],
 )
 ]
 )


async defhandle_get_prompt(ctx: ServerRequestContext, params: types.GetPromptRequestParams) -> types.GetPromptResult:
"""Get a specific prompt by name."""
 if params.name != "example-prompt":
 raise ValueError(f"Unknown prompt: {params.name}")

 arg1_value = (params.arguments or {}).get("arg1", "default")

 return types.GetPromptResult(
 description="Example prompt",
 messages=[
 types.PromptMessage(
 role="user",
 content=types.TextContent(type="text", text=f"Example prompt text with argument: {arg1_value}"),
 )
 ],
 )


server = Server(
 "example-server",
 on_list_prompts=handle_list_prompts,
 on_get_prompt=handle_get_prompt,
)


async defrun():
"""Run the basic low-level server."""
 async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
 await server.run(
 read_stream,
 write_stream,
 server.create_initialization_options(),
 )


if __name__ == "__main__":
 asyncio.run(run())

Full example: examples/snippets/servers/lowlevel/basic.py

Caution: The uv run mcp run and uv run mcp dev tool doesn't support low-level server.

Structured Output Support

The low-level server supports structured output for tools, allowing you to return both human-readable content and machine-readable structured data. Tools can define an outputSchema to validate their structured output:

"""Run from the repository root:
uv run examples/snippets/servers/lowlevel/structured_output.py
"""

importasyncio
importjson

importmcp_typesastypes

importmcp.server.stdio
frommcp.serverimport Server, ServerRequestContext


async defhandle_list_tools(
 ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
"""List available tools with structured output schemas."""
 return types.ListToolsResult(
 tools=[
 types.Tool(
 name="get_weather",
 description="Get current weather for a city",
 input_schema={
 "type": "object",
 "properties": {"city": {"type": "string", "description": "City name"}},
 "required": ["city"],
 },
 output_schema={
 "type": "object",
 "properties": {
 "temperature": {"type": "number", "description": "Temperature in Celsius"},
 "condition": {"type": "string", "description": "Weather condition"},
 "humidity": {"type": "number", "description": "Humidity percentage"},
 "city": {"type": "string", "description": "City name"},
 },
 "required": ["temperature", "condition", "humidity", "city"],
 },
 )
 ]
 )


async defhandle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
"""Handle tool calls with structured output."""
 if params.name == "get_weather":
 city = (params.arguments or {})["city"]

 weather_data = {
 "temperature": 22.5,
 "condition": "partly cloudy",
 "humidity": 65,
 "city": city,
 }

 return types.CallToolResult(
 content=[types.TextContent(type="text", text=json.dumps(weather_data, indent=2))],
 structured_content=weather_data,
 )

 raise ValueError(f"Unknown tool: {params.name}")


server = Server(
 "example-server",
 on_list_tools=handle_list_tools,
 on_call_tool=handle_call_tool,
)


async defrun():
"""Run the structured output server."""
 async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
 await server.run(
 read_stream,
 write_stream,
 server.create_initialization_options(),
 )


if __name__ == "__main__":
 asyncio.run(run())

Full example: examples/snippets/servers/lowlevel/structured_output.py

With the low-level server, handlers always return CallToolResult directly. You construct both the human-readable content and the machine-readable structured_content yourself, giving you full control over the response.

Returning CallToolResult with _meta

For passing data to client applications without exposing it to the model, use the _meta field on CallToolResult:

"""Run from the repository root:
uv run examples/snippets/servers/lowlevel/direct_call_tool_result.py
"""

importasyncio

importmcp_typesastypes

importmcp.server.stdio
frommcp.serverimport Server, ServerRequestContext


async defhandle_list_tools(
 ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListToolsResult:
"""List available tools."""
 return types.ListToolsResult(
 tools=[
 types.Tool(
 name="advanced_tool",
 description="Tool with full control including _meta field",
 input_schema={
 "type": "object",
 "properties": {"message": {"type": "string"}},
 "required": ["message"],
 },
 )
 ]
 )


async defhandle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:
"""Handle tool calls by returning CallToolResult directly."""
 if params.name == "advanced_tool":
 message = (params.arguments or {}).get("message", "")
 return types.CallToolResult(
 content=[types.TextContent(type="text", text=f"Processed: {message}")],
 structured_content={"result": "success", "message": message},
 _meta={"hidden": "data for client applications only"},
 )

 raise ValueError(f"Unknown tool: {params.name}")


server = Server(
 "example-server",
 on_list_tools=handle_list_tools,
 on_call_tool=handle_call_tool,
)


async defrun():
"""Run the server."""
 async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
 await server.run(
 read_stream,
 write_stream,
 server.create_initialization_options(),
 )


if __name__ == "__main__":
 asyncio.run(run())

Full example: examples/snippets/servers/lowlevel/direct_call_tool_result.py

Pagination (Advanced)

For servers that need to handle large datasets, the low-level server provides paginated versions of list operations. This is an optional optimization - most servers won't need pagination unless they're dealing with hundreds or thousands of items.

Server-side Implementation

"""Example of implementing pagination with the low-level MCP server."""

importmcp_typesastypes

frommcp.serverimport Server, ServerRequestContext

# Sample data to paginate
ITEMS = [f"Item {i}" for i in range(1, 101)] # 100 items


async defhandle_list_resources(
 ctx: ServerRequestContext, params: types.PaginatedRequestParams | None
) -> types.ListResourcesResult:
"""List resources with pagination support."""
 page_size = 10

 # Extract cursor from request params
 cursor = params.cursor if params is not None else None

 # Parse cursor to get offset
 start = 0 if cursor is None else int(cursor)
 end = start + page_size

 # Get page of resources
 page_items = [
 types.Resource(uri=f"resource://items/{item}", name=item, description=f"Description for {item}")
 for item in ITEMS[start:end]
 ]

 # Determine next cursor
 next_cursor = str(end) if end < len(ITEMS) else None

 return types.ListResourcesResult(resources=page_items, next_cursor=next_cursor)


server = Server("paginated-server", on_list_resources=handle_list_resources)

Full example: examples/snippets/servers/pagination_example.py

Client-side Consumption

"""Example of consuming paginated MCP endpoints from a client."""

importasyncio

frommcp_typesimport PaginatedRequestParams, Resource

frommcp.client.sessionimport ClientSession
frommcp.client.stdioimport StdioServerParameters, stdio_client


async deflist_all_resources() -> None:
"""Fetch all resources using pagination."""
 async with stdio_client(StdioServerParameters(command="uv", args=["run", "mcp-simple-pagination"])) as (
 read,
 write,
 ):
 async with ClientSession(read, write) as session:
 await session.initialize()

 all_resources: list[Resource] = []
 cursor = None

 while True:
 # Fetch a page of resources
 result = await session.list_resources(params=PaginatedRequestParams(cursor=cursor))
 all_resources.extend(result.resources)

 print(f"Fetched {len(result.resources)} resources")

 # Check if there are more pages
 if result.next_cursor:
 cursor = result.next_cursor
 else:
 break

 print(f"Total resources: {len(all_resources)}")


if __name__ == "__main__":
 asyncio.run(list_all_resources())

Full example: examples/snippets/clients/pagination_client.py

Key Points

  • Cursors are opaque strings - the server defines the format (numeric offsets, timestamps, etc.)
  • Return nextCursor=None when there are no more pages
  • Backward compatible - clients that don't support pagination will still work (they'll just get the first page)
  • Flexible page sizes - Each endpoint can define its own page size based on data characteristics

See the simple-pagination example for a complete implementation.

Writing MCP Clients

The SDK provides a high-level client interface for connecting to MCP servers using various transports:

"""cd to the `examples/snippets/clients` directory and run:
uv run client
"""

importasyncio
importos

importmcp_typesastypes

frommcpimport ClientSession, StdioServerParameters
frommcp.client.contextimport ClientRequestContext
frommcp.client.stdioimport stdio_client

# Create server parameters for stdio connection
server_params = StdioServerParameters(
 command="uv", # Using uv to run the server
 args=["run", "server", "mcpserver_quickstart", "stdio"], # We're already in snippets dir
 env={"UV_INDEX": os.environ.get("UV_INDEX", "")},
)


# Optional: create a sampling callback
async defhandle_sampling_message(
 context: ClientRequestContext, params: types.CreateMessageRequestParams
) -> types.CreateMessageResult:
 print(f"Sampling request: {params.messages}")
 return types.CreateMessageResult(
 role="assistant",
 content=types.TextContent(
 type="text",
 text="Hello, world! from model",
 ),
 model="gpt-3.5-turbo",
 stop_reason="endTurn",
 )


async defrun():
 async with stdio_client(server_params) as (read, write):
 async with ClientSession(read, write, sampling_callback=handle_sampling_message) as session:
 # Initialize the connection
 await session.initialize()

 # List available prompts
 prompts = await session.list_prompts()
 print(f"Available prompts: {[p.nameforpinprompts.prompts]}")

 # Get a prompt (greet_user prompt from mcpserver_quickstart)
 if prompts.prompts:
 prompt = await session.get_prompt("greet_user", arguments={"name": "Alice", "style": "friendly"})
 print(f"Prompt result: {prompt.messages[0].content}")

 # List available resources
 resources = await session.list_resources()
 print(f"Available resources: {[r.uriforrinresources.resources]}")

 # List available tools
 tools = await session.list_tools()
 print(f"Available tools: {[t.namefortintools.tools]}")

 # Read a resource (greeting resource from mcpserver_quickstart)
 resource_content = await session.read_resource("greeting://World")
 content_block = resource_content.contents[0]
 if isinstance(content_block, types.TextResourceContents):
 print(f"Resource content: {content_block.text}")

 # Call a tool (add tool from mcpserver_quickstart)
 result = await session.call_tool("add", arguments={"a": 5, "b": 3})
 result_unstructured = result.content[0]
 if isinstance(result_unstructured, types.TextContent):
 print(f"Tool result: {result_unstructured.text}")
 result_structured = result.structured_content
 print(f"Structured tool result: {result_structured}")


defmain():
"""Entry point for the client script."""
 asyncio.run(run())


if __name__ == "__main__":
 main()

Full example: examples/snippets/clients/stdio_client.py

Clients can also connect using Streamable HTTP transport:

"""Run from the repository root:
uv run examples/snippets/clients/streamable_basic.py
"""

importasyncio

frommcpimport ClientSession
frommcp.client.streamable_httpimport streamable_http_client


async defmain():
 # Connect to a streamable HTTP server
 async with streamable_http_client("http://localhost:8000/mcp") as (read_stream, write_stream):
 # Create a session using the client streams
 async with ClientSession(read_stream, write_stream) as session:
 # Initialize the connection
 await session.initialize()
 # List available tools
 tools = await session.list_tools()
 print(f"Available tools: {[tool.namefortoolintools.tools]}")


if __name__ == "__main__":
 asyncio.run(main())

Full example: examples/snippets/clients/streamable_basic.py

Client Display Utilities

When building MCP clients, the SDK provides utilities to help display human-readable names for tools, resources, and prompts:

"""cd to the `examples/snippets` directory and run:
uv run display-utilities-client
"""

importasyncio
importos

frommcpimport ClientSession, StdioServerParameters
frommcp.client.stdioimport stdio_client
frommcp.shared.metadata_utilsimport get_display_name

# Create server parameters for stdio connection
server_params = StdioServerParameters(
 command="uv", # Using uv to run the server
 args=["run", "server", "mcpserver_quickstart", "stdio"],
 env={"UV_INDEX": os.environ.get("UV_INDEX", "")},
)


async defdisplay_tools(session: ClientSession):
"""Display available tools with human-readable names"""
 tools_response = await session.list_tools()

 for tool in tools_response.tools:
 # get_display_name() returns the title if available, otherwise the name
 display_name = get_display_name(tool)
 print(f"Tool: {display_name}")
 if tool.description:
 print(f" {tool.description}")


async defdisplay_resources(session: ClientSession):
"""Display available resources with human-readable names"""
 resources_response = await session.list_resources()

 for resource in resources_response.resources:
 display_name = get_display_name(resource)
 print(f"Resource: {display_name} ({resource.uri})")

 templates_response = await session.list_resource_templates()
 for template in templates_response.resource_templates:
 display_name = get_display_name(template)
 print(f"Resource Template: {display_name}")


async defrun():
"""Run the display utilities example."""
 async with stdio_client(server_params) as (read, write):
 async with ClientSession(read, write) as session:
 # Initialize the connection
 await session.initialize()

 print("=== Available Tools ===")
 await display_tools(session)

 print("\n=== Available Resources ===")
 await display_resources(session)


defmain():
"""Entry point for the display utilities client."""
 asyncio.run(run())


if __name__ == "__main__":
 main()

Full example: examples/snippets/clients/display_utilities.py

The get_display_name() function implements the proper precedence rules for displaying names:

  • For tools: title > annotations.title > name
  • For other objects: title > name

This ensures your client UI shows the most user-friendly names that servers provide.

OAuth Authentication for Clients

The SDK includes authorization support for connecting to protected MCP servers:

"""Before running, specify running MCP RS server URL.
To spin up RS server locally, see
 examples/servers/simple-auth/README.md

cd to the `examples/snippets` directory and run:
 uv run oauth-client
"""

importasyncio
fromurllib.parseimport parse_qs, urlparse

importhttpx
frompydanticimport AnyUrl

frommcpimport ClientSession
frommcp.client.authimport AuthorizationCodeResult, OAuthClientProvider, TokenStorage
frommcp.client.streamable_httpimport streamable_http_client
frommcp.shared.authimport OAuthClientInformationFull, OAuthClientMetadata, OAuthToken


classInMemoryTokenStorage(TokenStorage):
"""Demo In-memory token storage implementation."""

 def__init__(self):
 self.tokens: OAuthToken | None = None
 self.client_info: OAuthClientInformationFull | None = None

 async defget_tokens(self) -> OAuthToken | None:
"""Get stored tokens."""
 return self.tokens

 async defset_tokens(self, tokens: OAuthToken) -> None:
"""Store tokens."""
 self.tokens = tokens

 async defget_client_info(self) -> OAuthClientInformationFull | None:
"""Get stored client information."""
 return self.client_info

 async defset_client_info(self, client_info: OAuthClientInformationFull) -> None:
"""Store client information."""
 self.client_info = client_info


async defhandle_redirect(auth_url: str) -> None:
 print(f"Visit: {auth_url}")


async defhandle_callback() -> AuthorizationCodeResult:
 callback_url = input("Paste callback URL: ")
 params = parse_qs(urlparse(callback_url).query)
 return AuthorizationCodeResult(
 code=params["code"][0],
 state=params.get("state", [None])[0],
 iss=params.get("iss", [None])[0],
 )


async defmain():
"""Run the OAuth client example."""
 oauth_auth = OAuthClientProvider(
 server_url="http://localhost:8001",
 client_metadata=OAuthClientMetadata(
 client_name="Example MCP Client",
 redirect_uris=[AnyUrl("http://localhost:3000/callback")],
 grant_types=["authorization_code", "refresh_token"],
 response_types=["code"],
 scope="user",
 ),
 storage=InMemoryTokenStorage(),
 redirect_handler=handle_redirect,
 callback_handler=handle_callback,
 )

 async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:
 async with streamable_http_client("http://localhost:8001/mcp", http_client=custom_client) as (read, write):
 async with ClientSession(read, write) as session:
 await session.initialize()

 tools = await session.list_tools()
 print(f"Available tools: {[tool.namefortoolintools.tools]}")

 resources = await session.list_resources()
 print(f"Available resources: {[r.uriforrinresources.resources]}")


defrun():
 asyncio.run(main())


if __name__ == "__main__":
 run()

Full example: examples/snippets/clients/oauth_client.py

For a complete working example, see examples/clients/simple-auth-client/.

Parsing Tool Results

When calling tools through MCP, the CallToolResult object contains the tool's response in a structured format. Understanding how to parse this result is essential for properly handling tool outputs.

"""examples/snippets/clients/parsing_tool_results.py"""

importasyncio

importmcp_typesastypes

frommcpimport ClientSession, StdioServerParameters
frommcp.client.stdioimport stdio_client


async defparse_tool_results():
"""Demonstrates how to parse different types of content in CallToolResult."""
 server_params = StdioServerParameters(command="python", args=["path/to/mcp_server.py"])

 async with stdio_client(server_params) as (read, write):
 async with ClientSession(read, write) as session:
 await session.initialize()

 # Example 1: Parsing text content
 result = await session.call_tool("get_data", {"format": "text"})
 for content in result.content:
 if isinstance(content, types.TextContent):
 print(f"Text: {content.text}")

 # Example 2: Parsing structured content from JSON tools
 result = await session.call_tool("get_user", {"id": "123"})
 if hasattr(result, "structured_content") and result.structured_content:
 # Access structured data directly
 user_data = result.structured_content
 print(f"User: {user_data.get('name')}, Age: {user_data.get('age')}")

 # Example 3: Parsing embedded resources
 result = await session.call_tool("read_config", {})
 for content in result.content:
 if isinstance(content, types.EmbeddedResource):
 resource = content.resource
 if isinstance(resource, types.TextResourceContents):
 print(f"Config from {resource.uri}: {resource.text}")
 else:
 print(f"Binary data from {resource.uri}")

 # Example 4: Parsing image content
 result = await session.call_tool("generate_chart", {"data": [1, 2, 3]})
 for content in result.content:
 if isinstance(content, types.ImageContent):
 print(f"Image ({content.mime_type}): {len(content.data)} bytes")

 # Example 5: Handling errors
 result = await session.call_tool("failing_tool", {})
 if result.is_error:
 print("Tool execution failed!")
 for content in result.content:
 if isinstance(content, types.TextContent):
 print(f"Error: {content.text}")


async defmain():
 await parse_tool_results()


if __name__ == "__main__":
 asyncio.run(main())

Full example: examples/snippets/clients/parsing_tool_results.py

MCP Primitives

The MCP protocol defines three core primitives that servers can implement:

Primitive Control Description Example Use
Prompts User-controlled Interactive templates invoked by user choice Slash commands, menu options
Resources Application-controlled Contextual data managed by the client application File contents, API responses
Tools Model-controlled Functions exposed to the LLM to take actions API calls, data updates

Server Capabilities

MCP servers declare capabilities during initialization:

Capability Feature Flag Description
prompts listChanged Prompt template management
resources subscribe
listChanged
Resource exposure and updates
tools listChanged Tool discovery and execution
logging - Server logging configuration
completions - Argument completion suggestions

Documentation

Contributing

We are passionate about supporting contributors of all levels of experience and would love to see you get involved in the project. See the contributing guide to get started.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Project details

Unverified details

These details have not been verified by PyPI
Project links
Meta
  • License: MIT License (MIT)
  • Author: Model Context Protocol a Series of LF Projects, LLC.
  • Maintainer: David Soria Parra
  • Tags automation , llm , mcp
  • Requires: Python >=3.10
  • Provides-Extra: cli , rich

Release history Release notifications | RSS feed

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

mcp-2.0.0a3.tar.gz (1.0 MB view details)

Uploaded Source

Built Distribution

Filter files by name, interpreter, ABI, and platform.

If you're not sure about the file name format, learn more about wheel file names.

Copy a direct link to the current filters

mcp-2.0.0a3-py3-none-any.whl (249.3 kB view details)

Uploaded Python 3

File details

Details for the file mcp-2.0.0a3.tar.gz.

File metadata

  • Download URL: mcp-2.0.0a3.tar.gz
  • Upload date:
  • Size: 1.0 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for mcp-2.0.0a3.tar.gz
Algorithm Hash digest
SHA256 360e38f4aea326db67bfb3a6028e78319c86c21923e694d30cbc6d127287042f
MD5 3164ace3287afa02ae3614dac3a044de
BLAKE2b-256 12ec2006c54684cf65d5f6e7405e1e604444c7cb000b47de0df8891294a99f0f

See more details on using hashes here.

Provenance

The following attestation bundles were made for mcp-2.0.0a3.tar.gz:

Publisher: publish-pypi.yml on modelcontextprotocol/python-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file mcp-2.0.0a3-py3-none-any.whl.

File metadata

  • Download URL: mcp-2.0.0a3-py3-none-any.whl
  • Upload date:
  • Size: 249.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for mcp-2.0.0a3-py3-none-any.whl
Algorithm Hash digest
SHA256 850716457d0813231feab512052e8e6397524be9b9e284c3c27ec281ad542962
MD5 2ed41b83e90305f8e811e20d9e3b1f98
BLAKE2b-256 76d4a86c7536ada67fdf7935df778e1879d7b654235fd2ebab90b11caef46a9f

See more details on using hashes here.

Provenance

The following attestation bundles were made for mcp-2.0.0a3-py3-none-any.whl:

Publisher: publish-pypi.yml on modelcontextprotocol/python-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

๐Ÿ‘ Image
AWS Cloud computing and Security Sponsor ๐Ÿ‘ Image
Datadog Monitoring ๐Ÿ‘ Image
Depot Continuous Integration ๐Ÿ‘ Image
Fastly CDN ๐Ÿ‘ Image
Google Download Analytics ๐Ÿ‘ Image
Pingdom Monitoring ๐Ÿ‘ Image
Sentry Error logging ๐Ÿ‘ Image
StatusPage Status page