chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
"""cd to the `examples/snippets` directory and run:
|
||||
uv run completion-client
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from mcp_types import PromptReference, ResourceTemplateReference
|
||||
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import 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 def run():
|
||||
"""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}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point for the completion client."""
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,66 @@
|
||||
"""cd to the `examples/snippets` directory and run:
|
||||
uv run display-utilities-client
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
from mcp.shared.metadata_utils import 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 def display_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 def display_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 def run():
|
||||
"""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)
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point for the display utilities client."""
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,82 @@
|
||||
"""Client side of SEP-990 (enterprise IdP policy controls).
|
||||
|
||||
`IdentityAssertionOAuthProvider` presents an Identity Assertion Authorization Grant (ID-JAG) issued
|
||||
by the enterprise IdP to the MCP authorization server using the RFC 7523 jwt-bearer grant
|
||||
(`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`, ID-JAG as `assertion`), and receives an
|
||||
MCP access token. No browser redirect or dynamic client registration is involved.
|
||||
|
||||
Obtaining the ID-JAG (logging into the IdP and the leg-1 exchange against it) is deployment-specific
|
||||
and out of scope for the SDK; supply it through the `assertion_provider` callback. The callback
|
||||
receives the authorization server's issuer (the ID-JAG `aud`) and the MCP server's resource
|
||||
identifier (the ID-JAG `resource` claim). SEP-990 requires a confidential client, so a client secret
|
||||
is mandatory, and `issuer` is the authorization server the credentials are provisioned for - the
|
||||
provider fetches metadata from that issuer's well-known and never asks the resource server which AS
|
||||
to use.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import httpx
|
||||
|
||||
from mcp import ClientSession
|
||||
from mcp.client.auth.extensions.identity_assertion import IdentityAssertionOAuthProvider
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
|
||||
|
||||
|
||||
class InMemoryTokenStorage:
|
||||
"""Demo in-memory token storage."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.tokens: OAuthToken | None = None
|
||||
self.client_info: OAuthClientInformationFull | None = None
|
||||
|
||||
async def get_tokens(self) -> OAuthToken | None:
|
||||
return self.tokens
|
||||
|
||||
async def set_tokens(self, tokens: OAuthToken) -> None:
|
||||
self.tokens = tokens
|
||||
|
||||
async def get_client_info(self) -> OAuthClientInformationFull | None:
|
||||
return self.client_info
|
||||
|
||||
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
|
||||
self.client_info = client_info
|
||||
|
||||
|
||||
async def fetch_id_jag(audience: str, resource: str) -> str:
|
||||
"""Return the ID-JAG to present.
|
||||
|
||||
`audience` is the MCP authorization server's issuer (the ID-JAG `aud` claim); `resource` is the
|
||||
MCP server's RFC 9728 identifier (the ID-JAG `resource` claim, which the AS audience-restricts
|
||||
the issued token against). In production this exchanges the user's IdP ID token for an ID-JAG
|
||||
against the enterprise identity provider.
|
||||
"""
|
||||
raise NotImplementedError("Obtain the ID-JAG from your enterprise identity provider")
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
oauth_auth = IdentityAssertionOAuthProvider(
|
||||
server_url="http://localhost:8001/mcp",
|
||||
storage=InMemoryTokenStorage(),
|
||||
client_id="enterprise-mcp-client",
|
||||
client_secret="enterprise-mcp-secret",
|
||||
issuer="http://localhost:8001",
|
||||
assertion_provider=fetch_id_jag,
|
||||
scope="user",
|
||||
)
|
||||
|
||||
async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as http_client:
|
||||
async with streamable_http_client("http://localhost:8001/mcp", http_client=http_client) as (read, write):
|
||||
async with ClientSession(read, write) as session:
|
||||
await session.initialize()
|
||||
tools = await session.list_tools()
|
||||
print(f"Available tools: {[tool.name for tool in tools.tools]}")
|
||||
|
||||
|
||||
def run() -> None:
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,92 @@
|
||||
"""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
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
import httpx
|
||||
from pydantic import AnyUrl
|
||||
|
||||
from mcp import ClientSession
|
||||
from mcp.client.auth import AuthorizationCodeResult, OAuthClientProvider, TokenStorage
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken
|
||||
|
||||
|
||||
class InMemoryTokenStorage(TokenStorage):
|
||||
"""Demo In-memory token storage implementation."""
|
||||
|
||||
def __init__(self):
|
||||
self.tokens: OAuthToken | None = None
|
||||
self.client_info: OAuthClientInformationFull | None = None
|
||||
|
||||
async def get_tokens(self) -> OAuthToken | None:
|
||||
"""Get stored tokens."""
|
||||
return self.tokens
|
||||
|
||||
async def set_tokens(self, tokens: OAuthToken) -> None:
|
||||
"""Store tokens."""
|
||||
self.tokens = tokens
|
||||
|
||||
async def get_client_info(self) -> OAuthClientInformationFull | None:
|
||||
"""Get stored client information."""
|
||||
return self.client_info
|
||||
|
||||
async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:
|
||||
"""Store client information."""
|
||||
self.client_info = client_info
|
||||
|
||||
|
||||
async def handle_redirect(auth_url: str) -> None:
|
||||
print(f"Visit: {auth_url}")
|
||||
|
||||
|
||||
async def handle_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 def main():
|
||||
"""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.name for tool in tools.tools]}")
|
||||
|
||||
resources = await session.list_resources()
|
||||
print(f"Available resources: {[r.uri for r in resources.resources]}")
|
||||
|
||||
|
||||
def run():
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Example of consuming paginated MCP endpoints from a client."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from mcp_types import PaginatedRequestParams, Resource
|
||||
|
||||
from mcp.client.session import ClientSession
|
||||
from mcp.client.stdio import StdioServerParameters, stdio_client
|
||||
|
||||
|
||||
async def list_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())
|
||||
@@ -0,0 +1,62 @@
|
||||
"""examples/snippets/clients/parsing_tool_results.py"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import mcp_types as types
|
||||
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
||||
|
||||
async def parse_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 def main():
|
||||
await parse_tool_results()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,82 @@
|
||||
"""cd to the `examples/snippets/clients` directory and run:
|
||||
uv run client
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
import mcp_types as types
|
||||
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.context import ClientRequestContext
|
||||
from mcp.client.stdio import 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 def handle_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 def run():
|
||||
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.name for p in prompts.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.uri for r in resources.resources]}")
|
||||
|
||||
# List available tools
|
||||
tools = await session.list_tools()
|
||||
print(f"Available tools: {[t.name for t in tools.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}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point for the client script."""
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Run from the repository root:
|
||||
uv run examples/snippets/clients/streamable_basic.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
from mcp import ClientSession
|
||||
from mcp.client.streamable_http import streamable_http_client
|
||||
|
||||
|
||||
async def main():
|
||||
# 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.name for tool in tools.tools]}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,318 @@
|
||||
"""URL Elicitation Client Example.
|
||||
|
||||
Demonstrates how clients handle URL elicitation requests from servers.
|
||||
This is the Python equivalent of TypeScript SDK's elicitationUrlExample.ts,
|
||||
focused on URL elicitation patterns without OAuth complexity.
|
||||
|
||||
Features demonstrated:
|
||||
1. Client elicitation capability declaration
|
||||
2. Handling elicitation requests from servers via callback
|
||||
3. Catching UrlElicitationRequiredError from tool calls
|
||||
4. Browser interaction with security warnings
|
||||
5. Interactive CLI for testing
|
||||
|
||||
Run with:
|
||||
cd examples/snippets
|
||||
uv run elicitation-client
|
||||
|
||||
Requires a server with URL elicitation tools running. Start the elicitation
|
||||
server first:
|
||||
uv run server elicitation sse
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import webbrowser
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import mcp_types as types
|
||||
from mcp_types import URL_ELICITATION_REQUIRED
|
||||
|
||||
from mcp import ClientSession
|
||||
from mcp.client.context import ClientRequestContext
|
||||
from mcp.client.sse import sse_client
|
||||
from mcp.shared.exceptions import MCPError, UrlElicitationRequiredError
|
||||
|
||||
|
||||
async def handle_elicitation(
|
||||
context: ClientRequestContext,
|
||||
params: types.ElicitRequestParams,
|
||||
) -> types.ElicitResult | types.ErrorData:
|
||||
"""Handle elicitation requests from the server.
|
||||
|
||||
This callback is invoked when the server sends an elicitation/request.
|
||||
For URL mode, we prompt the user and optionally open their browser.
|
||||
"""
|
||||
if params.mode == "url":
|
||||
return await handle_url_elicitation(params)
|
||||
else:
|
||||
# We only support URL mode in this example
|
||||
return types.ErrorData(
|
||||
code=types.INVALID_REQUEST,
|
||||
message=f"Unsupported elicitation mode: {params.mode}",
|
||||
)
|
||||
|
||||
|
||||
ALLOWED_SCHEMES = {"http", "https"}
|
||||
|
||||
|
||||
async def handle_url_elicitation(
|
||||
params: types.ElicitRequestParams,
|
||||
) -> types.ElicitResult:
|
||||
"""Handle URL mode elicitation - show security warning and optionally open browser.
|
||||
|
||||
This function demonstrates the security-conscious approach to URL elicitation:
|
||||
1. Validate the URL scheme before prompting the user
|
||||
2. Display the full URL and domain for user inspection
|
||||
3. Show the server's reason for requesting this interaction
|
||||
4. Require explicit user consent before opening any URL
|
||||
"""
|
||||
# Extract URL parameters - these are available on URL mode requests
|
||||
url = getattr(params, "url", None)
|
||||
elicitation_id = getattr(params, "elicitationId", None)
|
||||
message = params.message
|
||||
|
||||
if not url:
|
||||
print("Error: No URL provided in elicitation request")
|
||||
return types.ElicitResult(action="cancel")
|
||||
|
||||
# Reject dangerous URL schemes before prompting the user
|
||||
parsed = urlparse(str(url))
|
||||
if parsed.scheme.lower() not in ALLOWED_SCHEMES:
|
||||
print(f"\nRejecting URL with disallowed scheme '{parsed.scheme}': {url}")
|
||||
return types.ElicitResult(action="decline")
|
||||
|
||||
# Extract domain for security display
|
||||
domain = extract_domain(url)
|
||||
|
||||
# Security warning - always show the user what they're being asked to do
|
||||
print("\n" + "=" * 60)
|
||||
print("SECURITY WARNING: External URL Request")
|
||||
print("=" * 60)
|
||||
print("\nThe server is requesting you to open an external URL.")
|
||||
print(f"\n Domain: {domain}")
|
||||
print(f" Full URL: {url}")
|
||||
print("\n Server's reason:")
|
||||
print(f" {message}")
|
||||
print(f"\n Elicitation ID: {elicitation_id}")
|
||||
print("\n" + "-" * 60)
|
||||
|
||||
# Get explicit user consent
|
||||
try:
|
||||
response = input("\nOpen this URL in your browser? (y/n): ").strip().lower()
|
||||
except EOFError:
|
||||
return types.ElicitResult(action="cancel")
|
||||
|
||||
if response in ("n", "no"):
|
||||
print("URL navigation declined.")
|
||||
return types.ElicitResult(action="decline")
|
||||
elif response not in ("y", "yes"):
|
||||
print("Invalid response. Cancelling.")
|
||||
return types.ElicitResult(action="cancel")
|
||||
|
||||
# Open the browser
|
||||
print(f"\nOpening browser to: {url}")
|
||||
try:
|
||||
webbrowser.open(url)
|
||||
except Exception as e:
|
||||
print(f"Failed to open browser: {e}")
|
||||
print(f"Please manually open: {url}")
|
||||
|
||||
print("Waiting for you to complete the interaction in your browser...")
|
||||
print("(The server will continue once you've finished)")
|
||||
|
||||
return types.ElicitResult(action="accept")
|
||||
|
||||
|
||||
def extract_domain(url: str) -> str:
|
||||
"""Extract domain from URL for security display."""
|
||||
try:
|
||||
return urlparse(url).netloc
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
|
||||
async def call_tool_with_error_handling(
|
||||
session: ClientSession,
|
||||
tool_name: str,
|
||||
arguments: dict[str, Any],
|
||||
) -> types.CallToolResult | None:
|
||||
"""Call a tool, handling UrlElicitationRequiredError if raised.
|
||||
|
||||
When a server tool needs URL elicitation before it can proceed,
|
||||
it can either:
|
||||
1. Send an elicitation request directly (handled by elicitation_callback)
|
||||
2. Return an error with code -32042 (URL_ELICITATION_REQUIRED)
|
||||
|
||||
This function demonstrates handling case 2 - catching the error
|
||||
and processing the required URL elicitations.
|
||||
"""
|
||||
try:
|
||||
result = await session.call_tool(tool_name, arguments)
|
||||
|
||||
# Check if the tool returned an error in the result
|
||||
if result.is_error:
|
||||
print(f"Tool returned error: {result.content}")
|
||||
return None
|
||||
|
||||
return result
|
||||
|
||||
except MCPError as e:
|
||||
# Check if this is a URL elicitation required error
|
||||
if e.code == URL_ELICITATION_REQUIRED:
|
||||
print("\n[Tool requires URL elicitation to proceed]")
|
||||
|
||||
# Convert to typed error to access elicitations
|
||||
url_error = UrlElicitationRequiredError.from_error(e.error)
|
||||
|
||||
# Process each required elicitation
|
||||
for elicitation in url_error.elicitations:
|
||||
await handle_url_elicitation(elicitation)
|
||||
|
||||
return None
|
||||
else:
|
||||
# Re-raise other MCP errors
|
||||
print(f"MCP Error: {e.error.message} (code: {e.error.code})")
|
||||
return None
|
||||
|
||||
|
||||
def print_help() -> None:
|
||||
"""Print available commands."""
|
||||
print("\nAvailable commands:")
|
||||
print(" list-tools - List available tools")
|
||||
print(" call <name> [json-args] - Call a tool with optional JSON arguments")
|
||||
print(" secure-payment - Test URL elicitation via ctx.elicit_url()")
|
||||
print(" connect-service - Test URL elicitation via UrlElicitationRequiredError")
|
||||
print(" help - Show this help")
|
||||
print(" quit - Exit the program")
|
||||
|
||||
|
||||
def print_tool_result(result: types.CallToolResult | None) -> None:
|
||||
"""Print a tool call result."""
|
||||
if not result:
|
||||
return
|
||||
print("\nTool result:")
|
||||
for content in result.content:
|
||||
if isinstance(content, types.TextContent):
|
||||
print(f" {content.text}")
|
||||
else:
|
||||
print(f" [{content.type}]")
|
||||
|
||||
|
||||
async def handle_list_tools(session: ClientSession) -> None:
|
||||
"""Handle the list-tools command."""
|
||||
tools = await session.list_tools()
|
||||
if tools.tools:
|
||||
print("\nAvailable tools:")
|
||||
for tool in tools.tools:
|
||||
print(f" - {tool.name}: {tool.description or 'No description'}")
|
||||
else:
|
||||
print("No tools available")
|
||||
|
||||
|
||||
async def handle_call_command(session: ClientSession, command: str) -> None:
|
||||
"""Handle the call command."""
|
||||
parts = command.split(maxsplit=2)
|
||||
if len(parts) < 2:
|
||||
print("Usage: call <tool-name> [json-args]")
|
||||
return
|
||||
|
||||
tool_name = parts[1]
|
||||
args: dict[str, Any] = {}
|
||||
if len(parts) > 2:
|
||||
try:
|
||||
args = json.loads(parts[2])
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Invalid JSON arguments: {e}")
|
||||
return
|
||||
|
||||
print(f"\nCalling tool '{tool_name}' with args: {args}")
|
||||
result = await call_tool_with_error_handling(session, tool_name, args)
|
||||
print_tool_result(result)
|
||||
|
||||
|
||||
async def process_command(session: ClientSession, command: str) -> bool:
|
||||
"""Process a single command. Returns False if should exit."""
|
||||
if command in {"quit", "exit"}:
|
||||
print("Goodbye!")
|
||||
return False
|
||||
|
||||
if command == "help":
|
||||
print_help()
|
||||
elif command == "list-tools":
|
||||
await handle_list_tools(session)
|
||||
elif command.startswith("call "):
|
||||
await handle_call_command(session, command)
|
||||
elif command == "secure-payment":
|
||||
print("\nTesting secure_payment tool (uses ctx.elicit_url())...")
|
||||
result = await call_tool_with_error_handling(session, "secure_payment", {"amount": 99.99})
|
||||
print_tool_result(result)
|
||||
elif command == "connect-service":
|
||||
print("\nTesting connect_service tool (raises UrlElicitationRequiredError)...")
|
||||
result = await call_tool_with_error_handling(session, "connect_service", {"service_name": "github"})
|
||||
print_tool_result(result)
|
||||
else:
|
||||
print(f"Unknown command: {command}")
|
||||
print("Type 'help' for available commands.")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def run_command_loop(session: ClientSession) -> None:
|
||||
"""Run the interactive command loop."""
|
||||
while True:
|
||||
try:
|
||||
command = input("> ").strip()
|
||||
except EOFError:
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
print("\n")
|
||||
break
|
||||
|
||||
if not command:
|
||||
continue
|
||||
|
||||
if not await process_command(session, command):
|
||||
break
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Run the interactive URL elicitation client."""
|
||||
server_url = "http://localhost:8000/sse"
|
||||
|
||||
print("=" * 60)
|
||||
print("URL Elicitation Client Example")
|
||||
print("=" * 60)
|
||||
print(f"\nConnecting to: {server_url}")
|
||||
print("(Start server with: cd examples/snippets && uv run server elicitation sse)")
|
||||
|
||||
try:
|
||||
async with sse_client(server_url) as (read, write):
|
||||
async with ClientSession(
|
||||
read,
|
||||
write,
|
||||
elicitation_callback=handle_elicitation,
|
||||
) as session:
|
||||
await session.initialize()
|
||||
print("\nConnected! Type 'help' for available commands.\n")
|
||||
await run_command_loop(session)
|
||||
|
||||
except ConnectionRefusedError:
|
||||
print(f"\nError: Could not connect to {server_url}")
|
||||
print("Make sure the elicitation server is running:")
|
||||
print(" cd examples/snippets && uv run server elicitation sse")
|
||||
except Exception as e:
|
||||
print(f"\nError: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def run() -> None:
|
||||
"""Entry point for the client script."""
|
||||
asyncio.run(main())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
@@ -0,0 +1,25 @@
|
||||
[project]
|
||||
name = "mcp-snippets"
|
||||
version = "0.1.0"
|
||||
description = "MCP Example Snippets"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"mcp",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools]
|
||||
packages = ["servers", "clients"]
|
||||
|
||||
[project.scripts]
|
||||
server = "servers:run_server"
|
||||
client = "clients.stdio_client:main"
|
||||
completion-client = "clients.completion_client:main"
|
||||
direct-execution-server = "servers.direct_execution:main"
|
||||
display-utilities-client = "clients.display_utilities:main"
|
||||
oauth-client = "clients.oauth_client:run"
|
||||
identity-assertion-client = "clients.identity_assertion_client:run"
|
||||
elicitation-client = "clients.url_elicitation_client:run"
|
||||
@@ -0,0 +1,37 @@
|
||||
"""MCP Snippets.
|
||||
|
||||
This package contains simple examples of MCP server features.
|
||||
Each server demonstrates a single feature and can be run as a standalone server.
|
||||
|
||||
To run a server, use the command:
|
||||
uv run server basic_tool sse
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from typing import Literal, cast
|
||||
|
||||
|
||||
def run_server():
|
||||
"""Run a server by name with optional transport.
|
||||
|
||||
Usage: server <server-name> [transport]
|
||||
Example: server basic_tool sse
|
||||
"""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: server <server-name> [transport]")
|
||||
print("Available servers: basic_tool, basic_resource, basic_prompt, tool_progress,")
|
||||
print(" sampling, elicitation, completion, notifications,")
|
||||
print(" mcpserver_quickstart, structured_output, images")
|
||||
print("Available transports: stdio (default), sse, streamable-http")
|
||||
sys.exit(1)
|
||||
|
||||
server_name = sys.argv[1]
|
||||
transport = sys.argv[2] if len(sys.argv) > 2 else "stdio"
|
||||
|
||||
try:
|
||||
module = importlib.import_module(f".{server_name}", package=__name__)
|
||||
module.mcp.run(cast(Literal["stdio", "sse", "streamable-http"], transport))
|
||||
except ImportError:
|
||||
print(f"Error: Server '{server_name}' not found")
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,18 @@
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
from mcp.server.mcpserver.prompts import base
|
||||
|
||||
mcp = MCPServer(name="Prompt Example")
|
||||
|
||||
|
||||
@mcp.prompt(title="Code Review")
|
||||
def review_code(code: str) -> str:
|
||||
return f"Please review this code:\n\n{code}"
|
||||
|
||||
|
||||
@mcp.prompt(title="Debug Assistant")
|
||||
def debug_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?"),
|
||||
]
|
||||
@@ -0,0 +1,20 @@
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
mcp = MCPServer(name="Resource Example")
|
||||
|
||||
|
||||
@mcp.resource("file://documents/{name}")
|
||||
def read_document(name: str) -> str:
|
||||
"""Read a document by name."""
|
||||
# This would normally read from disk
|
||||
return f"Content of {name}"
|
||||
|
||||
|
||||
@mcp.resource("config://settings")
|
||||
def get_settings() -> str:
|
||||
"""Get application settings."""
|
||||
return """{
|
||||
"theme": "dark",
|
||||
"language": "en",
|
||||
"debug": false
|
||||
}"""
|
||||
@@ -0,0 +1,16 @@
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
mcp = MCPServer(name="Tool Example")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def sum(a: int, b: int) -> int:
|
||||
"""Add two numbers together."""
|
||||
return a + b
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_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()}"
|
||||
@@ -0,0 +1,50 @@
|
||||
from mcp_types import (
|
||||
Completion,
|
||||
CompletionArgument,
|
||||
CompletionContext,
|
||||
PromptReference,
|
||||
ResourceTemplateReference,
|
||||
)
|
||||
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
mcp = MCPServer(name="Example")
|
||||
|
||||
|
||||
@mcp.resource("github://repos/{owner}/{repo}")
|
||||
def github_repo(owner: str, repo: str) -> str:
|
||||
"""GitHub repository resource."""
|
||||
return f"Repository: {owner}/{repo}"
|
||||
|
||||
|
||||
@mcp.prompt(description="Code review prompt")
|
||||
def review_code(language: str, code: str) -> str:
|
||||
"""Generate a code review."""
|
||||
return f"Review this {language} code:\n{code}"
|
||||
|
||||
|
||||
@mcp.completion()
|
||||
async def handle_completion(
|
||||
ref: PromptReference | ResourceTemplateReference,
|
||||
argument: CompletionArgument,
|
||||
context: CompletionContext | None,
|
||||
) -> Completion | None:
|
||||
"""Provide completions for prompts and resources."""
|
||||
|
||||
# Complete programming languages for the prompt
|
||||
if isinstance(ref, PromptReference):
|
||||
if ref.name == "review_code" and argument.name == "language":
|
||||
languages = ["python", "javascript", "typescript", "go", "rust"]
|
||||
return Completion(
|
||||
values=[lang for lang in languages if lang.startswith(argument.value)],
|
||||
has_more=False,
|
||||
)
|
||||
|
||||
# Complete repository names for GitHub resources
|
||||
if isinstance(ref, ResourceTemplateReference):
|
||||
if ref.uri == "github://repos/{owner}/{repo}" and argument.name == "repo":
|
||||
if context and context.arguments and context.arguments.get("owner") == "modelcontextprotocol":
|
||||
repos = ["python-sdk", "typescript-sdk", "specification"]
|
||||
return Completion(values=repos, has_more=False)
|
||||
|
||||
return None
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Example showing direct CallToolResult return for advanced control."""
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from mcp_types import CallToolResult, TextContent
|
||||
from pydantic import BaseModel
|
||||
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
mcp = MCPServer("CallToolResult Example")
|
||||
|
||||
|
||||
class ValidationModel(BaseModel):
|
||||
"""Model for validating structured output."""
|
||||
|
||||
status: str
|
||||
data: dict[str, int]
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def advanced_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()
|
||||
def validated_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()
|
||||
def empty_result_tool() -> CallToolResult:
|
||||
"""For empty results, return CallToolResult with empty content."""
|
||||
return CallToolResult(content=[])
|
||||
@@ -0,0 +1,27 @@
|
||||
"""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
|
||||
"""
|
||||
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
mcp = MCPServer("My App")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def hello(name: str = "World") -> str:
|
||||
"""Say hello to someone."""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
|
||||
def main():
|
||||
"""Entry point for the direct execution server."""
|
||||
mcp.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,98 @@
|
||||
"""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.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from mcp_types import ElicitRequestURLParams
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from mcp.server.mcpserver import Context, MCPServer
|
||||
from mcp.shared.exceptions import UrlElicitationRequiredError
|
||||
|
||||
mcp = MCPServer(name="Elicitation Example")
|
||||
|
||||
|
||||
class BookingPreferences(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 def book_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 def secure_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 def connect_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,
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Authorization-server side of SEP-990 (enterprise IdP policy controls).
|
||||
|
||||
An authorization server enables the Identity Assertion Authorization Grant by setting
|
||||
`identity_assertion_enabled=True` and implementing `exchange_identity_assertion` on its provider.
|
||||
The client presents the IdP-issued ID-JAG using the RFC 7523 jwt-bearer grant; the provider
|
||||
validates the assertion and mints an MCP access token.
|
||||
|
||||
Validating the ID-JAG is the provider's responsibility and is only stubbed here. A real
|
||||
implementation MUST, per RFC 7523 §3 and SEP-990 §5.1:
|
||||
|
||||
- verify the JWT signature, `iss`, and `exp`, and that `typ` is `oauth-id-jag+jwt`;
|
||||
- require `aud` to identify this authorization server;
|
||||
- require the ID-JAG's `client_id` claim to match the authenticated client;
|
||||
- audience-restrict the issued token to the resource named in the ID-JAG's `resource` claim
|
||||
(NOT the client-supplied `params.resource`);
|
||||
- derive the granted scopes from the ID-JAG and policy.
|
||||
|
||||
`_decode_and_validate_id_jag` below raises `NotImplementedError` so this snippet fails closed and
|
||||
forces a real implementation. Wire the returned routes into a Starlette app with
|
||||
`create_auth_routes(..., identity_assertion_enabled=True)`, or set
|
||||
`AuthSettings(identity_assertion_enabled=True)` with `MCPServer`/`Server`.
|
||||
"""
|
||||
|
||||
import secrets
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
from mcp.server.auth.provider import (
|
||||
AccessToken,
|
||||
AuthorizationCode,
|
||||
IdentityAssertionParams,
|
||||
OAuthAuthorizationServerProvider,
|
||||
RefreshToken,
|
||||
)
|
||||
from mcp.shared.auth import OAuthClientInformationFull, OAuthToken
|
||||
|
||||
|
||||
@dataclass
|
||||
class IdJagClaims:
|
||||
"""The trusted claims extracted from a validated ID-JAG."""
|
||||
|
||||
subject: str # the end user the ID-JAG was issued for
|
||||
client_id: str # the ID-JAG `client_id` claim; §5.1 requires it to match the authenticated client
|
||||
resource: str # the MCP server the issued token must be audience-restricted to
|
||||
scopes: list[str]
|
||||
|
||||
|
||||
class IdentityAssertionProvider(OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]):
|
||||
"""Authorization-server provider that accepts an ID-JAG via the RFC 7523 jwt-bearer grant."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.access_tokens: dict[str, AccessToken] = {}
|
||||
# SEP-990 clients are pre-registered out of band (DCR refuses the grant) and must be
|
||||
# confidential. `get_client` must return them, or the token endpoint 401s before the
|
||||
# exchange runs. Real deployments load these from their own store.
|
||||
self.clients: dict[str, OAuthClientInformationFull] = {
|
||||
"enterprise-mcp-client": OAuthClientInformationFull(
|
||||
client_id="enterprise-mcp-client",
|
||||
client_secret="enterprise-mcp-secret",
|
||||
redirect_uris=None,
|
||||
grant_types=["urn:ietf:params:oauth:grant-type:jwt-bearer"],
|
||||
token_endpoint_auth_method="client_secret_post",
|
||||
)
|
||||
}
|
||||
|
||||
async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:
|
||||
return self.clients.get(client_id)
|
||||
|
||||
async def exchange_identity_assertion(
|
||||
self, client: OAuthClientInformationFull, params: IdentityAssertionParams
|
||||
) -> OAuthToken:
|
||||
claims = self._decode_and_validate_id_jag(params.assertion, client)
|
||||
|
||||
access_token = f"access_{secrets.token_hex(16)}"
|
||||
self.access_tokens[access_token] = AccessToken(
|
||||
token=access_token,
|
||||
client_id=claims.client_id,
|
||||
scopes=claims.scopes,
|
||||
expires_at=int(time.time()) + 3600,
|
||||
# Bind to the resource from the validated ID-JAG, not the client-controlled request.
|
||||
resource=claims.resource,
|
||||
subject=claims.subject,
|
||||
)
|
||||
# No refresh token: SEP-990 relies on the IdP re-issuing ID-JAGs to control session lifetime.
|
||||
return OAuthToken(
|
||||
access_token=access_token,
|
||||
token_type="Bearer",
|
||||
expires_in=3600,
|
||||
scope=" ".join(claims.scopes),
|
||||
)
|
||||
|
||||
def _decode_and_validate_id_jag(self, assertion: str, client: OAuthClientInformationFull) -> IdJagClaims:
|
||||
"""Verify the ID-JAG and return its trusted claims, or reject the request.
|
||||
|
||||
Replace this stub with real RFC 7523 §3 / SEP-990 §5.1 validation. It fails closed - it
|
||||
raises rather than trusting the assertion - so a copy of this example cannot accidentally
|
||||
accept unverified tokens. RFC 7523 §3.1 / RFC 6749 §5.2 specify `invalid_grant` for a
|
||||
rejected assertion.
|
||||
"""
|
||||
raise NotImplementedError("Validate the ID-JAG (signature, iss/aud/exp/typ, client_id, resource)")
|
||||
|
||||
async def load_access_token(self, token: str) -> AccessToken | None:
|
||||
return self.access_tokens.get(token)
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Example showing image handling with MCPServer."""
|
||||
|
||||
from PIL import Image as PILImage
|
||||
|
||||
from mcp.server.mcpserver import Image, MCPServer
|
||||
|
||||
mcp = MCPServer("Image Example")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def create_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")
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Example showing lifespan support for startup/shutdown with strong typing."""
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
|
||||
from mcp.server.mcpserver import Context, MCPServer
|
||||
|
||||
|
||||
# Mock database class for example
|
||||
class Database:
|
||||
"""Mock database class for example."""
|
||||
|
||||
@classmethod
|
||||
async def connect(cls) -> "Database":
|
||||
"""Connect to database."""
|
||||
return cls()
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
"""Disconnect from database."""
|
||||
pass
|
||||
|
||||
def query(self) -> str:
|
||||
"""Execute a query."""
|
||||
return "Query result"
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppContext:
|
||||
"""Application context with typed dependencies."""
|
||||
|
||||
db: Database
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def app_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()
|
||||
def query_db(ctx: Context[AppContext]) -> str:
|
||||
"""Tool that uses initialized resources."""
|
||||
db = ctx.request_context.lifespan_context.db
|
||||
return db.query()
|
||||
@@ -0,0 +1 @@
|
||||
"""Low-level server examples for MCP Python SDK."""
|
||||
@@ -0,0 +1,64 @@
|
||||
"""Run from the repository root:
|
||||
uv run examples/snippets/servers/lowlevel/basic.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import mcp_types as types
|
||||
|
||||
import mcp.server.stdio
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
|
||||
|
||||
async def handle_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 def handle_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 def run():
|
||||
"""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())
|
||||
@@ -0,0 +1,63 @@
|
||||
"""Run from the repository root:
|
||||
uv run examples/snippets/servers/lowlevel/direct_call_tool_result.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
import mcp_types as types
|
||||
|
||||
import mcp.server.stdio
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
|
||||
|
||||
async def handle_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 def handle_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 def run():
|
||||
"""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())
|
||||
@@ -0,0 +1,102 @@
|
||||
"""Run from the repository root:
|
||||
uv run examples/snippets/servers/lowlevel/lifespan.py
|
||||
"""
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import TypedDict
|
||||
|
||||
import mcp_types as types
|
||||
|
||||
import mcp.server.stdio
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
|
||||
|
||||
# Mock database class for example
|
||||
class Database:
|
||||
"""Mock database class for example."""
|
||||
|
||||
@classmethod
|
||||
async def connect(cls) -> "Database":
|
||||
"""Connect to database."""
|
||||
print("Database connected")
|
||||
return cls()
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
"""Disconnect from database."""
|
||||
print("Database disconnected")
|
||||
|
||||
async def query(self, query_str: str) -> list[dict[str, str]]:
|
||||
"""Execute a query."""
|
||||
# Simulate database query
|
||||
return [{"id": "1", "name": "Example", "query": query_str}]
|
||||
|
||||
|
||||
class AppContext(TypedDict):
|
||||
db: Database
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def server_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 def handle_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 def handle_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 def run():
|
||||
"""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__":
|
||||
import asyncio
|
||||
|
||||
asyncio.run(run())
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Run from the repository root:
|
||||
uv run examples/snippets/servers/lowlevel/structured_output.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import mcp_types as types
|
||||
|
||||
import mcp.server.stdio
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
|
||||
|
||||
async def handle_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 def handle_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 def run():
|
||||
"""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())
|
||||
@@ -0,0 +1,42 @@
|
||||
"""MCPServer quickstart example.
|
||||
|
||||
Run from the repository root:
|
||||
uv run examples/snippets/servers/mcpserver_quickstart.py
|
||||
"""
|
||||
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
# Create an MCP server
|
||||
mcp = MCPServer("Demo")
|
||||
|
||||
|
||||
# Add an addition tool
|
||||
@mcp.tool()
|
||||
def add(a: int, b: int) -> int:
|
||||
"""Add two numbers"""
|
||||
return a + b
|
||||
|
||||
|
||||
# Add a dynamic greeting resource
|
||||
@mcp.resource("greeting://{name}")
|
||||
def get_greeting(name: str) -> str:
|
||||
"""Get a personalized greeting"""
|
||||
return f"Hello, {name}!"
|
||||
|
||||
|
||||
# Add a prompt
|
||||
@mcp.prompt()
|
||||
def greet_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)
|
||||
@@ -0,0 +1,18 @@
|
||||
from mcp.server.mcpserver import Context, MCPServer
|
||||
|
||||
mcp = MCPServer(name="Notifications Example")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def process_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}"
|
||||
@@ -0,0 +1,45 @@
|
||||
"""Run from the repository root:
|
||||
uv run examples/snippets/servers/oauth_server.py
|
||||
"""
|
||||
|
||||
from pydantic import AnyHttpUrl
|
||||
|
||||
from mcp.server.auth.provider import AccessToken, TokenVerifier
|
||||
from mcp.server.auth.settings import AuthSettings
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
|
||||
class SimpleTokenVerifier(TokenVerifier):
|
||||
"""Simple token verifier for demonstration."""
|
||||
|
||||
async def verify_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 def get_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)
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Example of implementing pagination with the low-level MCP server."""
|
||||
|
||||
import mcp_types as types
|
||||
|
||||
from mcp.server import Server, ServerRequestContext
|
||||
|
||||
# Sample data to paginate
|
||||
ITEMS = [f"Item {i}" for i in range(1, 101)] # 100 items
|
||||
|
||||
|
||||
async def handle_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)
|
||||
@@ -0,0 +1,26 @@
|
||||
from mcp_types import SamplingMessage, TextContent
|
||||
|
||||
from mcp.server.mcpserver import Context, MCPServer
|
||||
|
||||
mcp = MCPServer(name="Sampling Example")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def generate_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)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Run from the repository root:
|
||||
uv run examples/snippets/servers/streamable_config.py
|
||||
"""
|
||||
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
mcp = MCPServer("StatelessServer")
|
||||
|
||||
|
||||
# Add a simple tool to demonstrate the server
|
||||
@mcp.tool()
|
||||
def greet(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")
|
||||
@@ -0,0 +1,38 @@
|
||||
"""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
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Mount
|
||||
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
# Create MCP server
|
||||
mcp = MCPServer("My App")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def hello() -> str:
|
||||
"""A simple hello tool"""
|
||||
return "Hello from MCP!"
|
||||
|
||||
|
||||
# Create a lifespan context manager to run the session manager
|
||||
@contextlib.asynccontextmanager
|
||||
async def lifespan(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,
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
"""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
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Host
|
||||
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
# Create MCP server
|
||||
mcp = MCPServer("MCP Host App")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def domain_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 def lifespan(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,
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
"""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
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Mount
|
||||
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
# Create multiple MCP servers
|
||||
api_mcp = MCPServer("API Server")
|
||||
chat_mcp = MCPServer("Chat Server")
|
||||
|
||||
|
||||
@api_mcp.tool()
|
||||
def api_status() -> str:
|
||||
"""Get API status"""
|
||||
return "API is running"
|
||||
|
||||
|
||||
@chat_mcp.tool()
|
||||
def send_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 def lifespan(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,
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Example showing path configuration when mounting MCPServer.
|
||||
|
||||
Run from the repository root:
|
||||
uvicorn examples.snippets.servers.streamable_http_path_config:app --reload
|
||||
"""
|
||||
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Mount
|
||||
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
# Create a simple MCPServer server
|
||||
mcp_at_root = MCPServer("My Server")
|
||||
|
||||
|
||||
@mcp_at_root.tool()
|
||||
def process_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="/"),
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
"""Run from the repository root:
|
||||
uvicorn examples.snippets.servers.streamable_starlette_mount:app --reload
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
|
||||
from starlette.applications import Starlette
|
||||
from starlette.routing import Mount
|
||||
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
# Create the Echo server
|
||||
echo_mcp = MCPServer(name="EchoServer")
|
||||
|
||||
|
||||
@echo_mcp.tool()
|
||||
def echo(message: str) -> str:
|
||||
"""A simple echo tool"""
|
||||
return f"Echo: {message}"
|
||||
|
||||
|
||||
# Create the Math server
|
||||
math_mcp = MCPServer(name="MathServer")
|
||||
|
||||
|
||||
@math_mcp.tool()
|
||||
def add_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 def lifespan(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)
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Example showing structured output with tools."""
|
||||
|
||||
from typing import TypedDict
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from mcp.server.mcpserver import MCPServer
|
||||
|
||||
mcp = MCPServer("Structured Output Example")
|
||||
|
||||
|
||||
# Using Pydantic models for rich structured data
|
||||
class WeatherData(BaseModel):
|
||||
"""Weather information structure."""
|
||||
|
||||
temperature: float = Field(description="Temperature in Celsius")
|
||||
humidity: float = Field(description="Humidity percentage")
|
||||
condition: str
|
||||
wind_speed: float
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_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
|
||||
class LocationInfo(TypedDict):
|
||||
latitude: float
|
||||
longitude: float
|
||||
name: str
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_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()
|
||||
def get_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
|
||||
class UserProfile:
|
||||
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()
|
||||
def get_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
|
||||
class UntypedConfig:
|
||||
def __init__(self, setting1, setting2): # type: ignore[reportMissingParameterType]
|
||||
self.setting1 = setting1
|
||||
self.setting2 = setting2
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_config() -> UntypedConfig:
|
||||
"""This returns unstructured output - no schema generated"""
|
||||
return UntypedConfig("value1", "value2")
|
||||
|
||||
|
||||
# Lists and other types are wrapped automatically
|
||||
@mcp.tool()
|
||||
def list_cities() -> list[str]:
|
||||
"""Get a list of cities"""
|
||||
return ["London", "Paris", "Tokyo"]
|
||||
# Returns: {"result": ["London", "Paris", "Tokyo"]}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def get_temperature(city: str) -> float:
|
||||
"""Get temperature as a simple float"""
|
||||
return 22.5
|
||||
# Returns: {"result": 22.5}
|
||||
@@ -0,0 +1,20 @@
|
||||
from mcp.server.mcpserver import Context, MCPServer
|
||||
|
||||
mcp = MCPServer(name="Progress Example")
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
async def long_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"
|
||||
Reference in New Issue
Block a user