Use 120 characters instead of 88 (#856)

This commit is contained in:
Marcelo Trylesinski 2025-06-11 02:45:50 -07:00 • committed by GitHub
commit 543961968c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
90 changed files with 687 additions and 2142 deletions

View file

@ -47,18 +47,14 @@ mcp = FastMCP(
DB_DSN = "postgresql://postgres:postgres@localhost:54320/memory_db" DB_DSN = "postgresql://postgres:postgres@localhost:54320/memory_db"
# reset memory with rm ~/.fastmcp/{USER}/memory/* # reset memory with rm ~/.fastmcp/{USER}/memory/*
PROFILE_DIR = ( PROFILE_DIR = (Path.home() / ".fastmcp" / os.environ.get("USER", "anon") / "memory").resolve()
Path.home() / ".fastmcp" / os.environ.get("USER", "anon") / "memory"
).resolve()
PROFILE_DIR.mkdir(parents=True, exist_ok=True) PROFILE_DIR.mkdir(parents=True, exist_ok=True)
def cosine_similarity(a: list[float], b: list[float]) -> float: def cosine_similarity(a: list[float], b: list[float]) -> float:
a_array = np.array(a, dtype=np.float64) a_array = np.array(a, dtype=np.float64)
b_array = np.array(b, dtype=np.float64) b_array = np.array(b, dtype=np.float64)
return np.dot(a_array, b_array) / ( return np.dot(a_array, b_array) / (np.linalg.norm(a_array) * np.linalg.norm(b_array))
np.linalg.norm(a_array) * np.linalg.norm(b_array)
)
async def do_ai[T]( async def do_ai[T](
@ -97,9 +93,7 @@ class MemoryNode(BaseModel):
summary: str = "" summary: str = ""
importance: float = 1.0 importance: float = 1.0
access_count: int = 0 access_count: int = 0
timestamp: float = Field( timestamp: float = Field(default_factory=lambda: datetime.now(timezone.utc).timestamp())
default_factory=lambda: datetime.now(timezone.utc).timestamp()
)
embedding: list[float] embedding: list[float]
@classmethod @classmethod
@ -152,9 +146,7 @@ class MemoryNode(BaseModel):
self.importance += other.importance self.importance += other.importance
self.access_count += other.access_count self.access_count += other.access_count
self.embedding = [(a + b) / 2 for a, b in zip(self.embedding, other.embedding)] self.embedding = [(a + b) / 2 for a, b in zip(self.embedding, other.embedding)]
self.summary = await do_ai( self.summary = await do_ai(self.content, "Summarize the following text concisely.", str, deps)
self.content, "Summarize the following text concisely.", str, deps
)
await self.save(deps) await self.save(deps)
# Delete the merged node from the database # Delete the merged node from the database
if other.id is not None: if other.id is not None:
@ -221,9 +213,7 @@ async def find_similar_memories(embedding: list[float], deps: Deps) -> list[Memo
async def update_importance(user_embedding: list[float], deps: Deps): async def update_importance(user_embedding: list[float], deps: Deps):
async with deps.pool.acquire() as conn: async with deps.pool.acquire() as conn:
rows = await conn.fetch( rows = await conn.fetch("SELECT id, importance, access_count, embedding FROM memories")
"SELECT id, importance, access_count, embedding FROM memories"
)
for row in rows: for row in rows:
memory_embedding = row["embedding"] memory_embedding = row["embedding"]
similarity = cosine_similarity(user_embedding, memory_embedding) similarity = cosine_similarity(user_embedding, memory_embedding)
@ -273,9 +263,7 @@ async def display_memory_tree(deps: Deps) -> str:
) )
result = "" result = ""
for row in rows: for row in rows:
effective_importance = row["importance"] * ( effective_importance = row["importance"] * (1 + math.log(row["access_count"] + 1))
1 + math.log(row["access_count"] + 1)
)
summary = row["summary"] or row["content"] summary = row["summary"] or row["content"]
result += f"- {summary} (Importance: {effective_importance:.2f})\n" result += f"- {summary} (Importance: {effective_importance:.2f})\n"
return result return result
@ -283,15 +271,11 @@ async def display_memory_tree(deps: Deps) -> str:
@mcp.tool() @mcp.tool()
async def remember( async def remember(
contents: list[str] = Field( contents: list[str] = Field(description="List of observations or memories to store"),
description="List of observations or memories to store"
),
): ):
deps = Deps(openai=AsyncOpenAI(), pool=await get_db_pool()) deps = Deps(openai=AsyncOpenAI(), pool=await get_db_pool())
try: try:
return "\n".join( return "\n".join(await asyncio.gather(*[add_memory(content, deps) for content in contents]))
await asyncio.gather(*[add_memory(content, deps) for content in contents])
)
finally: finally:
await deps.pool.close() await deps.pool.close()
@ -305,9 +289,7 @@ async def read_profile() -> str:
async def initialize_database(): async def initialize_database():
pool = await asyncpg.create_pool( pool = await asyncpg.create_pool("postgresql://postgres:postgres@localhost:54320/postgres")
"postgresql://postgres:postgres@localhost:54320/postgres"
)
try: try:
async with pool.acquire() as conn: async with pool.acquire() as conn:
await conn.execute(""" await conn.execute("""

View file

@ -28,15 +28,11 @@ from mcp.server.fastmcp import FastMCP
class SurgeSettings(BaseSettings): class SurgeSettings(BaseSettings):
model_config: SettingsConfigDict = SettingsConfigDict( model_config: SettingsConfigDict = SettingsConfigDict(env_prefix="SURGE_", env_file=".env")
env_prefix="SURGE_", env_file=".env"
)
api_key: str api_key: str
account_id: str account_id: str
my_phone_number: Annotated[ my_phone_number: Annotated[str, BeforeValidator(lambda v: "+" + v if not v.startswith("+") else v)]
str, BeforeValidator(lambda v: "+" + v if not v.startswith("+") else v)
]
my_first_name: str my_first_name: str
my_last_name: str my_last_name: str

View file

@ -8,10 +8,7 @@ from mcp.server.fastmcp import FastMCP
mcp = FastMCP() mcp = FastMCP()
@mcp.tool( @mcp.tool(description="🌟 A tool that uses various Unicode characters in its description: " "á é í ó ú ñ 漢字 🎉")
description="🌟 A tool that uses various Unicode characters in its description: "
"á é í ó ú ñ 漢字 🎉"
)
def hello_unicode(name: str = "世界", greeting: str = "¡Hola") -> str: def hello_unicode(name: str = "世界", greeting: str = "¡Hola") -> str:
""" """
A simple tool that demonstrates Unicode handling in: A simple tool that demonstrates Unicode handling in:

View file

@ -82,9 +82,7 @@ class SimpleGitHubOAuthProvider(OAuthAuthorizationServerProvider):
"""Register a new OAuth client.""" """Register a new OAuth client."""
self.clients[client_info.client_id] = client_info self.clients[client_info.client_id] = client_info
async def authorize( async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:
self, client: OAuthClientInformationFull, params: AuthorizationParams
) -> str:
"""Generate an authorization URL for GitHub OAuth flow.""" """Generate an authorization URL for GitHub OAuth flow."""
state = params.state or secrets.token_hex(16) state = params.state or secrets.token_hex(16)
@ -92,9 +90,7 @@ class SimpleGitHubOAuthProvider(OAuthAuthorizationServerProvider):
self.state_mapping[state] = { self.state_mapping[state] = {
"redirect_uri": str(params.redirect_uri), "redirect_uri": str(params.redirect_uri),
"code_challenge": params.code_challenge, "code_challenge": params.code_challenge,
"redirect_uri_provided_explicitly": str( "redirect_uri_provided_explicitly": str(params.redirect_uri_provided_explicitly),
params.redirect_uri_provided_explicitly
),
"client_id": client.client_id, "client_id": client.client_id,
} }
@ -117,9 +113,7 @@ class SimpleGitHubOAuthProvider(OAuthAuthorizationServerProvider):
redirect_uri = state_data["redirect_uri"] redirect_uri = state_data["redirect_uri"]
code_challenge = state_data["code_challenge"] code_challenge = state_data["code_challenge"]
redirect_uri_provided_explicitly = ( redirect_uri_provided_explicitly = state_data["redirect_uri_provided_explicitly"] == "True"
state_data["redirect_uri_provided_explicitly"] == "True"
)
client_id = state_data["client_id"] client_id = state_data["client_id"]
# Exchange code for token with GitHub # Exchange code for token with GitHub
@ -200,8 +194,7 @@ class SimpleGitHubOAuthProvider(OAuthAuthorizationServerProvider):
for token, data in self.tokens.items() for token, data in self.tokens.items()
# see https://github.blog/engineering/platform-security/behind-githubs-new-authentication-token-formats/ # see https://github.blog/engineering/platform-security/behind-githubs-new-authentication-token-formats/
# which you get depends on your GH app setup. # which you get depends on your GH app setup.
if (token.startswith("ghu_") or token.startswith("gho_")) if (token.startswith("ghu_") or token.startswith("gho_")) and data.client_id == client.client_id
and data.client_id == client.client_id
), ),
None, None,
) )
@ -232,9 +225,7 @@ class SimpleGitHubOAuthProvider(OAuthAuthorizationServerProvider):
return access_token return access_token
async def load_refresh_token( async def load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None:
self, client: OAuthClientInformationFull, refresh_token: str
) -> RefreshToken | None:
"""Load a refresh token - not supported.""" """Load a refresh token - not supported."""
return None return None
@ -247,9 +238,7 @@ class SimpleGitHubOAuthProvider(OAuthAuthorizationServerProvider):
"""Exchange refresh token""" """Exchange refresh token"""
raise NotImplementedError("Not supported") raise NotImplementedError("Not supported")
async def revoke_token( async def revoke_token(self, token: str, token_type_hint: str | None = None) -> None:
self, token: str, token_type_hint: str | None = None
) -> None:
"""Revoke a token.""" """Revoke a token."""
if token in self.tokens: if token in self.tokens:
del self.tokens[token] del self.tokens[token]
@ -335,9 +324,7 @@ def create_simple_mcp_server(settings: ServerSettings) -> FastMCP:
) )
if response.status_code != 200: if response.status_code != 200:
raise ValueError( raise ValueError(f"GitHub API error: {response.status_code} - {response.text}")
f"GitHub API error: {response.status_code} - {response.text}"
)
return response.json() return response.json()
@ -361,9 +348,7 @@ def main(port: int, host: str, transport: Literal["sse", "streamable-http"]) ->
# No hardcoded credentials - all from environment variables # No hardcoded credentials - all from environment variables
settings = ServerSettings(host=host, port=port) settings = ServerSettings(host=host, port=port)
except ValueError as e: except ValueError as e:
logger.error( logger.error("Failed to load settings. Make sure environment variables are set:")
"Failed to load settings. Make sure environment variables are set:"
)
logger.error(" MCP_GITHUB_GITHUB_CLIENT_ID=<your-client-id>") logger.error(" MCP_GITHUB_GITHUB_CLIENT_ID=<your-client-id>")
logger.error(" MCP_GITHUB_GITHUB_CLIENT_SECRET=<your-client-secret>") logger.error(" MCP_GITHUB_GITHUB_CLIENT_SECRET=<your-client-secret>")
logger.error(f"Error: {e}") logger.error(f"Error: {e}")

View file

@ -96,7 +96,7 @@ select = ["C4", "E", "F", "I", "PERF", "UP"]
ignore = ["PERF203"] ignore = ["PERF203"]
[tool.ruff] [tool.ruff]
line-length = 88 line-length = 120
target-version = "py310" target-version = "py310"
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]

View file

@ -21,9 +21,7 @@ def get_claude_config_path() -> Path | None:
elif sys.platform == "darwin": elif sys.platform == "darwin":
path = Path(Path.home(), "Library", "Application Support", "Claude") path = Path(Path.home(), "Library", "Application Support", "Claude")
elif sys.platform.startswith("linux"): elif sys.platform.startswith("linux"):
path = Path( path = Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"), "Claude")
os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config"), "Claude"
)
else: else:
return None return None
@ -37,8 +35,7 @@ def get_uv_path() -> str:
uv_path = shutil.which("uv") uv_path = shutil.which("uv")
if not uv_path: if not uv_path:
logger.error( logger.error(
"uv executable not found in PATH, falling back to 'uv'. " "uv executable not found in PATH, falling back to 'uv'. " "Please ensure uv is installed and in your PATH"
"Please ensure uv is installed and in your PATH"
) )
return "uv" # Fall back to just "uv" if not found return "uv" # Fall back to just "uv" if not found
return uv_path return uv_path
@ -94,10 +91,7 @@ def update_claude_config(
config["mcpServers"] = {} config["mcpServers"] = {}
# Always preserve existing env vars and merge with new ones # Always preserve existing env vars and merge with new ones
if ( if server_name in config["mcpServers"] and "env" in config["mcpServers"][server_name]:
server_name in config["mcpServers"]
and "env" in config["mcpServers"][server_name]
):
existing_env = config["mcpServers"][server_name]["env"] existing_env = config["mcpServers"][server_name]["env"]
if env_vars: if env_vars:
# New vars take precedence over existing ones # New vars take precedence over existing ones

View file

@ -45,9 +45,7 @@ def _get_npx_command():
# Try both npx.cmd and npx.exe on Windows # Try both npx.cmd and npx.exe on Windows
for cmd in ["npx.cmd", "npx.exe", "npx"]: for cmd in ["npx.cmd", "npx.exe", "npx"]:
try: try:
subprocess.run( subprocess.run([cmd, "--version"], check=True, capture_output=True, shell=True)
[cmd, "--version"], check=True, capture_output=True, shell=True
)
return cmd return cmd
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
continue continue
@ -58,9 +56,7 @@ def _get_npx_command():
def _parse_env_var(env_var: str) -> tuple[str, str]: def _parse_env_var(env_var: str) -> tuple[str, str]:
"""Parse environment variable string in format KEY=VALUE.""" """Parse environment variable string in format KEY=VALUE."""
if "=" not in env_var: if "=" not in env_var:
logger.error( logger.error(f"Invalid environment variable format: {env_var}. Must be KEY=VALUE")
f"Invalid environment variable format: {env_var}. Must be KEY=VALUE"
)
sys.exit(1) sys.exit(1)
key, value = env_var.split("=", 1) key, value = env_var.split("=", 1)
return key.strip(), value.strip() return key.strip(), value.strip()
@ -154,14 +150,10 @@ def _import_server(file: Path, server_object: str | None = None):
True if it's supported. True if it's supported.
""" """
if not isinstance(server_object, FastMCP): if not isinstance(server_object, FastMCP):
logger.error( logger.error(f"The server object {object_name} is of type " f"{type(server_object)} (expecting {FastMCP}).")
f"The server object {object_name} is of type "
f"{type(server_object)} (expecting {FastMCP})."
)
if isinstance(server_object, LowLevelServer): if isinstance(server_object, LowLevelServer):
logger.warning( logger.warning(
"Note that only FastMCP server is supported. Low level " "Note that only FastMCP server is supported. Low level " "Server class is not yet supported."
"Server class is not yet supported."
) )
return False return False
return True return True
@ -172,10 +164,7 @@ def _import_server(file: Path, server_object: str | None = None):
for name in ["mcp", "server", "app"]: for name in ["mcp", "server", "app"]:
if hasattr(module, name): if hasattr(module, name):
if not _check_server_object(getattr(module, name), f"{file}:{name}"): if not _check_server_object(getattr(module, name), f"{file}:{name}"):
logger.error( logger.error(f"Ignoring object '{file}:{name}' as it's not a valid " "server object")
f"Ignoring object '{file}:{name}' as it's not a valid "
"server object"
)
continue continue
return getattr(module, name) return getattr(module, name)
@ -280,8 +269,7 @@ def dev(
npx_cmd = _get_npx_command() npx_cmd = _get_npx_command()
if not npx_cmd: if not npx_cmd:
logger.error( logger.error(
"npx not found. Please ensure Node.js and npm are properly installed " "npx not found. Please ensure Node.js and npm are properly installed " "and added to your system PATH."
"and added to your system PATH."
) )
sys.exit(1) sys.exit(1)
@ -383,8 +371,7 @@ def install(
typer.Option( typer.Option(
"--name", "--name",
"-n", "-n",
help="Custom name for the server (defaults to server's name attribute or" help="Custom name for the server (defaults to server's name attribute or" " file name)",
" file name)",
), ),
] = None, ] = None,
with_editable: Annotated[ with_editable: Annotated[
@ -458,8 +445,7 @@ def install(
name = server.name name = server.name
except (ImportError, ModuleNotFoundError) as e: except (ImportError, ModuleNotFoundError) as e:
logger.debug( logger.debug(
"Could not import server (likely missing dependencies), using file" "Could not import server (likely missing dependencies), using file" " name",
" name",
extra={"error": str(e)}, extra={"error": str(e)},
) )
name = file.stem name = file.stem
@ -477,11 +463,7 @@ def install(
if env_file: if env_file:
if dotenv: if dotenv:
try: try:
env_dict |= { env_dict |= {k: v for k, v in dotenv.dotenv_values(env_file).items() if v is not None}
k: v
for k, v in dotenv.dotenv_values(env_file).items()
if v is not None
}
except Exception as e: except Exception as e:
logger.error(f"Failed to load .env file: {e}") logger.error(f"Failed to load .env file: {e}")
sys.exit(1) sys.exit(1)

View file

@ -24,9 +24,7 @@ logger = logging.getLogger("client")
async def message_handler( async def message_handler(
message: RequestResponder[types.ServerRequest, types.ClientResult] message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
| types.ServerNotification
| Exception,
) -> None: ) -> None:
if isinstance(message, Exception): if isinstance(message, Exception):
logger.error("Error: %s", message) logger.error("Error: %s", message)
@ -60,9 +58,7 @@ async def main(command_or_url: str, args: list[str], env: list[tuple[str, str]])
await run_session(*streams) await run_session(*streams)
else: else:
# Use stdio client for commands # Use stdio client for commands
server_parameters = StdioServerParameters( server_parameters = StdioServerParameters(command=command_or_url, args=args, env=env_dict)
command=command_or_url, args=args, env=env_dict
)
async with stdio_client(server_parameters) as streams: async with stdio_client(server_parameters) as streams:
await run_session(*streams) await run_session(*streams)

View file

@ -17,12 +17,7 @@ from urllib.parse import urlencode, urljoin
import anyio import anyio
import httpx import httpx
from mcp.shared.auth import ( from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthMetadata, OAuthToken
OAuthClientInformationFull,
OAuthClientMetadata,
OAuthMetadata,
OAuthToken,
)
from mcp.types import LATEST_PROTOCOL_VERSION from mcp.types import LATEST_PROTOCOL_VERSION
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -100,10 +95,7 @@ class OAuthClientProvider(httpx.Auth):
def _generate_code_verifier(self) -> str: def _generate_code_verifier(self) -> str:
"""Generate a cryptographically random code verifier for PKCE.""" """Generate a cryptographically random code verifier for PKCE."""
return "".join( return "".join(secrets.choice(string.ascii_letters + string.digits + "-._~") for _ in range(128))
secrets.choice(string.ascii_letters + string.digits + "-._~")
for _ in range(128)
)
def _generate_code_challenge(self, code_verifier: str) -> str: def _generate_code_challenge(self, code_verifier: str) -> str:
"""Generate a code challenge from a code verifier using SHA256.""" """Generate a code challenge from a code verifier using SHA256."""
@ -148,9 +140,7 @@ class OAuthClientProvider(httpx.Auth):
return None return None
response.raise_for_status() response.raise_for_status()
metadata_json = response.json() metadata_json = response.json()
logger.debug( logger.debug(f"OAuth metadata discovered (no MCP header): {metadata_json}")
f"OAuth metadata discovered (no MCP header): {metadata_json}"
)
return OAuthMetadata.model_validate(metadata_json) return OAuthMetadata.model_validate(metadata_json)
except Exception: except Exception:
logger.exception("Failed to discover OAuth metadata") logger.exception("Failed to discover OAuth metadata")
@ -176,17 +166,11 @@ class OAuthClientProvider(httpx.Auth):
registration_url = urljoin(auth_base_url, "/register") registration_url = urljoin(auth_base_url, "/register")
# Handle default scope # Handle default scope
if ( if client_metadata.scope is None and metadata and metadata.scopes_supported is not None:
client_metadata.scope is None
and metadata
and metadata.scopes_supported is not None
):
client_metadata.scope = " ".join(metadata.scopes_supported) client_metadata.scope = " ".join(metadata.scopes_supported)
# Serialize client metadata # Serialize client metadata
registration_data = client_metadata.model_dump( registration_data = client_metadata.model_dump(by_alias=True, mode="json", exclude_none=True)
by_alias=True, mode="json", exclude_none=True
)
async with httpx.AsyncClient() as client: async with httpx.AsyncClient() as client:
try: try:
@ -213,9 +197,7 @@ class OAuthClientProvider(httpx.Auth):
logger.exception("Registration error") logger.exception("Registration error")
raise raise
async def async_auth_flow( async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]:
self, request: httpx.Request
) -> AsyncGenerator[httpx.Request, httpx.Response]:
""" """
HTTPX auth flow integration. HTTPX auth flow integration.
""" """
@ -225,9 +207,7 @@ class OAuthClientProvider(httpx.Auth):
await self.ensure_token() await self.ensure_token()
# Add Bearer token if available # Add Bearer token if available
if self._current_tokens and self._current_tokens.access_token: if self._current_tokens and self._current_tokens.access_token:
request.headers["Authorization"] = ( request.headers["Authorization"] = f"Bearer {self._current_tokens.access_token}"
f"Bearer {self._current_tokens.access_token}"
)
response = yield request response = yield request
@ -305,11 +285,7 @@ class OAuthClientProvider(httpx.Auth):
return return
# Try refreshing existing token # Try refreshing existing token
if ( if self._current_tokens and self._current_tokens.refresh_token and await self._refresh_access_token():
self._current_tokens
and self._current_tokens.refresh_token
and await self._refresh_access_token()
):
return return
# Fall back to full OAuth flow # Fall back to full OAuth flow
@ -361,12 +337,8 @@ class OAuthClientProvider(httpx.Auth):
auth_code, returned_state = await self.callback_handler() auth_code, returned_state = await self.callback_handler()
# Validate state parameter for CSRF protection # Validate state parameter for CSRF protection
if returned_state is None or not secrets.compare_digest( if returned_state is None or not secrets.compare_digest(returned_state, self._auth_state):
returned_state, self._auth_state raise Exception(f"State parameter mismatch: {returned_state} != {self._auth_state}")
):
raise Exception(
f"State parameter mismatch: {returned_state} != {self._auth_state}"
)
# Clear state after validation # Clear state after validation
self._auth_state = None self._auth_state = None
@ -377,9 +349,7 @@ class OAuthClientProvider(httpx.Auth):
# Exchange authorization code for tokens # Exchange authorization code for tokens
await self._exchange_code_for_token(auth_code, client_info) await self._exchange_code_for_token(auth_code, client_info)
async def _exchange_code_for_token( async def _exchange_code_for_token(self, auth_code: str, client_info: OAuthClientInformationFull) -> None:
self, auth_code: str, client_info: OAuthClientInformationFull
) -> None:
"""Exchange authorization code for access token.""" """Exchange authorization code for access token."""
# Get token endpoint # Get token endpoint
if self._metadata and self._metadata.token_endpoint: if self._metadata and self._metadata.token_endpoint:
@ -412,17 +382,10 @@ class OAuthClientProvider(httpx.Auth):
# Parse OAuth error response # Parse OAuth error response
try: try:
error_data = response.json() error_data = response.json()
error_msg = error_data.get( error_msg = error_data.get("error_description", error_data.get("error", "Unknown error"))
"error_description", error_data.get("error", "Unknown error") raise Exception(f"Token exchange failed: {error_msg} " f"(HTTP {response.status_code})")
)
raise Exception(
f"Token exchange failed: {error_msg} "
f"(HTTP {response.status_code})"
)
except Exception: except Exception:
raise Exception( raise Exception(f"Token exchange failed: {response.status_code} {response.text}")
f"Token exchange failed: {response.status_code} {response.text}"
)
# Parse token response # Parse token response
token_response = OAuthToken.model_validate(response.json()) token_response = OAuthToken.model_validate(response.json())

View file

@ -38,16 +38,12 @@ class LoggingFnT(Protocol):
class MessageHandlerFnT(Protocol): class MessageHandlerFnT(Protocol):
async def __call__( async def __call__(
self, self,
message: RequestResponder[types.ServerRequest, types.ClientResult] message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
| types.ServerNotification
| Exception,
) -> None: ... ) -> None: ...
async def _default_message_handler( async def _default_message_handler(
message: RequestResponder[types.ServerRequest, types.ClientResult] message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
| types.ServerNotification
| Exception,
) -> None: ) -> None:
await anyio.lowlevel.checkpoint() await anyio.lowlevel.checkpoint()
@ -77,9 +73,7 @@ async def _default_logging_callback(
pass pass
ClientResponse: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter( ClientResponse: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter(types.ClientResult | types.ErrorData)
types.ClientResult | types.ErrorData
)
class ClientSession( class ClientSession(
@ -116,11 +110,7 @@ class ClientSession(
self._message_handler = message_handler or _default_message_handler self._message_handler = message_handler or _default_message_handler
async def initialize(self) -> types.InitializeResult: async def initialize(self) -> types.InitializeResult:
sampling = ( sampling = types.SamplingCapability() if self._sampling_callback is not _default_sampling_callback else None
types.SamplingCapability()
if self._sampling_callback is not _default_sampling_callback
else None
)
roots = ( roots = (
# TODO: Should this be based on whether we # TODO: Should this be based on whether we
# _will_ send notifications, or only whether # _will_ send notifications, or only whether
@ -149,15 +139,10 @@ class ClientSession(
) )
if result.protocolVersion not in SUPPORTED_PROTOCOL_VERSIONS: if result.protocolVersion not in SUPPORTED_PROTOCOL_VERSIONS:
raise RuntimeError( raise RuntimeError("Unsupported protocol version from the server: " f"{result.protocolVersion}")
"Unsupported protocol version from the server: "
f"{result.protocolVersion}"
)
await self.send_notification( await self.send_notification(
types.ClientNotification( types.ClientNotification(types.InitializedNotification(method="notifications/initialized"))
types.InitializedNotification(method="notifications/initialized")
)
) )
return result return result
@ -207,33 +192,25 @@ class ClientSession(
types.EmptyResult, types.EmptyResult,
) )
async def list_resources( async def list_resources(self, cursor: str | None = None) -> types.ListResourcesResult:
self, cursor: str | None = None
) -> types.ListResourcesResult:
"""Send a resources/list request.""" """Send a resources/list request."""
return await self.send_request( return await self.send_request(
types.ClientRequest( types.ClientRequest(
types.ListResourcesRequest( types.ListResourcesRequest(
method="resources/list", method="resources/list",
params=types.PaginatedRequestParams(cursor=cursor) params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
if cursor is not None
else None,
) )
), ),
types.ListResourcesResult, types.ListResourcesResult,
) )
async def list_resource_templates( async def list_resource_templates(self, cursor: str | None = None) -> types.ListResourceTemplatesResult:
self, cursor: str | None = None
) -> types.ListResourceTemplatesResult:
"""Send a resources/templates/list request.""" """Send a resources/templates/list request."""
return await self.send_request( return await self.send_request(
types.ClientRequest( types.ClientRequest(
types.ListResourceTemplatesRequest( types.ListResourceTemplatesRequest(
method="resources/templates/list", method="resources/templates/list",
params=types.PaginatedRequestParams(cursor=cursor) params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
if cursor is not None
else None,
) )
), ),
types.ListResourceTemplatesResult, types.ListResourceTemplatesResult,
@ -305,17 +282,13 @@ class ClientSession(
types.ClientRequest( types.ClientRequest(
types.ListPromptsRequest( types.ListPromptsRequest(
method="prompts/list", method="prompts/list",
params=types.PaginatedRequestParams(cursor=cursor) params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
if cursor is not None
else None,
) )
), ),
types.ListPromptsResult, types.ListPromptsResult,
) )
async def get_prompt( async def get_prompt(self, name: str, arguments: dict[str, str] | None = None) -> types.GetPromptResult:
self, name: str, arguments: dict[str, str] | None = None
) -> types.GetPromptResult:
"""Send a prompts/get request.""" """Send a prompts/get request."""
return await self.send_request( return await self.send_request(
types.ClientRequest( types.ClientRequest(
@ -352,9 +325,7 @@ class ClientSession(
types.ClientRequest( types.ClientRequest(
types.ListToolsRequest( types.ListToolsRequest(
method="tools/list", method="tools/list",
params=types.PaginatedRequestParams(cursor=cursor) params=types.PaginatedRequestParams(cursor=cursor) if cursor is not None else None,
if cursor is not None
else None,
) )
), ),
types.ListToolsResult, types.ListToolsResult,
@ -370,9 +341,7 @@ class ClientSession(
) )
) )
async def _received_request( async def _received_request(self, responder: RequestResponder[types.ServerRequest, types.ClientResult]) -> None:
self, responder: RequestResponder[types.ServerRequest, types.ClientResult]
) -> None:
ctx = RequestContext[ClientSession, Any]( ctx = RequestContext[ClientSession, Any](
request_id=responder.request_id, request_id=responder.request_id,
meta=responder.request_meta, meta=responder.request_meta,
@ -395,22 +364,16 @@ class ClientSession(
case types.PingRequest(): case types.PingRequest():
with responder: with responder:
return await responder.respond( return await responder.respond(types.ClientResult(root=types.EmptyResult()))
types.ClientResult(root=types.EmptyResult())
)
async def _handle_incoming( async def _handle_incoming(
self, self,
req: RequestResponder[types.ServerRequest, types.ClientResult] req: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,
| types.ServerNotification
| Exception,
) -> None: ) -> None:
"""Handle incoming messages by forwarding to the message handler.""" """Handle incoming messages by forwarding to the message handler."""
await self._message_handler(req) await self._message_handler(req)
async def _received_notification( async def _received_notification(self, notification: types.ServerNotification) -> None:
self, notification: types.ServerNotification
) -> None:
"""Handle notifications from the server.""" """Handle notifications from the server."""
# Process specific notification types # Process specific notification types
match notification.root: match notification.root:

View file

@ -62,9 +62,7 @@ class StreamableHttpParameters(BaseModel):
terminate_on_close: bool = True terminate_on_close: bool = True
ServerParameters: TypeAlias = ( ServerParameters: TypeAlias = StdioServerParameters | SseServerParameters | StreamableHttpParameters
StdioServerParameters | SseServerParameters | StreamableHttpParameters
)
class ClientSessionGroup: class ClientSessionGroup:
@ -261,9 +259,7 @@ class ClientSessionGroup:
) )
read, write, _ = await session_stack.enter_async_context(client) read, write, _ = await session_stack.enter_async_context(client)
session = await session_stack.enter_async_context( session = await session_stack.enter_async_context(mcp.ClientSession(read, write))
mcp.ClientSession(read, write)
)
result = await session.initialize() result = await session.initialize()
# Session successfully initialized. # Session successfully initialized.
@ -280,9 +276,7 @@ class ClientSessionGroup:
await session_stack.aclose() await session_stack.aclose()
raise raise
async def _aggregate_components( async def _aggregate_components(self, server_info: types.Implementation, session: mcp.ClientSession) -> None:
self, server_info: types.Implementation, session: mcp.ClientSession
) -> None:
"""Aggregates prompts, resources, and tools from a given session.""" """Aggregates prompts, resources, and tools from a given session."""
# Create a reverse index so we can find all prompts, resources, and # Create a reverse index so we can find all prompts, resources, and

View file

@ -73,20 +73,16 @@ async def sse_client(
match sse.event: match sse.event:
case "endpoint": case "endpoint":
endpoint_url = urljoin(url, sse.data) endpoint_url = urljoin(url, sse.data)
logger.debug( logger.debug(f"Received endpoint URL: {endpoint_url}")
f"Received endpoint URL: {endpoint_url}"
)
url_parsed = urlparse(url) url_parsed = urlparse(url)
endpoint_parsed = urlparse(endpoint_url) endpoint_parsed = urlparse(endpoint_url)
if ( if (
url_parsed.netloc != endpoint_parsed.netloc url_parsed.netloc != endpoint_parsed.netloc
or url_parsed.scheme or url_parsed.scheme != endpoint_parsed.scheme
!= endpoint_parsed.scheme
): ):
error_msg = ( error_msg = (
"Endpoint origin does not match " "Endpoint origin does not match " f"connection origin: {endpoint_url}"
f"connection origin: {endpoint_url}"
) )
logger.error(error_msg) logger.error(error_msg)
raise ValueError(error_msg) raise ValueError(error_msg)
@ -98,22 +94,16 @@ async def sse_client(
message = types.JSONRPCMessage.model_validate_json( # noqa: E501 message = types.JSONRPCMessage.model_validate_json( # noqa: E501
sse.data sse.data
) )
logger.debug( logger.debug(f"Received server message: {message}")
f"Received server message: {message}"
)
except Exception as exc: except Exception as exc:
logger.error( logger.error(f"Error parsing server message: {exc}")
f"Error parsing server message: {exc}"
)
await read_stream_writer.send(exc) await read_stream_writer.send(exc)
continue continue
session_message = SessionMessage(message) session_message = SessionMessage(message)
await read_stream_writer.send(session_message) await read_stream_writer.send(session_message)
case _: case _:
logger.warning( logger.warning(f"Unknown SSE event: {sse.event}")
f"Unknown SSE event: {sse.event}"
)
except Exception as exc: except Exception as exc:
logger.error(f"Error in sse_reader: {exc}") logger.error(f"Error in sse_reader: {exc}")
await read_stream_writer.send(exc) await read_stream_writer.send(exc)
@ -124,9 +114,7 @@ async def sse_client(
try: try:
async with write_stream_reader: async with write_stream_reader:
async for session_message in write_stream_reader: async for session_message in write_stream_reader:
logger.debug( logger.debug(f"Sending client message: {session_message}")
f"Sending client message: {session_message}"
)
response = await client.post( response = await client.post(
endpoint_url, endpoint_url,
json=session_message.message.model_dump( json=session_message.message.model_dump(
@ -136,19 +124,14 @@ async def sse_client(
), ),
) )
response.raise_for_status() response.raise_for_status()
logger.debug( logger.debug("Client message sent successfully: " f"{response.status_code}")
"Client message sent successfully: "
f"{response.status_code}"
)
except Exception as exc: except Exception as exc:
logger.error(f"Error in post_writer: {exc}") logger.error(f"Error in post_writer: {exc}")
finally: finally:
await write_stream.aclose() await write_stream.aclose()
endpoint_url = await tg.start(sse_reader) endpoint_url = await tg.start(sse_reader)
logger.debug( logger.debug(f"Starting post writer with endpoint URL: {endpoint_url}")
f"Starting post writer with endpoint URL: {endpoint_url}"
)
tg.start_soon(post_writer, endpoint_url) tg.start_soon(post_writer, endpoint_url)
try: try:

View file

@ -115,11 +115,7 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder
process = await _create_platform_compatible_process( process = await _create_platform_compatible_process(
command=command, command=command,
args=server.args, args=server.args,
env=( env=({**get_default_environment(), **server.env} if server.env is not None else get_default_environment()),
{**get_default_environment(), **server.env}
if server.env is not None
else get_default_environment()
),
errlog=errlog, errlog=errlog,
cwd=server.cwd, cwd=server.cwd,
) )
@ -163,9 +159,7 @@ async def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stder
try: try:
async with write_stream_reader: async with write_stream_reader:
async for session_message in write_stream_reader: async for session_message in write_stream_reader:
json = session_message.message.model_dump_json( json = session_message.message.model_dump_json(by_alias=True, exclude_none=True)
by_alias=True, exclude_none=True
)
await process.stdin.send( await process.stdin.send(
(json + "\n").encode( (json + "\n").encode(
encoding=server.encoding, encoding=server.encoding,
@ -229,8 +223,6 @@ async def _create_platform_compatible_process(
if sys.platform == "win32": if sys.platform == "win32":
process = await create_windows_process(command, args, env, errlog, cwd) process = await create_windows_process(command, args, env, errlog, cwd)
else: else:
process = await anyio.open_process( process = await anyio.open_process([command, *args], env=env, stderr=errlog, cwd=cwd)
[command, *args], env=env, stderr=errlog, cwd=cwd
)
return process return process

View file

@ -82,9 +82,7 @@ async def create_windows_process(
return process return process
except Exception: except Exception:
# Don't raise, let's try to create the process without creation flags # Don't raise, let's try to create the process without creation flags
process = await anyio.open_process( process = await anyio.open_process([command, *args], env=env, stderr=errlog, cwd=cwd)
[command, *args], env=env, stderr=errlog, cwd=cwd
)
return process return process

View file

@ -106,9 +106,7 @@ class StreamableHTTPTransport:
**self.headers, **self.headers,
} }
def _update_headers_with_session( def _update_headers_with_session(self, base_headers: dict[str, str]) -> dict[str, str]:
self, base_headers: dict[str, str]
) -> dict[str, str]:
"""Update headers with session ID if available.""" """Update headers with session ID if available."""
headers = base_headers.copy() headers = base_headers.copy()
if self.session_id: if self.session_id:
@ -117,17 +115,11 @@ class StreamableHTTPTransport:
def _is_initialization_request(self, message: JSONRPCMessage) -> bool: def _is_initialization_request(self, message: JSONRPCMessage) -> bool:
"""Check if the message is an initialization request.""" """Check if the message is an initialization request."""
return ( return isinstance(message.root, JSONRPCRequest) and message.root.method == "initialize"
isinstance(message.root, JSONRPCRequest)
and message.root.method == "initialize"
)
def _is_initialized_notification(self, message: JSONRPCMessage) -> bool: def _is_initialized_notification(self, message: JSONRPCMessage) -> bool:
"""Check if the message is an initialized notification.""" """Check if the message is an initialized notification."""
return ( return isinstance(message.root, JSONRPCNotification) and message.root.method == "notifications/initialized"
isinstance(message.root, JSONRPCNotification)
and message.root.method == "notifications/initialized"
)
def _maybe_extract_session_id_from_response( def _maybe_extract_session_id_from_response(
self, self,
@ -153,9 +145,7 @@ class StreamableHTTPTransport:
logger.debug(f"SSE message: {message}") logger.debug(f"SSE message: {message}")
# If this is a response and we have original_request_id, replace it # If this is a response and we have original_request_id, replace it
if original_request_id is not None and isinstance( if original_request_id is not None and isinstance(message.root, JSONRPCResponse | JSONRPCError):
message.root, JSONRPCResponse | JSONRPCError
):
message.root.id = original_request_id message.root.id = original_request_id
session_message = SessionMessage(message) session_message = SessionMessage(message)
@ -227,7 +217,8 @@ class StreamableHTTPTransport:
self.url, self.url,
headers=headers, headers=headers,
timeout=httpx.Timeout( timeout=httpx.Timeout(
self.timeout.total_seconds(), read=ctx.sse_read_timeout.total_seconds() self.timeout.total_seconds(),
read=ctx.sse_read_timeout.total_seconds(),
), ),
) as event_source: ) as event_source:
event_source.response.raise_for_status() event_source.response.raise_for_status()
@ -298,9 +289,7 @@ class StreamableHTTPTransport:
logger.error(f"Error parsing JSON response: {exc}") logger.error(f"Error parsing JSON response: {exc}")
await read_stream_writer.send(exc) await read_stream_writer.send(exc)
async def _handle_sse_response( async def _handle_sse_response(self, response: httpx.Response, ctx: RequestContext) -> None:
self, response: httpx.Response, ctx: RequestContext
) -> None:
"""Handle SSE response from the server.""" """Handle SSE response from the server."""
try: try:
event_source = EventSource(response) event_source = EventSource(response)
@ -308,11 +297,7 @@ class StreamableHTTPTransport:
is_complete = await self._handle_sse_event( is_complete = await self._handle_sse_event(
sse, sse,
ctx.read_stream_writer, ctx.read_stream_writer,
resumption_callback=( resumption_callback=(ctx.metadata.on_resumption_token_update if ctx.metadata else None),
ctx.metadata.on_resumption_token_update
if ctx.metadata
else None
),
) )
# If the SSE event indicates completion, like returning respose/error # If the SSE event indicates completion, like returning respose/error
# break the loop # break the loop
@ -455,12 +440,8 @@ async def streamablehttp_client(
""" """
transport = StreamableHTTPTransport(url, headers, timeout, sse_read_timeout, auth) transport = StreamableHTTPTransport(url, headers, timeout, sse_read_timeout, auth)
read_stream_writer, read_stream = anyio.create_memory_object_stream[ read_stream_writer, read_stream = anyio.create_memory_object_stream[SessionMessage | Exception](0)
SessionMessage | Exception write_stream, write_stream_reader = anyio.create_memory_object_stream[SessionMessage](0)
](0)
write_stream, write_stream_reader = anyio.create_memory_object_stream[
SessionMessage
](0)
async with anyio.create_task_group() as tg: async with anyio.create_task_group() as tg:
try: try:
@ -476,9 +457,7 @@ async def streamablehttp_client(
) as client: ) as client:
# Define callbacks that need access to tg # Define callbacks that need access to tg
def start_get_stream() -> None: def start_get_stream() -> None:
tg.start_soon( tg.start_soon(transport.handle_get_stream, client, read_stream_writer)
transport.handle_get_stream, client, read_stream_writer
)
tg.start_soon( tg.start_soon(
transport.post_writer, transport.post_writer,

View file

@ -19,10 +19,7 @@ logger = logging.getLogger(__name__)
async def websocket_client( async def websocket_client(
url: str, url: str,
) -> AsyncGenerator[ ) -> AsyncGenerator[
tuple[ tuple[MemoryObjectReceiveStream[SessionMessage | Exception], MemoryObjectSendStream[SessionMessage]],
MemoryObjectReceiveStream[SessionMessage | Exception],
MemoryObjectSendStream[SessionMessage],
],
None, None,
]: ]:
""" """
@ -74,9 +71,7 @@ async def websocket_client(
async with write_stream_reader: async with write_stream_reader:
async for session_message in write_stream_reader: async for session_message in write_stream_reader:
# Convert to a dict, then to JSON # Convert to a dict, then to JSON
msg_dict = session_message.message.model_dump( msg_dict = session_message.message.model_dump(by_alias=True, mode="json", exclude_none=True)
by_alias=True, mode="json", exclude_none=True
)
await ws.send(json.dumps(msg_dict)) await ws.send(json.dumps(msg_dict))
async with anyio.create_task_group() as tg: async with anyio.create_task_group() as tg:

View file

@ -2,7 +2,4 @@ from pydantic import ValidationError
def stringify_pydantic_error(validation_error: ValidationError) -> str: def stringify_pydantic_error(validation_error: ValidationError) -> str:
return "\n".join( return "\n".join(f"{'.'.join(str(loc) for loc in e['loc'])}: {e['msg']}" for e in validation_error.errors())
f"{'.'.join(str(loc) for loc in e['loc'])}: {e['msg']}"
for e in validation_error.errors()
)

View file

@ -7,9 +7,7 @@ from starlette.datastructures import FormData, QueryParams
from starlette.requests import Request from starlette.requests import Request
from starlette.responses import RedirectResponse, Response from starlette.responses import RedirectResponse, Response
from mcp.server.auth.errors import ( from mcp.server.auth.errors import stringify_pydantic_error
stringify_pydantic_error,
)
from mcp.server.auth.json_response import PydanticJSONResponse from mcp.server.auth.json_response import PydanticJSONResponse
from mcp.server.auth.provider import ( from mcp.server.auth.provider import (
AuthorizationErrorCode, AuthorizationErrorCode,
@ -18,10 +16,7 @@ from mcp.server.auth.provider import (
OAuthAuthorizationServerProvider, OAuthAuthorizationServerProvider,
construct_redirect_uri, construct_redirect_uri,
) )
from mcp.shared.auth import ( from mcp.shared.auth import InvalidRedirectUriError, InvalidScopeError
InvalidRedirectUriError,
InvalidScopeError,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -29,23 +24,16 @@ logger = logging.getLogger(__name__)
class AuthorizationRequest(BaseModel): class AuthorizationRequest(BaseModel):
# See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1 # See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1
client_id: str = Field(..., description="The client ID") client_id: str = Field(..., description="The client ID")
redirect_uri: AnyUrl | None = Field( redirect_uri: AnyUrl | None = Field(None, description="URL to redirect to after authorization")
None, description="URL to redirect to after authorization"
)
# see OAuthClientMetadata; we only support `code` # see OAuthClientMetadata; we only support `code`
response_type: Literal["code"] = Field( response_type: Literal["code"] = Field(..., description="Must be 'code' for authorization code flow")
..., description="Must be 'code' for authorization code flow"
)
code_challenge: str = Field(..., description="PKCE code challenge") code_challenge: str = Field(..., description="PKCE code challenge")
code_challenge_method: Literal["S256"] = Field( code_challenge_method: Literal["S256"] = Field("S256", description="PKCE code challenge method, must be S256")
"S256", description="PKCE code challenge method, must be S256"
)
state: str | None = Field(None, description="Optional state parameter") state: str | None = Field(None, description="Optional state parameter")
scope: str | None = Field( scope: str | None = Field(
None, None,
description="Optional scope; if specified, should be " description="Optional scope; if specified, should be " "a space-separated list of scope strings",
"a space-separated list of scope strings",
) )
@ -57,9 +45,7 @@ class AuthorizationErrorResponse(BaseModel):
state: str | None = None state: str | None = None
def best_effort_extract_string( def best_effort_extract_string(key: str, params: None | FormData | QueryParams) -> str | None:
key: str, params: None | FormData | QueryParams
) -> str | None:
if params is None: if params is None:
return None return None
value = params.get(key) value = params.get(key)
@ -138,9 +124,7 @@ class AuthorizationHandler:
if redirect_uri and client: if redirect_uri and client:
return RedirectResponse( return RedirectResponse(
url=construct_redirect_uri( url=construct_redirect_uri(str(redirect_uri), **error_resp.model_dump(exclude_none=True)),
str(redirect_uri), **error_resp.model_dump(exclude_none=True)
),
status_code=302, status_code=302,
headers={"Cache-Control": "no-store"}, headers={"Cache-Control": "no-store"},
) )
@ -172,9 +156,7 @@ class AuthorizationHandler:
if e["loc"] == ("response_type",) and e["type"] == "literal_error": if e["loc"] == ("response_type",) and e["type"] == "literal_error":
error = "unsupported_response_type" error = "unsupported_response_type"
break break
return await error_response( return await error_response(error, stringify_pydantic_error(validation_error))
error, stringify_pydantic_error(validation_error)
)
# Get client information # Get client information
client = await self.provider.get_client( client = await self.provider.get_client(
@ -229,16 +211,9 @@ class AuthorizationHandler:
) )
except AuthorizeError as e: except AuthorizeError as e:
# Handle authorization errors as defined in RFC 6749 Section 4.1.2.1 # Handle authorization errors as defined in RFC 6749 Section 4.1.2.1
return await error_response( return await error_response(error=e.error, error_description=e.error_description)
error=e.error,
error_description=e.error_description,
)
except Exception as validation_error: except Exception as validation_error:
# Catch-all for unexpected errors # Catch-all for unexpected errors
logger.exception( logger.exception("Unexpected error in authorization_handler", exc_info=validation_error)
"Unexpected error in authorization_handler", exc_info=validation_error return await error_response(error="server_error", error_description="An unexpected error occurred")
)
return await error_response(
error="server_error", error_description="An unexpected error occurred"
)

View file

@ -10,11 +10,7 @@ from starlette.responses import Response
from mcp.server.auth.errors import stringify_pydantic_error from mcp.server.auth.errors import stringify_pydantic_error
from mcp.server.auth.json_response import PydanticJSONResponse from mcp.server.auth.json_response import PydanticJSONResponse
from mcp.server.auth.provider import ( from mcp.server.auth.provider import OAuthAuthorizationServerProvider, RegistrationError, RegistrationErrorCode
OAuthAuthorizationServerProvider,
RegistrationError,
RegistrationErrorCode,
)
from mcp.server.auth.settings import ClientRegistrationOptions from mcp.server.auth.settings import ClientRegistrationOptions
from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata
@ -60,9 +56,7 @@ class RegistrationHandler:
if client_metadata.scope is None and self.options.default_scopes is not None: if client_metadata.scope is None and self.options.default_scopes is not None:
client_metadata.scope = " ".join(self.options.default_scopes) client_metadata.scope = " ".join(self.options.default_scopes)
elif ( elif client_metadata.scope is not None and self.options.valid_scopes is not None:
client_metadata.scope is not None and self.options.valid_scopes is not None
):
requested_scopes = set(client_metadata.scope.split()) requested_scopes = set(client_metadata.scope.split())
valid_scopes = set(self.options.valid_scopes) valid_scopes = set(self.options.valid_scopes)
if not requested_scopes.issubset(valid_scopes): if not requested_scopes.issubset(valid_scopes):
@ -78,8 +72,7 @@ class RegistrationHandler:
return PydanticJSONResponse( return PydanticJSONResponse(
content=RegistrationErrorResponse( content=RegistrationErrorResponse(
error="invalid_client_metadata", error="invalid_client_metadata",
error_description="grant_types must be authorization_code " error_description="grant_types must be authorization_code " "and refresh_token",
"and refresh_token",
), ),
status_code=400, status_code=400,
) )
@ -122,8 +115,6 @@ class RegistrationHandler:
except RegistrationError as e: except RegistrationError as e:
# Handle registration errors as defined in RFC 7591 Section 3.2.2 # Handle registration errors as defined in RFC 7591 Section 3.2.2
return PydanticJSONResponse( return PydanticJSONResponse(
content=RegistrationErrorResponse( content=RegistrationErrorResponse(error=e.error, error_description=e.error_description),
error=e.error, error_description=e.error_description
),
status_code=400, status_code=400,
) )

View file

@ -10,15 +10,8 @@ from mcp.server.auth.errors import (
stringify_pydantic_error, stringify_pydantic_error,
) )
from mcp.server.auth.json_response import PydanticJSONResponse from mcp.server.auth.json_response import PydanticJSONResponse
from mcp.server.auth.middleware.client_auth import ( from mcp.server.auth.middleware.client_auth import AuthenticationError, ClientAuthenticator
AuthenticationError, from mcp.server.auth.provider import AccessToken, OAuthAuthorizationServerProvider, RefreshToken
ClientAuthenticator,
)
from mcp.server.auth.provider import (
AccessToken,
OAuthAuthorizationServerProvider,
RefreshToken,
)
class RevocationRequest(BaseModel): class RevocationRequest(BaseModel):

Some files were not shown because too many files have changed in this diff Show more