Add pyright strict mode on the whole project (#1254)
This commit is contained in:
parent
ef4e167c1a
commit
c7671e470c
75 changed files with 652 additions and 674 deletions
3
.github/workflows/shared.yml
vendored
3
.github/workflows/shared.yml
vendored
|
|
@ -6,6 +6,9 @@ on:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
COLUMNS: 150
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pre-commit:
|
pre-commit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
|
||||||
32
README.md
32
README.md
|
|
@ -197,6 +197,7 @@ from contextlib import asynccontextmanager
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from mcp.server.fastmcp import Context, FastMCP
|
from mcp.server.fastmcp import Context, FastMCP
|
||||||
|
from mcp.server.session import ServerSession
|
||||||
|
|
||||||
|
|
||||||
# Mock database class for example
|
# Mock database class for example
|
||||||
|
|
@ -242,7 +243,7 @@ mcp = FastMCP("My App", lifespan=app_lifespan)
|
||||||
|
|
||||||
# Access type-safe lifespan context in tools
|
# Access type-safe lifespan context in tools
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
def query_db(ctx: Context) -> str:
|
def query_db(ctx: Context[ServerSession, AppContext]) -> str:
|
||||||
"""Tool that uses initialized resources."""
|
"""Tool that uses initialized resources."""
|
||||||
db = ctx.request_context.lifespan_context.db
|
db = ctx.request_context.lifespan_context.db
|
||||||
return db.query()
|
return db.query()
|
||||||
|
|
@ -314,12 +315,13 @@ Tools can optionally receive a Context object by including a parameter with the
|
||||||
<!-- snippet-source examples/snippets/servers/tool_progress.py -->
|
<!-- snippet-source examples/snippets/servers/tool_progress.py -->
|
||||||
```python
|
```python
|
||||||
from mcp.server.fastmcp import Context, FastMCP
|
from mcp.server.fastmcp import Context, FastMCP
|
||||||
|
from mcp.server.session import ServerSession
|
||||||
|
|
||||||
mcp = FastMCP(name="Progress Example")
|
mcp = FastMCP(name="Progress Example")
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:
|
async def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str:
|
||||||
"""Execute a task with progress updates."""
|
"""Execute a task with progress updates."""
|
||||||
await ctx.info(f"Starting: {task_name}")
|
await ctx.info(f"Starting: {task_name}")
|
||||||
|
|
||||||
|
|
@ -445,7 +447,7 @@ def get_user(user_id: str) -> UserProfile:
|
||||||
|
|
||||||
# Classes WITHOUT type hints cannot be used for structured output
|
# Classes WITHOUT type hints cannot be used for structured output
|
||||||
class UntypedConfig:
|
class UntypedConfig:
|
||||||
def __init__(self, setting1, setting2):
|
def __init__(self, setting1, setting2): # type: ignore[reportMissingParameterType]
|
||||||
self.setting1 = setting1
|
self.setting1 = setting1
|
||||||
self.setting2 = setting2
|
self.setting2 = setting2
|
||||||
|
|
||||||
|
|
@ -571,12 +573,13 @@ The Context object provides the following capabilities:
|
||||||
<!-- snippet-source examples/snippets/servers/tool_progress.py -->
|
<!-- snippet-source examples/snippets/servers/tool_progress.py -->
|
||||||
```python
|
```python
|
||||||
from mcp.server.fastmcp import Context, FastMCP
|
from mcp.server.fastmcp import Context, FastMCP
|
||||||
|
from mcp.server.session import ServerSession
|
||||||
|
|
||||||
mcp = FastMCP(name="Progress Example")
|
mcp = FastMCP(name="Progress Example")
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:
|
async def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str:
|
||||||
"""Execute a task with progress updates."""
|
"""Execute a task with progress updates."""
|
||||||
await ctx.info(f"Starting: {task_name}")
|
await ctx.info(f"Starting: {task_name}")
|
||||||
|
|
||||||
|
|
@ -694,6 +697,7 @@ Request additional information from users. This example shows an Elicitation dur
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from mcp.server.fastmcp import Context, FastMCP
|
from mcp.server.fastmcp import Context, FastMCP
|
||||||
|
from mcp.server.session import ServerSession
|
||||||
|
|
||||||
mcp = FastMCP(name="Elicitation Example")
|
mcp = FastMCP(name="Elicitation Example")
|
||||||
|
|
||||||
|
|
@ -709,12 +713,7 @@ class BookingPreferences(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def book_table(
|
async def book_table(date: str, time: str, party_size: int, ctx: Context[ServerSession, None]) -> str:
|
||||||
date: str,
|
|
||||||
time: str,
|
|
||||||
party_size: int,
|
|
||||||
ctx: Context,
|
|
||||||
) -> str:
|
|
||||||
"""Book a table with date availability check."""
|
"""Book a table with date availability check."""
|
||||||
# Check if date is available
|
# Check if date is available
|
||||||
if date == "2024-12-25":
|
if date == "2024-12-25":
|
||||||
|
|
@ -750,13 +749,14 @@ Tools can interact with LLMs through sampling (generating text):
|
||||||
<!-- snippet-source examples/snippets/servers/sampling.py -->
|
<!-- snippet-source examples/snippets/servers/sampling.py -->
|
||||||
```python
|
```python
|
||||||
from mcp.server.fastmcp import Context, FastMCP
|
from mcp.server.fastmcp import Context, FastMCP
|
||||||
|
from mcp.server.session import ServerSession
|
||||||
from mcp.types import SamplingMessage, TextContent
|
from mcp.types import SamplingMessage, TextContent
|
||||||
|
|
||||||
mcp = FastMCP(name="Sampling Example")
|
mcp = FastMCP(name="Sampling Example")
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def generate_poem(topic: str, ctx: Context) -> str:
|
async def generate_poem(topic: str, ctx: Context[ServerSession, None]) -> str:
|
||||||
"""Generate a poem using LLM sampling."""
|
"""Generate a poem using LLM sampling."""
|
||||||
prompt = f"Write a short poem about {topic}"
|
prompt = f"Write a short poem about {topic}"
|
||||||
|
|
||||||
|
|
@ -785,12 +785,13 @@ Tools can send logs and notifications through the context:
|
||||||
<!-- snippet-source examples/snippets/servers/notifications.py -->
|
<!-- snippet-source examples/snippets/servers/notifications.py -->
|
||||||
```python
|
```python
|
||||||
from mcp.server.fastmcp import Context, FastMCP
|
from mcp.server.fastmcp import Context, FastMCP
|
||||||
|
from mcp.server.session import ServerSession
|
||||||
|
|
||||||
mcp = FastMCP(name="Notifications Example")
|
mcp = FastMCP(name="Notifications Example")
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def process_data(data: str, ctx: Context) -> str:
|
async def process_data(data: str, ctx: Context[ServerSession, None]) -> str:
|
||||||
"""Process data with logging."""
|
"""Process data with logging."""
|
||||||
# Different log levels
|
# Different log levels
|
||||||
await ctx.debug(f"Debug: Processing '{data}'")
|
await ctx.debug(f"Debug: Processing '{data}'")
|
||||||
|
|
@ -1244,6 +1245,7 @@ Run from the repository root:
|
||||||
|
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import mcp.server.stdio
|
import mcp.server.stdio
|
||||||
import mcp.types as types
|
import mcp.types as types
|
||||||
|
|
@ -1272,7 +1274,7 @@ class Database:
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def server_lifespan(_server: Server) -> AsyncIterator[dict]:
|
async def server_lifespan(_server: Server) -> AsyncIterator[dict[str, Any]]:
|
||||||
"""Manage server startup and shutdown lifecycle."""
|
"""Manage server startup and shutdown lifecycle."""
|
||||||
# Initialize resources on startup
|
# Initialize resources on startup
|
||||||
db = await Database.connect()
|
db = await Database.connect()
|
||||||
|
|
@ -1304,7 +1306,7 @@ async def handle_list_tools() -> list[types.Tool]:
|
||||||
|
|
||||||
|
|
||||||
@server.call_tool()
|
@server.call_tool()
|
||||||
async def query_db(name: str, arguments: dict) -> list[types.TextContent]:
|
async def query_db(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:
|
||||||
"""Handle database query tool call."""
|
"""Handle database query tool call."""
|
||||||
if name != "query_db":
|
if name != "query_db":
|
||||||
raise ValueError(f"Unknown tool: {name}")
|
raise ValueError(f"Unknown tool: {name}")
|
||||||
|
|
@ -1558,7 +1560,7 @@ server_params = StdioServerParameters(
|
||||||
|
|
||||||
# Optional: create a sampling callback
|
# Optional: create a sampling callback
|
||||||
async def handle_sampling_message(
|
async def handle_sampling_message(
|
||||||
context: RequestContext, params: types.CreateMessageRequestParams
|
context: RequestContext[ClientSession, None], params: types.CreateMessageRequestParams
|
||||||
) -> types.CreateMessageResult:
|
) -> types.CreateMessageResult:
|
||||||
print(f"Sampling request: {params.messages}")
|
print(f"Sampling request: {params.messages}")
|
||||||
return types.CreateMessageResult(
|
return types.CreateMessageResult(
|
||||||
|
|
|
||||||
|
|
@ -188,9 +188,7 @@ class SimpleAuthClient:
|
||||||
# Create OAuth authentication handler using the new interface
|
# Create OAuth authentication handler using the new interface
|
||||||
oauth_auth = OAuthClientProvider(
|
oauth_auth = OAuthClientProvider(
|
||||||
server_url=self.server_url.replace("/mcp", ""),
|
server_url=self.server_url.replace("/mcp", ""),
|
||||||
client_metadata=OAuthClientMetadata.model_validate(
|
client_metadata=OAuthClientMetadata.model_validate(client_metadata_dict),
|
||||||
client_metadata_dict
|
|
||||||
),
|
|
||||||
storage=InMemoryTokenStorage(),
|
storage=InMemoryTokenStorage(),
|
||||||
redirect_handler=_default_redirect_handler,
|
redirect_handler=_default_redirect_handler,
|
||||||
callback_handler=callback_handler,
|
callback_handler=callback_handler,
|
||||||
|
|
@ -322,9 +320,7 @@ class SimpleAuthClient:
|
||||||
await self.call_tool(tool_name, arguments)
|
await self.call_tool(tool_name, arguments)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(
|
print("❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'")
|
||||||
"❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'"
|
|
||||||
)
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n\n👋 Goodbye!")
|
print("\n\n👋 Goodbye!")
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ select = ["E", "F", "I"]
|
||||||
ignore = []
|
ignore = []
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 88
|
line-length = 120
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,7 @@ from mcp import ClientSession, StdioServerParameters
|
||||||
from mcp.client.stdio import stdio_client
|
from mcp.client.stdio import stdio_client
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
||||||
level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Configuration:
|
class Configuration:
|
||||||
|
|
@ -75,29 +73,19 @@ class Server:
|
||||||
|
|
||||||
async def initialize(self) -> None:
|
async def initialize(self) -> None:
|
||||||
"""Initialize the server connection."""
|
"""Initialize the server connection."""
|
||||||
command = (
|
command = shutil.which("npx") if self.config["command"] == "npx" else self.config["command"]
|
||||||
shutil.which("npx")
|
|
||||||
if self.config["command"] == "npx"
|
|
||||||
else self.config["command"]
|
|
||||||
)
|
|
||||||
if command is None:
|
if command is None:
|
||||||
raise ValueError("The command must be a valid string and cannot be None.")
|
raise ValueError("The command must be a valid string and cannot be None.")
|
||||||
|
|
||||||
server_params = StdioServerParameters(
|
server_params = StdioServerParameters(
|
||||||
command=command,
|
command=command,
|
||||||
args=self.config["args"],
|
args=self.config["args"],
|
||||||
env={**os.environ, **self.config["env"]}
|
env={**os.environ, **self.config["env"]} if self.config.get("env") else None,
|
||||||
if self.config.get("env")
|
|
||||||
else None,
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
stdio_transport = await self.exit_stack.enter_async_context(
|
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
|
||||||
stdio_client(server_params)
|
|
||||||
)
|
|
||||||
read, write = stdio_transport
|
read, write = stdio_transport
|
||||||
session = await self.exit_stack.enter_async_context(
|
session = await self.exit_stack.enter_async_context(ClientSession(read, write))
|
||||||
ClientSession(read, write)
|
|
||||||
)
|
|
||||||
await session.initialize()
|
await session.initialize()
|
||||||
self.session = session
|
self.session = session
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -122,10 +110,7 @@ class Server:
|
||||||
|
|
||||||
for item in tools_response:
|
for item in tools_response:
|
||||||
if isinstance(item, tuple) and item[0] == "tools":
|
if isinstance(item, tuple) and item[0] == "tools":
|
||||||
tools.extend(
|
tools.extend(Tool(tool.name, tool.description, tool.inputSchema, tool.title) for tool in item[1])
|
||||||
Tool(tool.name, tool.description, tool.inputSchema, tool.title)
|
|
||||||
for tool in item[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
|
|
||||||
|
|
@ -164,9 +149,7 @@ class Server:
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
attempt += 1
|
attempt += 1
|
||||||
logging.warning(
|
logging.warning(f"Error executing tool: {e}. Attempt {attempt} of {retries}.")
|
||||||
f"Error executing tool: {e}. Attempt {attempt} of {retries}."
|
|
||||||
)
|
|
||||||
if attempt < retries:
|
if attempt < retries:
|
||||||
logging.info(f"Retrying in {delay} seconds...")
|
logging.info(f"Retrying in {delay} seconds...")
|
||||||
await asyncio.sleep(delay)
|
await asyncio.sleep(delay)
|
||||||
|
|
@ -209,9 +192,7 @@ class Tool:
|
||||||
args_desc = []
|
args_desc = []
|
||||||
if "properties" in self.input_schema:
|
if "properties" in self.input_schema:
|
||||||
for param_name, param_info in self.input_schema["properties"].items():
|
for param_name, param_info in self.input_schema["properties"].items():
|
||||||
arg_desc = (
|
arg_desc = f"- {param_name}: {param_info.get('description', 'No description')}"
|
||||||
f"- {param_name}: {param_info.get('description', 'No description')}"
|
|
||||||
)
|
|
||||||
if param_name in self.input_schema.get("required", []):
|
if param_name in self.input_schema.get("required", []):
|
||||||
arg_desc += " (required)"
|
arg_desc += " (required)"
|
||||||
args_desc.append(arg_desc)
|
args_desc.append(arg_desc)
|
||||||
|
|
@ -281,10 +262,7 @@ class LLMClient:
|
||||||
logging.error(f"Status code: {status_code}")
|
logging.error(f"Status code: {status_code}")
|
||||||
logging.error(f"Response details: {e.response.text}")
|
logging.error(f"Response details: {e.response.text}")
|
||||||
|
|
||||||
return (
|
return f"I encountered an error: {error_message}. Please try again or rephrase your request."
|
||||||
f"I encountered an error: {error_message}. "
|
|
||||||
"Please try again or rephrase your request."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ChatSession:
|
class ChatSession:
|
||||||
|
|
@ -323,17 +301,13 @@ class ChatSession:
|
||||||
tools = await server.list_tools()
|
tools = await server.list_tools()
|
||||||
if any(tool.name == tool_call["tool"] for tool in tools):
|
if any(tool.name == tool_call["tool"] for tool in tools):
|
||||||
try:
|
try:
|
||||||
result = await server.execute_tool(
|
result = await server.execute_tool(tool_call["tool"], tool_call["arguments"])
|
||||||
tool_call["tool"], tool_call["arguments"]
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(result, dict) and "progress" in result:
|
if isinstance(result, dict) and "progress" in result:
|
||||||
progress = result["progress"]
|
progress = result["progress"]
|
||||||
total = result["total"]
|
total = result["total"]
|
||||||
percentage = (progress / total) * 100
|
percentage = (progress / total) * 100
|
||||||
logging.info(
|
logging.info(f"Progress: {progress}/{total} ({percentage:.1f}%)")
|
||||||
f"Progress: {progress}/{total} ({percentage:.1f}%)"
|
|
||||||
)
|
|
||||||
|
|
||||||
return f"Tool execution result: {result}"
|
return f"Tool execution result: {result}"
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -408,9 +382,7 @@ class ChatSession:
|
||||||
|
|
||||||
final_response = self.llm_client.get_response(messages)
|
final_response = self.llm_client.get_response(messages)
|
||||||
logging.info("\nFinal response: %s", final_response)
|
logging.info("\nFinal response: %s", final_response)
|
||||||
messages.append(
|
messages.append({"role": "assistant", "content": final_response})
|
||||||
{"role": "assistant", "content": final_response}
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
messages.append({"role": "assistant", "content": llm_response})
|
messages.append({"role": "assistant", "content": llm_response})
|
||||||
|
|
||||||
|
|
@ -426,10 +398,7 @@ async def main() -> None:
|
||||||
"""Initialize and run the chat session."""
|
"""Initialize and run the chat session."""
|
||||||
config = Configuration()
|
config = Configuration()
|
||||||
server_config = config.load_config("servers_config.json")
|
server_config = config.load_config("servers_config.json")
|
||||||
servers = [
|
servers = [Server(name, srv_config) for name, srv_config in server_config["mcpServers"].items()]
|
||||||
Server(name, srv_config)
|
|
||||||
for name, srv_config in server_config["mcpServers"].items()
|
|
||||||
]
|
|
||||||
llm_client = LLMClient(config.llm_api_key)
|
llm_client = LLMClient(config.llm_api_key)
|
||||||
chat_session = ChatSession(servers, llm_client)
|
chat_session = ChatSession(servers, llm_client)
|
||||||
await chat_session.start()
|
await chat_session.start()
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ select = ["E", "F", "I"]
|
||||||
ignore = []
|
ignore = []
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 88
|
line-length = 120
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,8 @@ class ResourceServerSettings(BaseSettings):
|
||||||
# RFC 8707 resource validation
|
# RFC 8707 resource validation
|
||||||
oauth_strict: bool = False
|
oauth_strict: bool = False
|
||||||
|
|
||||||
def __init__(self, **data):
|
# TODO(Marcelo): Is this even needed? I didn't have time to check.
|
||||||
|
def __init__(self, **data: Any):
|
||||||
"""Initialize settings with values from environment variables."""
|
"""Initialize settings with values from environment variables."""
|
||||||
super().__init__(**data)
|
super().__init__(**data)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class SimpleAuthSettings(BaseSettings):
|
||||||
mcp_scope: str = "user"
|
mcp_scope: str = "user"
|
||||||
|
|
||||||
|
|
||||||
class SimpleOAuthProvider(OAuthAuthorizationServerProvider):
|
class SimpleOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]):
|
||||||
"""
|
"""
|
||||||
Simple OAuth provider for demo purposes.
|
Simple OAuth provider for demo purposes.
|
||||||
|
|
||||||
|
|
@ -264,7 +264,8 @@ class SimpleOAuthProvider(OAuthAuthorizationServerProvider):
|
||||||
"""Exchange refresh token - not supported in this example."""
|
"""Exchange refresh token - not supported in this example."""
|
||||||
raise NotImplementedError("Refresh tokens not supported")
|
raise NotImplementedError("Refresh tokens not supported")
|
||||||
|
|
||||||
async def revoke_token(self, token: str, token_type_hint: str | None = None) -> None:
|
# TODO(Marcelo): The type hint is wrong. We need to fix, and test to check if it works.
|
||||||
|
async def revoke_token(self, token: str, token_type_hint: str | None = None) -> None: # type: ignore
|
||||||
"""Revoke a token."""
|
"""Revoke a token."""
|
||||||
if token in self.tokens:
|
if token in self.tokens:
|
||||||
del self.tokens[token]
|
del self.tokens[token]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"""Example token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
|
"""Example token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662)."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from mcp.server.auth.provider import AccessToken, TokenVerifier
|
from mcp.server.auth.provider import AccessToken, TokenVerifier
|
||||||
from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
|
from mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url
|
||||||
|
|
@ -79,13 +80,13 @@ class IntrospectionTokenVerifier(TokenVerifier):
|
||||||
logger.warning(f"Token introspection failed: {e}")
|
logger.warning(f"Token introspection failed: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _validate_resource(self, token_data: dict) -> bool:
|
def _validate_resource(self, token_data: dict[str, Any]) -> bool:
|
||||||
"""Validate token was issued for this resource server."""
|
"""Validate token was issued for this resource server."""
|
||||||
if not self.server_url or not self.resource_url:
|
if not self.server_url or not self.resource_url:
|
||||||
return False # Fail if strict validation requested but URLs missing
|
return False # Fail if strict validation requested but URLs missing
|
||||||
|
|
||||||
# Check 'aud' claim first (standard JWT audience)
|
# Check 'aud' claim first (standard JWT audience)
|
||||||
aud = token_data.get("aud")
|
aud: list[str] | str | None = token_data.get("aud")
|
||||||
if isinstance(aud, list):
|
if isinstance(aud, list):
|
||||||
for audience in aud:
|
for audience in aud:
|
||||||
if self._is_valid_resource(audience):
|
if self._is_valid_resource(audience):
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,19 @@ import anyio
|
||||||
import click
|
import click
|
||||||
import mcp.types as types
|
import mcp.types as types
|
||||||
from mcp.server.lowlevel import Server
|
from mcp.server.lowlevel import Server
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
|
||||||
def create_messages(
|
def create_messages(context: str | None = None, topic: str | None = None) -> list[types.PromptMessage]:
|
||||||
context: str | None = None, topic: str | None = None
|
|
||||||
) -> list[types.PromptMessage]:
|
|
||||||
"""Create the messages for the prompt."""
|
"""Create the messages for the prompt."""
|
||||||
messages = []
|
messages: list[types.PromptMessage] = []
|
||||||
|
|
||||||
# Add context if provided
|
# Add context if provided
|
||||||
if context:
|
if context:
|
||||||
messages.append(
|
messages.append(
|
||||||
types.PromptMessage(
|
types.PromptMessage(
|
||||||
role="user",
|
role="user",
|
||||||
content=types.TextContent(
|
content=types.TextContent(type="text", text=f"Here is some relevant context: {context}"),
|
||||||
type="text", text=f"Here is some relevant context: {context}"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -28,11 +25,7 @@ def create_messages(
|
||||||
else:
|
else:
|
||||||
prompt += "whatever questions I may have."
|
prompt += "whatever questions I may have."
|
||||||
|
|
||||||
messages.append(
|
messages.append(types.PromptMessage(role="user", content=types.TextContent(type="text", text=prompt)))
|
||||||
types.PromptMessage(
|
|
||||||
role="user", content=types.TextContent(type="text", text=prompt)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
|
@ -54,8 +47,7 @@ def main(port: int, transport: str) -> int:
|
||||||
types.Prompt(
|
types.Prompt(
|
||||||
name="simple",
|
name="simple",
|
||||||
title="Simple Assistant Prompt",
|
title="Simple Assistant Prompt",
|
||||||
description="A simple prompt that can take optional context and topic "
|
description="A simple prompt that can take optional context and topic arguments",
|
||||||
"arguments",
|
|
||||||
arguments=[
|
arguments=[
|
||||||
types.PromptArgument(
|
types.PromptArgument(
|
||||||
name="context",
|
name="context",
|
||||||
|
|
@ -72,9 +64,7 @@ def main(port: int, transport: str) -> int:
|
||||||
]
|
]
|
||||||
|
|
||||||
@app.get_prompt()
|
@app.get_prompt()
|
||||||
async def get_prompt(
|
async def get_prompt(name: str, arguments: dict[str, str] | None = None) -> types.GetPromptResult:
|
||||||
name: str, arguments: dict[str, str] | None = None
|
|
||||||
) -> types.GetPromptResult:
|
|
||||||
if name != "simple":
|
if name != "simple":
|
||||||
raise ValueError(f"Unknown prompt: {name}")
|
raise ValueError(f"Unknown prompt: {name}")
|
||||||
|
|
||||||
|
|
@ -82,9 +72,7 @@ def main(port: int, transport: str) -> int:
|
||||||
arguments = {}
|
arguments = {}
|
||||||
|
|
||||||
return types.GetPromptResult(
|
return types.GetPromptResult(
|
||||||
messages=create_messages(
|
messages=create_messages(context=arguments.get("context"), topic=arguments.get("topic")),
|
||||||
context=arguments.get("context"), topic=arguments.get("topic")
|
|
||||||
),
|
|
||||||
description="A simple prompt with optional context and topic arguments",
|
description="A simple prompt with optional context and topic arguments",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -96,13 +84,9 @@ def main(port: int, transport: str) -> int:
|
||||||
|
|
||||||
sse = SseServerTransport("/messages/")
|
sse = SseServerTransport("/messages/")
|
||||||
|
|
||||||
async def handle_sse(request):
|
async def handle_sse(request: Request):
|
||||||
async with sse.connect_sse(
|
async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage]
|
||||||
request.scope, request.receive, request._send
|
await app.run(streams[0], streams[1], app.create_initialization_options())
|
||||||
) as streams:
|
|
||||||
await app.run(
|
|
||||||
streams[0], streams[1], app.create_initialization_options()
|
|
||||||
)
|
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
starlette_app = Starlette(
|
starlette_app = Starlette(
|
||||||
|
|
@ -121,9 +105,7 @@ def main(port: int, transport: str) -> int:
|
||||||
|
|
||||||
async def arun():
|
async def arun():
|
||||||
async with stdio_server() as streams:
|
async with stdio_server() as streams:
|
||||||
await app.run(
|
await app.run(streams[0], streams[1], app.create_initialization_options())
|
||||||
streams[0], streams[1], app.create_initialization_options()
|
|
||||||
)
|
|
||||||
|
|
||||||
anyio.run(arun)
|
anyio.run(arun)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ select = ["E", "F", "I"]
|
||||||
ignore = []
|
ignore = []
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 88
|
line-length = 120
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import click
|
||||||
import mcp.types as types
|
import mcp.types as types
|
||||||
from mcp.server.lowlevel import Server
|
from mcp.server.lowlevel import Server
|
||||||
from pydantic import AnyUrl, FileUrl
|
from pydantic import AnyUrl, FileUrl
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
SAMPLE_RESOURCES = {
|
SAMPLE_RESOURCES = {
|
||||||
"greeting": {
|
"greeting": {
|
||||||
|
|
@ -63,13 +64,9 @@ def main(port: int, transport: str) -> int:
|
||||||
|
|
||||||
sse = SseServerTransport("/messages/")
|
sse = SseServerTransport("/messages/")
|
||||||
|
|
||||||
async def handle_sse(request):
|
async def handle_sse(request: Request):
|
||||||
async with sse.connect_sse(
|
async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage]
|
||||||
request.scope, request.receive, request._send
|
await app.run(streams[0], streams[1], app.create_initialization_options())
|
||||||
) as streams:
|
|
||||||
await app.run(
|
|
||||||
streams[0], streams[1], app.create_initialization_options()
|
|
||||||
)
|
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
starlette_app = Starlette(
|
starlette_app = Starlette(
|
||||||
|
|
@ -88,9 +85,7 @@ def main(port: int, transport: str) -> int:
|
||||||
|
|
||||||
async def arun():
|
async def arun():
|
||||||
async with stdio_server() as streams:
|
async with stdio_server() as streams:
|
||||||
await app.run(
|
await app.run(streams[0], streams[1], app.create_initialization_options())
|
||||||
streams[0], streams[1], app.create_initialization_options()
|
|
||||||
)
|
|
||||||
|
|
||||||
anyio.run(arun)
|
anyio.run(arun)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ select = ["E", "F", "I"]
|
||||||
ignore = []
|
ignore = []
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 88
|
line-length = 120
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import contextlib
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
import click
|
import click
|
||||||
|
|
@ -41,7 +42,7 @@ def main(
|
||||||
app = Server("mcp-streamable-http-stateless-demo")
|
app = Server("mcp-streamable-http-stateless-demo")
|
||||||
|
|
||||||
@app.call_tool()
|
@app.call_tool()
|
||||||
async def call_tool(name: str, arguments: dict) -> list[types.ContentBlock]:
|
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]:
|
||||||
ctx = app.request_context
|
ctx = app.request_context
|
||||||
interval = arguments.get("interval", 1.0)
|
interval = arguments.get("interval", 1.0)
|
||||||
count = arguments.get("count", 5)
|
count = arguments.get("count", 5)
|
||||||
|
|
@ -61,10 +62,7 @@ def main(
|
||||||
return [
|
return [
|
||||||
types.TextContent(
|
types.TextContent(
|
||||||
type="text",
|
type="text",
|
||||||
text=(
|
text=(f"Sent {count} notifications with {interval}s interval for caller: {caller}"),
|
||||||
f"Sent {count} notifications with {interval}s interval"
|
|
||||||
f" for caller: {caller}"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -73,10 +71,7 @@ def main(
|
||||||
return [
|
return [
|
||||||
types.Tool(
|
types.Tool(
|
||||||
name="start-notification-stream",
|
name="start-notification-stream",
|
||||||
description=(
|
description=("Sends a stream of notifications with configurable count and interval"),
|
||||||
"Sends a stream of notifications with configurable count"
|
|
||||||
" and interval"
|
|
||||||
),
|
|
||||||
inputSchema={
|
inputSchema={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["interval", "count", "caller"],
|
"required": ["interval", "count", "caller"],
|
||||||
|
|
@ -91,9 +86,7 @@ def main(
|
||||||
},
|
},
|
||||||
"caller": {
|
"caller": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": (
|
"description": ("Identifier of the caller to include in notifications"),
|
||||||
"Identifier of the caller to include in notifications"
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -108,9 +101,7 @@ def main(
|
||||||
stateless=True,
|
stateless=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def handle_streamable_http(
|
async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
|
||||||
scope: Scope, receive: Receive, send: Send
|
|
||||||
) -> None:
|
|
||||||
await session_manager.handle_request(scope, receive, send)
|
await session_manager.handle_request(scope, receive, send)
|
||||||
|
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ select = ["E", "F", "I"]
|
||||||
ignore = []
|
ignore = []
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 88
|
line-length = 120
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,7 @@ from collections import deque
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from mcp.server.streamable_http import (
|
from mcp.server.streamable_http import EventCallback, EventId, EventMessage, EventStore, StreamId
|
||||||
EventCallback,
|
|
||||||
EventId,
|
|
||||||
EventMessage,
|
|
||||||
EventStore,
|
|
||||||
StreamId,
|
|
||||||
)
|
|
||||||
from mcp.types import JSONRPCMessage
|
from mcp.types import JSONRPCMessage
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
@ -54,14 +48,10 @@ class InMemoryEventStore(EventStore):
|
||||||
# event_id -> EventEntry for quick lookup
|
# event_id -> EventEntry for quick lookup
|
||||||
self.event_index: dict[EventId, EventEntry] = {}
|
self.event_index: dict[EventId, EventEntry] = {}
|
||||||
|
|
||||||
async def store_event(
|
async def store_event(self, stream_id: StreamId, message: JSONRPCMessage) -> EventId:
|
||||||
self, stream_id: StreamId, message: JSONRPCMessage
|
|
||||||
) -> EventId:
|
|
||||||
"""Stores an event with a generated event ID."""
|
"""Stores an event with a generated event ID."""
|
||||||
event_id = str(uuid4())
|
event_id = str(uuid4())
|
||||||
event_entry = EventEntry(
|
event_entry = EventEntry(event_id=event_id, stream_id=stream_id, message=message)
|
||||||
event_id=event_id, stream_id=stream_id, message=message
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get or create deque for this stream
|
# Get or create deque for this stream
|
||||||
if stream_id not in self.streams:
|
if stream_id not in self.streams:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import contextlib
|
import contextlib
|
||||||
import logging
|
import logging
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
import click
|
import click
|
||||||
|
|
@ -45,7 +46,7 @@ def main(
|
||||||
app = Server("mcp-streamable-http-demo")
|
app = Server("mcp-streamable-http-demo")
|
||||||
|
|
||||||
@app.call_tool()
|
@app.call_tool()
|
||||||
async def call_tool(name: str, arguments: dict) -> list[types.ContentBlock]:
|
async def call_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]:
|
||||||
ctx = app.request_context
|
ctx = app.request_context
|
||||||
interval = arguments.get("interval", 1.0)
|
interval = arguments.get("interval", 1.0)
|
||||||
count = arguments.get("count", 5)
|
count = arguments.get("count", 5)
|
||||||
|
|
@ -54,10 +55,7 @@ def main(
|
||||||
# Send the specified number of notifications with the given interval
|
# Send the specified number of notifications with the given interval
|
||||||
for i in range(count):
|
for i in range(count):
|
||||||
# Include more detailed message for resumability demonstration
|
# Include more detailed message for resumability demonstration
|
||||||
notification_msg = (
|
notification_msg = f"[{i + 1}/{count}] Event from '{caller}' - Use Last-Event-ID to resume if disconnected"
|
||||||
f"[{i + 1}/{count}] Event from '{caller}' - "
|
|
||||||
f"Use Last-Event-ID to resume if disconnected"
|
|
||||||
)
|
|
||||||
await ctx.session.send_log_message(
|
await ctx.session.send_log_message(
|
||||||
level="info",
|
level="info",
|
||||||
data=notification_msg,
|
data=notification_msg,
|
||||||
|
|
@ -79,10 +77,7 @@ def main(
|
||||||
return [
|
return [
|
||||||
types.TextContent(
|
types.TextContent(
|
||||||
type="text",
|
type="text",
|
||||||
text=(
|
text=(f"Sent {count} notifications with {interval}s interval for caller: {caller}"),
|
||||||
f"Sent {count} notifications with {interval}s interval"
|
|
||||||
f" for caller: {caller}"
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -91,10 +86,7 @@ def main(
|
||||||
return [
|
return [
|
||||||
types.Tool(
|
types.Tool(
|
||||||
name="start-notification-stream",
|
name="start-notification-stream",
|
||||||
description=(
|
description=("Sends a stream of notifications with configurable count and interval"),
|
||||||
"Sends a stream of notifications with configurable count"
|
|
||||||
" and interval"
|
|
||||||
),
|
|
||||||
inputSchema={
|
inputSchema={
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["interval", "count", "caller"],
|
"required": ["interval", "count", "caller"],
|
||||||
|
|
@ -109,9 +101,7 @@ def main(
|
||||||
},
|
},
|
||||||
"caller": {
|
"caller": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": (
|
"description": ("Identifier of the caller to include in notifications"),
|
||||||
"Identifier of the caller to include in notifications"
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -136,9 +126,7 @@ def main(
|
||||||
)
|
)
|
||||||
|
|
||||||
# ASGI handler for streamable HTTP connections
|
# ASGI handler for streamable HTTP connections
|
||||||
async def handle_streamable_http(
|
async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:
|
||||||
scope: Scope, receive: Receive, send: Send
|
|
||||||
) -> None:
|
|
||||||
await session_manager.handle_request(scope, receive, send)
|
await session_manager.handle_request(scope, receive, send)
|
||||||
|
|
||||||
@contextlib.asynccontextmanager
|
@contextlib.asynccontextmanager
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ select = ["E", "F", "I"]
|
||||||
ignore = []
|
ignore = []
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 88
|
line-length = 120
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import anyio
|
import anyio
|
||||||
import click
|
import click
|
||||||
import mcp.types as types
|
import mcp.types as types
|
||||||
from mcp.server.lowlevel import Server
|
from mcp.server.lowlevel import Server
|
||||||
from mcp.shared._httpx_utils import create_mcp_http_client
|
from mcp.shared._httpx_utils import create_mcp_http_client
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
|
||||||
async def fetch_website(
|
async def fetch_website(
|
||||||
url: str,
|
url: str,
|
||||||
) -> list[types.ContentBlock]:
|
) -> list[types.ContentBlock]:
|
||||||
headers = {
|
headers = {"User-Agent": "MCP Test Server (github.com/modelcontextprotocol/python-sdk)"}
|
||||||
"User-Agent": "MCP Test Server (github.com/modelcontextprotocol/python-sdk)"
|
|
||||||
}
|
|
||||||
async with create_mcp_http_client(headers=headers) as client:
|
async with create_mcp_http_client(headers=headers) as client:
|
||||||
response = await client.get(url)
|
response = await client.get(url)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
@ -29,7 +30,7 @@ def main(port: int, transport: str) -> int:
|
||||||
app = Server("mcp-website-fetcher")
|
app = Server("mcp-website-fetcher")
|
||||||
|
|
||||||
@app.call_tool()
|
@app.call_tool()
|
||||||
async def fetch_tool(name: str, arguments: dict) -> list[types.ContentBlock]:
|
async def fetch_tool(name: str, arguments: dict[str, Any]) -> list[types.ContentBlock]:
|
||||||
if name != "fetch":
|
if name != "fetch":
|
||||||
raise ValueError(f"Unknown tool: {name}")
|
raise ValueError(f"Unknown tool: {name}")
|
||||||
if "url" not in arguments:
|
if "url" not in arguments:
|
||||||
|
|
@ -64,13 +65,9 @@ def main(port: int, transport: str) -> int:
|
||||||
|
|
||||||
sse = SseServerTransport("/messages/")
|
sse = SseServerTransport("/messages/")
|
||||||
|
|
||||||
async def handle_sse(request):
|
async def handle_sse(request: Request):
|
||||||
async with sse.connect_sse(
|
async with sse.connect_sse(request.scope, request.receive, request._send) as streams: # type: ignore[reportPrivateUsage]
|
||||||
request.scope, request.receive, request._send
|
await app.run(streams[0], streams[1], app.create_initialization_options())
|
||||||
) as streams:
|
|
||||||
await app.run(
|
|
||||||
streams[0], streams[1], app.create_initialization_options()
|
|
||||||
)
|
|
||||||
return Response()
|
return Response()
|
||||||
|
|
||||||
starlette_app = Starlette(
|
starlette_app = Starlette(
|
||||||
|
|
@ -89,9 +86,7 @@ def main(port: int, transport: str) -> int:
|
||||||
|
|
||||||
async def arun():
|
async def arun():
|
||||||
async with stdio_server() as streams:
|
async with stdio_server() as streams:
|
||||||
await app.run(
|
await app.run(streams[0], streams[1], app.create_initialization_options())
|
||||||
streams[0], streams[1], app.create_initialization_options()
|
|
||||||
)
|
|
||||||
|
|
||||||
anyio.run(arun)
|
anyio.run(arun)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ select = ["E", "F", "I"]
|
||||||
ignore = []
|
ignore = []
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 88
|
line-length = 120
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue