[
  {
    "path": ".claude/commands/review-pr.md",
    "content": "Review the pull request: $ARGUMENTS\n\nFollow these steps carefully. Use the `gh` CLI for all GitHub interactions.\n\n## Step 1: Resolve the PR\n\nParse `$ARGUMENTS` to determine the PR. It can be:\n\n- A full URL like `https://github.com/owner/repo/pull/123`\n- A `owner/repo#123` reference\n- A bare number like `123` (use the current repo)\n- A description — search for it with `gh pr list --search \"<description>\" --limit 5` and pick the best match\n\nOnce resolved, fetch the PR metadata:\n\n```bash\ngh pr view <PR> --json number,title,body,author,state,baseRefName,headRefName,url,labels,milestone,additions,deletions,changedFiles,createdAt,updatedAt,mergedAt,reviewDecision,reviews,assignees\n```\n\n## Step 2: Gather the diff\n\nGet the full diff of the PR:\n\n```bash\ngh pr diff <PR>\n```\n\nIf the diff is very large (>3000 lines), focus on the most important files first and summarize the rest.\n\n## Step 3: Collect PR discussion context\n\nFetch all comments and review threads:\n\n```bash\ngh api repos/{owner}/{repo}/pulls/{number}/comments --paginate\ngh api repos/{owner}/{repo}/issues/{number}/comments --paginate\ngh api repos/{owner}/{repo}/pulls/{number}/reviews --paginate\n```\n\nPay attention to:\n\n- Reviewer feedback and requested changes\n- Author responses and explanations\n- Any unresolved conversations\n- Approval or rejection status\n\n## Step 4: Find and read linked issues\n\nLook for issue references in:\n\n- The PR body (patterns like `#123`, `fixes #123`, `closes #123`, `resolves #123`)\n- The PR branch name (patterns like `issue-123`, `fix/123`)\n- Commit messages\n\nFor each linked issue, fetch its content:\n\n```bash\ngh issue view <number> --json title,body,comments,labels,state\n```\n\nRead through issue comments to understand the original problem, user reports, and any discussed solutions.\n\n## Step 5: Analyze and validate\n\nWith all context gathered, analyze the PR critically:\n\n1. **Intent alignment**: Does the code change actually solve the problem described in the PR and/or linked issues?\n2. **Completeness**: Are there aspects of the issue or requested feature that the PR doesn't address?\n3. **Scope**: Does the PR include changes unrelated to the stated goal? Are there unnecessary modifications?\n4. **Correctness**: Based on the diff, are there obvious bugs, edge cases, or logic errors?\n5. **Testing**: Does the PR include tests? Are they meaningful and do they cover the important cases?\n6. **Breaking changes**: Could this PR break existing functionality or APIs?\n7. **Unresolved feedback**: Are there reviewer comments that haven't been addressed?\n\n## Step 6: Produce the review summary\n\nPresent the summary in this format:\n\n---\n\n### PR Review: `<title>` (<url>)\n\n**Author:** <author> | **Status:** <state> | **Review decision:** <decision>\n**Base:** `<base>` ← `<head>` | **Changed files:** <n> | **+<additions> / -<deletions>**\n\n#### Problem\n\n<1-3 sentences describing what problem this PR is trying to solve, based on the PR description and linked issues>\n\n#### Solution\n\n<1-3 sentences describing the approach taken in the code>\n\n#### Key changes\n\n<Bulleted list of the most important changes, grouped by theme. Include file paths.>\n\n#### Linked issues\n\n<List of linked issues with their title, state, and a one-line summary of the discussion>\n\n#### Discussion highlights\n\n<Summary of important comments from reviewers and the author. Flag any unresolved threads.>\n\n#### Concerns\n\n<List any issues found during validation: bugs, missing tests, scope creep, unaddressed feedback, etc. If none, say \"No concerns found.\">\n\n#### Verdict\n\n<One of: APPROVE / REQUEST CHANGES / NEEDS DISCUSSION, with a brief justification>\n\n#### Suggested action\n\n<Clear recommendation for the reviewer: what to approve, what to push back on, what to ask about>\n\n---\n"
  },
  {
    "path": ".git-blame-ignore-revs",
    "content": "# Applied 120 line-length rule to all files: https://github.com/modelcontextprotocol/python-sdk/pull/856\n543961968c0634e93d919d509cce23a1d6a56c21\n\n# Added 100% code coverage baseline with pragma comments: https://github.com/modelcontextprotocol/python-sdk/pull/1553\n89e9c43acf7e23cf766357d776ec1ce63ac2c58e\n"
  },
  {
    "path": ".gitattribute",
    "content": "# Generated\nuv.lock  linguist-generated=true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yaml",
    "content": "name: 🐛 MCP Python SDK Bug\ndescription: Report a bug or unexpected behavior in the MCP Python SDK\nlabels: [\"need confirmation\"]\n\nbody:\n  - type: markdown\n    attributes:\n      value: Thank you for contributing to the MCP Python SDK! ✊\n\n  - type: checkboxes\n    id: checks\n    attributes:\n      label: Initial Checks\n      description: Just making sure you're using the latest version of MCP Python SDK.\n      options:\n        - label: I confirm that I'm using the latest version of MCP Python SDK\n          required: true\n        - label: I confirm that I searched for my issue in https://github.com/modelcontextprotocol/python-sdk/issues before opening this issue\n          required: true\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: |\n        Please explain what you're seeing and what you would expect to see.\n\n        Please provide as much detail as possible to make understanding and solving your problem as quick as possible. 🙏\n    validations:\n      required: true\n\n  - type: textarea\n    id: example\n    attributes:\n      label: Example Code\n      description: >\n        If applicable, please add a self-contained,\n        [minimal, reproducible, example](https://stackoverflow.com/help/minimal-reproducible-example)\n        demonstrating the bug.\n\n      placeholder: |\n        from mcp.server.mcpserver import MCPServer\n\n        ...\n      render: Python\n\n  - type: textarea\n    id: version\n    attributes:\n      label: Python & MCP Python SDK\n      description: |\n        Which version of Python and MCP Python SDK are you using?\n      render: Text\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yaml",
    "content": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.yaml",
    "content": "name: 🚀 MCP Python SDK Feature Request\ndescription: \"Suggest a new feature for the MCP Python SDK\"\nlabels: [\"feature request\"]\n\nbody:\n  - type: markdown\n    attributes:\n      value: Thank you for contributing to the MCP Python SDK! ✊\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: |\n        Please give as much detail as possible about the feature you would like to suggest. 🙏\n\n        You might like to add:\n        * A demo of how code might look when using the feature\n        * Your use case(s) for the feature\n        * Reference to other projects that have a similar feature\n    validations:\n      required: true\n\n  - type: textarea\n    id: references\n    attributes:\n      label: References\n      description: |\n        Please add any links or references that might help us understand your feature request better. 📚\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.yaml",
    "content": "name: ❓ MCP Python SDK Question\ndescription: \"Ask a question about the MCP Python SDK\"\nlabels: [\"question\"]\n\nbody:\n  - type: markdown\n    attributes:\n      value: Thank you for reaching out to the MCP Python SDK community! We're here to help! 🤝\n\n  - type: textarea\n    id: question\n    attributes:\n      label: Question\n      description: |\n        Please provide as much detail as possible about your question. 🙏\n\n        You might like to include:\n        * Code snippets showing what you've tried\n        * Error messages you're encountering (if any)\n        * Expected vs actual behavior\n        * Your use case and what you're trying to achieve\n    validations:\n      required: true\n\n  - type: textarea\n    id: context\n    attributes:\n      label: Additional Context\n      description: |\n        Please provide any additional context that might help us better understand your question, such as:\n        * Your MCP Python SDK version\n        * Your Python version\n        * Relevant configuration or environment details 📝\n"
  },
  {
    "path": ".github/actions/conformance/client.py",
    "content": "\"\"\"MCP unified conformance test client.\n\nThis client is designed to work with the @modelcontextprotocol/conformance npm package.\nIt handles all conformance test scenarios via environment variables and CLI arguments.\n\nContract:\n    - MCP_CONFORMANCE_SCENARIO env var -> scenario name\n    - MCP_CONFORMANCE_CONTEXT env var -> optional JSON (for client-credentials scenarios)\n    - Server URL as last CLI argument (sys.argv[1])\n    - Must exit 0 within 30 seconds\n\nScenarios:\n    initialize                              - Connect, initialize, list tools, close\n    tools_call                              - Connect, call add_numbers(a=5, b=3), close\n    sse-retry                               - Connect, call test_reconnection, close\n    elicitation-sep1034-client-defaults     - Elicitation with default accept callback\n    auth/client-credentials-jwt             - Client credentials with private_key_jwt\n    auth/client-credentials-basic           - Client credentials with client_secret_basic\n    auth/*                                  - Authorization code flow (default for auth scenarios)\n\"\"\"\n\nimport asyncio\nimport json\nimport logging\nimport os\nimport sys\nfrom collections.abc import Callable, Coroutine\nfrom typing import Any, cast\nfrom urllib.parse import parse_qs, urlparse\n\nimport httpx\nfrom pydantic import AnyUrl\n\nfrom mcp import ClientSession, types\nfrom mcp.client.auth import OAuthClientProvider, TokenStorage\nfrom mcp.client.auth.extensions.client_credentials import (\n    ClientCredentialsOAuthProvider,\n    PrivateKeyJWTOAuthProvider,\n    SignedJWTParameters,\n)\nfrom mcp.client.context import ClientRequestContext\nfrom mcp.client.streamable_http import streamable_http_client\nfrom mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken\n\n# Set up logging to stderr (stdout is for conformance test output)\nlogging.basicConfig(\n    level=logging.DEBUG,\n    format=\"%(asctime)s - %(name)s - %(levelname)s - %(message)s\",\n    stream=sys.stderr,\n)\nlogger = logging.getLogger(__name__)\n\n# Type for async scenario handler functions\nScenarioHandler = Callable[[str], Coroutine[Any, None, None]]\n\n# Registry of scenario handlers\nHANDLERS: dict[str, ScenarioHandler] = {}\n\n\ndef register(name: str) -> Callable[[ScenarioHandler], ScenarioHandler]:\n    \"\"\"Register a scenario handler.\"\"\"\n\n    def decorator(fn: ScenarioHandler) -> ScenarioHandler:\n        HANDLERS[name] = fn\n        return fn\n\n    return decorator\n\n\ndef get_conformance_context() -> dict[str, Any]:\n    \"\"\"Load conformance test context from MCP_CONFORMANCE_CONTEXT environment variable.\"\"\"\n    context_json = os.environ.get(\"MCP_CONFORMANCE_CONTEXT\")\n    if not context_json:\n        raise RuntimeError(\n            \"MCP_CONFORMANCE_CONTEXT environment variable not set. \"\n            \"Expected JSON with client_id, client_secret, and/or private_key_pem.\"\n        )\n    try:\n        return json.loads(context_json)\n    except json.JSONDecodeError as e:\n        raise RuntimeError(f\"Failed to parse MCP_CONFORMANCE_CONTEXT as JSON: {e}\") from e\n\n\nclass InMemoryTokenStorage(TokenStorage):\n    \"\"\"Simple in-memory token storage for conformance testing.\"\"\"\n\n    def __init__(self) -> None:\n        self._tokens: OAuthToken | None = None\n        self._client_info: OAuthClientInformationFull | None = None\n\n    async def get_tokens(self) -> OAuthToken | None:\n        return self._tokens\n\n    async def set_tokens(self, tokens: OAuthToken) -> None:\n        self._tokens = tokens\n\n    async def get_client_info(self) -> OAuthClientInformationFull | None:\n        return self._client_info\n\n    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:\n        self._client_info = client_info\n\n\nclass ConformanceOAuthCallbackHandler:\n    \"\"\"OAuth callback handler that automatically fetches the authorization URL\n    and extracts the auth code, without requiring user interaction.\n    \"\"\"\n\n    def __init__(self) -> None:\n        self._auth_code: str | None = None\n        self._state: str | None = None\n\n    async def handle_redirect(self, authorization_url: str) -> None:\n        \"\"\"Fetch the authorization URL and extract the auth code from the redirect.\"\"\"\n        logger.debug(f\"Fetching authorization URL: {authorization_url}\")\n\n        async with httpx.AsyncClient() as client:\n            response = await client.get(\n                authorization_url,\n                follow_redirects=False,\n            )\n\n            if response.status_code in (301, 302, 303, 307, 308):\n                location = cast(str, response.headers.get(\"location\"))\n                if location:\n                    redirect_url = urlparse(location)\n                    query_params: dict[str, list[str]] = parse_qs(redirect_url.query)\n\n                    if \"code\" in query_params:\n                        self._auth_code = query_params[\"code\"][0]\n                        state_values = query_params.get(\"state\")\n                        self._state = state_values[0] if state_values else None\n                        logger.debug(f\"Got auth code from redirect: {self._auth_code[:10]}...\")\n                        return\n                    else:\n                        raise RuntimeError(f\"No auth code in redirect URL: {location}\")\n                else:\n                    raise RuntimeError(f\"No redirect location received from {authorization_url}\")\n            else:\n                raise RuntimeError(f\"Expected redirect response, got {response.status_code} from {authorization_url}\")\n\n    async def handle_callback(self) -> tuple[str, str | None]:\n        \"\"\"Return the captured auth code and state.\"\"\"\n        if self._auth_code is None:\n            raise RuntimeError(\"No authorization code available - was handle_redirect called?\")\n        auth_code = self._auth_code\n        state = self._state\n        self._auth_code = None\n        self._state = None\n        return auth_code, state\n\n\n# --- Scenario Handlers ---\n\n\n@register(\"initialize\")\nasync def run_initialize(server_url: str) -> None:\n    \"\"\"Connect, initialize, list tools, close.\"\"\"\n    async with streamable_http_client(url=server_url) as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            await session.initialize()\n            logger.debug(\"Initialized successfully\")\n            await session.list_tools()\n            logger.debug(\"Listed tools successfully\")\n\n\n@register(\"tools_call\")\nasync def run_tools_call(server_url: str) -> None:\n    \"\"\"Connect, initialize, list tools, call add_numbers(a=5, b=3), close.\"\"\"\n    async with streamable_http_client(url=server_url) as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            await session.initialize()\n            await session.list_tools()\n            result = await session.call_tool(\"add_numbers\", {\"a\": 5, \"b\": 3})\n            logger.debug(f\"add_numbers result: {result}\")\n\n\n@register(\"sse-retry\")\nasync def run_sse_retry(server_url: str) -> None:\n    \"\"\"Connect, initialize, list tools, call test_reconnection, close.\"\"\"\n    async with streamable_http_client(url=server_url) as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            await session.initialize()\n            await session.list_tools()\n            result = await session.call_tool(\"test_reconnection\", {})\n            logger.debug(f\"test_reconnection result: {result}\")\n\n\nasync def default_elicitation_callback(\n    context: ClientRequestContext,\n    params: types.ElicitRequestParams,\n) -> types.ElicitResult | types.ErrorData:\n    \"\"\"Accept elicitation and apply defaults from the schema (SEP-1034).\"\"\"\n    content: dict[str, str | int | float | bool | list[str] | None] = {}\n\n    # For form mode, extract defaults from the requested_schema\n    if isinstance(params, types.ElicitRequestFormParams):\n        schema = params.requested_schema\n        logger.debug(f\"Elicitation schema: {schema}\")\n        properties = schema.get(\"properties\", {})\n        for prop_name, prop_schema in properties.items():\n            if \"default\" in prop_schema:\n                content[prop_name] = prop_schema[\"default\"]\n        logger.debug(f\"Applied defaults: {content}\")\n\n    return types.ElicitResult(action=\"accept\", content=content)\n\n\n@register(\"elicitation-sep1034-client-defaults\")\nasync def run_elicitation_defaults(server_url: str) -> None:\n    \"\"\"Connect with elicitation callback that applies schema defaults.\"\"\"\n    async with streamable_http_client(url=server_url) as (read_stream, write_stream):\n        async with ClientSession(\n            read_stream, write_stream, elicitation_callback=default_elicitation_callback\n        ) as session:\n            await session.initialize()\n            await session.list_tools()\n            result = await session.call_tool(\"test_client_elicitation_defaults\", {})\n            logger.debug(f\"test_client_elicitation_defaults result: {result}\")\n\n\n@register(\"auth/client-credentials-jwt\")\nasync def run_client_credentials_jwt(server_url: str) -> None:\n    \"\"\"Client credentials flow with private_key_jwt authentication.\"\"\"\n    context = get_conformance_context()\n    client_id = context.get(\"client_id\")\n    private_key_pem = context.get(\"private_key_pem\")\n    signing_algorithm = context.get(\"signing_algorithm\", \"ES256\")\n\n    if not client_id:\n        raise RuntimeError(\"MCP_CONFORMANCE_CONTEXT missing 'client_id'\")\n    if not private_key_pem:\n        raise RuntimeError(\"MCP_CONFORMANCE_CONTEXT missing 'private_key_pem'\")\n\n    jwt_params = SignedJWTParameters(\n        issuer=client_id,\n        subject=client_id,\n        signing_algorithm=signing_algorithm,\n        signing_key=private_key_pem,\n    )\n\n    oauth_auth = PrivateKeyJWTOAuthProvider(\n        server_url=server_url,\n        storage=InMemoryTokenStorage(),\n        client_id=client_id,\n        assertion_provider=jwt_params.create_assertion_provider(),\n    )\n\n    await _run_auth_session(server_url, oauth_auth)\n\n\n@register(\"auth/client-credentials-basic\")\nasync def run_client_credentials_basic(server_url: str) -> None:\n    \"\"\"Client credentials flow with client_secret_basic authentication.\"\"\"\n    context = get_conformance_context()\n    client_id = context.get(\"client_id\")\n    client_secret = context.get(\"client_secret\")\n\n    if not client_id:\n        raise RuntimeError(\"MCP_CONFORMANCE_CONTEXT missing 'client_id'\")\n    if not client_secret:\n        raise RuntimeError(\"MCP_CONFORMANCE_CONTEXT missing 'client_secret'\")\n\n    oauth_auth = ClientCredentialsOAuthProvider(\n        server_url=server_url,\n        storage=InMemoryTokenStorage(),\n        client_id=client_id,\n        client_secret=client_secret,\n        token_endpoint_auth_method=\"client_secret_basic\",\n    )\n\n    await _run_auth_session(server_url, oauth_auth)\n\n\nasync def run_auth_code_client(server_url: str) -> None:\n    \"\"\"Authorization code flow (default for auth/* scenarios).\"\"\"\n    callback_handler = ConformanceOAuthCallbackHandler()\n    storage = InMemoryTokenStorage()\n\n    # Check for pre-registered client credentials from context\n    context_json = os.environ.get(\"MCP_CONFORMANCE_CONTEXT\")\n    if context_json:\n        try:\n            context = json.loads(context_json)\n            client_id = context.get(\"client_id\")\n            client_secret = context.get(\"client_secret\")\n            if client_id:\n                await storage.set_client_info(\n                    OAuthClientInformationFull(\n                        client_id=client_id,\n                        client_secret=client_secret,\n                        redirect_uris=[AnyUrl(\"http://localhost:3000/callback\")],\n                        token_endpoint_auth_method=\"client_secret_basic\" if client_secret else \"none\",\n                    )\n                )\n                logger.debug(f\"Pre-loaded client credentials: client_id={client_id}\")\n        except json.JSONDecodeError:\n            logger.exception(\"Failed to parse MCP_CONFORMANCE_CONTEXT\")\n\n    oauth_auth = OAuthClientProvider(\n        server_url=server_url,\n        client_metadata=OAuthClientMetadata(\n            client_name=\"conformance-client\",\n            redirect_uris=[AnyUrl(\"http://localhost:3000/callback\")],\n            grant_types=[\"authorization_code\", \"refresh_token\"],\n            response_types=[\"code\"],\n        ),\n        storage=storage,\n        redirect_handler=callback_handler.handle_redirect,\n        callback_handler=callback_handler.handle_callback,\n        client_metadata_url=\"https://conformance-test.local/client-metadata.json\",\n    )\n\n    await _run_auth_session(server_url, oauth_auth)\n\n\nasync def _run_auth_session(server_url: str, oauth_auth: OAuthClientProvider) -> None:\n    \"\"\"Common session logic for all OAuth flows.\"\"\"\n    client = httpx.AsyncClient(auth=oauth_auth, timeout=30.0)\n    async with streamable_http_client(url=server_url, http_client=client) as (read_stream, write_stream):\n        async with ClientSession(\n            read_stream, write_stream, elicitation_callback=default_elicitation_callback\n        ) as session:\n            await session.initialize()\n            logger.debug(\"Initialized successfully\")\n\n            tools_result = await session.list_tools()\n            logger.debug(f\"Listed tools: {[t.name for t in tools_result.tools]}\")\n\n            # Call the first available tool (different tests have different tools)\n            if tools_result.tools:\n                tool_name = tools_result.tools[0].name\n                try:\n                    result = await session.call_tool(tool_name, {})\n                    logger.debug(f\"Called {tool_name}, result: {result}\")\n                except Exception as e:\n                    logger.debug(f\"Tool call result/error: {e}\")\n\n    logger.debug(\"Connection closed successfully\")\n\n\ndef main() -> None:\n    \"\"\"Main entry point for the conformance client.\"\"\"\n    if len(sys.argv) < 2:\n        print(f\"Usage: {sys.argv[0]} <server-url>\", file=sys.stderr)\n        sys.exit(1)\n\n    server_url = sys.argv[1]\n    scenario = os.environ.get(\"MCP_CONFORMANCE_SCENARIO\")\n\n    if scenario:\n        logger.debug(f\"Running explicit scenario '{scenario}' against {server_url}\")\n        handler = HANDLERS.get(scenario)\n        if handler:\n            asyncio.run(handler(server_url))\n        elif scenario.startswith(\"auth/\"):\n            asyncio.run(run_auth_code_client(server_url))\n        else:\n            print(f\"Unknown scenario: {scenario}\", file=sys.stderr)\n            sys.exit(1)\n    else:\n        logger.debug(f\"Running default auth flow against {server_url}\")\n        asyncio.run(run_auth_code_client(server_url))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": ".github/actions/conformance/run-server.sh",
    "content": "#!/bin/bash\nset -e\n\nPORT=\"${PORT:-3001}\"\nSERVER_URL=\"http://localhost:${PORT}/mcp\"\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\ncd \"$SCRIPT_DIR/../../..\"\n\n# Start everything-server\nuv run --frozen mcp-everything-server --port \"$PORT\" &\nSERVER_PID=$!\ntrap \"kill $SERVER_PID 2>/dev/null || true; wait $SERVER_PID 2>/dev/null || true\" EXIT\n\n# Wait for server to be ready\nMAX_RETRIES=30\nRETRY_COUNT=0\nwhile ! curl -s \"$SERVER_URL\" > /dev/null 2>&1; do\n    RETRY_COUNT=$((RETRY_COUNT + 1))\n    if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then\n        echo \"Server failed to start after ${MAX_RETRIES} retries\" >&2\n        exit 1\n    fi\n    sleep 0.5\ndone\n\necho \"Server ready at $SERVER_URL\"\n\n# Run conformance tests\nnpx @modelcontextprotocol/conformance@0.1.10 server --url \"$SERVER_URL\" \"$@\"\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: monthly\n    groups:\n      github-actions:\n        patterns:\n          - \"*\"\n"
  },
  {
    "path": ".github/workflows/claude-code-review.yml",
    "content": "# Source: https://github.com/anthropics/claude-code-action/blob/main/docs/code-review.md\nname: Claude Code Review\n\non:\n  pull_request:\n    types: [opened, synchronize, ready_for_review, reopened]\n\njobs:\n  claude-review:\n    # Fork PRs don't have access to secrets or OIDC tokens, so the action\n    # cannot authenticate. See https://github.com/anthropics/claude-code-action/issues/339\n    if: github.event.pull_request.head.repo.fork == false && github.actor != 'dependabot[bot]'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: read\n      issues: read\n      id-token: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 1\n\n      - name: Run Claude Code Review\n        id: claude-review\n        uses: anthropics/claude-code-action@2f8ba26a219c06cfb0f468eef8d97055fa814f97 # v1.0.53\n        with:\n          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}\n          plugin_marketplaces: \"https://github.com/anthropics/claude-code.git\"\n          plugins: \"code-review@claude-code-plugins\"\n          prompt: \"/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}\"\n"
  },
  {
    "path": ".github/workflows/claude.yml",
    "content": "# Source: https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md\nname: Claude Code\n\non:\n  issue_comment:\n    types: [created]\n  pull_request_review_comment:\n    types: [created]\n  issues:\n    types: [opened, assigned]\n  pull_request_review:\n    types: [submitted]\n\njobs:\n  claude:\n    if: |\n      (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||\n      (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||\n      (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      pull-requests: read\n      issues: read\n      id-token: write\n      actions: read # Required for Claude to read CI results on PRs\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n        with:\n          fetch-depth: 1\n\n      - name: Run Claude Code\n        id: claude\n        uses: anthropics/claude-code-action@2f8ba26a219c06cfb0f468eef8d97055fa814f97 # v1.0.53\n        with:\n          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}\n          use_commit_signing: true\n          additional_permissions: |\n            actions: read\n"
  },
  {
    "path": ".github/workflows/comment-on-release.yml",
    "content": "name: Comment on PRs in Release\n\non:\n  release:\n    types: [published]\n\npermissions:\n  pull-requests: write\n  contents: read\n\njobs:\n  comment-on-prs:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\n        with:\n          fetch-depth: 0\n\n      - name: Get previous release\n        id: previous_release\n        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          script: |\n            const currentTag = '${{ github.event.release.tag_name }}';\n\n            // Get all releases\n            const { data: releases } = await github.rest.repos.listReleases({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              per_page: 100\n            });\n\n            // Find current release index\n            const currentIndex = releases.findIndex(r => r.tag_name === currentTag);\n\n            if (currentIndex === -1) {\n              console.log('Current release not found in list');\n              return null;\n            }\n\n            // Get previous release (next in the list since they're sorted by date desc)\n            const previousRelease = releases[currentIndex + 1];\n\n            if (!previousRelease) {\n              console.log('No previous release found, this might be the first release');\n              return null;\n            }\n\n            console.log(`Found previous release: ${previousRelease.tag_name}`);\n\n            return previousRelease.tag_name;\n\n      - name: Get merged PRs between releases\n        id: get_prs\n        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          script: |\n            const currentTag = '${{ github.event.release.tag_name }}';\n            const previousTag = ${{ steps.previous_release.outputs.result }};\n\n            if (!previousTag) {\n              console.log('No previous release found, skipping');\n              return [];\n            }\n\n            console.log(`Finding PRs between ${previousTag} and ${currentTag}`);\n\n            // Get commits between previous and current release\n            const comparison = await github.rest.repos.compareCommits({\n              owner: context.repo.owner,\n              repo: context.repo.repo,\n              base: previousTag,\n              head: currentTag\n            });\n\n            const commits = comparison.data.commits;\n            console.log(`Found ${commits.length} commits`);\n\n            // Get PRs associated with each commit using GitHub API\n            const prNumbers = new Set();\n\n            for (const commit of commits) {\n              try {\n                const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  commit_sha: commit.sha\n                });\n\n                for (const pr of prs) {\n                  if (pr.merged_at) {\n                    prNumbers.add(pr.number);\n                    console.log(`Found merged PR: #${pr.number}`);\n                  }\n                }\n              } catch (error) {\n                console.log(`Failed to get PRs for commit ${commit.sha}: ${error.message}`);\n              }\n            }\n\n            console.log(`Found ${prNumbers.size} merged PRs`);\n            return Array.from(prNumbers);\n\n      - name: Comment on PRs\n        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0\n        with:\n          script: |\n            const prNumbers = ${{ steps.get_prs.outputs.result }};\n            const releaseTag = '${{ github.event.release.tag_name }}';\n            const releaseUrl = '${{ github.event.release.html_url }}';\n\n            const comment = `This pull request is included in [${releaseTag}](${releaseUrl})`;\n\n            let commentedCount = 0;\n\n            for (const prNumber of prNumbers) {\n              try {\n                // Check if we've already commented on this PR for this release\n                const { data: comments } = await github.rest.issues.listComments({\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  issue_number: prNumber,\n                  per_page: 100\n                });\n\n                const alreadyCommented = comments.some(c =>\n                  c.user.type === 'Bot' && c.body.includes(releaseTag)\n                );\n\n                if (alreadyCommented) {\n                  console.log(`Skipping PR #${prNumber} - already commented for ${releaseTag}`);\n                  continue;\n                }\n\n                await github.rest.issues.createComment({\n                  owner: context.repo.owner,\n                  repo: context.repo.repo,\n                  issue_number: prNumber,\n                  body: comment\n                });\n                commentedCount++;\n                console.log(`Successfully commented on PR #${prNumber}`);\n              } catch (error) {\n                console.error(`Failed to comment on PR #${prNumber}:`, error.message);\n              }\n            }\n\n            console.log(`Commented on ${commentedCount} of ${prNumbers.length} PRs`);\n"
  },
  {
    "path": ".github/workflows/conformance.yml",
    "content": "name: Conformance Tests\n\non:\n  push:\n    branches: [main]\n  pull_request:\n  workflow_dispatch:\n\nconcurrency:\n  group: conformance-${{ github.ref }}\n  cancel-in-progress: true\n\npermissions:\n  contents: read\n\njobs:\n  server-conformance:\n    runs-on: ubuntu-latest\n    continue-on-error: true\n    steps:\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\n      - uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1\n        with:\n          enable-cache: true\n          version: 0.9.5\n      - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0\n        with:\n          node-version: 24\n      - run: uv sync --frozen --all-extras --package mcp-everything-server\n      - run: ./.github/actions/conformance/run-server.sh\n\n  client-conformance:\n    runs-on: ubuntu-latest\n    continue-on-error: true\n    steps:\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\n      - uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1\n        with:\n          enable-cache: true\n          version: 0.9.5\n      - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0\n        with:\n          node-version: 24\n      - run: uv sync --frozen --all-extras --package mcp\n      - run: npx @modelcontextprotocol/conformance@0.1.13 client --command 'uv run --frozen python .github/actions/conformance/client.py' --suite all\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [\"main\", \"v1.x\"]\n    tags: [\"v*.*.*\"]\n  pull_request:\n    branches: [\"main\", \"v1.x\"]\n\npermissions:\n  contents: read\n\njobs:\n  checks:\n    uses: ./.github/workflows/shared.yml\n\n  all-green:\n    if: always()\n    needs: [checks]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2\n        with:\n          jobs: ${{ toJSON(needs) }}\n"
  },
  {
    "path": ".github/workflows/publish-docs-manually.yml",
    "content": "name: Publish Docs manually\n\non:\n  workflow_dispatch:\n\njobs:\n  docs-publish:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write\n    steps:\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\n      - name: Configure Git Credentials\n        run: |\n          git config user.name github-actions[bot]\n          git config user.email 41898282+github-actions[bot]@users.noreply.github.com\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1\n        with:\n          enable-cache: true\n          version: 0.9.5\n\n      - run: echo \"cache_id=$(date --utc '+%V')\" >> $GITHUB_ENV\n      - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3\n        with:\n          key: mkdocs-material-${{ env.cache_id }}\n          path: .cache\n          restore-keys: |\n            mkdocs-material-\n\n      - run: uv sync --frozen --group docs\n      - run: uv run --frozen --no-sync mkdocs gh-deploy --force\n        env:\n          ENABLE_SOCIAL_CARDS: \"true\"\n"
  },
  {
    "path": ".github/workflows/publish-pypi.yml",
    "content": "name: Publishing\n\non:\n  release:\n    types: [published]\n\njobs:\n  release-build:\n    name: Build distribution\n    runs-on: ubuntu-latest\n    needs: [checks]\n    steps:\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1\n        with:\n          enable-cache: true\n          version: 0.9.5\n\n      - name: Set up Python 3.12\n        run: uv python install 3.12\n\n      - name: Build\n        run: uv build\n\n      - name: Upload artifacts\n        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0\n        with:\n          name: release-dists\n          path: dist/\n\n  checks:\n    uses: ./.github/workflows/shared.yml\n\n  pypi-publish:\n    name: Upload release to PyPI\n    runs-on: ubuntu-latest\n    environment: release\n    needs:\n      - release-build\n    permissions:\n      id-token: write # IMPORTANT: this permission is mandatory for trusted publishing\n\n    steps:\n      - name: Retrieve release distributions\n        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0\n        with:\n          name: release-dists\n          path: dist/\n\n      - name: Publish package distributions to PyPI\n        uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # release/v1\n\n  docs-publish:\n    runs-on: ubuntu-latest\n    needs: [\"pypi-publish\"]\n    permissions:\n      contents: write\n    steps:\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\n      - name: Configure Git Credentials\n        run: |\n          git config user.name github-actions[bot]\n          git config user.email 41898282+github-actions[bot]@users.noreply.github.com\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1\n        with:\n          enable-cache: true\n          version: 0.9.5\n\n      - run: echo \"cache_id=$(date --utc '+%V')\" >> $GITHUB_ENV\n      - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3\n        with:\n          key: mkdocs-material-${{ env.cache_id }}\n          path: .cache\n          restore-keys: |\n            mkdocs-material-\n\n      - run: uv sync --frozen --group docs\n      - run: uv run --frozen --no-sync mkdocs gh-deploy --force\n"
  },
  {
    "path": ".github/workflows/shared.yml",
    "content": "name: Shared Checks\n\non:\n  workflow_call:\n\npermissions:\n  contents: read\n\nenv:\n  COLUMNS: 150\n\njobs:\n  pre-commit:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\n\n      - uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1\n        with:\n          enable-cache: true\n          version: 0.9.5\n      - name: Install dependencies\n        run: uv sync --frozen --all-extras --python 3.10\n\n      - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1\n        with:\n          extra_args: --all-files --verbose\n        env:\n          SKIP: no-commit-to-branch,readme-v1-frozen\n\n      # TODO(Max): Drop this in v2.\n      - name: Check README.md is not modified\n        if: github.event_name == 'pull_request'\n        run: |\n          git fetch --no-tags --depth=1 origin \"$BASE_SHA\"\n          if git diff --name-only \"$BASE_SHA\" -- README.md | grep -q .; then\n            echo \"::error::README.md is frozen at v1. Edit README.v2.md instead.\"\n            exit 1\n          fi\n        env:\n          BASE_SHA: ${{ github.event.pull_request.base.sha }}\n\n  test:\n    name: test (${{ matrix.python-version }}, ${{ matrix.dep-resolution.name }}, ${{ matrix.os }})\n    runs-on: ${{ matrix.os }}\n    timeout-minutes: 10\n    continue-on-error: true\n    strategy:\n      matrix:\n        python-version: [\"3.10\", \"3.11\", \"3.12\", \"3.13\", \"3.14\"]\n        dep-resolution:\n          - name: lowest-direct\n            install-flags: \"--upgrade --resolution lowest-direct\"\n          - name: locked\n            install-flags: \"--frozen\"\n        os: [ubuntu-latest, windows-latest]\n\n    steps:\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\n\n      - name: Install uv\n        uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1\n        with:\n          enable-cache: true\n          version: 0.9.5\n\n      - name: Install the project\n        run: uv sync ${{ matrix.dep-resolution.install-flags }} --all-extras --python ${{ matrix.python-version }}\n\n      - name: Run pytest with coverage\n        shell: bash\n        run: |\n          uv run --frozen --no-sync coverage erase\n          uv run --frozen --no-sync coverage run -m pytest -n auto\n          uv run --frozen --no-sync coverage combine\n          uv run --frozen --no-sync coverage report\n\n      - name: Check for unnecessary no cover pragmas\n        if: runner.os != 'Windows'\n        run: uv run --frozen --no-sync strict-no-cover\n\n  readme-snippets:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\n\n      - uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1\n        with:\n          enable-cache: true\n          version: 0.9.5\n\n      - name: Install dependencies\n        run: uv sync --frozen --all-extras --python 3.10\n\n      - name: Check README snippets are up to date\n        run: uv run --frozen scripts/update_readme_snippets.py --check --readme README.v2.md\n"
  },
  {
    "path": ".github/workflows/weekly-lockfile-update.yml",
    "content": "name: Weekly Lockfile Update\n\non:\n  workflow_dispatch:\n  schedule:\n    # Every Thursday at 8:00 UTC\n    - cron: \"0 8 * * 4\"\n\npermissions:\n  contents: write\n  pull-requests: write\n\njobs:\n  update-lockfile:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2\n\n      - uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1\n        with:\n          version: 0.9.5\n\n      - name: Update lockfile\n        run: |\n          echo '## Updated Dependencies' > pr_body.md\n          echo '' >> pr_body.md\n          echo '```' >> pr_body.md\n          uv lock --upgrade 2>&1 | tee -a pr_body.md\n          echo '```' >> pr_body.md\n\n      - name: Create pull request\n        uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v7\n        with:\n          commit-message: \"chore: update uv.lock with latest dependencies\"\n          sign-commits: true\n          title: \"chore: weekly dependency update\"\n          body-path: pr_body.md\n          branch: weekly-lockfile-update\n          delete-branch: true\n          add-paths: uv.lock\n          labels: dependencies\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\nscratch/\n\n# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\nshare/python-wheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.nox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n*.py,cover\n.hypothesis/\n.pytest_cache/\n.ruff_cache/\ncover/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\ndb.sqlite3-journal\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\n.pybuilder/\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# IPython\nprofile_default/\nipython_config.py\n\n# pyenv\n#   For a library or package, you might want to ignore these files since the code is\n#   intended to run in multiple environments; otherwise, check them in:\n.python-version\n\n# pipenv\n#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.\n#   However, in case of collaboration, if having platform-specific dependencies or dependencies\n#   having no cross-platform support, pipenv may install dependencies that don't work, or not\n#   install all needed dependencies.\n#Pipfile.lock\n\n# poetry\n#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.\n#   This is especially recommended for binary packages to ensure reproducibility, and is more\n#   commonly ignored for libraries.\n#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control\n#poetry.lock\n\n# pdm\n#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.\n#pdm.lock\n#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it\n#   in version control.\n#   https://pdm.fming.dev/latest/usage/project/#working-with-version-control\n.pdm.toml\n.pdm-python\n.pdm-build/\n\n# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm\n__pypackages__/\n\n# Celery stuff\ncelerybeat-schedule\ncelerybeat.pid\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n.dmypy.json\ndmypy.json\n\n# Pyre type checker\n.pyre/\n\n# pytype static type analyzer\n.pytype/\n\n# Cython debug symbols\ncython_debug/\n\n# PyCharm\n#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can\n#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore\n#  and can be added to the global gitignore or merged into this file.  For a more nuclear\n#  option (not recommended) you can uncomment the following to ignore the entire idea folder.\n.idea/\n\n# vscode\n.vscode/\n.windsurfrules\n**/CLAUDE.local.md\n\n# claude code\nresults/\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "fail_fast: true\n\nrepos:\n  - repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v6.0.0\n    hooks:\n      - id: end-of-file-fixer\n\n  - repo: https://github.com/pre-commit/mirrors-prettier\n    rev: v3.1.0\n    hooks:\n      - id: prettier\n        types_or: [yaml, json5]\n\n  - repo: https://github.com/igorshubovych/markdownlint-cli\n    rev: v0.45.0\n    hooks:\n      - id: markdownlint\n        args:\n          [\n            \"--fix\",\n            \"--config\",\n            \"pyproject.toml\",\n            \"--configPointer\",\n            \"/tool/markdown/lint\",\n          ]\n        types: [markdown]\n\n  - repo: local\n    hooks:\n      - id: ruff-format\n        name: Ruff Format\n        entry: uv run --frozen ruff\n        args: [format]\n        language: system\n        types: [python]\n        pass_filenames: false\n      - id: ruff\n        name: Ruff\n        entry: uv run --frozen ruff\n        args: [\"check\", \"--fix\", \"--exit-non-zero-on-fix\"]\n        types: [python]\n        language: system\n        pass_filenames: false\n        exclude: ^README(\\.v2)?\\.md$\n      - id: pyright\n        name: pyright\n        entry: uv run --frozen pyright\n        language: system\n        types: [python]\n        pass_filenames: false\n      - id: uv-lock-check\n        name: Check uv.lock is up to date\n        entry: uv lock --check\n        language: system\n        files: ^(pyproject\\.toml|uv\\.lock)$\n        pass_filenames: false\n      # TODO(Max): Drop this in v2.\n      - id: readme-v1-frozen\n        name: README.md is frozen (v1 docs)\n        entry: README.md is frozen at v1. Edit README.v2.md instead.\n        language: fail\n        files: ^README\\.md$\n      - id: readme-snippets\n        name: Check README snippets are up to date\n        entry: uv run --frozen python scripts/update_readme_snippets.py --check\n        language: system\n        files: ^(README\\.v2\\.md|examples/.*\\.py|scripts/update_readme_snippets\\.py)$\n        pass_filenames: false\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# Development Guidelines\n\nThis document contains critical information about working with this codebase. Follow these guidelines precisely.\n\n## Core Development Rules\n\n1. Package Management\n   - ONLY use uv, NEVER pip\n   - Installation: `uv add <package>`\n   - Running tools: `uv run <tool>`\n   - Upgrading: `uv lock --upgrade-package <package>`\n   - FORBIDDEN: `uv pip install`, `@latest` syntax\n\n2. Code Quality\n   - Type hints required for all code\n   - Public APIs must have docstrings\n   - Functions must be focused and small\n   - Follow existing patterns exactly\n   - Line length: 120 chars maximum\n   - FORBIDDEN: imports inside functions. THEY SHOULD BE AT THE TOP OF THE FILE.\n\n3. Testing Requirements\n   - Framework: `uv run --frozen pytest`\n   - Async testing: use anyio, not asyncio\n   - Do not use `Test` prefixed classes, use functions\n   - Coverage: test edge cases and errors\n   - New features require tests\n   - Bug fixes require regression tests\n   - IMPORTANT: The `tests/client/test_client.py` is the most well designed test file. Follow its patterns.\n   - IMPORTANT: Be minimal, and focus on E2E tests: Use the `mcp.client.Client` whenever possible.\n   - Coverage: CI requires 100% (`fail_under = 100`, `branch = true`).\n     - Full check: `./scripts/test` (~23s). Runs coverage + `strict-no-cover` on the\n       default Python. Not identical to CI: CI also runs 3.10–3.14 × {ubuntu, windows},\n       and some branch-coverage quirks only surface on specific matrix entries.\n     - Targeted check while iterating (~4s, deterministic):\n\n       ```bash\n       uv run --frozen coverage erase\n       uv run --frozen coverage run -m pytest tests/path/test_foo.py\n       uv run --frozen coverage combine\n       uv run --frozen coverage report --include='src/mcp/path/foo.py' --fail-under=0\n       UV_FROZEN=1 uv run --frozen strict-no-cover\n       ```\n\n       Partial runs can't hit 100% (coverage tracks `tests/` too), so `--fail-under=0`\n       and `--include` scope the report. `strict-no-cover` has no false positives on\n       partial runs — if your new test executes a line marked `# pragma: no cover`,\n       even a single-file run catches it.\n   - Coverage pragmas:\n     - `# pragma: no cover` — line is never executed. CI's `strict-no-cover` fails if\n       it IS executed. When your test starts covering such a line, remove the pragma.\n     - `# pragma: lax no cover` — excluded from coverage but not checked by\n       `strict-no-cover`. Use for lines covered on some platforms/versions but not\n       others.\n     - `# pragma: no branch` — excludes branch arcs only. coverage.py misreports the\n       `->exit` arc for nested `async with` on Python 3.11+ (worse on 3.14/Windows).\n   - Avoid `anyio.sleep()` with a fixed duration to wait for async operations. Instead:\n     - Use `anyio.Event` — set it in the callback/handler, `await event.wait()` in the test\n     - For stream messages, use `await stream.receive()` instead of `sleep()` + `receive_nowait()`\n     - Exception: `sleep()` is appropriate when testing time-based features (e.g., timeouts)\n   - Wrap indefinite waits (`event.wait()`, `stream.receive()`) in `anyio.fail_after(5)` to prevent hangs\n\nTest files mirror the source tree: `src/mcp/client/streamable_http.py` → `tests/client/test_streamable_http.py`\nAdd tests to the existing file for that module.\n\n- For commits fixing bugs or adding features based on user reports add:\n\n  ```bash\n  git commit --trailer \"Reported-by:<name>\"\n  ```\n\n  Where `<name>` is the name of the user.\n\n- For commits related to a Github issue, add\n\n  ```bash\n  git commit --trailer \"Github-Issue:#<number>\"\n  ```\n\n- NEVER ever mention a `co-authored-by` or similar aspects. In particular, never\n  mention the tool used to create the commit message or PR.\n\n## Pull Requests\n\n- Create a detailed message of what changed. Focus on the high level description of\n  the problem it tries to solve, and how it is solved. Don't go into the specifics of the\n  code unless it adds clarity.\n\n- NEVER ever mention a `co-authored-by` or similar aspects. In particular, never\n  mention the tool used to create the commit message or PR.\n\n## Breaking Changes\n\nWhen making breaking changes, document them in `docs/migration.md`. Include:\n\n- What changed\n- Why it changed\n- How to migrate existing code\n\nSearch for related sections in the migration guide and group related changes together\nrather than adding new standalone sections.\n\n## Python Tools\n\n## Code Formatting\n\n1. Ruff\n   - Format: `uv run --frozen ruff format .`\n   - Check: `uv run --frozen ruff check .`\n   - Fix: `uv run --frozen ruff check . --fix`\n   - Critical issues:\n     - Line length (88 chars)\n     - Import sorting (I001)\n     - Unused imports\n   - Line wrapping:\n     - Strings: use parentheses\n     - Function calls: multi-line with proper indent\n     - Imports: try to use a single line\n\n2. Type Checking\n   - Tool: `uv run --frozen pyright`\n   - Requirements:\n     - Type narrowing for strings\n     - Version warnings can be ignored if checks pass\n\n3. Pre-commit\n   - Config: `.pre-commit-config.yaml`\n   - Runs: on git commit\n   - Tools: Prettier (YAML/JSON), Ruff (Python)\n   - Ruff updates:\n     - Check PyPI versions\n     - Update config rev\n     - Commit config first\n\n## Error Resolution\n\n1. CI Failures\n   - Fix order:\n     1. Formatting\n     2. Type errors\n     3. Linting\n   - Type errors:\n     - Get full line context\n     - Check Optional types\n     - Add type narrowing\n     - Verify function signatures\n\n2. Common Issues\n   - Line length:\n     - Break strings with parentheses\n     - Multi-line function calls\n     - Split imports\n   - Types:\n     - Add None checks\n     - Narrow string types\n     - Match existing patterns\n\n3. Best Practices\n   - Check git status before commits\n   - Run formatters before type checks\n   - Keep changes minimal\n   - Follow existing patterns\n   - Document public APIs\n   - Test thoroughly\n\n## Exception Handling\n\n- **Always use `logger.exception()` instead of `logger.error()` when catching exceptions**\n  - Don't include the exception in the message: `logger.exception(\"Failed\")` not `logger.exception(f\"Failed: {e}\")`\n- **Catch specific exceptions** where possible:\n  - File ops: `except (OSError, PermissionError):`\n  - JSON: `except json.JSONDecodeError:`\n  - Network: `except (ConnectionError, TimeoutError):`\n- **FORBIDDEN** `except Exception:` - unless in top-level handlers\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participation in our\ncommunity a harassment-free experience for everyone, regardless of age, body\nsize, visible or invisible disability, ethnicity, sex characteristics, gender\nidentity and expression, level of experience, education, socio-economic status,\nnationality, personal appearance, race, religion, or sexual identity\nand orientation.\n\nWe pledge to act and interact in ways that contribute to an open, welcoming,\ndiverse, inclusive, and healthy community.\n\n## Our Standards\n\nExamples of behavior that contributes to a positive environment for our\ncommunity include:\n\n* Demonstrating empathy and kindness toward other people\n* Being respectful of differing opinions, viewpoints, and experiences\n* Giving and gracefully accepting constructive feedback\n* Accepting responsibility and apologizing to those affected by our mistakes,\n  and learning from the experience\n* Focusing on what is best not just for us as individuals, but for the\n  overall community\n\nExamples of unacceptable behavior include:\n\n* The use of sexualized language or imagery, and sexual attention or\n  advances of any kind\n* Trolling, insulting or derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or email\n  address, without their explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n  professional setting\n\n## Enforcement Responsibilities\n\nCommunity leaders are responsible for clarifying and enforcing our standards of\nacceptable behavior and will take appropriate and fair corrective action in\nresponse to any behavior that they deem inappropriate, threatening, offensive,\nor harmful.\n\nCommunity leaders have the right and responsibility to remove, edit, or reject\ncomments, commits, code, wiki edits, issues, and other contributions that are\nnot aligned to this Code of Conduct, and will communicate reasons for moderation\ndecisions when appropriate.\n\n## Scope\n\nThis Code of Conduct applies within all community spaces, and also applies when\nan individual is officially representing the community in public spaces.\nExamples of representing our community include using an official e-mail address,\nposting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported to the community leaders responsible for enforcement at\n<mcp-coc@anthropic.com>.\nAll complaints will be reviewed and investigated promptly and fairly.\n\nAll community leaders are obligated to respect the privacy and security of the\nreporter of any incident.\n\n## Enforcement Guidelines\n\nCommunity leaders will follow these Community Impact Guidelines in determining\nthe consequences for any action they deem in violation of this Code of Conduct:\n\n### 1. Correction\n\n**Community Impact**: Use of inappropriate language or other behavior deemed\nunprofessional or unwelcome in the community.\n\n**Consequence**: A private, written warning from community leaders, providing\nclarity around the nature of the violation and an explanation of why the\nbehavior was inappropriate. A public apology may be requested.\n\n### 2. Warning\n\n**Community Impact**: A violation through a single incident or series\nof actions.\n\n**Consequence**: A warning with consequences for continued behavior. No\ninteraction with the people involved, including unsolicited interaction with\nthose enforcing the Code of Conduct, for a specified period of time. This\nincludes avoiding interactions in community spaces as well as external channels\nlike social media. Violating these terms may lead to a temporary or\npermanent ban.\n\n### 3. Temporary Ban\n\n**Community Impact**: A serious violation of community standards, including\nsustained inappropriate behavior.\n\n**Consequence**: A temporary ban from any sort of interaction or public\ncommunication with the community for a specified period of time. No public or\nprivate interaction with the people involved, including unsolicited interaction\nwith those enforcing the Code of Conduct, is allowed during this period.\nViolating these terms may lead to a permanent ban.\n\n### 4. Permanent Ban\n\n**Community Impact**: Demonstrating a pattern of violation of community\nstandards, including sustained inappropriate behavior,  harassment of an\nindividual, or aggression toward or disparagement of classes of individuals.\n\n**Consequence**: A permanent ban from any sort of public interaction within\nthe community.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage],\nversion 2.0, available at\n<https://www.contributor-covenant.org/version/2/0/code_of_conduct.html>.\n\nCommunity Impact Guidelines were inspired by [Mozilla's code of conduct\nenforcement ladder](https://github.com/mozilla/diversity).\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see the FAQ at\n<https://www.contributor-covenant.org/faq>. Translations are available at\n<https://www.contributor-covenant.org/translations>.\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\nThank you for your interest in contributing to the MCP Python SDK! This document provides guidelines and instructions for contributing.\n\n## Before You Start\n\nWe welcome contributions! These guidelines exist to save everyone time, yours included. Following them means your work is more likely to be accepted.\n\n**All pull requests require a corresponding issue.** Unless your change is trivial (typo, docs tweak, broken link), create an issue first. Every merged feature becomes ongoing maintenance, so we need to agree something is worth doing before reviewing code. PRs without a linked issue will be closed.\n\nHaving an issue doesn't guarantee acceptance. Wait for maintainer feedback or a `ready for work` label before starting. PRs for issues without buy-in may also be closed.\n\nUse issues to validate your idea before investing time in code. PRs are for execution, not exploration.\n\n### The SDK is Opinionated\n\nNot every contribution will be accepted, even with a working implementation. We prioritize maintainability and consistency over adding capabilities. This is at maintainers' discretion.\n\n### What Needs Discussion\n\nThese always require an issue first:\n\n- New public APIs or decorators\n- Architectural changes or refactoring\n- Changes that touch multiple modules\n- Features that might require spec changes (these need a [SEP](https://github.com/modelcontextprotocol/modelcontextprotocol) first)\n\nBug fixes for clear, reproducible issues are welcome—but still create an issue to track the fix.\n\n### Finding Issues to Work On\n\n| Label | For | Description |\n|-------|-----|-------------|\n| [`good first issue`](https://github.com/modelcontextprotocol/python-sdk/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) | Newcomers | Can tackle without deep codebase knowledge |\n| [`help wanted`](https://github.com/modelcontextprotocol/python-sdk/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) | Experienced contributors | Maintainers probably won't get to this |\n| [`ready for work`](https://github.com/modelcontextprotocol/python-sdk/issues?q=is%3Aopen+is%3Aissue+label%3A%22ready+for+work%22) | Maintainers | Triaged and ready for a maintainer to pick up |\n\nIssues labeled `needs confirmation` or `needs maintainer action` are **not** ready for work—wait for maintainer input first.\n\nBefore starting, comment on the issue so we can assign it to you. This prevents duplicate effort.\n\n## Development Setup\n\n1. Make sure you have Python 3.10+ installed\n2. Install [uv](https://docs.astral.sh/uv/getting-started/installation/)\n3. Fork the repository\n4. Clone your fork: `git clone https://github.com/YOUR-USERNAME/python-sdk.git`\n5. Install dependencies:\n\n```bash\nuv sync --frozen --all-extras --dev\n```\n\n6. Set up pre-commit hooks:\n\n```bash\nuv tool install pre-commit --with pre-commit-uv --force-reinstall\n```\n\n## Development Workflow\n\n1. Choose the correct branch for your changes:\n\n   | Change Type | Target Branch | Example |\n   |-------------|---------------|---------|\n   | New features, breaking changes | `main` | New APIs, refactors |\n   | Security fixes for v1 | `v1.x` | Critical patches |\n   | Bug fixes for v1 | `v1.x` | Non-breaking fixes |\n\n   > **Note:** `main` is the v2 development branch. Breaking changes are welcome on `main`. The `v1.x` branch receives only security and critical bug fixes.\n\n2. Create a new branch from your chosen base branch\n\n3. Make your changes\n\n4. Ensure tests pass:\n\n```bash\nuv run pytest\n```\n\n5. Run type checking:\n\n```bash\nuv run pyright\n```\n\n6. Run linting:\n\n```bash\nuv run ruff check .\nuv run ruff format .\n```\n\n7. Update README snippets if you modified example code:\n\n```bash\nuv run scripts/update_readme_snippets.py\n```\n\n8. (Optional) Run pre-commit hooks on all files:\n\n```bash\npre-commit run --all-files\n```\n\n9. Submit a pull request to the same branch you branched from\n\n## Code Style\n\n- We use `ruff` for linting and formatting\n- Follow PEP 8 style guidelines\n- Add type hints to all functions\n- Include docstrings for public APIs\n\n## Pull Requests\n\nBy the time you open a PR, the \"what\" and \"why\" should already be settled in an issue. This keeps reviews focused on implementation.\n\n### Scope\n\nSmall PRs get reviewed fast. Large PRs sit in the queue.\n\nA few dozen lines can be reviewed in minutes. Hundreds of lines across many files takes real effort and things slip through. If your change is big, break it into smaller PRs or get alignment from a maintainer first.\n\n### What Gets Rejected\n\n- **No prior discussion**: Features or significant changes without an approved issue\n- **Scope creep**: Changes that go beyond what was discussed\n- **Misalignment**: Even well-implemented features may be rejected if they don't fit the SDK's direction\n- **Overengineering**: Unnecessary complexity for simple problems\n\n### Checklist\n\n1. Update documentation as needed\n2. Add tests for new functionality\n3. Ensure CI passes\n4. Address review feedback\n\n## Code of Conduct\n\nPlease note that this project is released with a [Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms.\n\n## License\n\nBy contributing, you agree that your contributions will be licensed under the MIT License.\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2024 Anthropic, PBC\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# MCP Python SDK\n\n<div align=\"center\">\n\n<strong>Python implementation of the Model Context Protocol (MCP)</strong>\n\n[![PyPI][pypi-badge]][pypi-url]\n[![MIT licensed][mit-badge]][mit-url]\n[![Python Version][python-badge]][python-url]\n[![Documentation][docs-badge]][docs-url]\n[![Protocol][protocol-badge]][protocol-url]\n[![Specification][spec-badge]][spec-url]\n\n</div>\n\n<!-- TODO(v2): Replace this README with README.v2.md when v2 is released -->\n\n> [!NOTE]\n> **This README documents v1.x of the MCP Python SDK (the current stable release).**\n>\n> For v1.x code and documentation, see the [`v1.x` branch](https://github.com/modelcontextprotocol/python-sdk/tree/v1.x).\n> For the upcoming v2 documentation (pre-alpha, in development on `main`), see [`README.v2.md`](README.v2.md).\n\n<!-- omit in toc -->\n## Table of Contents\n\n- [MCP Python SDK](#mcp-python-sdk)\n  - [Overview](#overview)\n  - [Installation](#installation)\n    - [Adding MCP to your python project](#adding-mcp-to-your-python-project)\n    - [Running the standalone MCP development tools](#running-the-standalone-mcp-development-tools)\n  - [Quickstart](#quickstart)\n  - [What is MCP?](#what-is-mcp)\n  - [Core Concepts](#core-concepts)\n    - [Server](#server)\n    - [Resources](#resources)\n    - [Tools](#tools)\n      - [Structured Output](#structured-output)\n    - [Prompts](#prompts)\n    - [Images](#images)\n    - [Context](#context)\n      - [Getting Context in Functions](#getting-context-in-functions)\n      - [Context Properties and Methods](#context-properties-and-methods)\n    - [Completions](#completions)\n    - [Elicitation](#elicitation)\n    - [Sampling](#sampling)\n    - [Logging and Notifications](#logging-and-notifications)\n    - [Authentication](#authentication)\n    - [FastMCP Properties](#fastmcp-properties)\n    - [Session Properties and Methods](#session-properties-and-methods)\n    - [Request Context Properties](#request-context-properties)\n  - [Running Your Server](#running-your-server)\n    - [Development Mode](#development-mode)\n    - [Claude Desktop Integration](#claude-desktop-integration)\n    - [Direct Execution](#direct-execution)\n    - [Streamable HTTP Transport](#streamable-http-transport)\n      - [CORS Configuration for Browser-Based Clients](#cors-configuration-for-browser-based-clients)\n    - [Mounting to an Existing ASGI Server](#mounting-to-an-existing-asgi-server)\n      - [StreamableHTTP servers](#streamablehttp-servers)\n        - [Basic mounting](#basic-mounting)\n        - [Host-based routing](#host-based-routing)\n        - [Multiple servers with path configuration](#multiple-servers-with-path-configuration)\n        - [Path configuration at initialization](#path-configuration-at-initialization)\n      - [SSE servers](#sse-servers)\n  - [Advanced Usage](#advanced-usage)\n    - [Low-Level Server](#low-level-server)\n      - [Structured Output Support](#structured-output-support)\n    - [Pagination (Advanced)](#pagination-advanced)\n    - [Writing MCP Clients](#writing-mcp-clients)\n    - [Client Display Utilities](#client-display-utilities)\n    - [OAuth Authentication for Clients](#oauth-authentication-for-clients)\n    - [Parsing Tool Results](#parsing-tool-results)\n    - [MCP Primitives](#mcp-primitives)\n    - [Server Capabilities](#server-capabilities)\n  - [Documentation](#documentation)\n  - [Contributing](#contributing)\n  - [License](#license)\n\n[pypi-badge]: https://img.shields.io/pypi/v/mcp.svg\n[pypi-url]: https://pypi.org/project/mcp/\n[mit-badge]: https://img.shields.io/pypi/l/mcp.svg\n[mit-url]: https://github.com/modelcontextprotocol/python-sdk/blob/main/LICENSE\n[python-badge]: https://img.shields.io/pypi/pyversions/mcp.svg\n[python-url]: https://www.python.org/downloads/\n[docs-badge]: https://img.shields.io/badge/docs-python--sdk-blue.svg\n[docs-url]: https://modelcontextprotocol.github.io/python-sdk/\n[protocol-badge]: https://img.shields.io/badge/protocol-modelcontextprotocol.io-blue.svg\n[protocol-url]: https://modelcontextprotocol.io\n[spec-badge]: https://img.shields.io/badge/spec-spec.modelcontextprotocol.io-blue.svg\n[spec-url]: https://modelcontextprotocol.io/specification/latest\n\n## Overview\n\nThe Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This Python SDK implements the full MCP specification, making it easy to:\n\n- Build MCP clients that can connect to any MCP server\n- Create MCP servers that expose resources, prompts and tools\n- Use standard transports like stdio, SSE, and Streamable HTTP\n- Handle all MCP protocol messages and lifecycle events\n\n## Installation\n\n### Adding MCP to your python project\n\nWe recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects.\n\nIf you haven't created a uv-managed project yet, create one:\n\n   ```bash\n   uv init mcp-server-demo\n   cd mcp-server-demo\n   ```\n\n   Then add MCP to your project dependencies:\n\n   ```bash\n   uv add \"mcp[cli]\"\n   ```\n\nAlternatively, for projects using pip for dependencies:\n\n```bash\npip install \"mcp[cli]\"\n```\n\n### Running the standalone MCP development tools\n\nTo run the mcp command with uv:\n\n```bash\nuv run mcp\n```\n\n## Quickstart\n\nLet's create a simple MCP server that exposes a calculator tool and some data:\n\n<!-- snippet-source examples/snippets/servers/fastmcp_quickstart.py -->\n```python\n\"\"\"\nFastMCP quickstart example.\n\nRun from the repository root:\n    uv run examples/snippets/servers/fastmcp_quickstart.py\n\"\"\"\n\nfrom mcp.server.fastmcp import FastMCP\n\n# Create an MCP server\nmcp = FastMCP(\"Demo\", json_response=True)\n\n\n# Add an addition tool\n@mcp.tool()\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers\"\"\"\n    return a + b\n\n\n# Add a dynamic greeting resource\n@mcp.resource(\"greeting://{name}\")\ndef get_greeting(name: str) -> str:\n    \"\"\"Get a personalized greeting\"\"\"\n    return f\"Hello, {name}!\"\n\n\n# Add a prompt\n@mcp.prompt()\ndef greet_user(name: str, style: str = \"friendly\") -> str:\n    \"\"\"Generate a greeting prompt\"\"\"\n    styles = {\n        \"friendly\": \"Please write a warm, friendly greeting\",\n        \"formal\": \"Please write a formal, professional greeting\",\n        \"casual\": \"Please write a casual, relaxed greeting\",\n    }\n\n    return f\"{styles.get(style, styles['friendly'])} for someone named {name}.\"\n\n\n# Run with streamable HTTP transport\nif __name__ == \"__main__\":\n    mcp.run(transport=\"streamable-http\")\n```\n\n_Full example: [examples/snippets/servers/fastmcp_quickstart.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/fastmcp_quickstart.py)_\n<!-- /snippet-source -->\n\nYou can install this server in [Claude Code](https://docs.claude.com/en/docs/claude-code/mcp) and interact with it right away. First, run the server:\n\n```bash\nuv run --with mcp examples/snippets/servers/fastmcp_quickstart.py\n```\n\nThen add it to Claude Code:\n\n```bash\nclaude mcp add --transport http my-server http://localhost:8000/mcp\n```\n\nAlternatively, you can test it with the MCP Inspector. Start the server as above, then in a separate terminal:\n\n```bash\nnpx -y @modelcontextprotocol/inspector\n```\n\nIn the inspector UI, connect to `http://localhost:8000/mcp`.\n\n## What is MCP?\n\nThe [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can:\n\n- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context)\n- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)\n- Define interaction patterns through **Prompts** (reusable templates for LLM interactions)\n- And more!\n\n## Core Concepts\n\n### Server\n\nThe FastMCP server is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing:\n\n<!-- snippet-source examples/snippets/servers/lifespan_example.py -->\n```python\n\"\"\"Example showing lifespan support for startup/shutdown with strong typing.\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom dataclasses import dataclass\n\nfrom mcp.server.fastmcp import Context, FastMCP\nfrom mcp.server.session import ServerSession\n\n\n# Mock database class for example\nclass Database:\n    \"\"\"Mock database class for example.\"\"\"\n\n    @classmethod\n    async def connect(cls) -> \"Database\":\n        \"\"\"Connect to database.\"\"\"\n        return cls()\n\n    async def disconnect(self) -> None:\n        \"\"\"Disconnect from database.\"\"\"\n        pass\n\n    def query(self) -> str:\n        \"\"\"Execute a query.\"\"\"\n        return \"Query result\"\n\n\n@dataclass\nclass AppContext:\n    \"\"\"Application context with typed dependencies.\"\"\"\n\n    db: Database\n\n\n@asynccontextmanager\nasync def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:\n    \"\"\"Manage application lifecycle with type-safe context.\"\"\"\n    # Initialize on startup\n    db = await Database.connect()\n    try:\n        yield AppContext(db=db)\n    finally:\n        # Cleanup on shutdown\n        await db.disconnect()\n\n\n# Pass lifespan to server\nmcp = FastMCP(\"My App\", lifespan=app_lifespan)\n\n\n# Access type-safe lifespan context in tools\n@mcp.tool()\ndef query_db(ctx: Context[ServerSession, AppContext]) -> str:\n    \"\"\"Tool that uses initialized resources.\"\"\"\n    db = ctx.request_context.lifespan_context.db\n    return db.query()\n```\n\n_Full example: [examples/snippets/servers/lifespan_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lifespan_example.py)_\n<!-- /snippet-source -->\n\n### Resources\n\nResources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects:\n\n<!-- snippet-source examples/snippets/servers/basic_resource.py -->\n```python\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Resource Example\")\n\n\n@mcp.resource(\"file://documents/{name}\")\ndef read_document(name: str) -> str:\n    \"\"\"Read a document by name.\"\"\"\n    # This would normally read from disk\n    return f\"Content of {name}\"\n\n\n@mcp.resource(\"config://settings\")\ndef get_settings() -> str:\n    \"\"\"Get application settings.\"\"\"\n    return \"\"\"{\n  \"theme\": \"dark\",\n  \"language\": \"en\",\n  \"debug\": false\n}\"\"\"\n```\n\n_Full example: [examples/snippets/servers/basic_resource.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_resource.py)_\n<!-- /snippet-source -->\n\n### Tools\n\nTools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects:\n\n<!-- snippet-source examples/snippets/servers/basic_tool.py -->\n```python\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(name=\"Tool Example\")\n\n\n@mcp.tool()\ndef sum(a: int, b: int) -> int:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n\n\n@mcp.tool()\ndef get_weather(city: str, unit: str = \"celsius\") -> str:\n    \"\"\"Get weather for a city.\"\"\"\n    # This would normally call a weather API\n    return f\"Weather in {city}: 22degrees{unit[0].upper()}\"\n```\n\n_Full example: [examples/snippets/servers/basic_tool.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_tool.py)_\n<!-- /snippet-source -->\n\nTools can optionally receive a Context object by including a parameter with the `Context` type annotation. This context is automatically injected by the FastMCP framework and provides access to MCP capabilities:\n\n<!-- snippet-source examples/snippets/servers/tool_progress.py -->\n```python\nfrom mcp.server.fastmcp import Context, FastMCP\nfrom mcp.server.session import ServerSession\n\nmcp = FastMCP(name=\"Progress Example\")\n\n\n@mcp.tool()\nasync def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str:\n    \"\"\"Execute a task with progress updates.\"\"\"\n    await ctx.info(f\"Starting: {task_name}\")\n\n    for i in range(steps):\n        progress = (i + 1) / steps\n        await ctx.report_progress(\n            progress=progress,\n            total=1.0,\n            message=f\"Step {i + 1}/{steps}\",\n        )\n        await ctx.debug(f\"Completed step {i + 1}\")\n\n    return f\"Task '{task_name}' completed\"\n```\n\n_Full example: [examples/snippets/servers/tool_progress.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/tool_progress.py)_\n<!-- /snippet-source -->\n\n#### Structured Output\n\nTools will return structured results by default, if their return type\nannotation is compatible. Otherwise, they will return unstructured results.\n\nStructured output supports these return types:\n\n- Pydantic models (BaseModel subclasses)\n- TypedDicts\n- Dataclasses and other classes with type hints\n- `dict[str, T]` (where T is any JSON-serializable type)\n- Primitive types (str, int, float, bool, bytes, None) - wrapped in `{\"result\": value}`\n- Generic types (list, tuple, Union, Optional, etc.) - wrapped in `{\"result\": value}`\n\nClasses without type hints cannot be serialized for structured output. Only\nclasses with properly annotated attributes will be converted to Pydantic models\nfor schema generation and validation.\n\nStructured results are automatically validated against the output schema\ngenerated from the annotation. This ensures the tool returns well-typed,\nvalidated data that clients can easily process.\n\n**Note:** For backward compatibility, unstructured results are also\nreturned. Unstructured results are provided for backward compatibility\nwith previous versions of the MCP specification, and are quirks-compatible\nwith previous versions of FastMCP in the current version of the SDK.\n\n**Note:** In cases where a tool function's return type annotation\ncauses the tool to be classified as structured _and this is undesirable_,\nthe  classification can be suppressed by passing `structured_output=False`\nto the `@tool` decorator.\n\n##### Advanced: Direct CallToolResult\n\nFor full control over tool responses including the `_meta` field (for passing data to client applications without exposing it to the model), you can return `CallToolResult` directly:\n\n<!-- snippet-source examples/snippets/servers/direct_call_tool_result.py -->\n```python\n\"\"\"Example showing direct CallToolResult return for advanced control.\"\"\"\n\nfrom typing import Annotated\n\nfrom pydantic import BaseModel\n\nfrom mcp.server.fastmcp import FastMCP\nfrom mcp.types import CallToolResult, TextContent\n\nmcp = FastMCP(\"CallToolResult Example\")\n\n\nclass ValidationModel(BaseModel):\n    \"\"\"Model for validating structured output.\"\"\"\n\n    status: str\n    data: dict[str, int]\n\n\n@mcp.tool()\ndef advanced_tool() -> CallToolResult:\n    \"\"\"Return CallToolResult directly for full control including _meta field.\"\"\"\n    return CallToolResult(\n        content=[TextContent(type=\"text\", text=\"Response visible to the model\")],\n        _meta={\"hidden\": \"data for client applications only\"},\n    )\n\n\n@mcp.tool()\ndef validated_tool() -> Annotated[CallToolResult, ValidationModel]:\n    \"\"\"Return CallToolResult with structured output validation.\"\"\"\n    return CallToolResult(\n        content=[TextContent(type=\"text\", text=\"Validated response\")],\n        structuredContent={\"status\": \"success\", \"data\": {\"result\": 42}},\n        _meta={\"internal\": \"metadata\"},\n    )\n\n\n@mcp.tool()\ndef empty_result_tool() -> CallToolResult:\n    \"\"\"For empty results, return CallToolResult with empty content.\"\"\"\n    return CallToolResult(content=[])\n```\n\n_Full example: [examples/snippets/servers/direct_call_tool_result.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/direct_call_tool_result.py)_\n<!-- /snippet-source -->\n\n**Important:** `CallToolResult` must always be returned (no `Optional` or `Union`). For empty results, use `CallToolResult(content=[])`. For optional simple types, use `str | None` without `CallToolResult`.\n\n<!-- snippet-source examples/snippets/servers/structured_output.py -->\n```python\n\"\"\"Example showing structured output with tools.\"\"\"\n\nfrom typing import TypedDict\n\nfrom pydantic import BaseModel, Field\n\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(\"Structured Output Example\")\n\n\n# Using Pydantic models for rich structured data\nclass WeatherData(BaseModel):\n    \"\"\"Weather information structure.\"\"\"\n\n    temperature: float = Field(description=\"Temperature in Celsius\")\n    humidity: float = Field(description=\"Humidity percentage\")\n    condition: str\n    wind_speed: float\n\n\n@mcp.tool()\ndef get_weather(city: str) -> WeatherData:\n    \"\"\"Get weather for a city - returns structured data.\"\"\"\n    # Simulated weather data\n    return WeatherData(\n        temperature=22.5,\n        humidity=45.0,\n        condition=\"sunny\",\n        wind_speed=5.2,\n    )\n\n\n# Using TypedDict for simpler structures\nclass LocationInfo(TypedDict):\n    latitude: float\n    longitude: float\n    name: str\n\n\n@mcp.tool()\ndef get_location(address: str) -> LocationInfo:\n    \"\"\"Get location coordinates\"\"\"\n    return LocationInfo(latitude=51.5074, longitude=-0.1278, name=\"London, UK\")\n\n\n# Using dict[str, Any] for flexible schemas\n@mcp.tool()\ndef get_statistics(data_type: str) -> dict[str, float]:\n    \"\"\"Get various statistics\"\"\"\n    return {\"mean\": 42.5, \"median\": 40.0, \"std_dev\": 5.2}\n\n\n# Ordinary classes with type hints work for structured output\nclass UserProfile:\n    name: str\n    age: int\n    email: str | None = None\n\n    def __init__(self, name: str, age: int, email: str | None = None):\n        self.name = name\n        self.age = age\n        self.email = email\n\n\n@mcp.tool()\ndef get_user(user_id: str) -> UserProfile:\n    \"\"\"Get user profile - returns structured data\"\"\"\n    return UserProfile(name=\"Alice\", age=30, email=\"alice@example.com\")\n\n\n# Classes WITHOUT type hints cannot be used for structured output\nclass UntypedConfig:\n    def __init__(self, setting1, setting2):  # type: ignore[reportMissingParameterType]\n        self.setting1 = setting1\n        self.setting2 = setting2\n\n\n@mcp.tool()\ndef get_config() -> UntypedConfig:\n    \"\"\"This returns unstructured output - no schema generated\"\"\"\n    return UntypedConfig(\"value1\", \"value2\")\n\n\n# Lists and other types are wrapped automatically\n@mcp.tool()\ndef list_cities() -> list[str]:\n    \"\"\"Get a list of cities\"\"\"\n    return [\"London\", \"Paris\", \"Tokyo\"]\n    # Returns: {\"result\": [\"London\", \"Paris\", \"Tokyo\"]}\n\n\n@mcp.tool()\ndef get_temperature(city: str) -> float:\n    \"\"\"Get temperature as a simple float\"\"\"\n    return 22.5\n    # Returns: {\"result\": 22.5}\n```\n\n_Full example: [examples/snippets/servers/structured_output.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/structured_output.py)_\n<!-- /snippet-source -->\n\n### Prompts\n\nPrompts are reusable templates that help LLMs interact with your server effectively:\n\n<!-- snippet-source examples/snippets/servers/basic_prompt.py -->\n```python\nfrom mcp.server.fastmcp import FastMCP\nfrom mcp.server.fastmcp.prompts import base\n\nmcp = FastMCP(name=\"Prompt Example\")\n\n\n@mcp.prompt(title=\"Code Review\")\ndef review_code(code: str) -> str:\n    return f\"Please review this code:\\n\\n{code}\"\n\n\n@mcp.prompt(title=\"Debug Assistant\")\ndef debug_error(error: str) -> list[base.Message]:\n    return [\n        base.UserMessage(\"I'm seeing this error:\"),\n        base.UserMessage(error),\n        base.AssistantMessage(\"I'll help debug that. What have you tried so far?\"),\n    ]\n```\n\n_Full example: [examples/snippets/servers/basic_prompt.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_prompt.py)_\n<!-- /snippet-source -->\n\n### Icons\n\nMCP servers can provide icons for UI display. Icons can be added to the server implementation, tools, resources, and prompts:\n\n```python\nfrom mcp.server.fastmcp import FastMCP, Icon\n\n# Create an icon from a file path or URL\nicon = Icon(\n    src=\"icon.png\",\n    mimeType=\"image/png\",\n    sizes=\"64x64\"\n)\n\n# Add icons to server\nmcp = FastMCP(\n    \"My Server\",\n    website_url=\"https://example.com\",\n    icons=[icon]\n)\n\n# Add icons to tools, resources, and prompts\n@mcp.tool(icons=[icon])\ndef my_tool():\n    \"\"\"Tool with an icon.\"\"\"\n    return \"result\"\n\n@mcp.resource(\"demo://resource\", icons=[icon])\ndef my_resource():\n    \"\"\"Resource with an icon.\"\"\"\n    return \"content\"\n```\n\n_Full example: [examples/fastmcp/icons_demo.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/fastmcp/icons_demo.py)_\n\n### Images\n\nFastMCP provides an `Image` class that automatically handles image data:\n\n<!-- snippet-source examples/snippets/servers/images.py -->\n```python\n\"\"\"Example showing image handling with FastMCP.\"\"\"\n\nfrom PIL import Image as PILImage\n\nfrom mcp.server.fastmcp import FastMCP, Image\n\nmcp = FastMCP(\"Image Example\")\n\n\n@mcp.tool()\ndef create_thumbnail(image_path: str) -> Image:\n    \"\"\"Create a thumbnail from an image\"\"\"\n    img = PILImage.open(image_path)\n    img.thumbnail((100, 100))\n    return Image(data=img.tobytes(), format=\"png\")\n```\n\n_Full example: [examples/snippets/servers/images.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/images.py)_\n<!-- /snippet-source -->\n\n### Context\n\nThe Context object is automatically injected into tool and resource functions that request it via type hints. It provides access to MCP capabilities like logging, progress reporting, resource reading, user interaction, and request metadata.\n\n#### Getting Context in Functions\n\nTo use context in a tool or resource function, add a parameter with the `Context` type annotation:\n\n```python\nfrom mcp.server.fastmcp import Context, FastMCP\n\nmcp = FastMCP(name=\"Context Example\")\n\n\n@mcp.tool()\nasync def my_tool(x: int, ctx: Context) -> str:\n    \"\"\"Tool that uses context capabilities.\"\"\"\n    # The context parameter can have any name as long as it's type-annotated\n    return await process_with_context(x, ctx)\n```\n\n#### Context Properties and Methods\n\nThe Context object provides the following capabilities:\n\n- `ctx.request_id` - Unique ID for the current request\n- `ctx.client_id` - Client ID if available\n- `ctx.fastmcp` - Access to the FastMCP server instance (see [FastMCP Properties](#fastmcp-properties))\n- `ctx.session` - Access to the underlying session for advanced communication (see [Session Properties and Methods](#session-properties-and-methods))\n- `ctx.request_context` - Access to request-specific data and lifespan resources (see [Request Context Properties](#request-context-properties))\n- `await ctx.debug(message)` - Send debug log message\n- `await ctx.info(message)` - Send info log message  \n- `await ctx.warning(message)` - Send warning log message\n- `await ctx.error(message)` - Send error log message\n- `await ctx.log(level, message, logger_name=None)` - Send log with custom level\n- `await ctx.report_progress(progress, total=None, message=None)` - Report operation progress\n- `await ctx.read_resource(uri)` - Read a resource by URI\n- `await ctx.elicit(message, schema)` - Request additional information from user with validation\n\n<!-- snippet-source examples/snippets/servers/tool_progress.py -->\n```python\nfrom mcp.server.fastmcp import Context, FastMCP\nfrom mcp.server.session import ServerSession\n\nmcp = FastMCP(name=\"Progress Example\")\n\n\n@mcp.tool()\nasync def long_running_task(task_name: str, ctx: Context[ServerSession, None], steps: int = 5) -> str:\n    \"\"\"Execute a task with progress updates.\"\"\"\n    await ctx.info(f\"Starting: {task_name}\")\n\n    for i in range(steps):\n        progress = (i + 1) / steps\n        await ctx.report_progress(\n            progress=progress,\n            total=1.0,\n            message=f\"Step {i + 1}/{steps}\",\n        )\n        await ctx.debug(f\"Completed step {i + 1}\")\n\n    return f\"Task '{task_name}' completed\"\n```\n\n_Full example: [examples/snippets/servers/tool_progress.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/tool_progress.py)_\n<!-- /snippet-source -->\n\n### Completions\n\nMCP supports providing completion suggestions for prompt arguments and resource template parameters. With the context parameter, servers can provide completions based on previously resolved values:\n\nClient usage:\n\n<!-- snippet-source examples/snippets/clients/completion_client.py -->\n```python\n\"\"\"\ncd to the `examples/snippets` directory and run:\n    uv run completion-client\n\"\"\"\n\nimport asyncio\nimport os\n\nfrom mcp import ClientSession, StdioServerParameters\nfrom mcp.client.stdio import stdio_client\nfrom mcp.types import PromptReference, ResourceTemplateReference\n\n# Create server parameters for stdio connection\nserver_params = StdioServerParameters(\n    command=\"uv\",  # Using uv to run the server\n    args=[\"run\", \"server\", \"completion\", \"stdio\"],  # Server with completion support\n    env={\"UV_INDEX\": os.environ.get(\"UV_INDEX\", \"\")},\n)\n\n\nasync def run():\n    \"\"\"Run the completion client example.\"\"\"\n    async with stdio_client(server_params) as (read, write):\n        async with ClientSession(read, write) as session:\n            # Initialize the connection\n            await session.initialize()\n\n            # List available resource templates\n            templates = await session.list_resource_templates()\n            print(\"Available resource templates:\")\n            for template in templates.resourceTemplates:\n                print(f\"  - {template.uriTemplate}\")\n\n            # List available prompts\n            prompts = await session.list_prompts()\n            print(\"\\nAvailable prompts:\")\n            for prompt in prompts.prompts:\n                print(f\"  - {prompt.name}\")\n\n            # Complete resource template arguments\n            if templates.resourceTemplates:\n                template = templates.resourceTemplates[0]\n                print(f\"\\nCompleting arguments for resource template: {template.uriTemplate}\")\n\n                # Complete without context\n                result = await session.complete(\n                    ref=ResourceTemplateReference(type=\"ref/resource\", uri=template.uriTemplate),\n                    argument={\"name\": \"owner\", \"value\": \"model\"},\n                )\n                print(f\"Completions for 'owner' starting with 'model': {result.completion.values}\")\n\n                # Complete with context - repo suggestions based on owner\n                result = await session.complete(\n                    ref=ResourceTemplateReference(type=\"ref/resource\", uri=template.uriTemplate),\n                    argument={\"name\": \"repo\", \"value\": \"\"},\n                    context_arguments={\"owner\": \"modelcontextprotocol\"},\n                )\n                print(f\"Completions for 'repo' with owner='modelcontextprotocol': {result.completion.values}\")\n\n            # Complete prompt arguments\n            if prompts.prompts:\n                prompt_name = prompts.prompts[0].name\n                print(f\"\\nCompleting arguments for prompt: {prompt_name}\")\n\n                result = await session.complete(\n                    ref=PromptReference(type=\"ref/prompt\", name=prompt_name),\n                    argument={\"name\": \"style\", \"value\": \"\"},\n                )\n                print(f\"Completions for 'style' argument: {result.completion.values}\")\n\n\ndef main():\n    \"\"\"Entry point for the completion client.\"\"\"\n    asyncio.run(run())\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n_Full example: [examples/snippets/clients/completion_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/completion_client.py)_\n<!-- /snippet-source -->\n### Elicitation\n\nRequest additional information from users. This example shows an Elicitation during a Tool Call:\n\n<!-- snippet-source examples/snippets/servers/elicitation.py -->\n```python\n\"\"\"Elicitation examples demonstrating form and URL mode elicitation.\n\nForm mode elicitation collects structured, non-sensitive data through a schema.\nURL mode elicitation directs users to external URLs for sensitive operations\nlike OAuth flows, credential collection, or payment processing.\n\"\"\"\n\nimport uuid\n\nfrom pydantic import BaseModel, Field\n\nfrom mcp.server.fastmcp import Context, FastMCP\nfrom mcp.server.session import ServerSession\nfrom mcp.shared.exceptions import UrlElicitationRequiredError\nfrom mcp.types import ElicitRequestURLParams\n\nmcp = FastMCP(name=\"Elicitation Example\")\n\n\nclass BookingPreferences(BaseModel):\n    \"\"\"Schema for collecting user preferences.\"\"\"\n\n    checkAlternative: bool = Field(description=\"Would you like to check another date?\")\n    alternativeDate: str = Field(\n        default=\"2024-12-26\",\n        description=\"Alternative date (YYYY-MM-DD)\",\n    )\n\n\n@mcp.tool()\nasync def book_table(date: str, time: str, party_size: int, ctx: Context[ServerSession, None]) -> str:\n    \"\"\"Book a table with date availability check.\n\n    This demonstrates form mode elicitation for collecting non-sensitive user input.\n    \"\"\"\n    # Check if date is available\n    if date == \"2024-12-25\":\n        # Date unavailable - ask user for alternative\n        result = await ctx.elicit(\n            message=(f\"No tables available for {party_size} on {date}. Would you like to try another date?\"),\n            schema=BookingPreferences,\n        )\n\n        if result.action == \"accept\" and result.data:\n            if result.data.checkAlternative:\n                return f\"[SUCCESS] Booked for {result.data.alternativeDate}\"\n            return \"[CANCELLED] No booking made\"\n        return \"[CANCELLED] Booking cancelled\"\n\n    # Date available\n    return f\"[SUCCESS] Booked for {date} at {time}\"\n\n\n@mcp.tool()\nasync def secure_payment(amount: float, ctx: Context[ServerSession, None]) -> str:\n    \"\"\"Process a secure payment requiring URL confirmation.\n\n    This demonstrates URL mode elicitation using ctx.elicit_url() for\n    operations that require out-of-band user interaction.\n    \"\"\"\n    elicitation_id = str(uuid.uuid4())\n\n    result = await ctx.elicit_url(\n        message=f\"Please confirm payment of ${amount:.2f}\",\n        url=f\"https://payments.example.com/confirm?amount={amount}&id={elicitation_id}\",\n        elicitation_id=elicitation_id,\n    )\n\n    if result.action == \"accept\":\n        # In a real app, the payment confirmation would happen out-of-band\n        # and you'd verify the payment status from your backend\n        return f\"Payment of ${amount:.2f} initiated - check your browser to complete\"\n    elif result.action == \"decline\":\n        return \"Payment declined by user\"\n    return \"Payment cancelled\"\n\n\n@mcp.tool()\nasync def connect_service(service_name: str, ctx: Context[ServerSession, None]) -> str:\n    \"\"\"Connect to a third-party service requiring OAuth authorization.\n\n    This demonstrates the \"throw error\" pattern using UrlElicitationRequiredError.\n    Use this pattern when the tool cannot proceed without user authorization.\n    \"\"\"\n    elicitation_id = str(uuid.uuid4())\n\n    # Raise UrlElicitationRequiredError to signal that the client must complete\n    # a URL elicitation before this request can be processed.\n    # The MCP framework will convert this to a -32042 error response.\n    raise UrlElicitationRequiredError(\n        [\n            ElicitRequestURLParams(\n                mode=\"url\",\n                message=f\"Authorization required to connect to {service_name}\",\n                url=f\"https://{service_name}.example.com/oauth/authorize?elicit={elicitation_id}\",\n                elicitationId=elicitation_id,\n            )\n        ]\n    )\n```\n\n_Full example: [examples/snippets/servers/elicitation.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/elicitation.py)_\n<!-- /snippet-source -->\n\nElicitation schemas support default values for all field types. Default values are automatically included in the JSON schema sent to clients, allowing them to pre-populate forms.\n\nThe `elicit()` method returns an `ElicitationResult` with:\n\n- `action`: \"accept\", \"decline\", or \"cancel\"\n- `data`: The validated response (only when accepted)\n- `validation_error`: Any validation error message\n\n### Sampling\n\nTools can interact with LLMs through sampling (generating text):\n\n<!-- snippet-source examples/snippets/servers/sampling.py -->\n```python\nfrom mcp.server.fastmcp import Context, FastMCP\nfrom mcp.server.session import ServerSession\nfrom mcp.types import SamplingMessage, TextContent\n\nmcp = FastMCP(name=\"Sampling Example\")\n\n\n@mcp.tool()\nasync def generate_poem(topic: str, ctx: Context[ServerSession, None]) -> str:\n    \"\"\"Generate a poem using LLM sampling.\"\"\"\n    prompt = f\"Write a short poem about {topic}\"\n\n    result = await ctx.session.create_message(\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=TextContent(type=\"text\", text=prompt),\n            )\n        ],\n        max_tokens=100,\n    )\n\n    # Since we're not passing tools param, result.content is single content\n    if result.content.type == \"text\":\n        return result.content.text\n    return str(result.content)\n```\n\n_Full example: [examples/snippets/servers/sampling.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/sampling.py)_\n<!-- /snippet-source -->\n\n### Logging and Notifications\n\nTools can send logs and notifications through the context:\n\n<!-- snippet-source examples/snippets/servers/notifications.py -->\n```python\nfrom mcp.server.fastmcp import Context, FastMCP\nfrom mcp.server.session import ServerSession\n\nmcp = FastMCP(name=\"Notifications Example\")\n\n\n@mcp.tool()\nasync def process_data(data: str, ctx: Context[ServerSession, None]) -> str:\n    \"\"\"Process data with logging.\"\"\"\n    # Different log levels\n    await ctx.debug(f\"Debug: Processing '{data}'\")\n    await ctx.info(\"Info: Starting processing\")\n    await ctx.warning(\"Warning: This is experimental\")\n    await ctx.error(\"Error: (This is just a demo)\")\n\n    # Notify about resource changes\n    await ctx.session.send_resource_list_changed()\n\n    return f\"Processed: {data}\"\n```\n\n_Full example: [examples/snippets/servers/notifications.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/notifications.py)_\n<!-- /snippet-source -->\n\n### Authentication\n\nAuthentication can be used by servers that want to expose tools accessing protected resources.\n\n`mcp.server.auth` implements OAuth 2.1 resource server functionality, where MCP servers act as Resource Servers (RS) that validate tokens issued by separate Authorization Servers (AS). This follows the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) and implements RFC 9728 (Protected Resource Metadata) for AS discovery.\n\nMCP servers can use authentication by providing an implementation of the `TokenVerifier` protocol:\n\n<!-- snippet-source examples/snippets/servers/oauth_server.py -->\n```python\n\"\"\"\nRun from the repository root:\n    uv run examples/snippets/servers/oauth_server.py\n\"\"\"\n\nfrom pydantic import AnyHttpUrl\n\nfrom mcp.server.auth.provider import AccessToken, TokenVerifier\nfrom mcp.server.auth.settings import AuthSettings\nfrom mcp.server.fastmcp import FastMCP\n\n\nclass SimpleTokenVerifier(TokenVerifier):\n    \"\"\"Simple token verifier for demonstration.\"\"\"\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        pass  # This is where you would implement actual token validation\n\n\n# Create FastMCP instance as a Resource Server\nmcp = FastMCP(\n    \"Weather Service\",\n    json_response=True,\n    # Token verifier for authentication\n    token_verifier=SimpleTokenVerifier(),\n    # Auth settings for RFC 9728 Protected Resource Metadata\n    auth=AuthSettings(\n        issuer_url=AnyHttpUrl(\"https://auth.example.com\"),  # Authorization Server URL\n        resource_server_url=AnyHttpUrl(\"http://localhost:3001\"),  # This server's URL\n        required_scopes=[\"user\"],\n    ),\n)\n\n\n@mcp.tool()\nasync def get_weather(city: str = \"London\") -> dict[str, str]:\n    \"\"\"Get weather data for a city\"\"\"\n    return {\n        \"city\": city,\n        \"temperature\": \"22\",\n        \"condition\": \"Partly cloudy\",\n        \"humidity\": \"65%\",\n    }\n\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"streamable-http\")\n```\n\n_Full example: [examples/snippets/servers/oauth_server.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/oauth_server.py)_\n<!-- /snippet-source -->\n\nFor a complete example with separate Authorization Server and Resource Server implementations, see [`examples/servers/simple-auth/`](examples/servers/simple-auth/).\n\n**Architecture:**\n\n- **Authorization Server (AS)**: Handles OAuth flows, user authentication, and token issuance\n- **Resource Server (RS)**: Your MCP server that validates tokens and serves protected resources\n- **Client**: Discovers AS through RFC 9728, obtains tokens, and uses them with the MCP server\n\nSee [TokenVerifier](src/mcp/server/auth/provider.py) for more details on implementing token validation.\n\n### FastMCP Properties\n\nThe FastMCP server instance accessible via `ctx.fastmcp` provides access to server configuration and metadata:\n\n- `ctx.fastmcp.name` - The server's name as defined during initialization\n- `ctx.fastmcp.instructions` - Server instructions/description provided to clients\n- `ctx.fastmcp.website_url` - Optional website URL for the server\n- `ctx.fastmcp.icons` - Optional list of icons for UI display\n- `ctx.fastmcp.settings` - Complete server configuration object containing:\n  - `debug` - Debug mode flag\n  - `log_level` - Current logging level\n  - `host` and `port` - Server network configuration\n  - `mount_path`, `sse_path`, `streamable_http_path` - Transport paths\n  - `stateless_http` - Whether the server operates in stateless mode\n  - And other configuration options\n\n```python\n@mcp.tool()\ndef server_info(ctx: Context) -> dict:\n    \"\"\"Get information about the current server.\"\"\"\n    return {\n        \"name\": ctx.fastmcp.name,\n        \"instructions\": ctx.fastmcp.instructions,\n        \"debug_mode\": ctx.fastmcp.settings.debug,\n        \"log_level\": ctx.fastmcp.settings.log_level,\n        \"host\": ctx.fastmcp.settings.host,\n        \"port\": ctx.fastmcp.settings.port,\n    }\n```\n\n### Session Properties and Methods\n\nThe session object accessible via `ctx.session` provides advanced control over client communication:\n\n- `ctx.session.client_params` - Client initialization parameters and declared capabilities\n- `await ctx.session.send_log_message(level, data, logger)` - Send log messages with full control\n- `await ctx.session.create_message(messages, max_tokens)` - Request LLM sampling/completion\n- `await ctx.session.send_progress_notification(token, progress, total, message)` - Direct progress updates\n- `await ctx.session.send_resource_updated(uri)` - Notify clients that a specific resource changed\n- `await ctx.session.send_resource_list_changed()` - Notify clients that the resource list changed\n- `await ctx.session.send_tool_list_changed()` - Notify clients that the tool list changed\n- `await ctx.session.send_prompt_list_changed()` - Notify clients that the prompt list changed\n\n```python\n@mcp.tool()\nasync def notify_data_update(resource_uri: str, ctx: Context) -> str:\n    \"\"\"Update data and notify clients of the change.\"\"\"\n    # Perform data update logic here\n    \n    # Notify clients that this specific resource changed\n    await ctx.session.send_resource_updated(AnyUrl(resource_uri))\n    \n    # If this affects the overall resource list, notify about that too\n    await ctx.session.send_resource_list_changed()\n    \n    return f\"Updated {resource_uri} and notified clients\"\n```\n\n### Request Context Properties\n\nThe request context accessible via `ctx.request_context` contains request-specific information and resources:\n\n- `ctx.request_context.lifespan_context` - Access to resources initialized during server startup\n  - Database connections, configuration objects, shared services\n  - Type-safe access to resources defined in your server's lifespan function\n- `ctx.request_context.meta` - Request metadata from the client including:\n  - `progressToken` - Token for progress notifications\n  - Other client-provided metadata\n- `ctx.request_context.request` - The original MCP request object for advanced processing\n- `ctx.request_context.request_id` - Unique identifier for this request\n\n```python\n# Example with typed lifespan context\n@dataclass\nclass AppContext:\n    db: Database\n    config: AppConfig\n\n@mcp.tool()\ndef query_with_config(query: str, ctx: Context) -> str:\n    \"\"\"Execute a query using shared database and configuration.\"\"\"\n    # Access typed lifespan context\n    app_ctx: AppContext = ctx.request_context.lifespan_context\n    \n    # Use shared resources\n    connection = app_ctx.db\n    settings = app_ctx.config\n    \n    # Execute query with configuration\n    result = connection.execute(query, timeout=settings.query_timeout)\n    return str(result)\n```\n\n_Full lifespan example: [examples/snippets/servers/lifespan_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lifespan_example.py)_\n\n## Running Your Server\n\n### Development Mode\n\nThe fastest way to test and debug your server is with the MCP Inspector:\n\n```bash\nuv run mcp dev server.py\n\n# Add dependencies\nuv run mcp dev server.py --with pandas --with numpy\n\n# Mount local code\nuv run mcp dev server.py --with-editable .\n```\n\n### Claude Desktop Integration\n\nOnce your server is ready, install it in Claude Desktop:\n\n```bash\nuv run mcp install server.py\n\n# Custom name\nuv run mcp install server.py --name \"My Analytics Server\"\n\n# Environment variables\nuv run mcp install server.py -v API_KEY=abc123 -v DB_URL=postgres://...\nuv run mcp install server.py -f .env\n```\n\n### Direct Execution\n\nFor advanced scenarios like custom deployments:\n\n<!-- snippet-source examples/snippets/servers/direct_execution.py -->\n```python\n\"\"\"Example showing direct execution of an MCP server.\n\nThis is the simplest way to run an MCP server directly.\ncd to the `examples/snippets` directory and run:\n    uv run direct-execution-server\n    or\n    python servers/direct_execution.py\n\"\"\"\n\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(\"My App\")\n\n\n@mcp.tool()\ndef hello(name: str = \"World\") -> str:\n    \"\"\"Say hello to someone.\"\"\"\n    return f\"Hello, {name}!\"\n\n\ndef main():\n    \"\"\"Entry point for the direct execution server.\"\"\"\n    mcp.run()\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n_Full example: [examples/snippets/servers/direct_execution.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/direct_execution.py)_\n<!-- /snippet-source -->\n\nRun it with:\n\n```bash\npython servers/direct_execution.py\n# or\nuv run mcp run servers/direct_execution.py\n```\n\nNote that `uv run mcp run` or `uv run mcp dev` only supports server using FastMCP and not the low-level server variant.\n\n### Streamable HTTP Transport\n\n> **Note**: Streamable HTTP transport is the recommended transport for production deployments. Use `stateless_http=True` and `json_response=True` for optimal scalability.\n\n<!-- snippet-source examples/snippets/servers/streamable_config.py -->\n```python\n\"\"\"\nRun from the repository root:\n    uv run examples/snippets/servers/streamable_config.py\n\"\"\"\n\nfrom mcp.server.fastmcp import FastMCP\n\n# Stateless server with JSON responses (recommended)\nmcp = FastMCP(\"StatelessServer\", stateless_http=True, json_response=True)\n\n# Other configuration options:\n# Stateless server with SSE streaming responses\n# mcp = FastMCP(\"StatelessServer\", stateless_http=True)\n\n# Stateful server with session persistence\n# mcp = FastMCP(\"StatefulServer\")\n\n\n# Add a simple tool to demonstrate the server\n@mcp.tool()\ndef greet(name: str = \"World\") -> str:\n    \"\"\"Greet someone by name.\"\"\"\n    return f\"Hello, {name}!\"\n\n\n# Run server with streamable_http transport\nif __name__ == \"__main__\":\n    mcp.run(transport=\"streamable-http\")\n```\n\n_Full example: [examples/snippets/servers/streamable_config.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_config.py)_\n<!-- /snippet-source -->\n\nYou can mount multiple FastMCP servers in a Starlette application:\n\n<!-- snippet-source examples/snippets/servers/streamable_starlette_mount.py -->\n```python\n\"\"\"\nRun from the repository root:\n    uvicorn examples.snippets.servers.streamable_starlette_mount:app --reload\n\"\"\"\n\nimport contextlib\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom mcp.server.fastmcp import FastMCP\n\n# Create the Echo server\necho_mcp = FastMCP(name=\"EchoServer\", stateless_http=True, json_response=True)\n\n\n@echo_mcp.tool()\ndef echo(message: str) -> str:\n    \"\"\"A simple echo tool\"\"\"\n    return f\"Echo: {message}\"\n\n\n# Create the Math server\nmath_mcp = FastMCP(name=\"MathServer\", stateless_http=True, json_response=True)\n\n\n@math_mcp.tool()\ndef add_two(n: int) -> int:\n    \"\"\"Tool to add two to the input\"\"\"\n    return n + 2\n\n\n# Create a combined lifespan to manage both session managers\n@contextlib.asynccontextmanager\nasync def lifespan(app: Starlette):\n    async with contextlib.AsyncExitStack() as stack:\n        await stack.enter_async_context(echo_mcp.session_manager.run())\n        await stack.enter_async_context(math_mcp.session_manager.run())\n        yield\n\n\n# Create the Starlette app and mount the MCP servers\napp = Starlette(\n    routes=[\n        Mount(\"/echo\", echo_mcp.streamable_http_app()),\n        Mount(\"/math\", math_mcp.streamable_http_app()),\n    ],\n    lifespan=lifespan,\n)\n\n# Note: Clients connect to http://localhost:8000/echo/mcp and http://localhost:8000/math/mcp\n# To mount at the root of each path (e.g., /echo instead of /echo/mcp):\n# echo_mcp.settings.streamable_http_path = \"/\"\n# math_mcp.settings.streamable_http_path = \"/\"\n```\n\n_Full example: [examples/snippets/servers/streamable_starlette_mount.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_starlette_mount.py)_\n<!-- /snippet-source -->\n\nFor low level server with Streamable HTTP implementations, see:\n\n- Stateful server: [`examples/servers/simple-streamablehttp/`](examples/servers/simple-streamablehttp/)\n- Stateless server: [`examples/servers/simple-streamablehttp-stateless/`](examples/servers/simple-streamablehttp-stateless/)\n\nThe streamable HTTP transport supports:\n\n- Stateful and stateless operation modes\n- Resumability with event stores\n- JSON or SSE response formats\n- Better scalability for multi-node deployments\n\n#### CORS Configuration for Browser-Based Clients\n\nIf you'd like your server to be accessible by browser-based MCP clients, you'll need to configure CORS headers. The `Mcp-Session-Id` header must be exposed for browser clients to access it:\n\n```python\nfrom starlette.applications import Starlette\nfrom starlette.middleware.cors import CORSMiddleware\n\n# Create your Starlette app first\nstarlette_app = Starlette(routes=[...])\n\n# Then wrap it with CORS middleware\nstarlette_app = CORSMiddleware(\n    starlette_app,\n    allow_origins=[\"*\"],  # Configure appropriately for production\n    allow_methods=[\"GET\", \"POST\", \"DELETE\"],  # MCP streamable HTTP methods\n    expose_headers=[\"Mcp-Session-Id\"],\n)\n```\n\nThis configuration is necessary because:\n\n- The MCP streamable HTTP transport uses the `Mcp-Session-Id` header for session management\n- Browsers restrict access to response headers unless explicitly exposed via CORS\n- Without this configuration, browser-based clients won't be able to read the session ID from initialization responses\n\n### Mounting to an Existing ASGI Server\n\nBy default, SSE servers are mounted at `/sse` and Streamable HTTP servers are mounted at `/mcp`. You can customize these paths using the methods described below.\n\nFor more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes).\n\n#### StreamableHTTP servers\n\nYou can mount the StreamableHTTP server to an existing ASGI server using the `streamable_http_app` method. This allows you to integrate the StreamableHTTP server with other ASGI applications.\n\n##### Basic mounting\n\n<!-- snippet-source examples/snippets/servers/streamable_http_basic_mounting.py -->\n```python\n\"\"\"\nBasic example showing how to mount StreamableHTTP server in Starlette.\n\nRun from the repository root:\n    uvicorn examples.snippets.servers.streamable_http_basic_mounting:app --reload\n\"\"\"\n\nimport contextlib\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom mcp.server.fastmcp import FastMCP\n\n# Create MCP server\nmcp = FastMCP(\"My App\", json_response=True)\n\n\n@mcp.tool()\ndef hello() -> str:\n    \"\"\"A simple hello tool\"\"\"\n    return \"Hello from MCP!\"\n\n\n# Create a lifespan context manager to run the session manager\n@contextlib.asynccontextmanager\nasync def lifespan(app: Starlette):\n    async with mcp.session_manager.run():\n        yield\n\n\n# Mount the StreamableHTTP server to the existing ASGI server\napp = Starlette(\n    routes=[\n        Mount(\"/\", app=mcp.streamable_http_app()),\n    ],\n    lifespan=lifespan,\n)\n```\n\n_Full example: [examples/snippets/servers/streamable_http_basic_mounting.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_basic_mounting.py)_\n<!-- /snippet-source -->\n\n##### Host-based routing\n\n<!-- snippet-source examples/snippets/servers/streamable_http_host_mounting.py -->\n```python\n\"\"\"\nExample showing how to mount StreamableHTTP server using Host-based routing.\n\nRun from the repository root:\n    uvicorn examples.snippets.servers.streamable_http_host_mounting:app --reload\n\"\"\"\n\nimport contextlib\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Host\n\nfrom mcp.server.fastmcp import FastMCP\n\n# Create MCP server\nmcp = FastMCP(\"MCP Host App\", json_response=True)\n\n\n@mcp.tool()\ndef domain_info() -> str:\n    \"\"\"Get domain-specific information\"\"\"\n    return \"This is served from mcp.acme.corp\"\n\n\n# Create a lifespan context manager to run the session manager\n@contextlib.asynccontextmanager\nasync def lifespan(app: Starlette):\n    async with mcp.session_manager.run():\n        yield\n\n\n# Mount using Host-based routing\napp = Starlette(\n    routes=[\n        Host(\"mcp.acme.corp\", app=mcp.streamable_http_app()),\n    ],\n    lifespan=lifespan,\n)\n```\n\n_Full example: [examples/snippets/servers/streamable_http_host_mounting.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_host_mounting.py)_\n<!-- /snippet-source -->\n\n##### Multiple servers with path configuration\n\n<!-- snippet-source examples/snippets/servers/streamable_http_multiple_servers.py -->\n```python\n\"\"\"\nExample showing how to mount multiple StreamableHTTP servers with path configuration.\n\nRun from the repository root:\n    uvicorn examples.snippets.servers.streamable_http_multiple_servers:app --reload\n\"\"\"\n\nimport contextlib\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom mcp.server.fastmcp import FastMCP\n\n# Create multiple MCP servers\napi_mcp = FastMCP(\"API Server\", json_response=True)\nchat_mcp = FastMCP(\"Chat Server\", json_response=True)\n\n\n@api_mcp.tool()\ndef api_status() -> str:\n    \"\"\"Get API status\"\"\"\n    return \"API is running\"\n\n\n@chat_mcp.tool()\ndef send_message(message: str) -> str:\n    \"\"\"Send a chat message\"\"\"\n    return f\"Message sent: {message}\"\n\n\n# Configure servers to mount at the root of each path\n# This means endpoints will be at /api and /chat instead of /api/mcp and /chat/mcp\napi_mcp.settings.streamable_http_path = \"/\"\nchat_mcp.settings.streamable_http_path = \"/\"\n\n\n# Create a combined lifespan to manage both session managers\n@contextlib.asynccontextmanager\nasync def lifespan(app: Starlette):\n    async with contextlib.AsyncExitStack() as stack:\n        await stack.enter_async_context(api_mcp.session_manager.run())\n        await stack.enter_async_context(chat_mcp.session_manager.run())\n        yield\n\n\n# Mount the servers\napp = Starlette(\n    routes=[\n        Mount(\"/api\", app=api_mcp.streamable_http_app()),\n        Mount(\"/chat\", app=chat_mcp.streamable_http_app()),\n    ],\n    lifespan=lifespan,\n)\n```\n\n_Full example: [examples/snippets/servers/streamable_http_multiple_servers.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_multiple_servers.py)_\n<!-- /snippet-source -->\n\n##### Path configuration at initialization\n\n<!-- snippet-source examples/snippets/servers/streamable_http_path_config.py -->\n```python\n\"\"\"\nExample showing path configuration during FastMCP initialization.\n\nRun from the repository root:\n    uvicorn examples.snippets.servers.streamable_http_path_config:app --reload\n\"\"\"\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom mcp.server.fastmcp import FastMCP\n\n# Configure streamable_http_path during initialization\n# This server will mount at the root of wherever it's mounted\nmcp_at_root = FastMCP(\n    \"My Server\",\n    json_response=True,\n    streamable_http_path=\"/\",\n)\n\n\n@mcp_at_root.tool()\ndef process_data(data: str) -> str:\n    \"\"\"Process some data\"\"\"\n    return f\"Processed: {data}\"\n\n\n# Mount at /process - endpoints will be at /process instead of /process/mcp\napp = Starlette(\n    routes=[\n        Mount(\"/process\", app=mcp_at_root.streamable_http_app()),\n    ]\n)\n```\n\n_Full example: [examples/snippets/servers/streamable_http_path_config.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_path_config.py)_\n<!-- /snippet-source -->\n\n#### SSE servers\n\n> **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http).\n\nYou can mount the SSE server to an existing ASGI server using the `sse_app` method. This allows you to integrate the SSE server with other ASGI applications.\n\n```python\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount, Host\nfrom mcp.server.fastmcp import FastMCP\n\n\nmcp = FastMCP(\"My App\")\n\n# Mount the SSE server to the existing ASGI server\napp = Starlette(\n    routes=[\n        Mount('/', app=mcp.sse_app()),\n    ]\n)\n\n# or dynamically mount as host\napp.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app()))\n```\n\nWhen mounting multiple MCP servers under different paths, you can configure the mount path in several ways:\n\n```python\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\nfrom mcp.server.fastmcp import FastMCP\n\n# Create multiple MCP servers\ngithub_mcp = FastMCP(\"GitHub API\")\nbrowser_mcp = FastMCP(\"Browser\")\ncurl_mcp = FastMCP(\"Curl\")\nsearch_mcp = FastMCP(\"Search\")\n\n# Method 1: Configure mount paths via settings (recommended for persistent configuration)\ngithub_mcp.settings.mount_path = \"/github\"\nbrowser_mcp.settings.mount_path = \"/browser\"\n\n# Method 2: Pass mount path directly to sse_app (preferred for ad-hoc mounting)\n# This approach doesn't modify the server's settings permanently\n\n# Create Starlette app with multiple mounted servers\napp = Starlette(\n    routes=[\n        # Using settings-based configuration\n        Mount(\"/github\", app=github_mcp.sse_app()),\n        Mount(\"/browser\", app=browser_mcp.sse_app()),\n        # Using direct mount path parameter\n        Mount(\"/curl\", app=curl_mcp.sse_app(\"/curl\")),\n        Mount(\"/search\", app=search_mcp.sse_app(\"/search\")),\n    ]\n)\n\n# Method 3: For direct execution, you can also pass the mount path to run()\nif __name__ == \"__main__\":\n    search_mcp.run(transport=\"sse\", mount_path=\"/search\")\n```\n\nFor more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes).\n\n## Advanced Usage\n\n### Low-Level Server\n\nFor more control, you can use the low-level server implementation directly. This gives you full access to the protocol and allows you to customize every aspect of your server, including lifecycle management through the lifespan API:\n\n<!-- snippet-source examples/snippets/servers/lowlevel/lifespan.py -->\n```python\n\"\"\"\nRun from the repository root:\n    uv run examples/snippets/servers/lowlevel/lifespan.py\n\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom typing import Any\n\nimport mcp.server.stdio\nimport mcp.types as types\nfrom mcp.server.lowlevel import NotificationOptions, Server\nfrom mcp.server.models import InitializationOptions\n\n\n# Mock database class for example\nclass Database:\n    \"\"\"Mock database class for example.\"\"\"\n\n    @classmethod\n    async def connect(cls) -> \"Database\":\n        \"\"\"Connect to database.\"\"\"\n        print(\"Database connected\")\n        return cls()\n\n    async def disconnect(self) -> None:\n        \"\"\"Disconnect from database.\"\"\"\n        print(\"Database disconnected\")\n\n    async def query(self, query_str: str) -> list[dict[str, str]]:\n        \"\"\"Execute a query.\"\"\"\n        # Simulate database query\n        return [{\"id\": \"1\", \"name\": \"Example\", \"query\": query_str}]\n\n\n@asynccontextmanager\nasync def server_lifespan(_server: Server) -> AsyncIterator[dict[str, Any]]:\n    \"\"\"Manage server startup and shutdown lifecycle.\"\"\"\n    # Initialize resources on startup\n    db = await Database.connect()\n    try:\n        yield {\"db\": db}\n    finally:\n        # Clean up on shutdown\n        await db.disconnect()\n\n\n# Pass lifespan to server\nserver = Server(\"example-server\", lifespan=server_lifespan)\n\n\n@server.list_tools()\nasync def handle_list_tools() -> list[types.Tool]:\n    \"\"\"List available tools.\"\"\"\n    return [\n        types.Tool(\n            name=\"query_db\",\n            description=\"Query the database\",\n            inputSchema={\n                \"type\": \"object\",\n                \"properties\": {\"query\": {\"type\": \"string\", \"description\": \"SQL query to execute\"}},\n                \"required\": [\"query\"],\n            },\n        )\n    ]\n\n\n@server.call_tool()\nasync def query_db(name: str, arguments: dict[str, Any]) -> list[types.TextContent]:\n    \"\"\"Handle database query tool call.\"\"\"\n    if name != \"query_db\":\n        raise ValueError(f\"Unknown tool: {name}\")\n\n    # Access lifespan context\n    ctx = server.request_context\n    db = ctx.lifespan_context[\"db\"]\n\n    # Execute query\n    results = await db.query(arguments[\"query\"])\n\n    return [types.TextContent(type=\"text\", text=f\"Query results: {results}\")]\n\n\nasync def run():\n    \"\"\"Run the server with lifespan management.\"\"\"\n    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):\n        await server.run(\n            read_stream,\n            write_stream,\n            InitializationOptions(\n                server_name=\"example-server\",\n                server_version=\"0.1.0\",\n                capabilities=server.get_capabilities(\n                    notification_options=NotificationOptions(),\n                    experimental_capabilities={},\n                ),\n            ),\n        )\n\n\nif __name__ == \"__main__\":\n    import asyncio\n\n    asyncio.run(run())\n```\n\n_Full example: [examples/snippets/servers/lowlevel/lifespan.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/lifespan.py)_\n<!-- /snippet-source -->\n\nThe lifespan API provides:\n\n- A way to initialize resources when the server starts and clean them up when it stops\n- Access to initialized resources through the request context in handlers\n- Type-safe context passing between lifespan and request handlers\n\n<!-- snippet-source examples/snippets/servers/lowlevel/basic.py -->\n```python\n\"\"\"\nRun from the repository root:\nuv run examples/snippets/servers/lowlevel/basic.py\n\"\"\"\n\nimport asyncio\n\nimport mcp.server.stdio\nimport mcp.types as types\nfrom mcp.server.lowlevel import NotificationOptions, Server\nfrom mcp.server.models import InitializationOptions\n\n# Create a server instance\nserver = Server(\"example-server\")\n\n\n@server.list_prompts()\nasync def handle_list_prompts() -> list[types.Prompt]:\n    \"\"\"List available prompts.\"\"\"\n    return [\n        types.Prompt(\n            name=\"example-prompt\",\n            description=\"An example prompt template\",\n            arguments=[types.PromptArgument(name=\"arg1\", description=\"Example argument\", required=True)],\n        )\n    ]\n\n\n@server.get_prompt()\nasync def handle_get_prompt(name: str, arguments: dict[str, str] | None) -> types.GetPromptResult:\n    \"\"\"Get a specific prompt by name.\"\"\"\n    if name != \"example-prompt\":\n        raise ValueError(f\"Unknown prompt: {name}\")\n\n    arg1_value = (arguments or {}).get(\"arg1\", \"default\")\n\n    return types.GetPromptResult(\n        description=\"Example prompt\",\n        messages=[\n            types.PromptMessage(\n                role=\"user\",\n                content=types.TextContent(type=\"text\", text=f\"Example prompt text with argument: {arg1_value}\"),\n            )\n        ],\n    )\n\n\nasync def run():\n    \"\"\"Run the basic low-level server.\"\"\"\n    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):\n        await server.run(\n            read_stream,\n            write_stream,\n            InitializationOptions(\n                server_name=\"example\",\n                server_version=\"0.1.0\",\n                capabilities=server.get_capabilities(\n                    notification_options=NotificationOptions(),\n                    experimental_capabilities={},\n                ),\n            ),\n        )\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run())\n```\n\n_Full example: [examples/snippets/servers/lowlevel/basic.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/basic.py)_\n<!-- /snippet-source -->\n\nCaution: The `uv run mcp run` and `uv run mcp dev` tool doesn't support low-level server.\n\n#### Structured Output Support\n\nThe low-level server supports structured output for tools, allowing you to return both human-readable content and machine-readable structured data. Tools can define an `outputSchema` to validate their structured output:\n\n<!-- snippet-source examples/snippets/servers/lowlevel/structured_output.py -->\n```python\n\"\"\"\nRun from the repository root:\n    uv run examples/snippets/servers/lowlevel/structured_output.py\n\"\"\"\n\nimport asyncio\nfrom typing import Any\n\nimport mcp.server.stdio\nimport mcp.types as types\nfrom mcp.server.lowlevel import NotificationOptions, Server\nfrom mcp.server.models import InitializationOptions\n\nserver = Server(\"example-server\")\n\n\n@server.list_tools()\nasync def list_tools() -> list[types.Tool]:\n    \"\"\"List available tools with structured output schemas.\"\"\"\n    return [\n        types.Tool(\n            name=\"get_weather\",\n            description=\"Get current weather for a city\",\n            inputSchema={\n                \"type\": \"object\",\n                \"properties\": {\"city\": {\"type\": \"string\", \"description\": \"City name\"}},\n                \"required\": [\"city\"],\n            },\n            outputSchema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"temperature\": {\"type\": \"number\", \"description\": \"Temperature in Celsius\"},\n                    \"condition\": {\"type\": \"string\", \"description\": \"Weather condition\"},\n                    \"humidity\": {\"type\": \"number\", \"description\": \"Humidity percentage\"},\n                    \"city\": {\"type\": \"string\", \"description\": \"City name\"},\n                },\n                \"required\": [\"temperature\", \"condition\", \"humidity\", \"city\"],\n            },\n        )\n    ]\n\n\n@server.call_tool()\nasync def call_tool(name: str, arguments: dict[str, Any]) -> dict[str, Any]:\n    \"\"\"Handle tool calls with structured output.\"\"\"\n    if name == \"get_weather\":\n        city = arguments[\"city\"]\n\n        # Simulated weather data - in production, call a weather API\n        weather_data = {\n            \"temperature\": 22.5,\n            \"condition\": \"partly cloudy\",\n            \"humidity\": 65,\n            \"city\": city,  # Include the requested city\n        }\n\n        # low-level server will validate structured output against the tool's\n        # output schema, and additionally serialize it into a TextContent block\n        # for backwards compatibility with pre-2025-06-18 clients.\n        return weather_data\n    else:\n        raise ValueError(f\"Unknown tool: {name}\")\n\n\nasync def run():\n    \"\"\"Run the structured output server.\"\"\"\n    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):\n        await server.run(\n            read_stream,\n            write_stream,\n            InitializationOptions(\n                server_name=\"structured-output-example\",\n                server_version=\"0.1.0\",\n                capabilities=server.get_capabilities(\n                    notification_options=NotificationOptions(),\n                    experimental_capabilities={},\n                ),\n            ),\n        )\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run())\n```\n\n_Full example: [examples/snippets/servers/lowlevel/structured_output.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/structured_output.py)_\n<!-- /snippet-source -->\n\nTools can return data in four ways:\n\n1. **Content only**: Return a list of content blocks (default behavior before spec revision 2025-06-18)\n2. **Structured data only**: Return a dictionary that will be serialized to JSON (Introduced in spec revision 2025-06-18)\n3. **Both**: Return a tuple of (content, structured_data) preferred option to use for backwards compatibility\n4. **Direct CallToolResult**: Return `CallToolResult` directly for full control (including `_meta` field)\n\nWhen an `outputSchema` is defined, the server automatically validates the structured output against the schema. This ensures type safety and helps catch errors early.\n\n##### Returning CallToolResult Directly\n\nFor full control over the response including the `_meta` field (for passing data to client applications without exposing it to the model), return `CallToolResult` directly:\n\n<!-- snippet-source examples/snippets/servers/lowlevel/direct_call_tool_result.py -->\n```python\n\"\"\"\nRun from the repository root:\n    uv run examples/snippets/servers/lowlevel/direct_call_tool_result.py\n\"\"\"\n\nimport asyncio\nfrom typing import Any\n\nimport mcp.server.stdio\nimport mcp.types as types\nfrom mcp.server.lowlevel import NotificationOptions, Server\nfrom mcp.server.models import InitializationOptions\n\nserver = Server(\"example-server\")\n\n\n@server.list_tools()\nasync def list_tools() -> list[types.Tool]:\n    \"\"\"List available tools.\"\"\"\n    return [\n        types.Tool(\n            name=\"advanced_tool\",\n            description=\"Tool with full control including _meta field\",\n            inputSchema={\n                \"type\": \"object\",\n                \"properties\": {\"message\": {\"type\": \"string\"}},\n                \"required\": [\"message\"],\n            },\n        )\n    ]\n\n\n@server.call_tool()\nasync def handle_call_tool(name: str, arguments: dict[str, Any]) -> types.CallToolResult:\n    \"\"\"Handle tool calls by returning CallToolResult directly.\"\"\"\n    if name == \"advanced_tool\":\n        message = str(arguments.get(\"message\", \"\"))\n        return types.CallToolResult(\n            content=[types.TextContent(type=\"text\", text=f\"Processed: {message}\")],\n            structuredContent={\"result\": \"success\", \"message\": message},\n            _meta={\"hidden\": \"data for client applications only\"},\n        )\n\n    raise ValueError(f\"Unknown tool: {name}\")\n\n\nasync def run():\n    \"\"\"Run the server.\"\"\"\n    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):\n        await server.run(\n            read_stream,\n            write_stream,\n            InitializationOptions(\n                server_name=\"example\",\n                server_version=\"0.1.0\",\n                capabilities=server.get_capabilities(\n                    notification_options=NotificationOptions(),\n                    experimental_capabilities={},\n                ),\n            ),\n        )\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run())\n```\n\n_Full example: [examples/snippets/servers/lowlevel/direct_call_tool_result.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/direct_call_tool_result.py)_\n<!-- /snippet-source -->\n\n**Note:** When returning `CallToolResult`, you bypass the automatic content/structured conversion. You must construct the complete response yourself.\n\n### Pagination (Advanced)\n\nFor servers that need to handle large datasets, the low-level server provides paginated versions of list operations. This is an optional optimization - most servers won't need pagination unless they're dealing with hundreds or thousands of items.\n\n#### Server-side Implementation\n\n<!-- snippet-source examples/snippets/servers/pagination_example.py -->\n```python\n\"\"\"\nExample of implementing pagination with MCP server decorators.\n\"\"\"\n\nfrom pydantic import AnyUrl\n\nimport mcp.types as types\nfrom mcp.server.lowlevel import Server\n\n# Initialize the server\nserver = Server(\"paginated-server\")\n\n# Sample data to paginate\nITEMS = [f\"Item {i}\" for i in range(1, 101)]  # 100 items\n\n\n@server.list_resources()\nasync def list_resources_paginated(request: types.ListResourcesRequest) -> types.ListResourcesResult:\n    \"\"\"List resources with pagination support.\"\"\"\n    page_size = 10\n\n    # Extract cursor from request params\n    cursor = request.params.cursor if request.params is not None else None\n\n    # Parse cursor to get offset\n    start = 0 if cursor is None else int(cursor)\n    end = start + page_size\n\n    # Get page of resources\n    page_items = [\n        types.Resource(uri=AnyUrl(f\"resource://items/{item}\"), name=item, description=f\"Description for {item}\")\n        for item in ITEMS[start:end]\n    ]\n\n    # Determine next cursor\n    next_cursor = str(end) if end < len(ITEMS) else None\n\n    return types.ListResourcesResult(resources=page_items, nextCursor=next_cursor)\n```\n\n_Full example: [examples/snippets/servers/pagination_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/pagination_example.py)_\n<!-- /snippet-source -->\n\n#### Client-side Consumption\n\n<!-- snippet-source examples/snippets/clients/pagination_client.py -->\n```python\n\"\"\"\nExample of consuming paginated MCP endpoints from a client.\n\"\"\"\n\nimport asyncio\n\nfrom mcp.client.session import ClientSession\nfrom mcp.client.stdio import StdioServerParameters, stdio_client\nfrom mcp.types import PaginatedRequestParams, Resource\n\n\nasync def list_all_resources() -> None:\n    \"\"\"Fetch all resources using pagination.\"\"\"\n    async with stdio_client(StdioServerParameters(command=\"uv\", args=[\"run\", \"mcp-simple-pagination\"])) as (\n        read,\n        write,\n    ):\n        async with ClientSession(read, write) as session:\n            await session.initialize()\n\n            all_resources: list[Resource] = []\n            cursor = None\n\n            while True:\n                # Fetch a page of resources\n                result = await session.list_resources(params=PaginatedRequestParams(cursor=cursor))\n                all_resources.extend(result.resources)\n\n                print(f\"Fetched {len(result.resources)} resources\")\n\n                # Check if there are more pages\n                if result.nextCursor:\n                    cursor = result.nextCursor\n                else:\n                    break\n\n            print(f\"Total resources: {len(all_resources)}\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(list_all_resources())\n```\n\n_Full example: [examples/snippets/clients/pagination_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/pagination_client.py)_\n<!-- /snippet-source -->\n\n#### Key Points\n\n- **Cursors are opaque strings** - the server defines the format (numeric offsets, timestamps, etc.)\n- **Return `nextCursor=None`** when there are no more pages\n- **Backward compatible** - clients that don't support pagination will still work (they'll just get the first page)\n- **Flexible page sizes** - Each endpoint can define its own page size based on data characteristics\n\nSee the [simple-pagination example](examples/servers/simple-pagination) for a complete implementation.\n\n### Writing MCP Clients\n\nThe SDK provides a high-level client interface for connecting to MCP servers using various [transports](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports):\n\n<!-- snippet-source examples/snippets/clients/stdio_client.py -->\n```python\n\"\"\"\ncd to the `examples/snippets/clients` directory and run:\n    uv run client\n\"\"\"\n\nimport asyncio\nimport os\n\nfrom pydantic import AnyUrl\n\nfrom mcp import ClientSession, StdioServerParameters, types\nfrom mcp.client.stdio import stdio_client\nfrom mcp.shared.context import RequestContext\n\n# Create server parameters for stdio connection\nserver_params = StdioServerParameters(\n    command=\"uv\",  # Using uv to run the server\n    args=[\"run\", \"server\", \"fastmcp_quickstart\", \"stdio\"],  # We're already in snippets dir\n    env={\"UV_INDEX\": os.environ.get(\"UV_INDEX\", \"\")},\n)\n\n\n# Optional: create a sampling callback\nasync def handle_sampling_message(\n    context: RequestContext[ClientSession, None], params: types.CreateMessageRequestParams\n) -> types.CreateMessageResult:\n    print(f\"Sampling request: {params.messages}\")\n    return types.CreateMessageResult(\n        role=\"assistant\",\n        content=types.TextContent(\n            type=\"text\",\n            text=\"Hello, world! from model\",\n        ),\n        model=\"gpt-3.5-turbo\",\n        stopReason=\"endTurn\",\n    )\n\n\nasync def run():\n    async with stdio_client(server_params) as (read, write):\n        async with ClientSession(read, write, sampling_callback=handle_sampling_message) as session:\n            # Initialize the connection\n            await session.initialize()\n\n            # List available prompts\n            prompts = await session.list_prompts()\n            print(f\"Available prompts: {[p.name for p in prompts.prompts]}\")\n\n            # Get a prompt (greet_user prompt from fastmcp_quickstart)\n            if prompts.prompts:\n                prompt = await session.get_prompt(\"greet_user\", arguments={\"name\": \"Alice\", \"style\": \"friendly\"})\n                print(f\"Prompt result: {prompt.messages[0].content}\")\n\n            # List available resources\n            resources = await session.list_resources()\n            print(f\"Available resources: {[r.uri for r in resources.resources]}\")\n\n            # List available tools\n            tools = await session.list_tools()\n            print(f\"Available tools: {[t.name for t in tools.tools]}\")\n\n            # Read a resource (greeting resource from fastmcp_quickstart)\n            resource_content = await session.read_resource(AnyUrl(\"greeting://World\"))\n            content_block = resource_content.contents[0]\n            if isinstance(content_block, types.TextContent):\n                print(f\"Resource content: {content_block.text}\")\n\n            # Call a tool (add tool from fastmcp_quickstart)\n            result = await session.call_tool(\"add\", arguments={\"a\": 5, \"b\": 3})\n            result_unstructured = result.content[0]\n            if isinstance(result_unstructured, types.TextContent):\n                print(f\"Tool result: {result_unstructured.text}\")\n            result_structured = result.structuredContent\n            print(f\"Structured tool result: {result_structured}\")\n\n\ndef main():\n    \"\"\"Entry point for the client script.\"\"\"\n    asyncio.run(run())\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n_Full example: [examples/snippets/clients/stdio_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/stdio_client.py)_\n<!-- /snippet-source -->\n\nClients can also connect using [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http):\n\n<!-- snippet-source examples/snippets/clients/streamable_basic.py -->\n```python\n\"\"\"\nRun from the repository root:\n    uv run examples/snippets/clients/streamable_basic.py\n\"\"\"\n\nimport asyncio\n\nfrom mcp import ClientSession\nfrom mcp.client.streamable_http import streamable_http_client\n\n\nasync def main():\n    # Connect to a streamable HTTP server\n    async with streamable_http_client(\"http://localhost:8000/mcp\") as (\n        read_stream,\n        write_stream,\n        _,\n    ):\n        # Create a session using the client streams\n        async with ClientSession(read_stream, write_stream) as session:\n            # Initialize the connection\n            await session.initialize()\n            # List available tools\n            tools = await session.list_tools()\n            print(f\"Available tools: {[tool.name for tool in tools.tools]}\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n_Full example: [examples/snippets/clients/streamable_basic.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/streamable_basic.py)_\n<!-- /snippet-source -->\n\n### Client Display Utilities\n\nWhen building MCP clients, the SDK provides utilities to help display human-readable names for tools, resources, and prompts:\n\n<!-- snippet-source examples/snippets/clients/display_utilities.py -->\n```python\n\"\"\"\ncd to the `examples/snippets` directory and run:\n    uv run display-utilities-client\n\"\"\"\n\nimport asyncio\nimport os\n\nfrom mcp import ClientSession, StdioServerParameters\nfrom mcp.client.stdio import stdio_client\nfrom mcp.shared.metadata_utils import get_display_name\n\n# Create server parameters for stdio connection\nserver_params = StdioServerParameters(\n    command=\"uv\",  # Using uv to run the server\n    args=[\"run\", \"server\", \"fastmcp_quickstart\", \"stdio\"],\n    env={\"UV_INDEX\": os.environ.get(\"UV_INDEX\", \"\")},\n)\n\n\nasync def display_tools(session: ClientSession):\n    \"\"\"Display available tools with human-readable names\"\"\"\n    tools_response = await session.list_tools()\n\n    for tool in tools_response.tools:\n        # get_display_name() returns the title if available, otherwise the name\n        display_name = get_display_name(tool)\n        print(f\"Tool: {display_name}\")\n        if tool.description:\n            print(f\"   {tool.description}\")\n\n\nasync def display_resources(session: ClientSession):\n    \"\"\"Display available resources with human-readable names\"\"\"\n    resources_response = await session.list_resources()\n\n    for resource in resources_response.resources:\n        display_name = get_display_name(resource)\n        print(f\"Resource: {display_name} ({resource.uri})\")\n\n    templates_response = await session.list_resource_templates()\n    for template in templates_response.resourceTemplates:\n        display_name = get_display_name(template)\n        print(f\"Resource Template: {display_name}\")\n\n\nasync def run():\n    \"\"\"Run the display utilities example.\"\"\"\n    async with stdio_client(server_params) as (read, write):\n        async with ClientSession(read, write) as session:\n            # Initialize the connection\n            await session.initialize()\n\n            print(\"=== Available Tools ===\")\n            await display_tools(session)\n\n            print(\"\\n=== Available Resources ===\")\n            await display_resources(session)\n\n\ndef main():\n    \"\"\"Entry point for the display utilities client.\"\"\"\n    asyncio.run(run())\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n_Full example: [examples/snippets/clients/display_utilities.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/display_utilities.py)_\n<!-- /snippet-source -->\n\nThe `get_display_name()` function implements the proper precedence rules for displaying names:\n\n- For tools: `title` > `annotations.title` > `name`\n- For other objects: `title` > `name`\n\nThis ensures your client UI shows the most user-friendly names that servers provide.\n\n### OAuth Authentication for Clients\n\nThe SDK includes [authorization support](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for connecting to protected MCP servers:\n\n<!-- snippet-source examples/snippets/clients/oauth_client.py -->\n```python\n\"\"\"\nBefore running, specify running MCP RS server URL.\nTo spin up RS server locally, see\n    examples/servers/simple-auth/README.md\n\ncd to the `examples/snippets` directory and run:\n    uv run oauth-client\n\"\"\"\n\nimport asyncio\nfrom urllib.parse import parse_qs, urlparse\n\nimport httpx\nfrom pydantic import AnyUrl\n\nfrom mcp import ClientSession\nfrom mcp.client.auth import OAuthClientProvider, TokenStorage\nfrom mcp.client.streamable_http import streamable_http_client\nfrom mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken\n\n\nclass InMemoryTokenStorage(TokenStorage):\n    \"\"\"Demo In-memory token storage implementation.\"\"\"\n\n    def __init__(self):\n        self.tokens: OAuthToken | None = None\n        self.client_info: OAuthClientInformationFull | None = None\n\n    async def get_tokens(self) -> OAuthToken | None:\n        \"\"\"Get stored tokens.\"\"\"\n        return self.tokens\n\n    async def set_tokens(self, tokens: OAuthToken) -> None:\n        \"\"\"Store tokens.\"\"\"\n        self.tokens = tokens\n\n    async def get_client_info(self) -> OAuthClientInformationFull | None:\n        \"\"\"Get stored client information.\"\"\"\n        return self.client_info\n\n    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:\n        \"\"\"Store client information.\"\"\"\n        self.client_info = client_info\n\n\nasync def handle_redirect(auth_url: str) -> None:\n    print(f\"Visit: {auth_url}\")\n\n\nasync def handle_callback() -> tuple[str, str | None]:\n    callback_url = input(\"Paste callback URL: \")\n    params = parse_qs(urlparse(callback_url).query)\n    return params[\"code\"][0], params.get(\"state\", [None])[0]\n\n\nasync def main():\n    \"\"\"Run the OAuth client example.\"\"\"\n    oauth_auth = OAuthClientProvider(\n        server_url=\"http://localhost:8001\",\n        client_metadata=OAuthClientMetadata(\n            client_name=\"Example MCP Client\",\n            redirect_uris=[AnyUrl(\"http://localhost:3000/callback\")],\n            grant_types=[\"authorization_code\", \"refresh_token\"],\n            response_types=[\"code\"],\n            scope=\"user\",\n        ),\n        storage=InMemoryTokenStorage(),\n        redirect_handler=handle_redirect,\n        callback_handler=handle_callback,\n    )\n\n    async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:\n        async with streamable_http_client(\"http://localhost:8001/mcp\", http_client=custom_client) as (read, write, _):\n            async with ClientSession(read, write) as session:\n                await session.initialize()\n\n                tools = await session.list_tools()\n                print(f\"Available tools: {[tool.name for tool in tools.tools]}\")\n\n                resources = await session.list_resources()\n                print(f\"Available resources: {[r.uri for r in resources.resources]}\")\n\n\ndef run():\n    asyncio.run(main())\n\n\nif __name__ == \"__main__\":\n    run()\n```\n\n_Full example: [examples/snippets/clients/oauth_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/oauth_client.py)_\n<!-- /snippet-source -->\n\nFor a complete working example, see [`examples/clients/simple-auth-client/`](examples/clients/simple-auth-client/).\n\n### Parsing Tool Results\n\nWhen calling tools through MCP, the `CallToolResult` object contains the tool's response in a structured format. Understanding how to parse this result is essential for properly handling tool outputs.\n\n```python\n\"\"\"examples/snippets/clients/parsing_tool_results.py\"\"\"\n\nimport asyncio\n\nfrom mcp import ClientSession, StdioServerParameters, types\nfrom mcp.client.stdio import stdio_client\n\n\nasync def parse_tool_results():\n    \"\"\"Demonstrates how to parse different types of content in CallToolResult.\"\"\"\n    server_params = StdioServerParameters(\n        command=\"python\", args=[\"path/to/mcp_server.py\"]\n    )\n\n    async with stdio_client(server_params) as (read, write):\n        async with ClientSession(read, write) as session:\n            await session.initialize()\n\n            # Example 1: Parsing text content\n            result = await session.call_tool(\"get_data\", {\"format\": \"text\"})\n            for content in result.content:\n                if isinstance(content, types.TextContent):\n                    print(f\"Text: {content.text}\")\n\n            # Example 2: Parsing structured content from JSON tools\n            result = await session.call_tool(\"get_user\", {\"id\": \"123\"})\n            if hasattr(result, \"structuredContent\") and result.structuredContent:\n                # Access structured data directly\n                user_data = result.structuredContent\n                print(f\"User: {user_data.get('name')}, Age: {user_data.get('age')}\")\n\n            # Example 3: Parsing embedded resources\n            result = await session.call_tool(\"read_config\", {})\n            for content in result.content:\n                if isinstance(content, types.EmbeddedResource):\n                    resource = content.resource\n                    if isinstance(resource, types.TextResourceContents):\n                        print(f\"Config from {resource.uri}: {resource.text}\")\n                    elif isinstance(resource, types.BlobResourceContents):\n                        print(f\"Binary data from {resource.uri}\")\n\n            # Example 4: Parsing image content\n            result = await session.call_tool(\"generate_chart\", {\"data\": [1, 2, 3]})\n            for content in result.content:\n                if isinstance(content, types.ImageContent):\n                    print(f\"Image ({content.mimeType}): {len(content.data)} bytes\")\n\n            # Example 5: Handling errors\n            result = await session.call_tool(\"failing_tool\", {})\n            if result.isError:\n                print(\"Tool execution failed!\")\n                for content in result.content:\n                    if isinstance(content, types.TextContent):\n                        print(f\"Error: {content.text}\")\n\n\nasync def main():\n    await parse_tool_results()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### MCP Primitives\n\nThe MCP protocol defines three core primitives that servers can implement:\n\n| Primitive | Control               | Description                                         | Example Use                  |\n|-----------|-----------------------|-----------------------------------------------------|------------------------------|\n| Prompts   | User-controlled       | Interactive templates invoked by user choice        | Slash commands, menu options |\n| Resources | Application-controlled| Contextual data managed by the client application   | File contents, API responses |\n| Tools     | Model-controlled      | Functions exposed to the LLM to take actions        | API calls, data updates      |\n\n### Server Capabilities\n\nMCP servers declare capabilities during initialization:\n\n| Capability   | Feature Flag                 | Description                        |\n|--------------|------------------------------|------------------------------------|\n| `prompts`    | `listChanged`                | Prompt template management         |\n| `resources`  | `subscribe`<br/>`listChanged`| Resource exposure and updates      |\n| `tools`      | `listChanged`                | Tool discovery and execution       |\n| `logging`    | -                            | Server logging configuration       |\n| `completions`| -                            | Argument completion suggestions    |\n\n## Documentation\n\n- [API Reference](https://modelcontextprotocol.github.io/python-sdk/api/)\n- [Experimental Features (Tasks)](https://modelcontextprotocol.github.io/python-sdk/experimental/tasks/)\n- [Model Context Protocol documentation](https://modelcontextprotocol.io)\n- [Model Context Protocol specification](https://modelcontextprotocol.io/specification/latest)\n- [Officially supported servers](https://github.com/modelcontextprotocol/servers)\n\n## Contributing\n\nWe are passionate about supporting contributors of all levels of experience and would love to see you get involved in the project. See the [contributing guide](CONTRIBUTING.md) to get started.\n\n## License\n\nThis project is licensed under the MIT License - see the LICENSE file for details.\n"
  },
  {
    "path": "README.v2.md",
    "content": "# MCP Python SDK\n\n<div align=\"center\">\n\n<strong>Python implementation of the Model Context Protocol (MCP)</strong>\n\n[![PyPI][pypi-badge]][pypi-url]\n[![MIT licensed][mit-badge]][mit-url]\n[![Python Version][python-badge]][python-url]\n[![Documentation][docs-badge]][docs-url]\n[![Protocol][protocol-badge]][protocol-url]\n[![Specification][spec-badge]][spec-url]\n\n</div>\n\n<!-- TODO(v2): Move this content back to README.md when v2 is released -->\n\n> [!IMPORTANT]\n> **This documents v2 of the SDK (currently in development, pre-alpha on `main`).**\n>\n> We anticipate a stable v2 release in Q1 2026. Until then, **v1.x remains the recommended version** for production use. v1.x will continue to receive bug fixes and security updates for at least 6 months after v2 ships to give people time to upgrade.\n>\n> For v1 documentation (the current stable release), see [`README.md`](README.md).\n\n<!-- omit in toc -->\n## Table of Contents\n\n- [MCP Python SDK](#mcp-python-sdk)\n  - [Overview](#overview)\n  - [Installation](#installation)\n    - [Adding MCP to your python project](#adding-mcp-to-your-python-project)\n    - [Running the standalone MCP development tools](#running-the-standalone-mcp-development-tools)\n  - [Quickstart](#quickstart)\n  - [What is MCP?](#what-is-mcp)\n  - [Core Concepts](#core-concepts)\n    - [Server](#server)\n    - [Resources](#resources)\n    - [Tools](#tools)\n      - [Structured Output](#structured-output)\n    - [Prompts](#prompts)\n    - [Images](#images)\n    - [Context](#context)\n      - [Getting Context in Functions](#getting-context-in-functions)\n      - [Context Properties and Methods](#context-properties-and-methods)\n    - [Completions](#completions)\n    - [Elicitation](#elicitation)\n    - [Sampling](#sampling)\n    - [Logging and Notifications](#logging-and-notifications)\n    - [Authentication](#authentication)\n    - [MCPServer Properties](#mcpserver-properties)\n    - [Session Properties and Methods](#session-properties-and-methods)\n    - [Request Context Properties](#request-context-properties)\n  - [Running Your Server](#running-your-server)\n    - [Development Mode](#development-mode)\n    - [Claude Desktop Integration](#claude-desktop-integration)\n    - [Direct Execution](#direct-execution)\n    - [Streamable HTTP Transport](#streamable-http-transport)\n      - [CORS Configuration for Browser-Based Clients](#cors-configuration-for-browser-based-clients)\n    - [Mounting to an Existing ASGI Server](#mounting-to-an-existing-asgi-server)\n      - [StreamableHTTP servers](#streamablehttp-servers)\n        - [Basic mounting](#basic-mounting)\n        - [Host-based routing](#host-based-routing)\n        - [Multiple servers with path configuration](#multiple-servers-with-path-configuration)\n        - [Path configuration at initialization](#path-configuration-at-initialization)\n      - [SSE servers](#sse-servers)\n  - [Advanced Usage](#advanced-usage)\n    - [Low-Level Server](#low-level-server)\n      - [Structured Output Support](#structured-output-support)\n    - [Pagination (Advanced)](#pagination-advanced)\n    - [Writing MCP Clients](#writing-mcp-clients)\n    - [Client Display Utilities](#client-display-utilities)\n    - [OAuth Authentication for Clients](#oauth-authentication-for-clients)\n    - [Parsing Tool Results](#parsing-tool-results)\n    - [MCP Primitives](#mcp-primitives)\n    - [Server Capabilities](#server-capabilities)\n  - [Documentation](#documentation)\n  - [Contributing](#contributing)\n  - [License](#license)\n\n[pypi-badge]: https://img.shields.io/pypi/v/mcp.svg\n[pypi-url]: https://pypi.org/project/mcp/\n[mit-badge]: https://img.shields.io/pypi/l/mcp.svg\n[mit-url]: https://github.com/modelcontextprotocol/python-sdk/blob/main/LICENSE\n[python-badge]: https://img.shields.io/pypi/pyversions/mcp.svg\n[python-url]: https://www.python.org/downloads/\n[docs-badge]: https://img.shields.io/badge/docs-python--sdk-blue.svg\n[docs-url]: https://modelcontextprotocol.github.io/python-sdk/\n[protocol-badge]: https://img.shields.io/badge/protocol-modelcontextprotocol.io-blue.svg\n[protocol-url]: https://modelcontextprotocol.io\n[spec-badge]: https://img.shields.io/badge/spec-spec.modelcontextprotocol.io-blue.svg\n[spec-url]: https://modelcontextprotocol.io/specification/latest\n\n## Overview\n\nThe Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This Python SDK implements the full MCP specification, making it easy to:\n\n- Build MCP clients that can connect to any MCP server\n- Create MCP servers that expose resources, prompts and tools\n- Use standard transports like stdio, SSE, and Streamable HTTP\n- Handle all MCP protocol messages and lifecycle events\n\n## Installation\n\n### Adding MCP to your python project\n\nWe recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects.\n\nIf you haven't created a uv-managed project yet, create one:\n\n   ```bash\n   uv init mcp-server-demo\n   cd mcp-server-demo\n   ```\n\n   Then add MCP to your project dependencies:\n\n   ```bash\n   uv add \"mcp[cli]\"\n   ```\n\nAlternatively, for projects using pip for dependencies:\n\n```bash\npip install \"mcp[cli]\"\n```\n\n### Running the standalone MCP development tools\n\nTo run the mcp command with uv:\n\n```bash\nuv run mcp\n```\n\n## Quickstart\n\nLet's create a simple MCP server that exposes a calculator tool and some data:\n\n<!-- snippet-source examples/snippets/servers/mcpserver_quickstart.py -->\n```python\n\"\"\"MCPServer quickstart example.\n\nRun from the repository root:\n    uv run examples/snippets/servers/mcpserver_quickstart.py\n\"\"\"\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create an MCP server\nmcp = MCPServer(\"Demo\")\n\n\n# Add an addition tool\n@mcp.tool()\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers\"\"\"\n    return a + b\n\n\n# Add a dynamic greeting resource\n@mcp.resource(\"greeting://{name}\")\ndef get_greeting(name: str) -> str:\n    \"\"\"Get a personalized greeting\"\"\"\n    return f\"Hello, {name}!\"\n\n\n# Add a prompt\n@mcp.prompt()\ndef greet_user(name: str, style: str = \"friendly\") -> str:\n    \"\"\"Generate a greeting prompt\"\"\"\n    styles = {\n        \"friendly\": \"Please write a warm, friendly greeting\",\n        \"formal\": \"Please write a formal, professional greeting\",\n        \"casual\": \"Please write a casual, relaxed greeting\",\n    }\n\n    return f\"{styles.get(style, styles['friendly'])} for someone named {name}.\"\n\n\n# Run with streamable HTTP transport\nif __name__ == \"__main__\":\n    mcp.run(transport=\"streamable-http\", json_response=True)\n```\n\n_Full example: [examples/snippets/servers/mcpserver_quickstart.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/mcpserver_quickstart.py)_\n<!-- /snippet-source -->\n\nYou can install this server in [Claude Code](https://docs.claude.com/en/docs/claude-code/mcp) and interact with it right away. First, run the server:\n\n```bash\nuv run --with mcp examples/snippets/servers/mcpserver_quickstart.py\n```\n\nThen add it to Claude Code:\n\n```bash\nclaude mcp add --transport http my-server http://localhost:8000/mcp\n```\n\nAlternatively, you can test it with the MCP Inspector. Start the server as above, then in a separate terminal:\n\n```bash\nnpx -y @modelcontextprotocol/inspector\n```\n\nIn the inspector UI, connect to `http://localhost:8000/mcp`.\n\n## What is MCP?\n\nThe [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can:\n\n- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context)\n- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)\n- Define interaction patterns through **Prompts** (reusable templates for LLM interactions)\n- And more!\n\n## Core Concepts\n\n### Server\n\nThe MCPServer server is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing:\n\n<!-- snippet-source examples/snippets/servers/lifespan_example.py -->\n```python\n\"\"\"Example showing lifespan support for startup/shutdown with strong typing.\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom dataclasses import dataclass\n\nfrom mcp.server.mcpserver import Context, MCPServer\n\n\n# Mock database class for example\nclass Database:\n    \"\"\"Mock database class for example.\"\"\"\n\n    @classmethod\n    async def connect(cls) -> \"Database\":\n        \"\"\"Connect to database.\"\"\"\n        return cls()\n\n    async def disconnect(self) -> None:\n        \"\"\"Disconnect from database.\"\"\"\n        pass\n\n    def query(self) -> str:\n        \"\"\"Execute a query.\"\"\"\n        return \"Query result\"\n\n\n@dataclass\nclass AppContext:\n    \"\"\"Application context with typed dependencies.\"\"\"\n\n    db: Database\n\n\n@asynccontextmanager\nasync def app_lifespan(server: MCPServer) -> AsyncIterator[AppContext]:\n    \"\"\"Manage application lifecycle with type-safe context.\"\"\"\n    # Initialize on startup\n    db = await Database.connect()\n    try:\n        yield AppContext(db=db)\n    finally:\n        # Cleanup on shutdown\n        await db.disconnect()\n\n\n# Pass lifespan to server\nmcp = MCPServer(\"My App\", lifespan=app_lifespan)\n\n\n# Access type-safe lifespan context in tools\n@mcp.tool()\ndef query_db(ctx: Context[AppContext]) -> str:\n    \"\"\"Tool that uses initialized resources.\"\"\"\n    db = ctx.request_context.lifespan_context.db\n    return db.query()\n```\n\n_Full example: [examples/snippets/servers/lifespan_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lifespan_example.py)_\n<!-- /snippet-source -->\n\n### Resources\n\nResources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects:\n\n<!-- snippet-source examples/snippets/servers/basic_resource.py -->\n```python\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(name=\"Resource Example\")\n\n\n@mcp.resource(\"file://documents/{name}\")\ndef read_document(name: str) -> str:\n    \"\"\"Read a document by name.\"\"\"\n    # This would normally read from disk\n    return f\"Content of {name}\"\n\n\n@mcp.resource(\"config://settings\")\ndef get_settings() -> str:\n    \"\"\"Get application settings.\"\"\"\n    return \"\"\"{\n  \"theme\": \"dark\",\n  \"language\": \"en\",\n  \"debug\": false\n}\"\"\"\n```\n\n_Full example: [examples/snippets/servers/basic_resource.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_resource.py)_\n<!-- /snippet-source -->\n\n### Tools\n\nTools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects:\n\n<!-- snippet-source examples/snippets/servers/basic_tool.py -->\n```python\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(name=\"Tool Example\")\n\n\n@mcp.tool()\ndef sum(a: int, b: int) -> int:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n\n\n@mcp.tool()\ndef get_weather(city: str, unit: str = \"celsius\") -> str:\n    \"\"\"Get weather for a city.\"\"\"\n    # This would normally call a weather API\n    return f\"Weather in {city}: 22degrees{unit[0].upper()}\"\n```\n\n_Full example: [examples/snippets/servers/basic_tool.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_tool.py)_\n<!-- /snippet-source -->\n\nTools can optionally receive a Context object by including a parameter with the `Context` type annotation. This context is automatically injected by the MCPServer framework and provides access to MCP capabilities:\n\n<!-- snippet-source examples/snippets/servers/tool_progress.py -->\n```python\nfrom mcp.server.mcpserver import Context, MCPServer\n\nmcp = MCPServer(name=\"Progress Example\")\n\n\n@mcp.tool()\nasync def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:\n    \"\"\"Execute a task with progress updates.\"\"\"\n    await ctx.info(f\"Starting: {task_name}\")\n\n    for i in range(steps):\n        progress = (i + 1) / steps\n        await ctx.report_progress(\n            progress=progress,\n            total=1.0,\n            message=f\"Step {i + 1}/{steps}\",\n        )\n        await ctx.debug(f\"Completed step {i + 1}\")\n\n    return f\"Task '{task_name}' completed\"\n```\n\n_Full example: [examples/snippets/servers/tool_progress.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/tool_progress.py)_\n<!-- /snippet-source -->\n\n#### Structured Output\n\nTools will return structured results by default, if their return type\nannotation is compatible. Otherwise, they will return unstructured results.\n\nStructured output supports these return types:\n\n- Pydantic models (BaseModel subclasses)\n- TypedDicts\n- Dataclasses and other classes with type hints\n- `dict[str, T]` (where T is any JSON-serializable type)\n- Primitive types (str, int, float, bool, bytes, None) - wrapped in `{\"result\": value}`\n- Generic types (list, tuple, Union, Optional, etc.) - wrapped in `{\"result\": value}`\n\nClasses without type hints cannot be serialized for structured output. Only\nclasses with properly annotated attributes will be converted to Pydantic models\nfor schema generation and validation.\n\nStructured results are automatically validated against the output schema\ngenerated from the annotation. This ensures the tool returns well-typed,\nvalidated data that clients can easily process.\n\n**Note:** For backward compatibility, unstructured results are also\nreturned. Unstructured results are provided for backward compatibility\nwith previous versions of the MCP specification, and are quirks-compatible\nwith previous versions of MCPServer in the current version of the SDK.\n\n**Note:** In cases where a tool function's return type annotation\ncauses the tool to be classified as structured _and this is undesirable_,\nthe  classification can be suppressed by passing `structured_output=False`\nto the `@tool` decorator.\n\n##### Advanced: Direct CallToolResult\n\nFor full control over tool responses including the `_meta` field (for passing data to client applications without exposing it to the model), you can return `CallToolResult` directly:\n\n<!-- snippet-source examples/snippets/servers/direct_call_tool_result.py -->\n```python\n\"\"\"Example showing direct CallToolResult return for advanced control.\"\"\"\n\nfrom typing import Annotated\n\nfrom pydantic import BaseModel\n\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.types import CallToolResult, TextContent\n\nmcp = MCPServer(\"CallToolResult Example\")\n\n\nclass ValidationModel(BaseModel):\n    \"\"\"Model for validating structured output.\"\"\"\n\n    status: str\n    data: dict[str, int]\n\n\n@mcp.tool()\ndef advanced_tool() -> CallToolResult:\n    \"\"\"Return CallToolResult directly for full control including _meta field.\"\"\"\n    return CallToolResult(\n        content=[TextContent(type=\"text\", text=\"Response visible to the model\")],\n        _meta={\"hidden\": \"data for client applications only\"},\n    )\n\n\n@mcp.tool()\ndef validated_tool() -> Annotated[CallToolResult, ValidationModel]:\n    \"\"\"Return CallToolResult with structured output validation.\"\"\"\n    return CallToolResult(\n        content=[TextContent(type=\"text\", text=\"Validated response\")],\n        structured_content={\"status\": \"success\", \"data\": {\"result\": 42}},\n        _meta={\"internal\": \"metadata\"},\n    )\n\n\n@mcp.tool()\ndef empty_result_tool() -> CallToolResult:\n    \"\"\"For empty results, return CallToolResult with empty content.\"\"\"\n    return CallToolResult(content=[])\n```\n\n_Full example: [examples/snippets/servers/direct_call_tool_result.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/direct_call_tool_result.py)_\n<!-- /snippet-source -->\n\n**Important:** `CallToolResult` must always be returned (no `Optional` or `Union`). For empty results, use `CallToolResult(content=[])`. For optional simple types, use `str | None` without `CallToolResult`.\n\n<!-- snippet-source examples/snippets/servers/structured_output.py -->\n```python\n\"\"\"Example showing structured output with tools.\"\"\"\n\nfrom typing import TypedDict\n\nfrom pydantic import BaseModel, Field\n\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(\"Structured Output Example\")\n\n\n# Using Pydantic models for rich structured data\nclass WeatherData(BaseModel):\n    \"\"\"Weather information structure.\"\"\"\n\n    temperature: float = Field(description=\"Temperature in Celsius\")\n    humidity: float = Field(description=\"Humidity percentage\")\n    condition: str\n    wind_speed: float\n\n\n@mcp.tool()\ndef get_weather(city: str) -> WeatherData:\n    \"\"\"Get weather for a city - returns structured data.\"\"\"\n    # Simulated weather data\n    return WeatherData(\n        temperature=22.5,\n        humidity=45.0,\n        condition=\"sunny\",\n        wind_speed=5.2,\n    )\n\n\n# Using TypedDict for simpler structures\nclass LocationInfo(TypedDict):\n    latitude: float\n    longitude: float\n    name: str\n\n\n@mcp.tool()\ndef get_location(address: str) -> LocationInfo:\n    \"\"\"Get location coordinates\"\"\"\n    return LocationInfo(latitude=51.5074, longitude=-0.1278, name=\"London, UK\")\n\n\n# Using dict[str, Any] for flexible schemas\n@mcp.tool()\ndef get_statistics(data_type: str) -> dict[str, float]:\n    \"\"\"Get various statistics\"\"\"\n    return {\"mean\": 42.5, \"median\": 40.0, \"std_dev\": 5.2}\n\n\n# Ordinary classes with type hints work for structured output\nclass UserProfile:\n    name: str\n    age: int\n    email: str | None = None\n\n    def __init__(self, name: str, age: int, email: str | None = None):\n        self.name = name\n        self.age = age\n        self.email = email\n\n\n@mcp.tool()\ndef get_user(user_id: str) -> UserProfile:\n    \"\"\"Get user profile - returns structured data\"\"\"\n    return UserProfile(name=\"Alice\", age=30, email=\"alice@example.com\")\n\n\n# Classes WITHOUT type hints cannot be used for structured output\nclass UntypedConfig:\n    def __init__(self, setting1, setting2):  # type: ignore[reportMissingParameterType]\n        self.setting1 = setting1\n        self.setting2 = setting2\n\n\n@mcp.tool()\ndef get_config() -> UntypedConfig:\n    \"\"\"This returns unstructured output - no schema generated\"\"\"\n    return UntypedConfig(\"value1\", \"value2\")\n\n\n# Lists and other types are wrapped automatically\n@mcp.tool()\ndef list_cities() -> list[str]:\n    \"\"\"Get a list of cities\"\"\"\n    return [\"London\", \"Paris\", \"Tokyo\"]\n    # Returns: {\"result\": [\"London\", \"Paris\", \"Tokyo\"]}\n\n\n@mcp.tool()\ndef get_temperature(city: str) -> float:\n    \"\"\"Get temperature as a simple float\"\"\"\n    return 22.5\n    # Returns: {\"result\": 22.5}\n```\n\n_Full example: [examples/snippets/servers/structured_output.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/structured_output.py)_\n<!-- /snippet-source -->\n\n### Prompts\n\nPrompts are reusable templates that help LLMs interact with your server effectively:\n\n<!-- snippet-source examples/snippets/servers/basic_prompt.py -->\n```python\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.server.mcpserver.prompts import base\n\nmcp = MCPServer(name=\"Prompt Example\")\n\n\n@mcp.prompt(title=\"Code Review\")\ndef review_code(code: str) -> str:\n    return f\"Please review this code:\\n\\n{code}\"\n\n\n@mcp.prompt(title=\"Debug Assistant\")\ndef debug_error(error: str) -> list[base.Message]:\n    return [\n        base.UserMessage(\"I'm seeing this error:\"),\n        base.UserMessage(error),\n        base.AssistantMessage(\"I'll help debug that. What have you tried so far?\"),\n    ]\n```\n\n_Full example: [examples/snippets/servers/basic_prompt.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/basic_prompt.py)_\n<!-- /snippet-source -->\n\n### Icons\n\nMCP servers can provide icons for UI display. Icons can be added to the server implementation, tools, resources, and prompts:\n\n```python\nfrom mcp.server.mcpserver import MCPServer, Icon\n\n# Create an icon from a file path or URL\nicon = Icon(\n    src=\"icon.png\",\n    mimeType=\"image/png\",\n    sizes=\"64x64\"\n)\n\n# Add icons to server\nmcp = MCPServer(\n    \"My Server\",\n    website_url=\"https://example.com\",\n    icons=[icon]\n)\n\n# Add icons to tools, resources, and prompts\n@mcp.tool(icons=[icon])\ndef my_tool():\n    \"\"\"Tool with an icon.\"\"\"\n    return \"result\"\n\n@mcp.resource(\"demo://resource\", icons=[icon])\ndef my_resource():\n    \"\"\"Resource with an icon.\"\"\"\n    return \"content\"\n```\n\n_Full example: [examples/mcpserver/icons_demo.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/mcpserver/icons_demo.py)_\n\n### Images\n\nMCPServer provides an `Image` class that automatically handles image data:\n\n<!-- snippet-source examples/snippets/servers/images.py -->\n```python\n\"\"\"Example showing image handling with MCPServer.\"\"\"\n\nfrom PIL import Image as PILImage\n\nfrom mcp.server.mcpserver import Image, MCPServer\n\nmcp = MCPServer(\"Image Example\")\n\n\n@mcp.tool()\ndef create_thumbnail(image_path: str) -> Image:\n    \"\"\"Create a thumbnail from an image\"\"\"\n    img = PILImage.open(image_path)\n    img.thumbnail((100, 100))\n    return Image(data=img.tobytes(), format=\"png\")\n```\n\n_Full example: [examples/snippets/servers/images.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/images.py)_\n<!-- /snippet-source -->\n\n### Context\n\nThe Context object is automatically injected into tool and resource functions that request it via type hints. It provides access to MCP capabilities like logging, progress reporting, resource reading, user interaction, and request metadata.\n\n#### Getting Context in Functions\n\nTo use context in a tool or resource function, add a parameter with the `Context` type annotation:\n\n```python\nfrom mcp.server.mcpserver import Context, MCPServer\n\nmcp = MCPServer(name=\"Context Example\")\n\n\n@mcp.tool()\nasync def my_tool(x: int, ctx: Context) -> str:\n    \"\"\"Tool that uses context capabilities.\"\"\"\n    # The context parameter can have any name as long as it's type-annotated\n    return await process_with_context(x, ctx)\n```\n\n#### Context Properties and Methods\n\nThe Context object provides the following capabilities:\n\n- `ctx.request_id` - Unique ID for the current request\n- `ctx.client_id` - Client ID if available\n- `ctx.mcp_server` - Access to the MCPServer server instance (see [MCPServer Properties](#mcpserver-properties))\n- `ctx.session` - Access to the underlying session for advanced communication (see [Session Properties and Methods](#session-properties-and-methods))\n- `ctx.request_context` - Access to request-specific data and lifespan resources (see [Request Context Properties](#request-context-properties))\n- `await ctx.debug(message)` - Send debug log message\n- `await ctx.info(message)` - Send info log message\n- `await ctx.warning(message)` - Send warning log message\n- `await ctx.error(message)` - Send error log message\n- `await ctx.log(level, message, logger_name=None)` - Send log with custom level\n- `await ctx.report_progress(progress, total=None, message=None)` - Report operation progress\n- `await ctx.read_resource(uri)` - Read a resource by URI\n- `await ctx.elicit(message, schema)` - Request additional information from user with validation\n\n<!-- snippet-source examples/snippets/servers/tool_progress.py -->\n```python\nfrom mcp.server.mcpserver import Context, MCPServer\n\nmcp = MCPServer(name=\"Progress Example\")\n\n\n@mcp.tool()\nasync def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:\n    \"\"\"Execute a task with progress updates.\"\"\"\n    await ctx.info(f\"Starting: {task_name}\")\n\n    for i in range(steps):\n        progress = (i + 1) / steps\n        await ctx.report_progress(\n            progress=progress,\n            total=1.0,\n            message=f\"Step {i + 1}/{steps}\",\n        )\n        await ctx.debug(f\"Completed step {i + 1}\")\n\n    return f\"Task '{task_name}' completed\"\n```\n\n_Full example: [examples/snippets/servers/tool_progress.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/tool_progress.py)_\n<!-- /snippet-source -->\n\n### Completions\n\nMCP supports providing completion suggestions for prompt arguments and resource template parameters. With the context parameter, servers can provide completions based on previously resolved values:\n\nClient usage:\n\n<!-- snippet-source examples/snippets/clients/completion_client.py -->\n```python\n\"\"\"cd to the `examples/snippets` directory and run:\nuv run completion-client\n\"\"\"\n\nimport asyncio\nimport os\n\nfrom mcp import ClientSession, StdioServerParameters\nfrom mcp.client.stdio import stdio_client\nfrom mcp.types import PromptReference, ResourceTemplateReference\n\n# Create server parameters for stdio connection\nserver_params = StdioServerParameters(\n    command=\"uv\",  # Using uv to run the server\n    args=[\"run\", \"server\", \"completion\", \"stdio\"],  # Server with completion support\n    env={\"UV_INDEX\": os.environ.get(\"UV_INDEX\", \"\")},\n)\n\n\nasync def run():\n    \"\"\"Run the completion client example.\"\"\"\n    async with stdio_client(server_params) as (read, write):\n        async with ClientSession(read, write) as session:\n            # Initialize the connection\n            await session.initialize()\n\n            # List available resource templates\n            templates = await session.list_resource_templates()\n            print(\"Available resource templates:\")\n            for template in templates.resource_templates:\n                print(f\"  - {template.uri_template}\")\n\n            # List available prompts\n            prompts = await session.list_prompts()\n            print(\"\\nAvailable prompts:\")\n            for prompt in prompts.prompts:\n                print(f\"  - {prompt.name}\")\n\n            # Complete resource template arguments\n            if templates.resource_templates:\n                template = templates.resource_templates[0]\n                print(f\"\\nCompleting arguments for resource template: {template.uri_template}\")\n\n                # Complete without context\n                result = await session.complete(\n                    ref=ResourceTemplateReference(type=\"ref/resource\", uri=template.uri_template),\n                    argument={\"name\": \"owner\", \"value\": \"model\"},\n                )\n                print(f\"Completions for 'owner' starting with 'model': {result.completion.values}\")\n\n                # Complete with context - repo suggestions based on owner\n                result = await session.complete(\n                    ref=ResourceTemplateReference(type=\"ref/resource\", uri=template.uri_template),\n                    argument={\"name\": \"repo\", \"value\": \"\"},\n                    context_arguments={\"owner\": \"modelcontextprotocol\"},\n                )\n                print(f\"Completions for 'repo' with owner='modelcontextprotocol': {result.completion.values}\")\n\n            # Complete prompt arguments\n            if prompts.prompts:\n                prompt_name = prompts.prompts[0].name\n                print(f\"\\nCompleting arguments for prompt: {prompt_name}\")\n\n                result = await session.complete(\n                    ref=PromptReference(type=\"ref/prompt\", name=prompt_name),\n                    argument={\"name\": \"style\", \"value\": \"\"},\n                )\n                print(f\"Completions for 'style' argument: {result.completion.values}\")\n\n\ndef main():\n    \"\"\"Entry point for the completion client.\"\"\"\n    asyncio.run(run())\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n_Full example: [examples/snippets/clients/completion_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/completion_client.py)_\n<!-- /snippet-source -->\n### Elicitation\n\nRequest additional information from users. This example shows an Elicitation during a Tool Call:\n\n<!-- snippet-source examples/snippets/servers/elicitation.py -->\n```python\n\"\"\"Elicitation examples demonstrating form and URL mode elicitation.\n\nForm mode elicitation collects structured, non-sensitive data through a schema.\nURL mode elicitation directs users to external URLs for sensitive operations\nlike OAuth flows, credential collection, or payment processing.\n\"\"\"\n\nimport uuid\n\nfrom pydantic import BaseModel, Field\n\nfrom mcp.server.mcpserver import Context, MCPServer\nfrom mcp.shared.exceptions import UrlElicitationRequiredError\nfrom mcp.types import ElicitRequestURLParams\n\nmcp = MCPServer(name=\"Elicitation Example\")\n\n\nclass BookingPreferences(BaseModel):\n    \"\"\"Schema for collecting user preferences.\"\"\"\n\n    checkAlternative: bool = Field(description=\"Would you like to check another date?\")\n    alternativeDate: str = Field(\n        default=\"2024-12-26\",\n        description=\"Alternative date (YYYY-MM-DD)\",\n    )\n\n\n@mcp.tool()\nasync def book_table(date: str, time: str, party_size: int, ctx: Context) -> str:\n    \"\"\"Book a table with date availability check.\n\n    This demonstrates form mode elicitation for collecting non-sensitive user input.\n    \"\"\"\n    # Check if date is available\n    if date == \"2024-12-25\":\n        # Date unavailable - ask user for alternative\n        result = await ctx.elicit(\n            message=(f\"No tables available for {party_size} on {date}. Would you like to try another date?\"),\n            schema=BookingPreferences,\n        )\n\n        if result.action == \"accept\" and result.data:\n            if result.data.checkAlternative:\n                return f\"[SUCCESS] Booked for {result.data.alternativeDate}\"\n            return \"[CANCELLED] No booking made\"\n        return \"[CANCELLED] Booking cancelled\"\n\n    # Date available\n    return f\"[SUCCESS] Booked for {date} at {time}\"\n\n\n@mcp.tool()\nasync def secure_payment(amount: float, ctx: Context) -> str:\n    \"\"\"Process a secure payment requiring URL confirmation.\n\n    This demonstrates URL mode elicitation using ctx.elicit_url() for\n    operations that require out-of-band user interaction.\n    \"\"\"\n    elicitation_id = str(uuid.uuid4())\n\n    result = await ctx.elicit_url(\n        message=f\"Please confirm payment of ${amount:.2f}\",\n        url=f\"https://payments.example.com/confirm?amount={amount}&id={elicitation_id}\",\n        elicitation_id=elicitation_id,\n    )\n\n    if result.action == \"accept\":\n        # In a real app, the payment confirmation would happen out-of-band\n        # and you'd verify the payment status from your backend\n        return f\"Payment of ${amount:.2f} initiated - check your browser to complete\"\n    elif result.action == \"decline\":\n        return \"Payment declined by user\"\n    return \"Payment cancelled\"\n\n\n@mcp.tool()\nasync def connect_service(service_name: str, ctx: Context) -> str:\n    \"\"\"Connect to a third-party service requiring OAuth authorization.\n\n    This demonstrates the \"throw error\" pattern using UrlElicitationRequiredError.\n    Use this pattern when the tool cannot proceed without user authorization.\n    \"\"\"\n    elicitation_id = str(uuid.uuid4())\n\n    # Raise UrlElicitationRequiredError to signal that the client must complete\n    # a URL elicitation before this request can be processed.\n    # The MCP framework will convert this to a -32042 error response.\n    raise UrlElicitationRequiredError(\n        [\n            ElicitRequestURLParams(\n                mode=\"url\",\n                message=f\"Authorization required to connect to {service_name}\",\n                url=f\"https://{service_name}.example.com/oauth/authorize?elicit={elicitation_id}\",\n                elicitation_id=elicitation_id,\n            )\n        ]\n    )\n```\n\n_Full example: [examples/snippets/servers/elicitation.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/elicitation.py)_\n<!-- /snippet-source -->\n\nElicitation schemas support default values for all field types. Default values are automatically included in the JSON schema sent to clients, allowing them to pre-populate forms.\n\nThe `elicit()` method returns an `ElicitationResult` with:\n\n- `action`: \"accept\", \"decline\", or \"cancel\"\n- `data`: The validated response (only when accepted)\n- `validation_error`: Any validation error message\n\n### Sampling\n\nTools can interact with LLMs through sampling (generating text):\n\n<!-- snippet-source examples/snippets/servers/sampling.py -->\n```python\nfrom mcp.server.mcpserver import Context, MCPServer\nfrom mcp.types import SamplingMessage, TextContent\n\nmcp = MCPServer(name=\"Sampling Example\")\n\n\n@mcp.tool()\nasync def generate_poem(topic: str, ctx: Context) -> str:\n    \"\"\"Generate a poem using LLM sampling.\"\"\"\n    prompt = f\"Write a short poem about {topic}\"\n\n    result = await ctx.session.create_message(\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=TextContent(type=\"text\", text=prompt),\n            )\n        ],\n        max_tokens=100,\n    )\n\n    # Since we're not passing tools param, result.content is single content\n    if result.content.type == \"text\":\n        return result.content.text\n    return str(result.content)\n```\n\n_Full example: [examples/snippets/servers/sampling.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/sampling.py)_\n<!-- /snippet-source -->\n\n### Logging and Notifications\n\nTools can send logs and notifications through the context:\n\n<!-- snippet-source examples/snippets/servers/notifications.py -->\n```python\nfrom mcp.server.mcpserver import Context, MCPServer\n\nmcp = MCPServer(name=\"Notifications Example\")\n\n\n@mcp.tool()\nasync def process_data(data: str, ctx: Context) -> str:\n    \"\"\"Process data with logging.\"\"\"\n    # Different log levels\n    await ctx.debug(f\"Debug: Processing '{data}'\")\n    await ctx.info(\"Info: Starting processing\")\n    await ctx.warning(\"Warning: This is experimental\")\n    await ctx.error(\"Error: (This is just a demo)\")\n\n    # Notify about resource changes\n    await ctx.session.send_resource_list_changed()\n\n    return f\"Processed: {data}\"\n```\n\n_Full example: [examples/snippets/servers/notifications.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/notifications.py)_\n<!-- /snippet-source -->\n\n### Authentication\n\nAuthentication can be used by servers that want to expose tools accessing protected resources.\n\n`mcp.server.auth` implements OAuth 2.1 resource server functionality, where MCP servers act as Resource Servers (RS) that validate tokens issued by separate Authorization Servers (AS). This follows the [MCP authorization specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization) and implements RFC 9728 (Protected Resource Metadata) for AS discovery.\n\nMCP servers can use authentication by providing an implementation of the `TokenVerifier` protocol:\n\n<!-- snippet-source examples/snippets/servers/oauth_server.py -->\n```python\n\"\"\"Run from the repository root:\nuv run examples/snippets/servers/oauth_server.py\n\"\"\"\n\nfrom pydantic import AnyHttpUrl\n\nfrom mcp.server.auth.provider import AccessToken, TokenVerifier\nfrom mcp.server.auth.settings import AuthSettings\nfrom mcp.server.mcpserver import MCPServer\n\n\nclass SimpleTokenVerifier(TokenVerifier):\n    \"\"\"Simple token verifier for demonstration.\"\"\"\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        pass  # This is where you would implement actual token validation\n\n\n# Create MCPServer instance as a Resource Server\nmcp = MCPServer(\n    \"Weather Service\",\n    # Token verifier for authentication\n    token_verifier=SimpleTokenVerifier(),\n    # Auth settings for RFC 9728 Protected Resource Metadata\n    auth=AuthSettings(\n        issuer_url=AnyHttpUrl(\"https://auth.example.com\"),  # Authorization Server URL\n        resource_server_url=AnyHttpUrl(\"http://localhost:3001\"),  # This server's URL\n        required_scopes=[\"user\"],\n    ),\n)\n\n\n@mcp.tool()\nasync def get_weather(city: str = \"London\") -> dict[str, str]:\n    \"\"\"Get weather data for a city\"\"\"\n    return {\n        \"city\": city,\n        \"temperature\": \"22\",\n        \"condition\": \"Partly cloudy\",\n        \"humidity\": \"65%\",\n    }\n\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"streamable-http\", json_response=True)\n```\n\n_Full example: [examples/snippets/servers/oauth_server.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/oauth_server.py)_\n<!-- /snippet-source -->\n\nFor a complete example with separate Authorization Server and Resource Server implementations, see [`examples/servers/simple-auth/`](examples/servers/simple-auth/).\n\n**Architecture:**\n\n- **Authorization Server (AS)**: Handles OAuth flows, user authentication, and token issuance\n- **Resource Server (RS)**: Your MCP server that validates tokens and serves protected resources\n- **Client**: Discovers AS through RFC 9728, obtains tokens, and uses them with the MCP server\n\nSee [TokenVerifier](src/mcp/server/auth/provider.py) for more details on implementing token validation.\n\n### MCPServer Properties\n\nThe MCPServer server instance accessible via `ctx.mcp_server` provides access to server configuration and metadata:\n\n- `ctx.mcp_server.name` - The server's name as defined during initialization\n- `ctx.mcp_server.instructions` - Server instructions/description provided to clients\n- `ctx.mcp_server.website_url` - Optional website URL for the server\n- `ctx.mcp_server.icons` - Optional list of icons for UI display\n- `ctx.mcp_server.settings` - Complete server configuration object containing:\n  - `debug` - Debug mode flag\n  - `log_level` - Current logging level\n  - `host` and `port` - Server network configuration\n  - `sse_path`, `streamable_http_path` - Transport paths\n  - `stateless_http` - Whether the server operates in stateless mode\n  - And other configuration options\n\n```python\n@mcp.tool()\ndef server_info(ctx: Context) -> dict:\n    \"\"\"Get information about the current server.\"\"\"\n    return {\n        \"name\": ctx.mcp_server.name,\n        \"instructions\": ctx.mcp_server.instructions,\n        \"debug_mode\": ctx.mcp_server.settings.debug,\n        \"log_level\": ctx.mcp_server.settings.log_level,\n        \"host\": ctx.mcp_server.settings.host,\n        \"port\": ctx.mcp_server.settings.port,\n    }\n```\n\n### Session Properties and Methods\n\nThe session object accessible via `ctx.session` provides advanced control over client communication:\n\n- `ctx.session.client_params` - Client initialization parameters and declared capabilities\n- `await ctx.session.send_log_message(level, data, logger)` - Send log messages with full control\n- `await ctx.session.create_message(messages, max_tokens)` - Request LLM sampling/completion\n- `await ctx.session.send_progress_notification(token, progress, total, message)` - Direct progress updates\n- `await ctx.session.send_resource_updated(uri)` - Notify clients that a specific resource changed\n- `await ctx.session.send_resource_list_changed()` - Notify clients that the resource list changed\n- `await ctx.session.send_tool_list_changed()` - Notify clients that the tool list changed\n- `await ctx.session.send_prompt_list_changed()` - Notify clients that the prompt list changed\n\n```python\n@mcp.tool()\nasync def notify_data_update(resource_uri: str, ctx: Context) -> str:\n    \"\"\"Update data and notify clients of the change.\"\"\"\n    # Perform data update logic here\n\n    # Notify clients that this specific resource changed\n    await ctx.session.send_resource_updated(AnyUrl(resource_uri))\n\n    # If this affects the overall resource list, notify about that too\n    await ctx.session.send_resource_list_changed()\n\n    return f\"Updated {resource_uri} and notified clients\"\n```\n\n### Request Context Properties\n\nThe request context accessible via `ctx.request_context` contains request-specific information and resources:\n\n- `ctx.request_context.lifespan_context` - Access to resources initialized during server startup\n  - Database connections, configuration objects, shared services\n  - Type-safe access to resources defined in your server's lifespan function\n- `ctx.request_context.meta` - Request metadata from the client including:\n  - `progressToken` - Token for progress notifications\n  - Other client-provided metadata\n- `ctx.request_context.request` - The original MCP request object for advanced processing\n- `ctx.request_context.request_id` - Unique identifier for this request\n\n```python\n# Example with typed lifespan context\n@dataclass\nclass AppContext:\n    db: Database\n    config: AppConfig\n\n@mcp.tool()\ndef query_with_config(query: str, ctx: Context) -> str:\n    \"\"\"Execute a query using shared database and configuration.\"\"\"\n    # Access typed lifespan context\n    app_ctx: AppContext = ctx.request_context.lifespan_context\n\n    # Use shared resources\n    connection = app_ctx.db\n    settings = app_ctx.config\n\n    # Execute query with configuration\n    result = connection.execute(query, timeout=settings.query_timeout)\n    return str(result)\n```\n\n_Full lifespan example: [examples/snippets/servers/lifespan_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lifespan_example.py)_\n\n## Running Your Server\n\n### Development Mode\n\nThe fastest way to test and debug your server is with the MCP Inspector:\n\n```bash\nuv run mcp dev server.py\n\n# Add dependencies\nuv run mcp dev server.py --with pandas --with numpy\n\n# Mount local code\nuv run mcp dev server.py --with-editable .\n```\n\n### Claude Desktop Integration\n\nOnce your server is ready, install it in Claude Desktop:\n\n```bash\nuv run mcp install server.py\n\n# Custom name\nuv run mcp install server.py --name \"My Analytics Server\"\n\n# Environment variables\nuv run mcp install server.py -v API_KEY=abc123 -v DB_URL=postgres://...\nuv run mcp install server.py -f .env\n```\n\n### Direct Execution\n\nFor advanced scenarios like custom deployments:\n\n<!-- snippet-source examples/snippets/servers/direct_execution.py -->\n```python\n\"\"\"Example showing direct execution of an MCP server.\n\nThis is the simplest way to run an MCP server directly.\ncd to the `examples/snippets` directory and run:\n    uv run direct-execution-server\n    or\n    python servers/direct_execution.py\n\"\"\"\n\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(\"My App\")\n\n\n@mcp.tool()\ndef hello(name: str = \"World\") -> str:\n    \"\"\"Say hello to someone.\"\"\"\n    return f\"Hello, {name}!\"\n\n\ndef main():\n    \"\"\"Entry point for the direct execution server.\"\"\"\n    mcp.run()\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n_Full example: [examples/snippets/servers/direct_execution.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/direct_execution.py)_\n<!-- /snippet-source -->\n\nRun it with:\n\n```bash\npython servers/direct_execution.py\n# or\nuv run mcp run servers/direct_execution.py\n```\n\nNote that `uv run mcp run` or `uv run mcp dev` only supports server using MCPServer and not the low-level server variant.\n\n### Streamable HTTP Transport\n\n> **Note**: Streamable HTTP transport is the recommended transport for production deployments. Use `stateless_http=True` and `json_response=True` for optimal scalability.\n\n<!-- snippet-source examples/snippets/servers/streamable_config.py -->\n```python\n\"\"\"Run from the repository root:\nuv run examples/snippets/servers/streamable_config.py\n\"\"\"\n\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(\"StatelessServer\")\n\n\n# Add a simple tool to demonstrate the server\n@mcp.tool()\ndef greet(name: str = \"World\") -> str:\n    \"\"\"Greet someone by name.\"\"\"\n    return f\"Hello, {name}!\"\n\n\n# Run server with streamable_http transport\n# Transport-specific options (stateless_http, json_response) are passed to run()\nif __name__ == \"__main__\":\n    # Stateless server with JSON responses (recommended)\n    mcp.run(transport=\"streamable-http\", stateless_http=True, json_response=True)\n\n    # Other configuration options:\n    # Stateless server with SSE streaming responses\n    # mcp.run(transport=\"streamable-http\", stateless_http=True)\n\n    # Stateful server with session persistence\n    # mcp.run(transport=\"streamable-http\")\n```\n\n_Full example: [examples/snippets/servers/streamable_config.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_config.py)_\n<!-- /snippet-source -->\n\nYou can mount multiple MCPServer servers in a Starlette application:\n\n<!-- snippet-source examples/snippets/servers/streamable_starlette_mount.py -->\n```python\n\"\"\"Run from the repository root:\nuvicorn examples.snippets.servers.streamable_starlette_mount:app --reload\n\"\"\"\n\nimport contextlib\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create the Echo server\necho_mcp = MCPServer(name=\"EchoServer\")\n\n\n@echo_mcp.tool()\ndef echo(message: str) -> str:\n    \"\"\"A simple echo tool\"\"\"\n    return f\"Echo: {message}\"\n\n\n# Create the Math server\nmath_mcp = MCPServer(name=\"MathServer\")\n\n\n@math_mcp.tool()\ndef add_two(n: int) -> int:\n    \"\"\"Tool to add two to the input\"\"\"\n    return n + 2\n\n\n# Create a combined lifespan to manage both session managers\n@contextlib.asynccontextmanager\nasync def lifespan(app: Starlette):\n    async with contextlib.AsyncExitStack() as stack:\n        await stack.enter_async_context(echo_mcp.session_manager.run())\n        await stack.enter_async_context(math_mcp.session_manager.run())\n        yield\n\n\n# Create the Starlette app and mount the MCP servers\napp = Starlette(\n    routes=[\n        Mount(\"/echo\", echo_mcp.streamable_http_app(stateless_http=True, json_response=True)),\n        Mount(\"/math\", math_mcp.streamable_http_app(stateless_http=True, json_response=True)),\n    ],\n    lifespan=lifespan,\n)\n\n# Note: Clients connect to http://localhost:8000/echo/mcp and http://localhost:8000/math/mcp\n# To mount at the root of each path (e.g., /echo instead of /echo/mcp):\n# echo_mcp.streamable_http_app(streamable_http_path=\"/\", stateless_http=True, json_response=True)\n# math_mcp.streamable_http_app(streamable_http_path=\"/\", stateless_http=True, json_response=True)\n```\n\n_Full example: [examples/snippets/servers/streamable_starlette_mount.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_starlette_mount.py)_\n<!-- /snippet-source -->\n\nFor low level server with Streamable HTTP implementations, see:\n\n- Stateful server: [`examples/servers/simple-streamablehttp/`](examples/servers/simple-streamablehttp/)\n- Stateless server: [`examples/servers/simple-streamablehttp-stateless/`](examples/servers/simple-streamablehttp-stateless/)\n\nThe streamable HTTP transport supports:\n\n- Stateful and stateless operation modes\n- Resumability with event stores\n- JSON or SSE response formats\n- Better scalability for multi-node deployments\n\n#### CORS Configuration for Browser-Based Clients\n\nIf you'd like your server to be accessible by browser-based MCP clients, you'll need to configure CORS headers. The `Mcp-Session-Id` header must be exposed for browser clients to access it:\n\n```python\nfrom starlette.applications import Starlette\nfrom starlette.middleware.cors import CORSMiddleware\n\n# Create your Starlette app first\nstarlette_app = Starlette(routes=[...])\n\n# Then wrap it with CORS middleware\nstarlette_app = CORSMiddleware(\n    starlette_app,\n    allow_origins=[\"*\"],  # Configure appropriately for production\n    allow_methods=[\"GET\", \"POST\", \"DELETE\"],  # MCP streamable HTTP methods\n    expose_headers=[\"Mcp-Session-Id\"],\n)\n```\n\nThis configuration is necessary because:\n\n- The MCP streamable HTTP transport uses the `Mcp-Session-Id` header for session management\n- Browsers restrict access to response headers unless explicitly exposed via CORS\n- Without this configuration, browser-based clients won't be able to read the session ID from initialization responses\n\n### Mounting to an Existing ASGI Server\n\nBy default, SSE servers are mounted at `/sse` and Streamable HTTP servers are mounted at `/mcp`. You can customize these paths using the methods described below.\n\nFor more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes).\n\n#### StreamableHTTP servers\n\nYou can mount the StreamableHTTP server to an existing ASGI server using the `streamable_http_app` method. This allows you to integrate the StreamableHTTP server with other ASGI applications.\n\n##### Basic mounting\n\n<!-- snippet-source examples/snippets/servers/streamable_http_basic_mounting.py -->\n```python\n\"\"\"Basic example showing how to mount StreamableHTTP server in Starlette.\n\nRun from the repository root:\n    uvicorn examples.snippets.servers.streamable_http_basic_mounting:app --reload\n\"\"\"\n\nimport contextlib\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create MCP server\nmcp = MCPServer(\"My App\")\n\n\n@mcp.tool()\ndef hello() -> str:\n    \"\"\"A simple hello tool\"\"\"\n    return \"Hello from MCP!\"\n\n\n# Create a lifespan context manager to run the session manager\n@contextlib.asynccontextmanager\nasync def lifespan(app: Starlette):\n    async with mcp.session_manager.run():\n        yield\n\n\n# Mount the StreamableHTTP server to the existing ASGI server\n# Transport-specific options are passed to streamable_http_app()\napp = Starlette(\n    routes=[\n        Mount(\"/\", app=mcp.streamable_http_app(json_response=True)),\n    ],\n    lifespan=lifespan,\n)\n```\n\n_Full example: [examples/snippets/servers/streamable_http_basic_mounting.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_basic_mounting.py)_\n<!-- /snippet-source -->\n\n##### Host-based routing\n\n<!-- snippet-source examples/snippets/servers/streamable_http_host_mounting.py -->\n```python\n\"\"\"Example showing how to mount StreamableHTTP server using Host-based routing.\n\nRun from the repository root:\n    uvicorn examples.snippets.servers.streamable_http_host_mounting:app --reload\n\"\"\"\n\nimport contextlib\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Host\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create MCP server\nmcp = MCPServer(\"MCP Host App\")\n\n\n@mcp.tool()\ndef domain_info() -> str:\n    \"\"\"Get domain-specific information\"\"\"\n    return \"This is served from mcp.acme.corp\"\n\n\n# Create a lifespan context manager to run the session manager\n@contextlib.asynccontextmanager\nasync def lifespan(app: Starlette):\n    async with mcp.session_manager.run():\n        yield\n\n\n# Mount using Host-based routing\n# Transport-specific options are passed to streamable_http_app()\napp = Starlette(\n    routes=[\n        Host(\"mcp.acme.corp\", app=mcp.streamable_http_app(json_response=True)),\n    ],\n    lifespan=lifespan,\n)\n```\n\n_Full example: [examples/snippets/servers/streamable_http_host_mounting.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_host_mounting.py)_\n<!-- /snippet-source -->\n\n##### Multiple servers with path configuration\n\n<!-- snippet-source examples/snippets/servers/streamable_http_multiple_servers.py -->\n```python\n\"\"\"Example showing how to mount multiple StreamableHTTP servers with path configuration.\n\nRun from the repository root:\n    uvicorn examples.snippets.servers.streamable_http_multiple_servers:app --reload\n\"\"\"\n\nimport contextlib\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create multiple MCP servers\napi_mcp = MCPServer(\"API Server\")\nchat_mcp = MCPServer(\"Chat Server\")\n\n\n@api_mcp.tool()\ndef api_status() -> str:\n    \"\"\"Get API status\"\"\"\n    return \"API is running\"\n\n\n@chat_mcp.tool()\ndef send_message(message: str) -> str:\n    \"\"\"Send a chat message\"\"\"\n    return f\"Message sent: {message}\"\n\n\n# Create a combined lifespan to manage both session managers\n@contextlib.asynccontextmanager\nasync def lifespan(app: Starlette):\n    async with contextlib.AsyncExitStack() as stack:\n        await stack.enter_async_context(api_mcp.session_manager.run())\n        await stack.enter_async_context(chat_mcp.session_manager.run())\n        yield\n\n\n# Mount the servers with transport-specific options passed to streamable_http_app()\n# streamable_http_path=\"/\" means endpoints will be at /api and /chat instead of /api/mcp and /chat/mcp\napp = Starlette(\n    routes=[\n        Mount(\"/api\", app=api_mcp.streamable_http_app(json_response=True, streamable_http_path=\"/\")),\n        Mount(\"/chat\", app=chat_mcp.streamable_http_app(json_response=True, streamable_http_path=\"/\")),\n    ],\n    lifespan=lifespan,\n)\n```\n\n_Full example: [examples/snippets/servers/streamable_http_multiple_servers.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_multiple_servers.py)_\n<!-- /snippet-source -->\n\n##### Path configuration at initialization\n\n<!-- snippet-source examples/snippets/servers/streamable_http_path_config.py -->\n```python\n\"\"\"Example showing path configuration when mounting MCPServer.\n\nRun from the repository root:\n    uvicorn examples.snippets.servers.streamable_http_path_config:app --reload\n\"\"\"\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create a simple MCPServer server\nmcp_at_root = MCPServer(\"My Server\")\n\n\n@mcp_at_root.tool()\ndef process_data(data: str) -> str:\n    \"\"\"Process some data\"\"\"\n    return f\"Processed: {data}\"\n\n\n# Mount at /process with streamable_http_path=\"/\" so the endpoint is /process (not /process/mcp)\n# Transport-specific options like json_response are passed to streamable_http_app()\napp = Starlette(\n    routes=[\n        Mount(\n            \"/process\",\n            app=mcp_at_root.streamable_http_app(json_response=True, streamable_http_path=\"/\"),\n        ),\n    ]\n)\n```\n\n_Full example: [examples/snippets/servers/streamable_http_path_config.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/streamable_http_path_config.py)_\n<!-- /snippet-source -->\n\n#### SSE servers\n\n> **Note**: SSE transport is being superseded by [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http).\n\nYou can mount the SSE server to an existing ASGI server using the `sse_app` method. This allows you to integrate the SSE server with other ASGI applications.\n\n```python\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount, Host\nfrom mcp.server.mcpserver import MCPServer\n\n\nmcp = MCPServer(\"My App\")\n\n# Mount the SSE server to the existing ASGI server\napp = Starlette(\n    routes=[\n        Mount('/', app=mcp.sse_app()),\n    ]\n)\n\n# or dynamically mount as host\napp.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app()))\n```\n\nYou can also mount multiple MCP servers at different sub-paths. The SSE transport automatically detects the mount path via ASGI's `root_path` mechanism, so message endpoints are correctly routed:\n\n```python\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\nfrom mcp.server.mcpserver import MCPServer\n\n# Create multiple MCP servers\ngithub_mcp = MCPServer(\"GitHub API\")\nbrowser_mcp = MCPServer(\"Browser\")\nsearch_mcp = MCPServer(\"Search\")\n\n# Mount each server at its own sub-path\n# The SSE transport automatically uses ASGI's root_path to construct\n# the correct message endpoint (e.g., /github/messages/, /browser/messages/)\napp = Starlette(\n    routes=[\n        Mount(\"/github\", app=github_mcp.sse_app()),\n        Mount(\"/browser\", app=browser_mcp.sse_app()),\n        Mount(\"/search\", app=search_mcp.sse_app()),\n    ]\n)\n```\n\nFor more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes).\n\n## Advanced Usage\n\n### Low-Level Server\n\nFor more control, you can use the low-level server implementation directly. This gives you full access to the protocol and allows you to customize every aspect of your server, including lifecycle management through the lifespan API:\n\n<!-- snippet-source examples/snippets/servers/lowlevel/lifespan.py -->\n```python\n\"\"\"Run from the repository root:\nuv run examples/snippets/servers/lowlevel/lifespan.py\n\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom typing import TypedDict\n\nimport mcp.server.stdio\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\n\n\n# Mock database class for example\nclass Database:\n    \"\"\"Mock database class for example.\"\"\"\n\n    @classmethod\n    async def connect(cls) -> \"Database\":\n        \"\"\"Connect to database.\"\"\"\n        print(\"Database connected\")\n        return cls()\n\n    async def disconnect(self) -> None:\n        \"\"\"Disconnect from database.\"\"\"\n        print(\"Database disconnected\")\n\n    async def query(self, query_str: str) -> list[dict[str, str]]:\n        \"\"\"Execute a query.\"\"\"\n        # Simulate database query\n        return [{\"id\": \"1\", \"name\": \"Example\", \"query\": query_str}]\n\n\nclass AppContext(TypedDict):\n    db: Database\n\n\n@asynccontextmanager\nasync def server_lifespan(_server: Server[AppContext]) -> AsyncIterator[AppContext]:\n    \"\"\"Manage server startup and shutdown lifecycle.\"\"\"\n    db = await Database.connect()\n    try:\n        yield {\"db\": db}\n    finally:\n        await db.disconnect()\n\n\nasync def handle_list_tools(\n    ctx: ServerRequestContext[AppContext], params: types.PaginatedRequestParams | None\n) -> types.ListToolsResult:\n    \"\"\"List available tools.\"\"\"\n    return types.ListToolsResult(\n        tools=[\n            types.Tool(\n                name=\"query_db\",\n                description=\"Query the database\",\n                input_schema={\n                    \"type\": \"object\",\n                    \"properties\": {\"query\": {\"type\": \"string\", \"description\": \"SQL query to execute\"}},\n                    \"required\": [\"query\"],\n                },\n            )\n        ]\n    )\n\n\nasync def handle_call_tool(\n    ctx: ServerRequestContext[AppContext], params: types.CallToolRequestParams\n) -> types.CallToolResult:\n    \"\"\"Handle database query tool call.\"\"\"\n    if params.name != \"query_db\":\n        raise ValueError(f\"Unknown tool: {params.name}\")\n\n    db = ctx.lifespan_context[\"db\"]\n    results = await db.query((params.arguments or {})[\"query\"])\n\n    return types.CallToolResult(content=[types.TextContent(type=\"text\", text=f\"Query results: {results}\")])\n\n\nserver = Server(\n    \"example-server\",\n    lifespan=server_lifespan,\n    on_list_tools=handle_list_tools,\n    on_call_tool=handle_call_tool,\n)\n\n\nasync def run():\n    \"\"\"Run the server with lifespan management.\"\"\"\n    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):\n        await server.run(\n            read_stream,\n            write_stream,\n            server.create_initialization_options(),\n        )\n\n\nif __name__ == \"__main__\":\n    import asyncio\n\n    asyncio.run(run())\n```\n\n_Full example: [examples/snippets/servers/lowlevel/lifespan.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/lifespan.py)_\n<!-- /snippet-source -->\n\nThe lifespan API provides:\n\n- A way to initialize resources when the server starts and clean them up when it stops\n- Access to initialized resources through the request context in handlers\n- Type-safe context passing between lifespan and request handlers\n\n<!-- snippet-source examples/snippets/servers/lowlevel/basic.py -->\n```python\n\"\"\"Run from the repository root:\nuv run examples/snippets/servers/lowlevel/basic.py\n\"\"\"\n\nimport asyncio\n\nimport mcp.server.stdio\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\n\n\nasync def handle_list_prompts(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListPromptsResult:\n    \"\"\"List available prompts.\"\"\"\n    return types.ListPromptsResult(\n        prompts=[\n            types.Prompt(\n                name=\"example-prompt\",\n                description=\"An example prompt template\",\n                arguments=[types.PromptArgument(name=\"arg1\", description=\"Example argument\", required=True)],\n            )\n        ]\n    )\n\n\nasync def handle_get_prompt(ctx: ServerRequestContext, params: types.GetPromptRequestParams) -> types.GetPromptResult:\n    \"\"\"Get a specific prompt by name.\"\"\"\n    if params.name != \"example-prompt\":\n        raise ValueError(f\"Unknown prompt: {params.name}\")\n\n    arg1_value = (params.arguments or {}).get(\"arg1\", \"default\")\n\n    return types.GetPromptResult(\n        description=\"Example prompt\",\n        messages=[\n            types.PromptMessage(\n                role=\"user\",\n                content=types.TextContent(type=\"text\", text=f\"Example prompt text with argument: {arg1_value}\"),\n            )\n        ],\n    )\n\n\nserver = Server(\n    \"example-server\",\n    on_list_prompts=handle_list_prompts,\n    on_get_prompt=handle_get_prompt,\n)\n\n\nasync def run():\n    \"\"\"Run the basic low-level server.\"\"\"\n    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):\n        await server.run(\n            read_stream,\n            write_stream,\n            server.create_initialization_options(),\n        )\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run())\n```\n\n_Full example: [examples/snippets/servers/lowlevel/basic.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/basic.py)_\n<!-- /snippet-source -->\n\nCaution: The `uv run mcp run` and `uv run mcp dev` tool doesn't support low-level server.\n\n#### Structured Output Support\n\nThe low-level server supports structured output for tools, allowing you to return both human-readable content and machine-readable structured data. Tools can define an `outputSchema` to validate their structured output:\n\n<!-- snippet-source examples/snippets/servers/lowlevel/structured_output.py -->\n```python\n\"\"\"Run from the repository root:\nuv run examples/snippets/servers/lowlevel/structured_output.py\n\"\"\"\n\nimport asyncio\nimport json\n\nimport mcp.server.stdio\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\n\n\nasync def handle_list_tools(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListToolsResult:\n    \"\"\"List available tools with structured output schemas.\"\"\"\n    return types.ListToolsResult(\n        tools=[\n            types.Tool(\n                name=\"get_weather\",\n                description=\"Get current weather for a city\",\n                input_schema={\n                    \"type\": \"object\",\n                    \"properties\": {\"city\": {\"type\": \"string\", \"description\": \"City name\"}},\n                    \"required\": [\"city\"],\n                },\n                output_schema={\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"temperature\": {\"type\": \"number\", \"description\": \"Temperature in Celsius\"},\n                        \"condition\": {\"type\": \"string\", \"description\": \"Weather condition\"},\n                        \"humidity\": {\"type\": \"number\", \"description\": \"Humidity percentage\"},\n                        \"city\": {\"type\": \"string\", \"description\": \"City name\"},\n                    },\n                    \"required\": [\"temperature\", \"condition\", \"humidity\", \"city\"],\n                },\n            )\n        ]\n    )\n\n\nasync def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:\n    \"\"\"Handle tool calls with structured output.\"\"\"\n    if params.name == \"get_weather\":\n        city = (params.arguments or {})[\"city\"]\n\n        weather_data = {\n            \"temperature\": 22.5,\n            \"condition\": \"partly cloudy\",\n            \"humidity\": 65,\n            \"city\": city,\n        }\n\n        return types.CallToolResult(\n            content=[types.TextContent(type=\"text\", text=json.dumps(weather_data, indent=2))],\n            structured_content=weather_data,\n        )\n\n    raise ValueError(f\"Unknown tool: {params.name}\")\n\n\nserver = Server(\n    \"example-server\",\n    on_list_tools=handle_list_tools,\n    on_call_tool=handle_call_tool,\n)\n\n\nasync def run():\n    \"\"\"Run the structured output server.\"\"\"\n    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):\n        await server.run(\n            read_stream,\n            write_stream,\n            server.create_initialization_options(),\n        )\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run())\n```\n\n_Full example: [examples/snippets/servers/lowlevel/structured_output.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/structured_output.py)_\n<!-- /snippet-source -->\n\nWith the low-level server, handlers always return `CallToolResult` directly. You construct both the human-readable `content` and the machine-readable `structured_content` yourself, giving you full control over the response.\n\n##### Returning CallToolResult with `_meta`\n\nFor passing data to client applications without exposing it to the model, use the `_meta` field on `CallToolResult`:\n\n<!-- snippet-source examples/snippets/servers/lowlevel/direct_call_tool_result.py -->\n```python\n\"\"\"Run from the repository root:\nuv run examples/snippets/servers/lowlevel/direct_call_tool_result.py\n\"\"\"\n\nimport asyncio\n\nimport mcp.server.stdio\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\n\n\nasync def handle_list_tools(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListToolsResult:\n    \"\"\"List available tools.\"\"\"\n    return types.ListToolsResult(\n        tools=[\n            types.Tool(\n                name=\"advanced_tool\",\n                description=\"Tool with full control including _meta field\",\n                input_schema={\n                    \"type\": \"object\",\n                    \"properties\": {\"message\": {\"type\": \"string\"}},\n                    \"required\": [\"message\"],\n                },\n            )\n        ]\n    )\n\n\nasync def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:\n    \"\"\"Handle tool calls by returning CallToolResult directly.\"\"\"\n    if params.name == \"advanced_tool\":\n        message = (params.arguments or {}).get(\"message\", \"\")\n        return types.CallToolResult(\n            content=[types.TextContent(type=\"text\", text=f\"Processed: {message}\")],\n            structured_content={\"result\": \"success\", \"message\": message},\n            _meta={\"hidden\": \"data for client applications only\"},\n        )\n\n    raise ValueError(f\"Unknown tool: {params.name}\")\n\n\nserver = Server(\n    \"example-server\",\n    on_list_tools=handle_list_tools,\n    on_call_tool=handle_call_tool,\n)\n\n\nasync def run():\n    \"\"\"Run the server.\"\"\"\n    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):\n        await server.run(\n            read_stream,\n            write_stream,\n            server.create_initialization_options(),\n        )\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run())\n```\n\n_Full example: [examples/snippets/servers/lowlevel/direct_call_tool_result.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/lowlevel/direct_call_tool_result.py)_\n<!-- /snippet-source -->\n\n### Pagination (Advanced)\n\nFor servers that need to handle large datasets, the low-level server provides paginated versions of list operations. This is an optional optimization - most servers won't need pagination unless they're dealing with hundreds or thousands of items.\n\n#### Server-side Implementation\n\n<!-- snippet-source examples/snippets/servers/pagination_example.py -->\n```python\n\"\"\"Example of implementing pagination with the low-level MCP server.\"\"\"\n\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\n\n# Sample data to paginate\nITEMS = [f\"Item {i}\" for i in range(1, 101)]  # 100 items\n\n\nasync def handle_list_resources(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListResourcesResult:\n    \"\"\"List resources with pagination support.\"\"\"\n    page_size = 10\n\n    # Extract cursor from request params\n    cursor = params.cursor if params is not None else None\n\n    # Parse cursor to get offset\n    start = 0 if cursor is None else int(cursor)\n    end = start + page_size\n\n    # Get page of resources\n    page_items = [\n        types.Resource(uri=f\"resource://items/{item}\", name=item, description=f\"Description for {item}\")\n        for item in ITEMS[start:end]\n    ]\n\n    # Determine next cursor\n    next_cursor = str(end) if end < len(ITEMS) else None\n\n    return types.ListResourcesResult(resources=page_items, next_cursor=next_cursor)\n\n\nserver = Server(\"paginated-server\", on_list_resources=handle_list_resources)\n```\n\n_Full example: [examples/snippets/servers/pagination_example.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/servers/pagination_example.py)_\n<!-- /snippet-source -->\n\n#### Client-side Consumption\n\n<!-- snippet-source examples/snippets/clients/pagination_client.py -->\n```python\n\"\"\"Example of consuming paginated MCP endpoints from a client.\"\"\"\n\nimport asyncio\n\nfrom mcp.client.session import ClientSession\nfrom mcp.client.stdio import StdioServerParameters, stdio_client\nfrom mcp.types import PaginatedRequestParams, Resource\n\n\nasync def list_all_resources() -> None:\n    \"\"\"Fetch all resources using pagination.\"\"\"\n    async with stdio_client(StdioServerParameters(command=\"uv\", args=[\"run\", \"mcp-simple-pagination\"])) as (\n        read,\n        write,\n    ):\n        async with ClientSession(read, write) as session:\n            await session.initialize()\n\n            all_resources: list[Resource] = []\n            cursor = None\n\n            while True:\n                # Fetch a page of resources\n                result = await session.list_resources(params=PaginatedRequestParams(cursor=cursor))\n                all_resources.extend(result.resources)\n\n                print(f\"Fetched {len(result.resources)} resources\")\n\n                # Check if there are more pages\n                if result.next_cursor:\n                    cursor = result.next_cursor\n                else:\n                    break\n\n            print(f\"Total resources: {len(all_resources)}\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(list_all_resources())\n```\n\n_Full example: [examples/snippets/clients/pagination_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/pagination_client.py)_\n<!-- /snippet-source -->\n\n#### Key Points\n\n- **Cursors are opaque strings** - the server defines the format (numeric offsets, timestamps, etc.)\n- **Return `nextCursor=None`** when there are no more pages\n- **Backward compatible** - clients that don't support pagination will still work (they'll just get the first page)\n- **Flexible page sizes** - Each endpoint can define its own page size based on data characteristics\n\nSee the [simple-pagination example](examples/servers/simple-pagination) for a complete implementation.\n\n### Writing MCP Clients\n\nThe SDK provides a high-level client interface for connecting to MCP servers using various [transports](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports):\n\n<!-- snippet-source examples/snippets/clients/stdio_client.py -->\n```python\n\"\"\"cd to the `examples/snippets/clients` directory and run:\nuv run client\n\"\"\"\n\nimport asyncio\nimport os\n\nfrom mcp import ClientSession, StdioServerParameters, types\nfrom mcp.client.context import ClientRequestContext\nfrom mcp.client.stdio import stdio_client\n\n# Create server parameters for stdio connection\nserver_params = StdioServerParameters(\n    command=\"uv\",  # Using uv to run the server\n    args=[\"run\", \"server\", \"mcpserver_quickstart\", \"stdio\"],  # We're already in snippets dir\n    env={\"UV_INDEX\": os.environ.get(\"UV_INDEX\", \"\")},\n)\n\n\n# Optional: create a sampling callback\nasync def handle_sampling_message(\n    context: ClientRequestContext, params: types.CreateMessageRequestParams\n) -> types.CreateMessageResult:\n    print(f\"Sampling request: {params.messages}\")\n    return types.CreateMessageResult(\n        role=\"assistant\",\n        content=types.TextContent(\n            type=\"text\",\n            text=\"Hello, world! from model\",\n        ),\n        model=\"gpt-3.5-turbo\",\n        stop_reason=\"endTurn\",\n    )\n\n\nasync def run():\n    async with stdio_client(server_params) as (read, write):\n        async with ClientSession(read, write, sampling_callback=handle_sampling_message) as session:\n            # Initialize the connection\n            await session.initialize()\n\n            # List available prompts\n            prompts = await session.list_prompts()\n            print(f\"Available prompts: {[p.name for p in prompts.prompts]}\")\n\n            # Get a prompt (greet_user prompt from mcpserver_quickstart)\n            if prompts.prompts:\n                prompt = await session.get_prompt(\"greet_user\", arguments={\"name\": \"Alice\", \"style\": \"friendly\"})\n                print(f\"Prompt result: {prompt.messages[0].content}\")\n\n            # List available resources\n            resources = await session.list_resources()\n            print(f\"Available resources: {[r.uri for r in resources.resources]}\")\n\n            # List available tools\n            tools = await session.list_tools()\n            print(f\"Available tools: {[t.name for t in tools.tools]}\")\n\n            # Read a resource (greeting resource from mcpserver_quickstart)\n            resource_content = await session.read_resource(\"greeting://World\")\n            content_block = resource_content.contents[0]\n            if isinstance(content_block, types.TextContent):\n                print(f\"Resource content: {content_block.text}\")\n\n            # Call a tool (add tool from mcpserver_quickstart)\n            result = await session.call_tool(\"add\", arguments={\"a\": 5, \"b\": 3})\n            result_unstructured = result.content[0]\n            if isinstance(result_unstructured, types.TextContent):\n                print(f\"Tool result: {result_unstructured.text}\")\n            result_structured = result.structured_content\n            print(f\"Structured tool result: {result_structured}\")\n\n\ndef main():\n    \"\"\"Entry point for the client script.\"\"\"\n    asyncio.run(run())\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n_Full example: [examples/snippets/clients/stdio_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/stdio_client.py)_\n<!-- /snippet-source -->\n\nClients can also connect using [Streamable HTTP transport](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http):\n\n<!-- snippet-source examples/snippets/clients/streamable_basic.py -->\n```python\n\"\"\"Run from the repository root:\nuv run examples/snippets/clients/streamable_basic.py\n\"\"\"\n\nimport asyncio\n\nfrom mcp import ClientSession\nfrom mcp.client.streamable_http import streamable_http_client\n\n\nasync def main():\n    # Connect to a streamable HTTP server\n    async with streamable_http_client(\"http://localhost:8000/mcp\") as (read_stream, write_stream):\n        # Create a session using the client streams\n        async with ClientSession(read_stream, write_stream) as session:\n            # Initialize the connection\n            await session.initialize()\n            # List available tools\n            tools = await session.list_tools()\n            print(f\"Available tools: {[tool.name for tool in tools.tools]}\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n_Full example: [examples/snippets/clients/streamable_basic.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/streamable_basic.py)_\n<!-- /snippet-source -->\n\n### Client Display Utilities\n\nWhen building MCP clients, the SDK provides utilities to help display human-readable names for tools, resources, and prompts:\n\n<!-- snippet-source examples/snippets/clients/display_utilities.py -->\n```python\n\"\"\"cd to the `examples/snippets` directory and run:\nuv run display-utilities-client\n\"\"\"\n\nimport asyncio\nimport os\n\nfrom mcp import ClientSession, StdioServerParameters\nfrom mcp.client.stdio import stdio_client\nfrom mcp.shared.metadata_utils import get_display_name\n\n# Create server parameters for stdio connection\nserver_params = StdioServerParameters(\n    command=\"uv\",  # Using uv to run the server\n    args=[\"run\", \"server\", \"mcpserver_quickstart\", \"stdio\"],\n    env={\"UV_INDEX\": os.environ.get(\"UV_INDEX\", \"\")},\n)\n\n\nasync def display_tools(session: ClientSession):\n    \"\"\"Display available tools with human-readable names\"\"\"\n    tools_response = await session.list_tools()\n\n    for tool in tools_response.tools:\n        # get_display_name() returns the title if available, otherwise the name\n        display_name = get_display_name(tool)\n        print(f\"Tool: {display_name}\")\n        if tool.description:\n            print(f\"   {tool.description}\")\n\n\nasync def display_resources(session: ClientSession):\n    \"\"\"Display available resources with human-readable names\"\"\"\n    resources_response = await session.list_resources()\n\n    for resource in resources_response.resources:\n        display_name = get_display_name(resource)\n        print(f\"Resource: {display_name} ({resource.uri})\")\n\n    templates_response = await session.list_resource_templates()\n    for template in templates_response.resource_templates:\n        display_name = get_display_name(template)\n        print(f\"Resource Template: {display_name}\")\n\n\nasync def run():\n    \"\"\"Run the display utilities example.\"\"\"\n    async with stdio_client(server_params) as (read, write):\n        async with ClientSession(read, write) as session:\n            # Initialize the connection\n            await session.initialize()\n\n            print(\"=== Available Tools ===\")\n            await display_tools(session)\n\n            print(\"\\n=== Available Resources ===\")\n            await display_resources(session)\n\n\ndef main():\n    \"\"\"Entry point for the display utilities client.\"\"\"\n    asyncio.run(run())\n\n\nif __name__ == \"__main__\":\n    main()\n```\n\n_Full example: [examples/snippets/clients/display_utilities.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/display_utilities.py)_\n<!-- /snippet-source -->\n\nThe `get_display_name()` function implements the proper precedence rules for displaying names:\n\n- For tools: `title` > `annotations.title` > `name`\n- For other objects: `title` > `name`\n\nThis ensures your client UI shows the most user-friendly names that servers provide.\n\n### OAuth Authentication for Clients\n\nThe SDK includes [authorization support](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for connecting to protected MCP servers:\n\n<!-- snippet-source examples/snippets/clients/oauth_client.py -->\n```python\n\"\"\"Before running, specify running MCP RS server URL.\nTo spin up RS server locally, see\n    examples/servers/simple-auth/README.md\n\ncd to the `examples/snippets` directory and run:\n    uv run oauth-client\n\"\"\"\n\nimport asyncio\nfrom urllib.parse import parse_qs, urlparse\n\nimport httpx\nfrom pydantic import AnyUrl\n\nfrom mcp import ClientSession\nfrom mcp.client.auth import OAuthClientProvider, TokenStorage\nfrom mcp.client.streamable_http import streamable_http_client\nfrom mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken\n\n\nclass InMemoryTokenStorage(TokenStorage):\n    \"\"\"Demo In-memory token storage implementation.\"\"\"\n\n    def __init__(self):\n        self.tokens: OAuthToken | None = None\n        self.client_info: OAuthClientInformationFull | None = None\n\n    async def get_tokens(self) -> OAuthToken | None:\n        \"\"\"Get stored tokens.\"\"\"\n        return self.tokens\n\n    async def set_tokens(self, tokens: OAuthToken) -> None:\n        \"\"\"Store tokens.\"\"\"\n        self.tokens = tokens\n\n    async def get_client_info(self) -> OAuthClientInformationFull | None:\n        \"\"\"Get stored client information.\"\"\"\n        return self.client_info\n\n    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:\n        \"\"\"Store client information.\"\"\"\n        self.client_info = client_info\n\n\nasync def handle_redirect(auth_url: str) -> None:\n    print(f\"Visit: {auth_url}\")\n\n\nasync def handle_callback() -> tuple[str, str | None]:\n    callback_url = input(\"Paste callback URL: \")\n    params = parse_qs(urlparse(callback_url).query)\n    return params[\"code\"][0], params.get(\"state\", [None])[0]\n\n\nasync def main():\n    \"\"\"Run the OAuth client example.\"\"\"\n    oauth_auth = OAuthClientProvider(\n        server_url=\"http://localhost:8001\",\n        client_metadata=OAuthClientMetadata(\n            client_name=\"Example MCP Client\",\n            redirect_uris=[AnyUrl(\"http://localhost:3000/callback\")],\n            grant_types=[\"authorization_code\", \"refresh_token\"],\n            response_types=[\"code\"],\n            scope=\"user\",\n        ),\n        storage=InMemoryTokenStorage(),\n        redirect_handler=handle_redirect,\n        callback_handler=handle_callback,\n    )\n\n    async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:\n        async with streamable_http_client(\"http://localhost:8001/mcp\", http_client=custom_client) as (read, write):\n            async with ClientSession(read, write) as session:\n                await session.initialize()\n\n                tools = await session.list_tools()\n                print(f\"Available tools: {[tool.name for tool in tools.tools]}\")\n\n                resources = await session.list_resources()\n                print(f\"Available resources: {[r.uri for r in resources.resources]}\")\n\n\ndef run():\n    asyncio.run(main())\n\n\nif __name__ == \"__main__\":\n    run()\n```\n\n_Full example: [examples/snippets/clients/oauth_client.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/examples/snippets/clients/oauth_client.py)_\n<!-- /snippet-source -->\n\nFor a complete working example, see [`examples/clients/simple-auth-client/`](examples/clients/simple-auth-client/).\n\n### Parsing Tool Results\n\nWhen calling tools through MCP, the `CallToolResult` object contains the tool's response in a structured format. Understanding how to parse this result is essential for properly handling tool outputs.\n\n```python\n\"\"\"examples/snippets/clients/parsing_tool_results.py\"\"\"\n\nimport asyncio\n\nfrom mcp import ClientSession, StdioServerParameters, types\nfrom mcp.client.stdio import stdio_client\n\n\nasync def parse_tool_results():\n    \"\"\"Demonstrates how to parse different types of content in CallToolResult.\"\"\"\n    server_params = StdioServerParameters(\n        command=\"python\", args=[\"path/to/mcp_server.py\"]\n    )\n\n    async with stdio_client(server_params) as (read, write):\n        async with ClientSession(read, write) as session:\n            await session.initialize()\n\n            # Example 1: Parsing text content\n            result = await session.call_tool(\"get_data\", {\"format\": \"text\"})\n            for content in result.content:\n                if isinstance(content, types.TextContent):\n                    print(f\"Text: {content.text}\")\n\n            # Example 2: Parsing structured content from JSON tools\n            result = await session.call_tool(\"get_user\", {\"id\": \"123\"})\n            if hasattr(result, \"structuredContent\") and result.structuredContent:\n                # Access structured data directly\n                user_data = result.structuredContent\n                print(f\"User: {user_data.get('name')}, Age: {user_data.get('age')}\")\n\n            # Example 3: Parsing embedded resources\n            result = await session.call_tool(\"read_config\", {})\n            for content in result.content:\n                if isinstance(content, types.EmbeddedResource):\n                    resource = content.resource\n                    if isinstance(resource, types.TextResourceContents):\n                        print(f\"Config from {resource.uri}: {resource.text}\")\n                    elif isinstance(resource, types.BlobResourceContents):\n                        print(f\"Binary data from {resource.uri}\")\n\n            # Example 4: Parsing image content\n            result = await session.call_tool(\"generate_chart\", {\"data\": [1, 2, 3]})\n            for content in result.content:\n                if isinstance(content, types.ImageContent):\n                    print(f\"Image ({content.mimeType}): {len(content.data)} bytes\")\n\n            # Example 5: Handling errors\n            result = await session.call_tool(\"failing_tool\", {})\n            if result.isError:\n                print(\"Tool execution failed!\")\n                for content in result.content:\n                    if isinstance(content, types.TextContent):\n                        print(f\"Error: {content.text}\")\n\n\nasync def main():\n    await parse_tool_results()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n```\n\n### MCP Primitives\n\nThe MCP protocol defines three core primitives that servers can implement:\n\n| Primitive | Control               | Description                                         | Example Use                  |\n|-----------|-----------------------|-----------------------------------------------------|------------------------------|\n| Prompts   | User-controlled       | Interactive templates invoked by user choice        | Slash commands, menu options |\n| Resources | Application-controlled| Contextual data managed by the client application   | File contents, API responses |\n| Tools     | Model-controlled      | Functions exposed to the LLM to take actions        | API calls, data updates      |\n\n### Server Capabilities\n\nMCP servers declare capabilities during initialization:\n\n| Capability   | Feature Flag                 | Description                        |\n|--------------|------------------------------|------------------------------------|\n| `prompts`    | `listChanged`                | Prompt template management         |\n| `resources`  | `subscribe`<br/>`listChanged`| Resource exposure and updates      |\n| `tools`      | `listChanged`                | Tool discovery and execution       |\n| `logging`    | -                            | Server logging configuration       |\n| `completions`| -                            | Argument completion suggestions    |\n\n## Documentation\n\n- [API Reference](https://modelcontextprotocol.github.io/python-sdk/api/)\n- [Experimental Features (Tasks)](https://modelcontextprotocol.github.io/python-sdk/experimental/tasks/)\n- [Model Context Protocol documentation](https://modelcontextprotocol.io)\n- [Model Context Protocol specification](https://modelcontextprotocol.io/specification/latest)\n- [Officially supported servers](https://github.com/modelcontextprotocol/servers)\n\n## Contributing\n\nWe are passionate about supporting contributors of all levels of experience and would love to see you get involved in the project. See the [contributing guide](CONTRIBUTING.md) to get started.\n\n## License\n\nThis project is licensed under the MIT License - see the LICENSE file for details.\n"
  },
  {
    "path": "RELEASE.md",
    "content": "# Release Process\n\n## Bumping Dependencies\n\n1. Change dependency version in `pyproject.toml`\n2. Upgrade lock with `uv lock --resolution lowest-direct`\n\n## Major or Minor Release\n\nCreate a GitHub release via UI with the tag being `vX.Y.Z` where `X.Y.Z` is the version,\nand the release title being the same. Then ask someone to review the release.\n\nThe package version will be set automatically from the tag.\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security Policy\n\nThank you for helping keep the Model Context Protocol and its ecosystem secure.\n\n## Reporting Security Issues\n\nIf you discover a security vulnerability in this repository, please report it through\nthe [GitHub Security Advisory process](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability)\nfor this repository.\n\nPlease **do not** report security vulnerabilities through public GitHub issues, discussions,\nor pull requests.\n\n## What to Include\n\nTo help us triage and respond quickly, please include:\n\n- A description of the vulnerability\n- Steps to reproduce the issue\n- The potential impact\n- Any suggested fixes (optional)\n"
  },
  {
    "path": "docs/authorization.md",
    "content": "# Authorization\n\n!!! warning \"Under Construction\"\n\n    This page is currently being written. Check back soon for complete documentation.\n"
  },
  {
    "path": "docs/concepts.md",
    "content": "# Concepts\n\n!!! warning \"Under Construction\"\n\n    This page is currently being written. Check back soon for complete documentation.\n\n<!--\n  - Server vs Client\n  - Three primitives (tools, resources, prompts)\n  - Transports (stdio, SSE, streamable HTTP)\n  - Context and sessions\n  - Lifecycle and state\n -->\n"
  },
  {
    "path": "docs/experimental/index.md",
    "content": "# Experimental Features\n\n!!! warning \"Experimental APIs\"\n\n    The features in this section are experimental and may change without notice.\n    They track the evolving MCP specification and are not yet stable.\n\nThis section documents experimental features in the MCP Python SDK. These features\nimplement draft specifications that are still being refined.\n\n## Available Experimental Features\n\n### [Tasks](tasks.md)\n\nTasks enable asynchronous execution of MCP operations. Instead of waiting for a\nlong-running operation to complete, the server returns a task reference immediately.\nClients can then poll for status updates and retrieve results when ready.\n\nTasks are useful for:\n\n- **Long-running computations** that would otherwise block\n- **Batch operations** that process many items\n- **Interactive workflows** that require user input (elicitation) or LLM assistance (sampling)\n\n## Using Experimental APIs\n\nExperimental features are accessed via the `.experimental` property:\n\n```python\n# Server-side: enable task support (auto-registers default handlers)\nserver = Server(name=\"my-server\")\nserver.experimental.enable_tasks()\n\n# Client-side\nresult = await session.experimental.call_tool_as_task(\"tool_name\", {\"arg\": \"value\"})\n```\n\n## Providing Feedback\n\nSince these features are experimental, feedback is especially valuable. If you encounter\nissues or have suggestions, please open an issue on the\n[python-sdk repository](https://github.com/modelcontextprotocol/python-sdk/issues).\n"
  },
  {
    "path": "docs/experimental/tasks-client.md",
    "content": "# Client Task Usage\n\n!!! warning \"Experimental\"\n\n    Tasks are an experimental feature. The API may change without notice.\n\nThis guide covers calling task-augmented tools from clients, handling the `input_required` status, and advanced patterns like receiving task requests from servers.\n\n## Quick Start\n\nCall a tool as a task and poll for the result:\n\n```python\nfrom mcp.client.session import ClientSession\nfrom mcp.types import CallToolResult\n\nasync with ClientSession(read, write) as session:\n    await session.initialize()\n\n    # Call tool as task\n    result = await session.experimental.call_tool_as_task(\n        \"process_data\",\n        {\"input\": \"hello\"},\n        ttl=60000,\n    )\n    task_id = result.task.taskId\n\n    # Poll until complete\n    async for status in session.experimental.poll_task(task_id):\n        print(f\"Status: {status.status} - {status.statusMessage or ''}\")\n\n    # Get result\n    final = await session.experimental.get_task_result(task_id, CallToolResult)\n    print(f\"Result: {final.content[0].text}\")\n```\n\n## Calling Tools as Tasks\n\nUse `call_tool_as_task()` to invoke a tool with task augmentation:\n\n```python\nresult = await session.experimental.call_tool_as_task(\n    \"my_tool\",           # Tool name\n    {\"arg\": \"value\"},    # Arguments\n    ttl=60000,           # Time-to-live in milliseconds\n    meta={\"key\": \"val\"}, # Optional metadata\n)\n\ntask_id = result.task.taskId\nprint(f\"Task: {task_id}, Status: {result.task.status}\")\n```\n\nThe response is a `CreateTaskResult` containing:\n\n- `task.taskId` - Unique identifier for polling\n- `task.status` - Initial status (usually `\"working\"`)\n- `task.pollInterval` - Suggested polling interval (milliseconds)\n- `task.ttl` - Time-to-live for results\n- `task.createdAt` - Creation timestamp\n\n## Polling with poll_task\n\nThe `poll_task()` async iterator polls until the task reaches a terminal state:\n\n```python\nasync for status in session.experimental.poll_task(task_id):\n    print(f\"Status: {status.status}\")\n    if status.statusMessage:\n        print(f\"Progress: {status.statusMessage}\")\n```\n\nIt automatically:\n\n- Respects the server's suggested `pollInterval`\n- Stops when status is `completed`, `failed`, or `cancelled`\n- Yields each status for progress display\n\n### Handling input_required\n\nWhen a task needs user input (elicitation), it transitions to `input_required`. You must call `get_task_result()` to receive and respond to the elicitation:\n\n```python\nasync for status in session.experimental.poll_task(task_id):\n    print(f\"Status: {status.status}\")\n\n    if status.status == \"input_required\":\n        # This delivers the elicitation and waits for completion\n        final = await session.experimental.get_task_result(task_id, CallToolResult)\n        break\n```\n\nThe elicitation callback (set during session creation) handles the actual user interaction.\n\n## Elicitation Callbacks\n\nTo handle elicitation requests from the server, provide a callback when creating the session:\n\n```python\nfrom mcp.types import ElicitRequestParams, ElicitResult\n\nasync def handle_elicitation(context, params: ElicitRequestParams) -> ElicitResult:\n    # Display the message to the user\n    print(f\"Server asks: {params.message}\")\n\n    # Collect user input (this is a simplified example)\n    response = input(\"Your response (y/n): \")\n    confirmed = response.lower() == \"y\"\n\n    return ElicitResult(\n        action=\"accept\",\n        content={\"confirm\": confirmed},\n    )\n\nasync with ClientSession(\n    read,\n    write,\n    elicitation_callback=handle_elicitation,\n) as session:\n    await session.initialize()\n    # ... call tasks that may require elicitation\n```\n\n## Sampling Callbacks\n\nSimilarly, handle sampling requests with a callback:\n\n```python\nfrom mcp.types import CreateMessageRequestParams, CreateMessageResult, TextContent\n\nasync def handle_sampling(context, params: CreateMessageRequestParams) -> CreateMessageResult:\n    # In a real implementation, call your LLM here\n    prompt = params.messages[-1].content.text if params.messages else \"\"\n\n    # Return a mock response\n    return CreateMessageResult(\n        role=\"assistant\",\n        content=TextContent(type=\"text\", text=f\"Response to: {prompt}\"),\n        model=\"my-model\",\n    )\n\nasync with ClientSession(\n    read,\n    write,\n    sampling_callback=handle_sampling,\n) as session:\n    # ...\n```\n\n## Retrieving Results\n\nOnce a task completes, retrieve the result:\n\n```python\nif status.status == \"completed\":\n    result = await session.experimental.get_task_result(task_id, CallToolResult)\n    for content in result.content:\n        if hasattr(content, \"text\"):\n            print(content.text)\n\nelif status.status == \"failed\":\n    print(f\"Task failed: {status.statusMessage}\")\n\nelif status.status == \"cancelled\":\n    print(\"Task was cancelled\")\n```\n\nThe result type matches the original request:\n\n- `tools/call` → `CallToolResult`\n- `sampling/createMessage` → `CreateMessageResult`\n- `elicitation/create` → `ElicitResult`\n\n## Cancellation\n\nCancel a running task:\n\n```python\ncancel_result = await session.experimental.cancel_task(task_id)\nprint(f\"Cancelled, status: {cancel_result.status}\")\n```\n\nNote: Cancellation is cooperative—the server must check for and handle cancellation.\n\n## Listing Tasks\n\nView all tasks on the server:\n\n```python\nresult = await session.experimental.list_tasks()\nfor task in result.tasks:\n    print(f\"{task.taskId}: {task.status}\")\n\n# Handle pagination\nwhile result.nextCursor:\n    result = await session.experimental.list_tasks(cursor=result.nextCursor)\n    for task in result.tasks:\n        print(f\"{task.taskId}: {task.status}\")\n```\n\n## Advanced: Client as Task Receiver\n\nServers can send task-augmented requests to clients. This is useful when the server needs the client to perform async work (like complex sampling or user interaction).\n\n### Declaring Client Capabilities\n\nRegister task handlers to declare what task-augmented requests your client accepts:\n\n```python\nfrom mcp.client.experimental.task_handlers import ExperimentalTaskHandlers\nfrom mcp.types import (\n    CreateTaskResult, GetTaskResult, GetTaskPayloadResult,\n    TaskMetadata, ElicitRequestParams,\n)\nfrom mcp.shared.experimental.tasks import InMemoryTaskStore\n\n# Client-side task store\nclient_store = InMemoryTaskStore()\n\nasync def handle_augmented_elicitation(context, params: ElicitRequestParams, task_metadata: TaskMetadata):\n    \"\"\"Handle task-augmented elicitation from server.\"\"\"\n    # Create a task for this elicitation\n    task = await client_store.create_task(task_metadata)\n\n    # Start async work (e.g., show UI, wait for user)\n    async def complete_elicitation():\n        # ... do async work ...\n        result = ElicitResult(action=\"accept\", content={\"confirm\": True})\n        await client_store.store_result(task.taskId, result)\n        await client_store.update_task(task.taskId, status=\"completed\")\n\n    context.session._task_group.start_soon(complete_elicitation)\n\n    # Return task reference immediately\n    return CreateTaskResult(task=task)\n\nasync def handle_get_task(context, params):\n    \"\"\"Handle tasks/get from server.\"\"\"\n    task = await client_store.get_task(params.taskId)\n    return GetTaskResult(\n        taskId=task.taskId,\n        status=task.status,\n        statusMessage=task.statusMessage,\n        createdAt=task.createdAt,\n        lastUpdatedAt=task.lastUpdatedAt,\n        ttl=task.ttl,\n        pollInterval=100,\n    )\n\nasync def handle_get_task_result(context, params):\n    \"\"\"Handle tasks/result from server.\"\"\"\n    result = await client_store.get_result(params.taskId)\n    return GetTaskPayloadResult.model_validate(result.model_dump())\n\ntask_handlers = ExperimentalTaskHandlers(\n    augmented_elicitation=handle_augmented_elicitation,\n    get_task=handle_get_task,\n    get_task_result=handle_get_task_result,\n)\n\nasync with ClientSession(\n    read,\n    write,\n    experimental_task_handlers=task_handlers,\n) as session:\n    # Client now accepts task-augmented elicitation from server\n    await session.initialize()\n```\n\nThis enables flows where:\n\n1. Client calls a task-augmented tool\n2. Server's tool work calls `task.elicit_as_task()`\n3. Client receives task-augmented elicitation\n4. Client creates its own task, does async work\n5. Server polls client's task\n6. Eventually both tasks complete\n\n## Complete Example\n\nA client that handles all task scenarios:\n\n```python\nimport anyio\nfrom mcp.client.session import ClientSession\nfrom mcp.client.stdio import stdio_client\nfrom mcp.types import CallToolResult, ElicitRequestParams, ElicitResult\n\n\nasync def elicitation_callback(context, params: ElicitRequestParams) -> ElicitResult:\n    print(f\"\\n[Elicitation] {params.message}\")\n    response = input(\"Confirm? (y/n): \")\n    return ElicitResult(action=\"accept\", content={\"confirm\": response.lower() == \"y\"})\n\n\nasync def main():\n    async with stdio_client(command=\"python\", args=[\"server.py\"]) as (read, write):\n        async with ClientSession(\n            read,\n            write,\n            elicitation_callback=elicitation_callback,\n        ) as session:\n            await session.initialize()\n\n            # List available tools\n            tools = await session.list_tools()\n            print(\"Tools:\", [t.name for t in tools.tools])\n\n            # Call a task-augmented tool\n            print(\"\\nCalling task tool...\")\n            result = await session.experimental.call_tool_as_task(\n                \"confirm_action\",\n                {\"action\": \"delete files\"},\n            )\n            task_id = result.task.taskId\n            print(f\"Task created: {task_id}\")\n\n            # Poll and handle input_required\n            async for status in session.experimental.poll_task(task_id):\n                print(f\"Status: {status.status}\")\n\n                if status.status == \"input_required\":\n                    final = await session.experimental.get_task_result(task_id, CallToolResult)\n                    print(f\"Result: {final.content[0].text}\")\n                    break\n\n            if status.status == \"completed\":\n                final = await session.experimental.get_task_result(task_id, CallToolResult)\n                print(f\"Result: {final.content[0].text}\")\n\n\nif __name__ == \"__main__\":\n    anyio.run(main)\n```\n\n## Error Handling\n\nHandle task errors gracefully:\n\n```python\nfrom mcp.shared.exceptions import MCPError\n\ntry:\n    result = await session.experimental.call_tool_as_task(\"my_tool\", args)\n    task_id = result.task.taskId\n\n    async for status in session.experimental.poll_task(task_id):\n        if status.status == \"failed\":\n            raise RuntimeError(f\"Task failed: {status.statusMessage}\")\n\n    final = await session.experimental.get_task_result(task_id, CallToolResult)\n\nexcept MCPError as e:\n    print(f\"MCP error: {e.message}\")\nexcept Exception as e:\n    print(f\"Error: {e}\")\n```\n\n## Next Steps\n\n- [Server Implementation](tasks-server.md) - Build task-supporting servers\n- [Tasks Overview](tasks.md) - Review lifecycle and concepts\n"
  },
  {
    "path": "docs/experimental/tasks-server.md",
    "content": "# Server Task Implementation\n\n!!! warning \"Experimental\"\n\n    Tasks are an experimental feature. The API may change without notice.\n\nThis guide covers implementing task support in MCP servers, from basic setup to advanced patterns like elicitation and sampling within tasks.\n\n## Quick Start\n\nThe simplest way to add task support:\n\n```python\nfrom mcp.server import Server\nfrom mcp.server.experimental.task_context import ServerTaskContext\nfrom mcp.types import CallToolResult, CreateTaskResult, TextContent, Tool, ToolExecution, TASK_REQUIRED\n\nserver = Server(\"my-server\")\nserver.experimental.enable_tasks()  # Registers all task handlers automatically\n\n@server.list_tools()\nasync def list_tools():\n    return [\n        Tool(\n            name=\"process_data\",\n            description=\"Process data asynchronously\",\n            inputSchema={\"type\": \"object\", \"properties\": {\"input\": {\"type\": \"string\"}}},\n            execution=ToolExecution(taskSupport=TASK_REQUIRED),\n        )\n    ]\n\n@server.call_tool()\nasync def handle_tool(name: str, arguments: dict) -> CallToolResult | CreateTaskResult:\n    if name == \"process_data\":\n        return await handle_process_data(arguments)\n    return CallToolResult(content=[TextContent(type=\"text\", text=f\"Unknown: {name}\")], isError=True)\n\nasync def handle_process_data(arguments: dict) -> CreateTaskResult:\n    ctx = server.request_context\n    ctx.experimental.validate_task_mode(TASK_REQUIRED)\n\n    async def work(task: ServerTaskContext) -> CallToolResult:\n        await task.update_status(\"Processing...\")\n        result = arguments.get(\"input\", \"\").upper()\n        return CallToolResult(content=[TextContent(type=\"text\", text=result)])\n\n    return await ctx.experimental.run_task(work)\n```\n\nThat's it. `enable_tasks()` automatically:\n\n- Creates an in-memory task store\n- Registers handlers for `tasks/get`, `tasks/result`, `tasks/list`, `tasks/cancel`\n- Updates server capabilities\n\n## Tool Declaration\n\nTools declare task support via the `execution.taskSupport` field:\n\n```python\nfrom mcp.types import Tool, ToolExecution, TASK_REQUIRED, TASK_OPTIONAL, TASK_FORBIDDEN\n\nTool(\n    name=\"my_tool\",\n    inputSchema={\"type\": \"object\"},\n    execution=ToolExecution(taskSupport=TASK_REQUIRED),  # or TASK_OPTIONAL, TASK_FORBIDDEN\n)\n```\n\n| Value | Meaning |\n|-------|---------|\n| `TASK_REQUIRED` | Tool **must** be called as a task |\n| `TASK_OPTIONAL` | Tool supports both sync and task execution |\n| `TASK_FORBIDDEN` | Tool **cannot** be called as a task (default) |\n\nValidate the request matches your tool's requirements:\n\n```python\n@server.call_tool()\nasync def handle_tool(name: str, arguments: dict):\n    ctx = server.request_context\n\n    if name == \"required_task_tool\":\n        ctx.experimental.validate_task_mode(TASK_REQUIRED)  # Raises if not task mode\n        return await handle_as_task(arguments)\n\n    elif name == \"optional_task_tool\":\n        if ctx.experimental.is_task:\n            return await handle_as_task(arguments)\n        else:\n            return handle_sync(arguments)\n```\n\n## The run_task Pattern\n\n`run_task()` is the recommended way to execute task work:\n\n```python\nasync def handle_my_tool(arguments: dict) -> CreateTaskResult:\n    ctx = server.request_context\n    ctx.experimental.validate_task_mode(TASK_REQUIRED)\n\n    async def work(task: ServerTaskContext) -> CallToolResult:\n        # Your work here\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"Done\")])\n\n    return await ctx.experimental.run_task(work)\n```\n\n**What `run_task()` does:**\n\n1. Creates a task in the store\n2. Spawns your work function in the background\n3. Returns `CreateTaskResult` immediately\n4. Auto-completes the task when your function returns\n5. Auto-fails the task if your function raises\n\n**The `ServerTaskContext` provides:**\n\n- `task.task_id` - The task identifier\n- `task.update_status(message)` - Update progress\n- `task.complete(result)` - Explicitly complete (usually automatic)\n- `task.fail(error)` - Explicitly fail\n- `task.is_cancelled` - Check if cancellation requested\n\n## Status Updates\n\nKeep clients informed of progress:\n\n```python\nasync def work(task: ServerTaskContext) -> CallToolResult:\n    await task.update_status(\"Starting...\")\n\n    for i, item in enumerate(items):\n        await task.update_status(f\"Processing {i+1}/{len(items)}\")\n        await process_item(item)\n\n    await task.update_status(\"Finalizing...\")\n    return CallToolResult(content=[TextContent(type=\"text\", text=\"Complete\")])\n```\n\nStatus messages appear in `tasks/get` responses, letting clients show progress to users.\n\n## Elicitation Within Tasks\n\nTasks can request user input via elicitation. This transitions the task to `input_required` status.\n\n### Form Elicitation\n\nCollect structured data from the user:\n\n```python\nasync def work(task: ServerTaskContext) -> CallToolResult:\n    await task.update_status(\"Waiting for confirmation...\")\n\n    result = await task.elicit(\n        message=\"Delete these files?\",\n        requestedSchema={\n            \"type\": \"object\",\n            \"properties\": {\n                \"confirm\": {\"type\": \"boolean\"},\n                \"reason\": {\"type\": \"string\"},\n            },\n            \"required\": [\"confirm\"],\n        },\n    )\n\n    if result.action == \"accept\" and result.content.get(\"confirm\"):\n        # User confirmed\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"Files deleted\")])\n    else:\n        # User declined or cancelled\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"Cancelled\")])\n```\n\n### URL Elicitation\n\nDirect users to external URLs for OAuth, payments, or other out-of-band flows:\n\n```python\nasync def work(task: ServerTaskContext) -> CallToolResult:\n    await task.update_status(\"Waiting for OAuth...\")\n\n    result = await task.elicit_url(\n        message=\"Please authorize with GitHub\",\n        url=\"https://github.com/login/oauth/authorize?client_id=...\",\n        elicitation_id=\"oauth-github-123\",\n    )\n\n    if result.action == \"accept\":\n        # User completed OAuth flow\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"Connected to GitHub\")])\n    else:\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"OAuth cancelled\")])\n```\n\n## Sampling Within Tasks\n\nTasks can request LLM completions from the client:\n\n```python\nfrom mcp.types import SamplingMessage, TextContent\n\nasync def work(task: ServerTaskContext) -> CallToolResult:\n    await task.update_status(\"Generating response...\")\n\n    result = await task.create_message(\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=TextContent(type=\"text\", text=\"Write a haiku about coding\"),\n            )\n        ],\n        max_tokens=100,\n    )\n\n    haiku = result.content.text if isinstance(result.content, TextContent) else \"Error\"\n    return CallToolResult(content=[TextContent(type=\"text\", text=haiku)])\n```\n\nSampling supports additional parameters:\n\n```python\nresult = await task.create_message(\n    messages=[...],\n    max_tokens=500,\n    system_prompt=\"You are a helpful assistant\",\n    temperature=0.7,\n    stop_sequences=[\"\\n\\n\"],\n    model_preferences=ModelPreferences(hints=[ModelHint(name=\"claude-3\")]),\n)\n```\n\n## Cancellation Support\n\nCheck for cancellation in long-running work:\n\n```python\nasync def work(task: ServerTaskContext) -> CallToolResult:\n    for i in range(1000):\n        if task.is_cancelled:\n            # Clean up and exit\n            return CallToolResult(content=[TextContent(type=\"text\", text=\"Cancelled\")])\n\n        await task.update_status(f\"Step {i}/1000\")\n        await process_step(i)\n\n    return CallToolResult(content=[TextContent(type=\"text\", text=\"Complete\")])\n```\n\nThe SDK's default cancel handler updates the task status. Your work function should check `is_cancelled` periodically.\n\n## Custom Task Store\n\nFor production, implement `TaskStore` with persistent storage:\n\n```python\nfrom mcp.shared.experimental.tasks.store import TaskStore\nfrom mcp.types import Task, TaskMetadata, Result\n\nclass RedisTaskStore(TaskStore):\n    def __init__(self, redis_client):\n        self.redis = redis_client\n\n    async def create_task(self, metadata: TaskMetadata, task_id: str | None = None) -> Task:\n        # Create and persist task\n        ...\n\n    async def get_task(self, task_id: str) -> Task | None:\n        # Retrieve task from Redis\n        ...\n\n    async def update_task(self, task_id: str, status: str | None = None, ...) -> Task:\n        # Update and persist\n        ...\n\n    async def store_result(self, task_id: str, result: Result) -> None:\n        # Store result in Redis\n        ...\n\n    async def get_result(self, task_id: str) -> Result | None:\n        # Retrieve result\n        ...\n\n    # ... implement remaining methods\n```\n\nUse your custom store:\n\n```python\nstore = RedisTaskStore(redis_client)\nserver.experimental.enable_tasks(store=store)\n```\n\n## Complete Example\n\nA server with multiple task-supporting tools:\n\n```python\nfrom mcp.server import Server\nfrom mcp.server.experimental.task_context import ServerTaskContext\nfrom mcp.types import (\n    CallToolResult, CreateTaskResult, TextContent, Tool, ToolExecution,\n    SamplingMessage, TASK_REQUIRED,\n)\n\nserver = Server(\"task-demo\")\nserver.experimental.enable_tasks()\n\n\n@server.list_tools()\nasync def list_tools():\n    return [\n        Tool(\n            name=\"confirm_action\",\n            description=\"Requires user confirmation\",\n            inputSchema={\"type\": \"object\", \"properties\": {\"action\": {\"type\": \"string\"}}},\n            execution=ToolExecution(taskSupport=TASK_REQUIRED),\n        ),\n        Tool(\n            name=\"generate_text\",\n            description=\"Generate text via LLM\",\n            inputSchema={\"type\": \"object\", \"properties\": {\"prompt\": {\"type\": \"string\"}}},\n            execution=ToolExecution(taskSupport=TASK_REQUIRED),\n        ),\n    ]\n\n\nasync def handle_confirm_action(arguments: dict) -> CreateTaskResult:\n    ctx = server.request_context\n    ctx.experimental.validate_task_mode(TASK_REQUIRED)\n\n    action = arguments.get(\"action\", \"unknown action\")\n\n    async def work(task: ServerTaskContext) -> CallToolResult:\n        result = await task.elicit(\n            message=f\"Confirm: {action}?\",\n            requestedSchema={\n                \"type\": \"object\",\n                \"properties\": {\"confirm\": {\"type\": \"boolean\"}},\n                \"required\": [\"confirm\"],\n            },\n        )\n\n        if result.action == \"accept\" and result.content.get(\"confirm\"):\n            return CallToolResult(content=[TextContent(type=\"text\", text=f\"Executed: {action}\")])\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"Cancelled\")])\n\n    return await ctx.experimental.run_task(work)\n\n\nasync def handle_generate_text(arguments: dict) -> CreateTaskResult:\n    ctx = server.request_context\n    ctx.experimental.validate_task_mode(TASK_REQUIRED)\n\n    prompt = arguments.get(\"prompt\", \"Hello\")\n\n    async def work(task: ServerTaskContext) -> CallToolResult:\n        await task.update_status(\"Generating...\")\n\n        result = await task.create_message(\n            messages=[SamplingMessage(role=\"user\", content=TextContent(type=\"text\", text=prompt))],\n            max_tokens=200,\n        )\n\n        text = result.content.text if isinstance(result.content, TextContent) else \"Error\"\n        return CallToolResult(content=[TextContent(type=\"text\", text=text)])\n\n    return await ctx.experimental.run_task(work)\n\n\n@server.call_tool()\nasync def handle_tool(name: str, arguments: dict) -> CallToolResult | CreateTaskResult:\n    if name == \"confirm_action\":\n        return await handle_confirm_action(arguments)\n    elif name == \"generate_text\":\n        return await handle_generate_text(arguments)\n    return CallToolResult(content=[TextContent(type=\"text\", text=f\"Unknown: {name}\")], isError=True)\n```\n\n## Error Handling in Tasks\n\nTasks handle errors automatically, but you can also fail explicitly:\n\n```python\nasync def work(task: ServerTaskContext) -> CallToolResult:\n    try:\n        result = await risky_operation()\n        return CallToolResult(content=[TextContent(type=\"text\", text=result)])\n    except PermissionError:\n        await task.fail(\"Access denied - insufficient permissions\")\n        raise\n    except TimeoutError:\n        await task.fail(\"Operation timed out after 30 seconds\")\n        raise\n```\n\nWhen `run_task()` catches an exception, it automatically:\n\n1. Marks the task as `failed`\n2. Sets `statusMessage` to the exception message\n3. Propagates the exception (which is caught by the task group)\n\nFor custom error messages, call `task.fail()` before raising.\n\n## HTTP Transport Example\n\nFor web applications, use the Streamable HTTP transport:\n\n```python\nimport uvicorn\n\nfrom mcp.server import Server\nfrom mcp.server.experimental.task_context import ServerTaskContext\nfrom mcp.types import (\n    CallToolResult, CreateTaskResult, TextContent, Tool, ToolExecution, TASK_REQUIRED,\n)\n\n\nserver = Server(\"http-task-server\")\nserver.experimental.enable_tasks()\n\n\n@server.list_tools()\nasync def list_tools():\n    return [\n        Tool(\n            name=\"long_operation\",\n            description=\"A long-running operation\",\n            inputSchema={\"type\": \"object\", \"properties\": {\"duration\": {\"type\": \"number\"}}},\n            execution=ToolExecution(taskSupport=TASK_REQUIRED),\n        )\n    ]\n\n\nasync def handle_long_operation(arguments: dict) -> CreateTaskResult:\n    ctx = server.request_context\n    ctx.experimental.validate_task_mode(TASK_REQUIRED)\n\n    duration = arguments.get(\"duration\", 5)\n\n    async def work(task: ServerTaskContext) -> CallToolResult:\n        import anyio\n        for i in range(int(duration)):\n            await task.update_status(f\"Step {i+1}/{int(duration)}\")\n            await anyio.sleep(1)\n        return CallToolResult(content=[TextContent(type=\"text\", text=f\"Completed after {duration}s\")])\n\n    return await ctx.experimental.run_task(work)\n\n\n@server.call_tool()\nasync def handle_tool(name: str, arguments: dict) -> CallToolResult | CreateTaskResult:\n    if name == \"long_operation\":\n        return await handle_long_operation(arguments)\n    return CallToolResult(content=[TextContent(type=\"text\", text=f\"Unknown: {name}\")], isError=True)\n\n\nif __name__ == \"__main__\":\n    uvicorn.run(server.streamable_http_app(), host=\"127.0.0.1\", port=8000)\n```\n\n## Testing Task Servers\n\nTest task functionality with the SDK's testing utilities:\n\n```python\nimport pytest\nimport anyio\nfrom mcp.client.session import ClientSession\nfrom mcp.types import CallToolResult\n\n\n@pytest.mark.anyio\nasync def test_task_tool():\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream(10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream(10)\n\n    async def run_server():\n        await server.run(\n            client_to_server_receive,\n            server_to_client_send,\n            server.create_initialization_options(),\n        )\n\n    async def run_client():\n        async with ClientSession(server_to_client_receive, client_to_server_send) as session:\n            await session.initialize()\n\n            # Call the tool as a task\n            result = await session.experimental.call_tool_as_task(\"my_tool\", {\"arg\": \"value\"})\n            task_id = result.task.taskId\n            assert result.task.status == \"working\"\n\n            # Poll until complete\n            async for status in session.experimental.poll_task(task_id):\n                if status.status in (\"completed\", \"failed\"):\n                    break\n\n            # Get result\n            final = await session.experimental.get_task_result(task_id, CallToolResult)\n            assert len(final.content) > 0\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(run_server)\n        tg.start_soon(run_client)\n```\n\n## Best Practices\n\n### Keep Work Functions Focused\n\n```python\n# Good: focused work function\nasync def work(task: ServerTaskContext) -> CallToolResult:\n    await task.update_status(\"Validating...\")\n    validate_input(arguments)\n\n    await task.update_status(\"Processing...\")\n    result = await process_data(arguments)\n\n    return CallToolResult(content=[TextContent(type=\"text\", text=result)])\n```\n\n### Check Cancellation in Loops\n\n```python\nasync def work(task: ServerTaskContext) -> CallToolResult:\n    results = []\n    for item in large_dataset:\n        if task.is_cancelled:\n            return CallToolResult(content=[TextContent(type=\"text\", text=\"Cancelled\")])\n\n        results.append(await process(item))\n\n    return CallToolResult(content=[TextContent(type=\"text\", text=str(results))])\n```\n\n### Use Meaningful Status Messages\n\n```python\nasync def work(task: ServerTaskContext) -> CallToolResult:\n    await task.update_status(\"Connecting to database...\")\n    db = await connect()\n\n    await task.update_status(\"Fetching records (0/1000)...\")\n    for i, record in enumerate(records):\n        if i % 100 == 0:\n            await task.update_status(f\"Processing records ({i}/1000)...\")\n        await process(record)\n\n    await task.update_status(\"Finalizing results...\")\n    return CallToolResult(content=[TextContent(type=\"text\", text=\"Done\")])\n```\n\n### Handle Elicitation Responses\n\n```python\nasync def work(task: ServerTaskContext) -> CallToolResult:\n    result = await task.elicit(message=\"Continue?\", requestedSchema={...})\n\n    match result.action:\n        case \"accept\":\n            # User accepted, process content\n            return await process_accepted(result.content)\n        case \"decline\":\n            # User explicitly declined\n            return CallToolResult(content=[TextContent(type=\"text\", text=\"User declined\")])\n        case \"cancel\":\n            # User cancelled the elicitation\n            return CallToolResult(content=[TextContent(type=\"text\", text=\"Cancelled\")])\n```\n\n## Next Steps\n\n- [Client Usage](tasks-client.md) - Learn how clients interact with task servers\n- [Tasks Overview](tasks.md) - Review lifecycle and concepts\n"
  },
  {
    "path": "docs/experimental/tasks.md",
    "content": "# Tasks\n\n!!! warning \"Experimental\"\n\n    Tasks are an experimental feature tracking the draft MCP specification.\n    The API may change without notice.\n\nTasks enable asynchronous request handling in MCP. Instead of blocking until an operation completes, the receiver creates a task, returns immediately, and the requestor polls for the result.\n\n## When to Use Tasks\n\nTasks are designed for operations that:\n\n- Take significant time (seconds to minutes)\n- Need progress updates during execution\n- Require user input mid-execution (elicitation, sampling)\n- Should run without blocking the requestor\n\nCommon use cases:\n\n- Long-running data processing\n- Multi-step workflows with user confirmation\n- LLM-powered operations requiring sampling\n- OAuth flows requiring user browser interaction\n\n## Task Lifecycle\n\n```text\n                    ┌─────────────┐\n                    │   working   │\n                    └──────┬──────┘\n                           │\n              ┌────────────┼────────────┐\n              │            │            │\n              ▼            ▼            ▼\n     ┌────────────┐  ┌───────────┐  ┌───────────┐\n     │ completed  │  │  failed   │  │ cancelled │\n     └────────────┘  └───────────┘  └───────────┘\n              ▲\n              │\n     ┌────────┴────────┐\n     │ input_required  │◄──────┐\n     └────────┬────────┘       │\n              │                │\n              └────────────────┘\n```\n\n| Status | Description |\n|--------|-------------|\n| `working` | Task is being processed |\n| `input_required` | Receiver needs input from requestor (elicitation/sampling) |\n| `completed` | Task finished successfully |\n| `failed` | Task encountered an error |\n| `cancelled` | Task was cancelled by requestor |\n\nTerminal states (`completed`, `failed`, `cancelled`) are final—tasks cannot transition out of them.\n\n## Bidirectional Flow\n\nTasks work in both directions:\n\n**Client → Server** (most common):\n\n```text\nClient                              Server\n  │                                    │\n  │── tools/call (task) ──────────────>│ Creates task\n  │<── CreateTaskResult ───────────────│\n  │                                    │\n  │── tasks/get ──────────────────────>│\n  │<── status: working ────────────────│\n  │                                    │ ... work continues ...\n  │── tasks/get ──────────────────────>│\n  │<── status: completed ──────────────│\n  │                                    │\n  │── tasks/result ───────────────────>│\n  │<── CallToolResult ─────────────────│\n```\n\n**Server → Client** (for elicitation/sampling):\n\n```text\nServer                              Client\n  │                                    │\n  │── elicitation/create (task) ──────>│ Creates task\n  │<── CreateTaskResult ───────────────│\n  │                                    │\n  │── tasks/get ──────────────────────>│\n  │<── status: working ────────────────│\n  │                                    │ ... user interaction ...\n  │── tasks/get ──────────────────────>│\n  │<── status: completed ──────────────│\n  │                                    │\n  │── tasks/result ───────────────────>│\n  │<── ElicitResult ───────────────────│\n```\n\n## Key Concepts\n\n### Task Metadata\n\nWhen augmenting a request with task execution, include `TaskMetadata`:\n\n```python\nfrom mcp.types import TaskMetadata\n\ntask = TaskMetadata(ttl=60000)  # TTL in milliseconds\n```\n\nThe `ttl` (time-to-live) specifies how long the task and result are retained after completion.\n\n### Task Store\n\nServers persist task state in a `TaskStore`. The SDK provides `InMemoryTaskStore` for development:\n\n```python\nfrom mcp.shared.experimental.tasks import InMemoryTaskStore\n\nstore = InMemoryTaskStore()\n```\n\nFor production, implement `TaskStore` with a database or distributed cache.\n\n### Capabilities\n\nBoth servers and clients declare task support through capabilities:\n\n**Server capabilities:**\n\n- `tasks.requests.tools.call` - Server accepts task-augmented tool calls\n\n**Client capabilities:**\n\n- `tasks.requests.sampling.createMessage` - Client accepts task-augmented sampling\n- `tasks.requests.elicitation.create` - Client accepts task-augmented elicitation\n\nThe SDK manages these automatically when you enable task support.\n\n## Quick Example\n\n**Server** (simplified API):\n\n```python\nfrom mcp.server import Server\nfrom mcp.server.experimental.task_context import ServerTaskContext\nfrom mcp.types import CallToolResult, TextContent, TASK_REQUIRED\n\nserver = Server(\"my-server\")\nserver.experimental.enable_tasks()  # One-line setup\n\n@server.call_tool()\nasync def handle_tool(name: str, arguments: dict):\n    ctx = server.request_context\n    ctx.experimental.validate_task_mode(TASK_REQUIRED)\n\n    async def work(task: ServerTaskContext):\n        await task.update_status(\"Processing...\")\n        # ... do work ...\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"Done!\")])\n\n    return await ctx.experimental.run_task(work)\n```\n\n**Client:**\n\n```python\nfrom mcp.client.session import ClientSession\nfrom mcp.types import CallToolResult\n\nasync with ClientSession(read, write) as session:\n    await session.initialize()\n\n    # Call tool as task\n    result = await session.experimental.call_tool_as_task(\"my_tool\", {\"arg\": \"value\"})\n    task_id = result.task.taskId\n\n    # Poll until done\n    async for status in session.experimental.poll_task(task_id):\n        print(f\"Status: {status.status}\")\n\n    # Get result\n    final = await session.experimental.get_task_result(task_id, CallToolResult)\n```\n\n## Next Steps\n\n- [Server Implementation](tasks-server.md) - Build task-supporting servers\n- [Client Usage](tasks-client.md) - Call and poll tasks from clients\n"
  },
  {
    "path": "docs/hooks/gen_ref_pages.py",
    "content": "\"\"\"Generate the code reference pages and navigation.\"\"\"\n\nfrom pathlib import Path\n\nimport mkdocs_gen_files\n\nnav = mkdocs_gen_files.Nav()\n\nroot = Path(__file__).parent.parent.parent\nsrc = root / \"src\"\n\nfor path in sorted(src.rglob(\"*.py\")):\n    module_path = path.relative_to(src).with_suffix(\"\")\n    doc_path = path.relative_to(src).with_suffix(\".md\")\n    full_doc_path = Path(\"api\", doc_path)\n\n    parts = tuple(module_path.parts)\n\n    if parts[-1] == \"__init__\":\n        parts = parts[:-1]\n        doc_path = doc_path.with_name(\"index.md\")\n        full_doc_path = full_doc_path.with_name(\"index.md\")\n    elif parts[-1].startswith(\"_\"):\n        continue\n\n    nav[parts] = doc_path.as_posix()\n\n    with mkdocs_gen_files.open(full_doc_path, \"w\") as fd:\n        ident = \".\".join(parts)\n        fd.write(f\"::: {ident}\")\n\n    mkdocs_gen_files.set_edit_path(full_doc_path, path.relative_to(root))\n\nwith mkdocs_gen_files.open(\"api/SUMMARY.md\", \"w\") as nav_file:\n    nav_file.writelines(nav.build_literate_nav())\n"
  },
  {
    "path": "docs/index.md",
    "content": "# MCP Python SDK\n\nThe **Model Context Protocol (MCP)** allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction.\n\nThis Python SDK implements the full MCP specification, making it easy to:\n\n- **Build MCP servers** that expose resources, prompts, and tools\n- **Create MCP clients** that can connect to any MCP server\n- **Use standard transports** like stdio, SSE, and Streamable HTTP\n\nIf you want to read more about the specification, please visit the [MCP documentation](https://modelcontextprotocol.io).\n\n## Quick Example\n\nHere's a simple MCP server that exposes a tool, resource, and prompt:\n\n```python title=\"server.py\"\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(\"Test Server\", json_response=True)\n\n\n@mcp.tool()\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers\"\"\"\n    return a + b\n\n\n@mcp.resource(\"greeting://{name}\")\ndef get_greeting(name: str) -> str:\n    \"\"\"Get a personalized greeting\"\"\"\n    return f\"Hello, {name}!\"\n\n\n@mcp.prompt()\ndef greet_user(name: str, style: str = \"friendly\") -> str:\n    \"\"\"Generate a greeting prompt\"\"\"\n    return f\"Write a {style} greeting for someone named {name}.\"\n\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"streamable-http\")\n```\n\nRun the server:\n\n```bash\nuv run --with mcp server.py\n```\n\nThen open the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) and connect to `http://localhost:8000/mcp`:\n\n```bash\nnpx -y @modelcontextprotocol/inspector\n```\n\n## Getting Started\n\n<!-- TODO(Marcelo): automatically generate the follow references with a header on each of those files. -->\n1. **[Install](installation.md)** the MCP SDK\n2. **[Learn concepts](concepts.md)** - understand the three primitives and architecture\n3. **[Explore authorization](authorization.md)** - add security to your servers\n4. **[Use low-level APIs](low-level-server.md)** - for advanced customization\n\n## API Reference\n\nFull API documentation is available in the [API Reference](api/mcp/index.md).\n"
  },
  {
    "path": "docs/installation.md",
    "content": "# Installation\n\nThe Python SDK is available on PyPI as [`mcp`](https://pypi.org/project/mcp/) so installation is as simple as:\n\n=== \"pip\"\n\n    ```bash\n    pip install mcp\n    ```\n=== \"uv\"\n\n    ```bash\n    uv add mcp\n    ```\n\nThe following dependencies are automatically installed:\n\n- [`httpx`](https://pypi.org/project/httpx/): HTTP client to handle HTTP Streamable and SSE transports.\n- [`httpx-sse`](https://pypi.org/project/httpx-sse/): HTTP client to handle SSE transport.\n- [`pydantic`](https://pypi.org/project/pydantic/): Types, JSON schema generation, data validation, and [more](https://docs.pydantic.dev/latest/).\n- [`starlette`](https://pypi.org/project/starlette/): Web framework used to build the HTTP transport endpoints.\n- [`python-multipart`](https://pypi.org/project/python-multipart/): Handle HTTP body parsing.\n- [`sse-starlette`](https://pypi.org/project/sse-starlette/): Server-Sent Events for Starlette, used to build the SSE transport endpoint.\n- [`pydantic-settings`](https://pypi.org/project/pydantic-settings/): Settings management used in MCPServer.\n- [`uvicorn`](https://pypi.org/project/uvicorn/): ASGI server used to run the HTTP transport endpoints.\n- [`jsonschema`](https://pypi.org/project/jsonschema/): JSON schema validation.\n- [`pywin32`](https://pypi.org/project/pywin32/): Windows specific dependencies for the CLI tools.\n\nThis package has the following optional groups:\n\n- `cli`: Installs `typer` and `python-dotenv` for the MCP CLI tools.\n"
  },
  {
    "path": "docs/low-level-server.md",
    "content": "# Low-Level Server\n\n!!! warning \"Under Construction\"\n\n    This page is currently being written. Check back soon for complete documentation.\n"
  },
  {
    "path": "docs/migration.md",
    "content": "# Migration Guide: v1 to v2\n\nThis guide covers the breaking changes introduced in v2 of the MCP Python SDK and how to update your code.\n\n## Overview\n\nVersion 2 of the MCP Python SDK introduces several breaking changes to improve the API, align with the MCP specification, and provide better type safety.\n\n## Breaking Changes\n\n### `streamablehttp_client` removed\n\nThe deprecated `streamablehttp_client` function has been removed. Use `streamable_http_client` instead.\n\n**Before (v1):**\n\n```python\nfrom mcp.client.streamable_http import streamablehttp_client\n\nasync with streamablehttp_client(\n    url=\"http://localhost:8000/mcp\",\n    headers={\"Authorization\": \"Bearer token\"},\n    timeout=30,\n    sse_read_timeout=300,\n    auth=my_auth,\n) as (read_stream, write_stream, get_session_id):\n    ...\n```\n\n**After (v2):**\n\n```python\nimport httpx\nfrom mcp.client.streamable_http import streamable_http_client\n\n# Configure headers, timeout, and auth on the httpx.AsyncClient\nhttp_client = httpx.AsyncClient(\n    headers={\"Authorization\": \"Bearer token\"},\n    timeout=httpx.Timeout(30, read=300),\n    auth=my_auth,\n)\n\nasync with http_client:\n    async with streamable_http_client(\n        url=\"http://localhost:8000/mcp\",\n        http_client=http_client,\n    ) as (read_stream, write_stream):\n        ...\n```\n\n### `get_session_id` callback removed from `streamable_http_client`\n\nThe `get_session_id` callback (third element of the returned tuple) has been removed from `streamable_http_client`. The function now returns a 2-tuple `(read_stream, write_stream)` instead of a 3-tuple.\n\nIf you need to capture the session ID (e.g., for session resumption testing), you can use httpx event hooks to capture it from the response headers:\n\n**Before (v1):**\n\n```python\nfrom mcp.client.streamable_http import streamable_http_client\n\nasync with streamable_http_client(url) as (read_stream, write_stream, get_session_id):\n    async with ClientSession(read_stream, write_stream) as session:\n        await session.initialize()\n        session_id = get_session_id()  # Get session ID via callback\n```\n\n**After (v2):**\n\n```python\nimport httpx\nfrom mcp.client.streamable_http import streamable_http_client\n\n# Option 1: Simply ignore if you don't need the session ID\nasync with streamable_http_client(url) as (read_stream, write_stream):\n    async with ClientSession(read_stream, write_stream) as session:\n        await session.initialize()\n\n# Option 2: Capture session ID via httpx event hooks if needed\ncaptured_session_ids: list[str] = []\n\nasync def capture_session_id(response: httpx.Response) -> None:\n    session_id = response.headers.get(\"mcp-session-id\")\n    if session_id:\n        captured_session_ids.append(session_id)\n\nhttp_client = httpx.AsyncClient(\n    event_hooks={\"response\": [capture_session_id]},\n    follow_redirects=True,\n)\n\nasync with http_client:\n    async with streamable_http_client(url, http_client=http_client) as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            await session.initialize()\n            session_id = captured_session_ids[0] if captured_session_ids else None\n```\n\n### `StreamableHTTPTransport` parameters removed\n\nThe `headers`, `timeout`, `sse_read_timeout`, and `auth` parameters have been removed from `StreamableHTTPTransport`. Configure these on the `httpx.AsyncClient` instead (see example above).\n\n### Removed type aliases and classes\n\nThe following deprecated type aliases and classes have been removed from `mcp.types`:\n\n| Removed | Replacement |\n|---------|-------------|\n| `Content` | `ContentBlock` |\n| `ResourceReference` | `ResourceTemplateReference` |\n| `Cursor` | Use `str` directly |\n| `MethodT` | Internal TypeVar, not intended for public use |\n| `RequestParamsT` | Internal TypeVar, not intended for public use |\n| `NotificationParamsT` | Internal TypeVar, not intended for public use |\n\n**Before (v1):**\n\n```python\nfrom mcp.types import Content, ResourceReference, Cursor\n```\n\n**After (v2):**\n\n```python\nfrom mcp.types import ContentBlock, ResourceTemplateReference\n# Use `str` instead of `Cursor` for pagination cursors\n```\n\n### `args` parameter removed from `ClientSessionGroup.call_tool()`\n\nThe deprecated `args` parameter has been removed from `ClientSessionGroup.call_tool()`. Use `arguments` instead.\n\n**Before (v1):**\n\n```python\nresult = await session_group.call_tool(\"my_tool\", args={\"key\": \"value\"})\n```\n\n**After (v2):**\n\n```python\nresult = await session_group.call_tool(\"my_tool\", arguments={\"key\": \"value\"})\n```\n\n### `cursor` parameter removed from `ClientSession` list methods\n\nThe deprecated `cursor` parameter has been removed from the following `ClientSession` methods:\n\n- `list_resources()`\n- `list_resource_templates()`\n- `list_prompts()`\n- `list_tools()`\n\nUse `params=PaginatedRequestParams(cursor=...)` instead.\n\n**Before (v1):**\n\n```python\nresult = await session.list_resources(cursor=\"next_page_token\")\nresult = await session.list_tools(cursor=\"next_page_token\")\n```\n\n**After (v2):**\n\n```python\nfrom mcp.types import PaginatedRequestParams\n\nresult = await session.list_resources(params=PaginatedRequestParams(cursor=\"next_page_token\"))\nresult = await session.list_tools(params=PaginatedRequestParams(cursor=\"next_page_token\"))\n```\n\n### `ClientSession.get_server_capabilities()` replaced by `initialize_result` property\n\n`ClientSession` now stores the full `InitializeResult` via an `initialize_result` property. This provides access to `server_info`, `capabilities`, `instructions`, and the negotiated `protocol_version` through a single property. The `get_server_capabilities()` method has been removed.\n\n**Before (v1):**\n\n```python\ncapabilities = session.get_server_capabilities()\n# server_info, instructions, protocol_version were not stored — had to capture initialize() return value\n```\n\n**After (v2):**\n\n```python\nresult = session.initialize_result\nif result is not None:\n    capabilities = result.capabilities\n    server_info = result.server_info\n    instructions = result.instructions\n    version = result.protocol_version\n```\n\nThe high-level `Client.initialize_result` returns the same `InitializeResult` but is non-nullable — initialization is guaranteed inside the context manager, so no `None` check is needed. This replaces v1's `Client.server_capabilities`; use `client.initialize_result.capabilities` instead.\n\n### `McpError` renamed to `MCPError`\n\nThe `McpError` exception class has been renamed to `MCPError` for consistent naming with the MCP acronym style used throughout the SDK.\n\n**Before (v1):**\n\n```python\nfrom mcp.shared.exceptions import McpError\n\ntry:\n    result = await session.call_tool(\"my_tool\")\nexcept McpError as e:\n    print(f\"Error: {e.error.message}\")\n```\n\n**After (v2):**\n\n```python\nfrom mcp.shared.exceptions import MCPError\n\ntry:\n    result = await session.call_tool(\"my_tool\")\nexcept MCPError as e:\n    print(f\"Error: {e.message}\")\n```\n\n`MCPError` is also exported from the top-level `mcp` package:\n\n```python\nfrom mcp import MCPError\n```\n\n### `FastMCP` renamed to `MCPServer`\n\nThe `FastMCP` class has been renamed to `MCPServer` to better reflect its role as the main server class in the SDK. This is a simple rename with no functional changes to the class itself.\n\n**Before (v1):**\n\n```python\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(\"Demo\")\n```\n\n**After (v2):**\n\n```python\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(\"Demo\")\n```\n\n### `mount_path` parameter removed from MCPServer\n\nThe `mount_path` parameter has been removed from `MCPServer.__init__()`, `MCPServer.run()`, `MCPServer.run_sse_async()`, and `MCPServer.sse_app()`. It was also removed from the `Settings` class.\n\nThis parameter was redundant because the SSE transport already handles sub-path mounting via ASGI's standard `root_path` mechanism. When using Starlette's `Mount(\"/path\", app=mcp.sse_app())`, Starlette automatically sets `root_path` in the ASGI scope, and the `SseServerTransport` uses this to construct the correct message endpoint path.\n\n### Transport-specific parameters moved from MCPServer constructor to run()/app methods\n\nTransport-specific parameters have been moved from the `MCPServer` constructor to the `run()`, `sse_app()`, and `streamable_http_app()` methods. This provides better separation of concerns - the constructor now only handles server identity and authentication, while transport configuration is passed when starting the server.\n\n**Parameters moved:**\n\n- `host`, `port` - HTTP server binding\n- `sse_path`, `message_path` - SSE transport paths\n- `streamable_http_path` - StreamableHTTP endpoint path\n- `json_response`, `stateless_http` - StreamableHTTP behavior\n- `event_store`, `retry_interval` - StreamableHTTP event handling\n- `transport_security` - DNS rebinding protection\n\n**Before (v1):**\n\n```python\nfrom mcp.server.fastmcp import FastMCP\n\n# Transport params in constructor\nmcp = FastMCP(\"Demo\", json_response=True, stateless_http=True)\nmcp.run(transport=\"streamable-http\")\n\n# Or for SSE\nmcp = FastMCP(\"Server\", host=\"0.0.0.0\", port=9000, sse_path=\"/events\")\nmcp.run(transport=\"sse\")\n```\n\n**After (v2):**\n\n```python\nfrom mcp.server.mcpserver import MCPServer\n\n# Transport params passed to run()\nmcp = MCPServer(\"Demo\")\nmcp.run(transport=\"streamable-http\", json_response=True, stateless_http=True)\n\n# Or for SSE\nmcp = MCPServer(\"Server\")\nmcp.run(transport=\"sse\", host=\"0.0.0.0\", port=9000, sse_path=\"/events\")\n```\n\n**For mounted apps:**\n\nWhen mounting in a Starlette app, pass transport params to the app methods:\n\n```python\n# Before (v1)\nfrom mcp.server.fastmcp import FastMCP\n\nmcp = FastMCP(\"App\", json_response=True)\napp = Starlette(routes=[Mount(\"/\", app=mcp.streamable_http_app())])\n\n# After (v2)\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(\"App\")\napp = Starlette(routes=[Mount(\"/\", app=mcp.streamable_http_app(json_response=True))])\n```\n\n**Note:** DNS rebinding protection is automatically enabled when `host` is `127.0.0.1`, `localhost`, or `::1`. This now happens in `sse_app()` and `streamable_http_app()` instead of the constructor.\n\n### `MCPServer.get_context()` removed\n\n`MCPServer.get_context()` has been removed. Context is now injected by the framework and passed explicitly — there is no ambient ContextVar to read from.\n\n**If you were calling `get_context()` from inside a tool/resource/prompt:** use the `ctx: Context` parameter injection instead.\n\n**Before (v1):**\n\n```python\n@mcp.tool()\nasync def my_tool(x: int) -> str:\n    ctx = mcp.get_context()\n    await ctx.info(\"Processing...\")\n    return str(x)\n```\n\n**After (v2):**\n\n```python\n@mcp.tool()\nasync def my_tool(x: int, ctx: Context) -> str:\n    await ctx.info(\"Processing...\")\n    return str(x)\n```\n\n### `MCPServer.call_tool()`, `read_resource()`, `get_prompt()` now accept a `context` parameter\n\n`MCPServer.call_tool()`, `MCPServer.read_resource()`, and `MCPServer.get_prompt()` now accept an optional `context: Context | None = None` parameter. The framework passes this automatically during normal request handling. If you call these methods directly and omit `context`, a Context with no active request is constructed for you — tools that don't use `ctx` work normally, but any attempt to use `ctx.session`, `ctx.request_id`, etc. will raise.\n\nThe internal layers (`ToolManager.call_tool`, `Tool.run`, `Prompt.render`, `ResourceTemplate.create_resource`, etc.) now require `context` as a positional argument.\n\n### Replace `RootModel` by union types with `TypeAdapter` validation\n\nThe following union types are no longer `RootModel` subclasses:\n\n- `ClientRequest`\n- `ServerRequest`\n- `ClientNotification`\n- `ServerNotification`\n- `ClientResult`\n- `ServerResult`\n- `JSONRPCMessage`\n\nThis means you can no longer access `.root` on these types or use `model_validate()` directly on them. Instead, use the provided `TypeAdapter` instances for validation.\n\n**Before (v1):**\n\n```python\nfrom mcp.types import ClientRequest, ServerNotification\n\n# Using RootModel.model_validate()\nrequest = ClientRequest.model_validate(data)\nactual_request = request.root  # Accessing the wrapped value\n\nnotification = ServerNotification.model_validate(data)\nactual_notification = notification.root\n```\n\n**After (v2):**\n\n```python\nfrom mcp.types import client_request_adapter, server_notification_adapter\n\n# Using TypeAdapter.validate_python()\nrequest = client_request_adapter.validate_python(data)\n# No .root access needed - request is the actual type\n\nnotification = server_notification_adapter.validate_python(data)\n# No .root access needed - notification is the actual type\n```\n\n**Available adapters:**\n\n| Union Type | Adapter |\n|------------|---------|\n| `ClientRequest` | `client_request_adapter` |\n| `ServerRequest` | `server_request_adapter` |\n| `ClientNotification` | `client_notification_adapter` |\n| `ServerNotification` | `server_notification_adapter` |\n| `ClientResult` | `client_result_adapter` |\n| `ServerResult` | `server_result_adapter` |\n| `JSONRPCMessage` | `jsonrpc_message_adapter` |\n\nAll adapters are exported from `mcp.types`.\n\n### `RequestParams.Meta` replaced with `RequestParamsMeta` TypedDict\n\nThe nested `RequestParams.Meta` Pydantic model class has been replaced with a top-level `RequestParamsMeta` TypedDict. This affects the `ctx.meta` field in request handlers and any code that imports or references this type.\n\n**Key changes:**\n\n- `RequestParams.Meta` (Pydantic model) → `RequestParamsMeta` (TypedDict)\n- Attribute access (`meta.progress_token`) → Dictionary access (`meta.get(\"progress_token\")`)\n- `progress_token` field changed from `ProgressToken | None = None` to `NotRequired[ProgressToken]`\n\n**In request context handlers:**\n\n```python\n# Before (v1)\n@server.call_tool()\nasync def handle_tool(name: str, arguments: dict) -> list[TextContent]:\n    ctx = server.request_context\n    if ctx.meta and ctx.meta.progress_token:\n        await ctx.session.send_progress_notification(ctx.meta.progress_token, 0.5, 100)\n\n# After (v2)\nasync def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:\n    if ctx.meta and \"progress_token\" in ctx.meta:\n        await ctx.session.send_progress_notification(ctx.meta[\"progress_token\"], 0.5, 100)\n    ...\n\nserver = Server(\"my-server\", on_call_tool=handle_call_tool)\n```\n\n### `RequestContext` type parameters simplified\n\nThe `RequestContext` class has been split to separate shared fields from server-specific fields. The shared `RequestContext` now only takes 1 type parameter (the session type) instead of 3.\n\n**`RequestContext` changes:**\n\n- Type parameters reduced from `RequestContext[SessionT, LifespanContextT, RequestT]` to `RequestContext[SessionT]`\n- Server-specific fields (`lifespan_context`, `experimental`, `request`, `close_sse_stream`, `close_standalone_sse_stream`) moved to new `ServerRequestContext` class in `mcp.server.context`\n\n**Before (v1):**\n\n```python\nfrom mcp.client.session import ClientSession\nfrom mcp.shared.context import RequestContext, LifespanContextT, RequestT\n\n# RequestContext with 3 type parameters\nctx: RequestContext[ClientSession, LifespanContextT, RequestT]\n```\n\n**After (v2):**\n\n```python\nfrom mcp.client.context import ClientRequestContext\nfrom mcp.server.context import ServerRequestContext, LifespanContextT, RequestT\n\n# For client-side context (sampling, elicitation, list_roots callbacks)\nctx: ClientRequestContext\n\n# For server-specific context with lifespan and request types\nserver_ctx: ServerRequestContext[LifespanContextT, RequestT]\n```\n\n### `ProgressContext` and `progress()` context manager removed\n\nThe `mcp.shared.progress` module (`ProgressContext`, `Progress`, and the `progress()` context manager) has been removed. This module had no real-world adoption — all users send progress notifications via `Context.report_progress()` or `session.send_progress_notification()` directly.\n\n**Before:**\n\n```python\nfrom mcp.shared.progress import progress\n\nwith progress(ctx, total=100) as p:\n    await p.progress(25)\n```\n\n**After — use `Context.report_progress()` (recommended):**\n\n```python\n@server.tool()\nasync def my_tool(x: int, ctx: Context) -> str:\n    await ctx.report_progress(25, 100)\n    return \"done\"\n```\n\n**After — use `session.send_progress_notification()` (low-level):**\n\n```python\nawait session.send_progress_notification(\n    progress_token=progress_token,\n    progress=25,\n    total=100,\n)\n```\n\n### Resource URI type changed from `AnyUrl` to `str`\n\nThe `uri` field on resource-related types now uses `str` instead of Pydantic's `AnyUrl`. This aligns with the [MCP specification schema](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/draft/schema.ts) which defines URIs as plain strings (`uri: string`) without strict URL validation. This change allows relative paths like `users/me` that were previously rejected.\n\n**Before (v1):**\n\n```python\nfrom pydantic import AnyUrl\nfrom mcp.types import Resource\n\n# Required wrapping in AnyUrl\nresource = Resource(name=\"test\", uri=AnyUrl(\"users/me\"))  # Would fail validation\n```\n\n**After (v2):**\n\n```python\nfrom mcp.types import Resource\n\n# Plain strings accepted\nresource = Resource(name=\"test\", uri=\"users/me\")  # Works\nresource = Resource(name=\"test\", uri=\"custom://scheme\")  # Works\nresource = Resource(name=\"test\", uri=\"https://example.com\")  # Works\n```\n\nIf your code passes `AnyUrl` objects to URI fields, convert them to strings:\n\n```python\n# If you have an AnyUrl from elsewhere\nuri = str(my_any_url)  # Convert to string\n```\n\nAffected types:\n\n- `Resource.uri`\n- `ReadResourceRequestParams.uri`\n- `ResourceContents.uri` (and subclasses `TextResourceContents`, `BlobResourceContents`)\n- `SubscribeRequestParams.uri`\n- `UnsubscribeRequestParams.uri`\n- `ResourceUpdatedNotificationParams.uri`\n\nThe `Client` and `ClientSession` methods `read_resource()`, `subscribe_resource()`, and `unsubscribe_resource()` now only accept `str` for the `uri` parameter. If you were passing `AnyUrl` objects, convert them to strings:\n\n```python\n# Before (v1)\nfrom pydantic import AnyUrl\n\nawait client.read_resource(AnyUrl(\"test://resource\"))\n\n# After (v2)\nawait client.read_resource(\"test://resource\")\n# Or if you have an AnyUrl from elsewhere:\nawait client.read_resource(str(my_any_url))\n```\n\n### Lowlevel `Server`: constructor parameters are now keyword-only\n\nAll parameters after `name` are now keyword-only. If you were passing `version` or other parameters positionally, use keyword arguments instead:\n\n```python\n# Before (v1)\nserver = Server(\"my-server\", \"1.0\")\n\n# After (v2)\nserver = Server(\"my-server\", version=\"1.0\")\n```\n\n### Lowlevel `Server`: type parameter reduced from 2 to 1\n\nThe `Server` class previously had two type parameters: `Server[LifespanResultT, RequestT]`. The `RequestT` parameter has been removed — handlers now receive typed params directly rather than a generic request type.\n\n```python\n# Before (v1)\nfrom typing import Any\n\nfrom mcp.server.lowlevel.server import Server\n\nserver: Server[dict[str, Any], Any] = Server(...)\n\n# After (v2)\nfrom typing import Any\n\nfrom mcp.server import Server\n\nserver: Server[dict[str, Any]] = Server(...)\n```\n\n### Lowlevel `Server`: `request_handlers` and `notification_handlers` attributes removed\n\nThe public `server.request_handlers` and `server.notification_handlers` dictionaries have been removed. Handler registration is now done exclusively through constructor `on_*` keyword arguments. There is no public API to register handlers after construction.\n\n```python\n# Before (v1) — direct dict access\nfrom mcp.types import ListToolsRequest\n\nif ListToolsRequest in server.request_handlers:\n    ...\n\n# After (v2) — no public access to handler dicts\n# Use the on_* constructor params to register handlers\nserver = Server(\"my-server\", on_list_tools=handle_list_tools)\n```\n\n### Lowlevel `Server`: decorator-based handlers replaced with constructor `on_*` params\n\nThe lowlevel `Server` class no longer uses decorator methods for handler registration. Instead, handlers are passed as `on_*` keyword arguments to the constructor.\n\n**Before (v1):**\n\n```python\nfrom mcp.server.lowlevel.server import Server\n\nserver = Server(\"my-server\")\n\n@server.list_tools()\nasync def handle_list_tools():\n    return [types.Tool(name=\"my_tool\", description=\"A tool\", inputSchema={})]\n\n@server.call_tool()\nasync def handle_call_tool(name: str, arguments: dict):\n    return [types.TextContent(type=\"text\", text=f\"Called {name}\")]\n```\n\n**After (v2):**\n\n```python\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.types import (\n    CallToolRequestParams,\n    CallToolResult,\n    ListToolsResult,\n    PaginatedRequestParams,\n    TextContent,\n    Tool,\n)\n\nasync def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n    return ListToolsResult(tools=[Tool(name=\"my_tool\", description=\"A tool\", input_schema={})])\n\n\nasync def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:\n    return CallToolResult(\n        content=[TextContent(type=\"text\", text=f\"Called {params.name}\")],\n        is_error=False,\n    )\n\nserver = Server(\"my-server\", on_list_tools=handle_list_tools, on_call_tool=handle_call_tool)\n```\n\n**Key differences:**\n\n- Handlers receive `(ctx, params)` instead of the full request object or unpacked arguments. `ctx` is a `ServerRequestContext` with `session`, `lifespan_context`, and `experimental` fields (plus `request_id`, `meta`, etc. for request handlers). `params` is the typed request params object.\n- Handlers return the full result type (e.g. `ListToolsResult`) rather than unwrapped values (e.g. `list[Tool]`).\n- The automatic `jsonschema` input/output validation that the old `call_tool()` decorator performed has been removed. There is no built-in replacement — if you relied on schema validation in the lowlevel server, you will need to validate inputs yourself in your handler.\n\n**Notification handlers:**\n\n```python\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.types import ProgressNotificationParams\n\n\nasync def handle_progress(ctx: ServerRequestContext, params: ProgressNotificationParams) -> None:\n    print(f\"Progress: {params.progress}/{params.total}\")\n\nserver = Server(\"my-server\", on_progress=handle_progress)\n```\n\n### Lowlevel `Server`: automatic return value wrapping removed\n\nThe old decorator-based handlers performed significant automatic wrapping of return values. This magic has been removed — handlers now return fully constructed result types. If you want these conveniences, use `MCPServer` (previously `FastMCP`) instead of the lowlevel `Server`.\n\n**`call_tool()` — structured output wrapping removed:**\n\nThe old decorator accepted several return types and auto-wrapped them into `CallToolResult`:\n\n```python\n# Before (v1) — returning a dict auto-wrapped into structured_content + JSON TextContent\n@server.call_tool()\nasync def handle(name: str, arguments: dict) -> dict:\n    return {\"temperature\": 22.5, \"city\": \"London\"}\n\n# Before (v1) — returning a list auto-wrapped into CallToolResult.content\n@server.call_tool()\nasync def handle(name: str, arguments: dict) -> list[TextContent]:\n    return [TextContent(type=\"text\", text=\"Done\")]\n```\n\n```python\n# After (v2) — construct the full result yourself\nimport json\n\nasync def handle(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:\n    data = {\"temperature\": 22.5, \"city\": \"London\"}\n    return CallToolResult(\n        content=[TextContent(type=\"text\", text=json.dumps(data, indent=2))],\n        structured_content=data,\n    )\n```\n\nNote: `params.arguments` can be `None` (the old decorator defaulted it to `{}`). Use `params.arguments or {}` to preserve the old behavior.\n\n**`read_resource()` — content type wrapping removed:**\n\nThe old decorator auto-wrapped `str` into `TextResourceContents` and `bytes` into `BlobResourceContents` (with base64 encoding), and applied a default mime type of `text/plain`:\n\n```python\n# Before (v1) — str/bytes auto-wrapped with mime type defaulting\n@server.read_resource()\nasync def handle(uri: str) -> str:\n    return \"file contents\"\n\n@server.read_resource()\nasync def handle(uri: str) -> bytes:\n    return b\"\\x89PNG...\"\n```\n\n```python\n# After (v2) — construct TextResourceContents or BlobResourceContents yourself\nimport base64\n\nasync def handle_read(ctx: ServerRequestContext, params: ReadResourceRequestParams) -> ReadResourceResult:\n    # Text content\n    return ReadResourceResult(\n        contents=[TextResourceContents(uri=str(params.uri), text=\"file contents\", mime_type=\"text/plain\")]\n    )\n\nasync def handle_read(ctx: ServerRequestContext, params: ReadResourceRequestParams) -> ReadResourceResult:\n    # Binary content — you must base64-encode it yourself\n    return ReadResourceResult(\n        contents=[BlobResourceContents(\n            uri=str(params.uri),\n            blob=base64.b64encode(b\"\\x89PNG...\").decode(\"utf-8\"),\n            mime_type=\"image/png\",\n        )]\n    )\n```\n\n**`list_tools()`, `list_resources()`, `list_prompts()` — list wrapping removed:**\n\nThe old decorators accepted bare lists and wrapped them into the result type:\n\n```python\n# Before (v1)\n@server.list_tools()\nasync def handle() -> list[Tool]:\n    return [Tool(name=\"my_tool\", ...)]\n\n# After (v2)\nasync def handle(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n    return ListToolsResult(tools=[Tool(name=\"my_tool\", ...)])\n```\n\n**Using `MCPServer` instead:**\n\nIf you prefer the convenience of automatic wrapping, use `MCPServer` which still provides these features through its `@mcp.tool()`, `@mcp.resource()`, and `@mcp.prompt()` decorators. The lowlevel `Server` is intentionally minimal — it provides no magic and gives you full control over the MCP protocol types.\n\n### Lowlevel `Server`: `request_context` property removed\n\nThe `server.request_context` property has been removed. Request context is now passed directly to handlers as the first argument (`ctx`). The `request_ctx` module-level contextvar has been removed entirely.\n\n**Before (v1):**\n\n```python\nfrom mcp.server.lowlevel.server import request_ctx\n\n@server.call_tool()\nasync def handle_call_tool(name: str, arguments: dict):\n    ctx = server.request_context  # or request_ctx.get()\n    await ctx.session.send_log_message(level=\"info\", data=\"Processing...\")\n    return [types.TextContent(type=\"text\", text=\"Done\")]\n```\n\n**After (v2):**\n\n```python\nfrom mcp.server import ServerRequestContext\nfrom mcp.types import CallToolRequestParams, CallToolResult, TextContent\n\n\nasync def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:\n    await ctx.session.send_log_message(level=\"info\", data=\"Processing...\")\n    return CallToolResult(\n        content=[TextContent(type=\"text\", text=\"Done\")],\n        is_error=False,\n    )\n```\n\n### `RequestContext`: request-specific fields are now optional\n\nThe `RequestContext` class now uses optional fields for request-specific data (`request_id`, `meta`, etc.) so it can be used for both request and notification handlers. In notification handlers, these fields are `None`.\n\n```python\nfrom mcp.server import ServerRequestContext\n\n# request_id, meta, etc. are available in request handlers\n# but None in notification handlers\n```\n\n### Experimental: task handler decorators removed\n\nThe experimental decorator methods on `ExperimentalHandlers` (`@server.experimental.list_tasks()`, `@server.experimental.get_task()`, etc.) have been removed.\n\nDefault task handlers are still registered automatically via `server.experimental.enable_tasks()`. Custom handlers can be passed as `on_*` kwargs to override specific defaults.\n\n**Before (v1):**\n\n```python\nserver = Server(\"my-server\")\nserver.experimental.enable_tasks()\n\n@server.experimental.get_task()\nasync def custom_get_task(request: GetTaskRequest) -> GetTaskResult:\n    ...\n```\n\n**After (v2):**\n\n```python\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.types import GetTaskRequestParams, GetTaskResult\n\n\nasync def custom_get_task(ctx: ServerRequestContext, params: GetTaskRequestParams) -> GetTaskResult:\n    ...\n\n\nserver = Server(\"my-server\")\nserver.experimental.enable_tasks(on_get_task=custom_get_task)\n```\n\n## Deprecations\n\n<!-- Add deprecations below -->\n\n## Bug Fixes\n\n### Lowlevel `Server`: `subscribe` capability now correctly reported\n\nPreviously, the lowlevel `Server` hardcoded `subscribe=False` in resource capabilities even when a `subscribe_resource()` handler was registered. The `subscribe` capability is now dynamically set to `True` when an `on_subscribe_resource` handler is provided. Clients that previously didn't see `subscribe: true` in capabilities will now see it when a handler is registered, which may change client behavior.\n\n### Extra fields no longer allowed on top-level MCP types\n\nMCP protocol types no longer accept arbitrary extra fields at the top level. This matches the MCP specification which only allows extra fields within `_meta` objects, not on the types themselves.\n\n```python\n# This will now raise a validation error\nfrom mcp.types import CallToolRequestParams\n\nparams = CallToolRequestParams(\n    name=\"my_tool\",\n    arguments={},\n    unknown_field=\"value\",  # ValidationError: extra fields not permitted\n)\n\n# Extra fields are still allowed in _meta\nparams = CallToolRequestParams(\n    name=\"my_tool\",\n    arguments={},\n    _meta={\"progressToken\": \"tok\", \"customField\": \"value\"},  # OK\n)\n```\n\n## New Features\n\n### `streamable_http_app()` available on lowlevel Server\n\nThe `streamable_http_app()` method is now available directly on the lowlevel `Server` class, not just `MCPServer`. This allows using the streamable HTTP transport without the MCPServer wrapper.\n\n```python\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.types import ListToolsResult, PaginatedRequestParams\n\n\nasync def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n    return ListToolsResult(tools=[...])\n\n\nserver = Server(\"my-server\", on_list_tools=handle_list_tools)\n\napp = server.streamable_http_app(\n    streamable_http_path=\"/mcp\",\n    json_response=False,\n    stateless_http=False,\n)\n```\n\nThe lowlevel `Server` also now exposes a `session_manager` property to access the `StreamableHTTPSessionManager` after calling `streamable_http_app()`.\n\n## Need Help?\n\nIf you encounter issues during migration:\n\n1. Check the [API Reference](api/mcp/index.md) for updated method signatures\n2. Review the [examples](https://github.com/modelcontextprotocol/python-sdk/tree/main/examples) for updated usage patterns\n3. Open an issue on [GitHub](https://github.com/modelcontextprotocol/python-sdk/issues) if you find a bug or need further assistance\n"
  },
  {
    "path": "docs/testing.md",
    "content": "# Testing MCP Servers\n\nThe Python SDK provides a `Client` class for testing MCP servers with an in-memory transport.\nThis makes it easy to write tests without network overhead.\n\n## Basic Usage\n\nLet's assume you have a simple server with a single tool:\n\n```python title=\"server.py\"\nfrom mcp.server import MCPServer\n\napp = MCPServer(\"Calculator\")\n\n@app.tool()\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers.\"\"\"  # (1)!\n    return a + b\n```\n\n1. The docstring is automatically added as the description of the tool.\n\nTo run the below test, you'll need to install the following dependencies:\n\n=== \"pip\"\n    ```bash\n    pip install inline-snapshot pytest\n    ```\n\n=== \"uv\"\n    ```bash\n    uv add inline-snapshot pytest\n    ```\n\n!!! info\n    I think [`pytest`](https://docs.pytest.org/en/stable/) is a pretty standard testing framework,\n    so I won't go into details here.\n\n    The [`inline-snapshot`](https://15r10nk.github.io/inline-snapshot/latest/) is a library that allows\n    you to take snapshots of the output of your tests. Which makes it easier to create tests for your\n    server - you don't need to use it, but we are spreading the word for best practices.\n\n```python title=\"test_server.py\"\nimport pytest\nfrom inline_snapshot import snapshot\nfrom mcp import Client\nfrom mcp.types import CallToolResult, TextContent\n\nfrom server import app\n\n\n@pytest.fixture\ndef anyio_backend():  # (1)!\n    return \"asyncio\"\n\n\n@pytest.fixture\nasync def client():  # (2)!\n    async with Client(app, raise_exceptions=True) as c:\n        yield c\n\n\n@pytest.mark.anyio\nasync def test_call_add_tool(client: Client):\n    result = await client.call_tool(\"add\", {\"a\": 1, \"b\": 2})\n    assert result == snapshot(\n        CallToolResult(\n            content=[TextContent(type=\"text\", text=\"3\")],\n            structuredContent={\"result\": 3},\n        )\n    )\n```\n\n1. If you are using `trio`, you should set `\"trio\"` as the `anyio_backend`. Check more information in the [anyio documentation](https://anyio.readthedocs.io/en/stable/testing.html#specifying-the-backends-to-run-on).\n2. The `client` fixture creates a connected client that can be reused across multiple tests.\n\nThere you go! You can now extend your tests to cover more scenarios.\n"
  },
  {
    "path": "examples/README.md",
    "content": "# Python SDK Examples\n\nThis folders aims to provide simple examples of using the Python SDK. Please refer to the\n[servers repository](https://github.com/modelcontextprotocol/servers)\nfor real-world servers.\n"
  },
  {
    "path": "examples/clients/simple-auth-client/README.md",
    "content": "# Simple Auth Client Example\n\nA demonstration of how to use the MCP Python SDK with OAuth authentication over streamable HTTP or SSE transport.\n\n## Features\n\n- OAuth 2.0 authentication with PKCE\n- Support for both StreamableHTTP and SSE transports\n- Interactive command-line interface\n\n## Installation\n\n```bash\ncd examples/clients/simple-auth-client\nuv sync --reinstall\n```\n\n## Usage\n\n### 1. Start an MCP server with OAuth support\n\nThe simple-auth server example provides three server configurations. See [examples/servers/simple-auth/README.md](../../servers/simple-auth/README.md) for full details.\n\n#### Option A: New Architecture (Recommended)\n\nSeparate Authorization Server and Resource Server:\n\n```bash\n# Terminal 1: Start Authorization Server on port 9000\ncd examples/servers/simple-auth\nuv run mcp-simple-auth-as --port=9000\n\n# Terminal 2: Start Resource Server on port 8001\ncd examples/servers/simple-auth\nuv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http\n```\n\n#### Option B: Legacy Server (Backwards Compatibility)\n\n```bash\n# Single server that acts as both AS and RS (port 8000)\ncd examples/servers/simple-auth\nuv run mcp-simple-auth-legacy --port=8000 --transport=streamable-http\n```\n\n### 2. Run the client\n\n```bash\n# Connect to Resource Server (new architecture, default port 8001)\nMCP_SERVER_PORT=8001 uv run mcp-simple-auth-client\n\n# Connect to Legacy Server (port 8000)\nuv run mcp-simple-auth-client\n\n# Use SSE transport\nMCP_SERVER_PORT=8001 MCP_TRANSPORT_TYPE=sse uv run mcp-simple-auth-client\n```\n\n### 3. Complete OAuth flow\n\nThe client will open your browser for authentication. After completing OAuth, you can use commands:\n\n- `list` - List available tools\n- `call <tool_name> [args]` - Call a tool with optional JSON arguments\n- `quit` - Exit\n\n## Example\n\n```markdown\n🚀 Simple MCP Auth Client\nConnecting to: http://localhost:8001/mcp\nTransport type: streamable-http\n\n🔗 Attempting to connect to http://localhost:8001/mcp...\n📡 Opening StreamableHTTP transport connection with auth...\nOpening browser for authorization: http://localhost:9000/authorize?...\n\n✅ Connected to MCP server at http://localhost:8001/mcp\n\nmcp> list\n📋 Available tools:\n1. get_time\n   Description: Get the current server time.\n\nmcp> call get_time\n🔧 Tool 'get_time' result:\n{\"current_time\": \"2024-01-15T10:30:00\", \"timezone\": \"UTC\", ...}\n\nmcp> quit\n```\n\n## Configuration\n\n| Environment Variable | Description | Default |\n|---------------------|-------------|---------|\n| `MCP_SERVER_PORT` | Port number of the MCP server | `8000` |\n| `MCP_TRANSPORT_TYPE` | Transport type: `streamable-http` or `sse` | `streamable-http` |\n| `MCP_CLIENT_METADATA_URL` | Optional URL for client metadata (CIMD) | None |\n"
  },
  {
    "path": "examples/clients/simple-auth-client/mcp_simple_auth_client/__init__.py",
    "content": "\"\"\"Simple OAuth client for MCP simple-auth server.\"\"\"\n"
  },
  {
    "path": "examples/clients/simple-auth-client/mcp_simple_auth_client/main.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Simple MCP client example with OAuth authentication support.\n\nThis client connects to an MCP server using streamable HTTP transport with OAuth.\n\n\"\"\"\n\nfrom __future__ import annotations as _annotations\n\nimport asyncio\nimport os\nimport socketserver\nimport threading\nimport time\nimport webbrowser\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\nfrom typing import Any\nfrom urllib.parse import parse_qs, urlparse\n\nimport httpx\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\nfrom mcp.client.auth import OAuthClientProvider, TokenStorage\nfrom mcp.client.session import ClientSession\nfrom mcp.client.sse import sse_client\nfrom mcp.client.streamable_http import streamable_http_client\nfrom mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken\nfrom mcp.shared.message import SessionMessage\n\n\nclass InMemoryTokenStorage(TokenStorage):\n    \"\"\"Simple in-memory token storage implementation.\"\"\"\n\n    def __init__(self):\n        self._tokens: OAuthToken | None = None\n        self._client_info: OAuthClientInformationFull | None = None\n\n    async def get_tokens(self) -> OAuthToken | None:\n        return self._tokens\n\n    async def set_tokens(self, tokens: OAuthToken) -> None:\n        self._tokens = tokens\n\n    async def get_client_info(self) -> OAuthClientInformationFull | None:\n        return self._client_info\n\n    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:\n        self._client_info = client_info\n\n\nclass CallbackHandler(BaseHTTPRequestHandler):\n    \"\"\"Simple HTTP handler to capture OAuth callback.\"\"\"\n\n    def __init__(\n        self,\n        request: Any,\n        client_address: tuple[str, int],\n        server: socketserver.BaseServer,\n        callback_data: dict[str, Any],\n    ):\n        \"\"\"Initialize with callback data storage.\"\"\"\n        self.callback_data = callback_data\n        super().__init__(request, client_address, server)\n\n    def do_GET(self):\n        \"\"\"Handle GET request from OAuth redirect.\"\"\"\n        parsed = urlparse(self.path)\n        query_params = parse_qs(parsed.query)\n\n        if \"code\" in query_params:\n            self.callback_data[\"authorization_code\"] = query_params[\"code\"][0]\n            self.callback_data[\"state\"] = query_params.get(\"state\", [None])[0]\n            self.send_response(200)\n            self.send_header(\"Content-type\", \"text/html\")\n            self.end_headers()\n            self.wfile.write(b\"\"\"\n            <html>\n            <body>\n                <h1>Authorization Successful!</h1>\n                <p>You can close this window and return to the terminal.</p>\n                <script>setTimeout(() => window.close(), 2000);</script>\n            </body>\n            </html>\n            \"\"\")\n        elif \"error\" in query_params:\n            self.callback_data[\"error\"] = query_params[\"error\"][0]\n            self.send_response(400)\n            self.send_header(\"Content-type\", \"text/html\")\n            self.end_headers()\n            self.wfile.write(\n                f\"\"\"\n            <html>\n            <body>\n                <h1>Authorization Failed</h1>\n                <p>Error: {query_params[\"error\"][0]}</p>\n                <p>You can close this window and return to the terminal.</p>\n            </body>\n            </html>\n            \"\"\".encode()\n            )\n        else:\n            self.send_response(404)\n            self.end_headers()\n\n    def log_message(self, format: str, *args: Any):\n        \"\"\"Suppress default logging.\"\"\"\n\n\nclass CallbackServer:\n    \"\"\"Simple server to handle OAuth callbacks.\"\"\"\n\n    def __init__(self, port: int = 3000):\n        self.port = port\n        self.server = None\n        self.thread = None\n        self.callback_data = {\"authorization_code\": None, \"state\": None, \"error\": None}\n\n    def _create_handler_with_data(self):\n        \"\"\"Create a handler class with access to callback data.\"\"\"\n        callback_data = self.callback_data\n\n        class DataCallbackHandler(CallbackHandler):\n            def __init__(\n                self,\n                request: BaseHTTPRequestHandler,\n                client_address: tuple[str, int],\n                server: socketserver.BaseServer,\n            ):\n                super().__init__(request, client_address, server, callback_data)\n\n        return DataCallbackHandler\n\n    def start(self):\n        \"\"\"Start the callback server in a background thread.\"\"\"\n        handler_class = self._create_handler_with_data()\n        self.server = HTTPServer((\"localhost\", self.port), handler_class)\n        self.thread = threading.Thread(target=self.server.serve_forever, daemon=True)\n        self.thread.start()\n        print(f\"🖥️  Started callback server on http://localhost:{self.port}\")\n\n    def stop(self):\n        \"\"\"Stop the callback server.\"\"\"\n        if self.server:\n            self.server.shutdown()\n            self.server.server_close()\n        if self.thread:\n            self.thread.join(timeout=1)\n\n    def wait_for_callback(self, timeout: int = 300):\n        \"\"\"Wait for OAuth callback with timeout.\"\"\"\n        start_time = time.time()\n        while time.time() - start_time < timeout:\n            if self.callback_data[\"authorization_code\"]:\n                return self.callback_data[\"authorization_code\"]\n            elif self.callback_data[\"error\"]:\n                raise Exception(f\"OAuth error: {self.callback_data['error']}\")\n            time.sleep(0.1)\n        raise Exception(\"Timeout waiting for OAuth callback\")\n\n    def get_state(self):\n        \"\"\"Get the received state parameter.\"\"\"\n        return self.callback_data[\"state\"]\n\n\nclass SimpleAuthClient:\n    \"\"\"Simple MCP client with auth support.\"\"\"\n\n    def __init__(\n        self,\n        server_url: str,\n        transport_type: str = \"streamable-http\",\n        client_metadata_url: str | None = None,\n    ):\n        self.server_url = server_url\n        self.transport_type = transport_type\n        self.client_metadata_url = client_metadata_url\n        self.session: ClientSession | None = None\n\n    async def connect(self):\n        \"\"\"Connect to the MCP server.\"\"\"\n        print(f\"🔗 Attempting to connect to {self.server_url}...\")\n\n        try:\n            callback_server = CallbackServer(port=3030)\n            callback_server.start()\n\n            async def callback_handler() -> tuple[str, str | None]:\n                \"\"\"Wait for OAuth callback and return auth code and state.\"\"\"\n                print(\"⏳ Waiting for authorization callback...\")\n                try:\n                    auth_code = callback_server.wait_for_callback(timeout=300)\n                    return auth_code, callback_server.get_state()\n                finally:\n                    callback_server.stop()\n\n            client_metadata_dict = {\n                \"client_name\": \"Simple Auth Client\",\n                \"redirect_uris\": [\"http://localhost:3030/callback\"],\n                \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n                \"response_types\": [\"code\"],\n            }\n\n            async def _default_redirect_handler(authorization_url: str) -> None:\n                \"\"\"Default redirect handler that opens the URL in a browser.\"\"\"\n                print(f\"Opening browser for authorization: {authorization_url}\")\n                webbrowser.open(authorization_url)\n\n            # Create OAuth authentication handler using the new interface\n            # Use client_metadata_url to enable CIMD when the server supports it\n            oauth_auth = OAuthClientProvider(\n                server_url=self.server_url.replace(\"/mcp\", \"\"),\n                client_metadata=OAuthClientMetadata.model_validate(client_metadata_dict),\n                storage=InMemoryTokenStorage(),\n                redirect_handler=_default_redirect_handler,\n                callback_handler=callback_handler,\n                client_metadata_url=self.client_metadata_url,\n            )\n\n            # Create transport with auth handler based on transport type\n            if self.transport_type == \"sse\":\n                print(\"📡 Opening SSE transport connection with auth...\")\n                async with sse_client(\n                    url=self.server_url,\n                    auth=oauth_auth,\n                    timeout=60.0,\n                ) as (read_stream, write_stream):\n                    await self._run_session(read_stream, write_stream)\n            else:\n                print(\"📡 Opening StreamableHTTP transport connection with auth...\")\n                async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:\n                    async with streamable_http_client(url=self.server_url, http_client=custom_client) as (\n                        read_stream,\n                        write_stream,\n                    ):\n                        await self._run_session(read_stream, write_stream)\n\n        except Exception as e:\n            print(f\"❌ Failed to connect: {e}\")\n            import traceback\n\n            traceback.print_exc()\n\n    async def _run_session(\n        self,\n        read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],\n        write_stream: MemoryObjectSendStream[SessionMessage],\n    ):\n        \"\"\"Run the MCP session with the given streams.\"\"\"\n        print(\"🤝 Initializing MCP session...\")\n        async with ClientSession(read_stream, write_stream) as session:\n            self.session = session\n            print(\"⚡ Starting session initialization...\")\n            await session.initialize()\n            print(\"✨ Session initialization complete!\")\n\n            print(f\"\\n✅ Connected to MCP server at {self.server_url}\")\n\n            # Run interactive loop\n            await self.interactive_loop()\n\n    async def list_tools(self):\n        \"\"\"List available tools from the server.\"\"\"\n        if not self.session:\n            print(\"❌ Not connected to server\")\n            return\n\n        try:\n            result = await self.session.list_tools()\n            if hasattr(result, \"tools\") and result.tools:\n                print(\"\\n📋 Available tools:\")\n                for i, tool in enumerate(result.tools, 1):\n                    print(f\"{i}. {tool.name}\")\n                    if tool.description:\n                        print(f\"   Description: {tool.description}\")\n                    print()\n            else:\n                print(\"No tools available\")\n        except Exception as e:\n            print(f\"❌ Failed to list tools: {e}\")\n\n    async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None = None):\n        \"\"\"Call a specific tool.\"\"\"\n        if not self.session:\n            print(\"❌ Not connected to server\")\n            return\n\n        try:\n            result = await self.session.call_tool(tool_name, arguments or {})\n            print(f\"\\n🔧 Tool '{tool_name}' result:\")\n            if hasattr(result, \"content\"):\n                for content in result.content:\n                    if content.type == \"text\":\n                        print(content.text)\n                    else:\n                        print(content)\n            else:\n                print(result)\n        except Exception as e:\n            print(f\"❌ Failed to call tool '{tool_name}': {e}\")\n\n    async def interactive_loop(self):\n        \"\"\"Run interactive command loop.\"\"\"\n        print(\"\\n🎯 Interactive MCP Client\")\n        print(\"Commands:\")\n        print(\"  list - List available tools\")\n        print(\"  call <tool_name> [args] - Call a tool\")\n        print(\"  quit - Exit the client\")\n        print()\n\n        while True:\n            try:\n                command = input(\"mcp> \").strip()\n\n                if not command:\n                    continue\n\n                if command == \"quit\":\n                    break\n\n                elif command == \"list\":\n                    await self.list_tools()\n\n                elif command.startswith(\"call \"):\n                    parts = command.split(maxsplit=2)\n                    tool_name = parts[1] if len(parts) > 1 else \"\"\n\n                    if not tool_name:\n                        print(\"❌ Please specify a tool name\")\n                        continue\n\n                    # Parse arguments (simple JSON-like format)\n                    arguments: dict[str, Any] = {}\n                    if len(parts) > 2:\n                        import json\n\n                        try:\n                            arguments = json.loads(parts[2])\n                        except json.JSONDecodeError:\n                            print(\"❌ Invalid arguments format (expected JSON)\")\n                            continue\n\n                    await self.call_tool(tool_name, arguments)\n\n                else:\n                    print(\"❌ Unknown command. Try 'list', 'call <tool_name>', or 'quit'\")\n\n            except KeyboardInterrupt:\n                print(\"\\n\\n👋 Goodbye!\")\n                break\n            except EOFError:\n                break\n\n\nasync def main():\n    \"\"\"Main entry point.\"\"\"\n    # Default server URL - can be overridden with environment variable\n    # Most MCP streamable HTTP servers use /mcp as the endpoint\n    server_url = os.getenv(\"MCP_SERVER_PORT\", 8000)\n    transport_type = os.getenv(\"MCP_TRANSPORT_TYPE\", \"streamable-http\")\n    client_metadata_url = os.getenv(\"MCP_CLIENT_METADATA_URL\")\n    server_url = (\n        f\"http://localhost:{server_url}/mcp\"\n        if transport_type == \"streamable-http\"\n        else f\"http://localhost:{server_url}/sse\"\n    )\n\n    print(\"🚀 Simple MCP Auth Client\")\n    print(f\"Connecting to: {server_url}\")\n    print(f\"Transport type: {transport_type}\")\n    if client_metadata_url:\n        print(f\"Client metadata URL: {client_metadata_url}\")\n\n    # Start connection flow - OAuth will be handled automatically\n    client = SimpleAuthClient(server_url, transport_type, client_metadata_url)\n    await client.connect()\n\n\ndef cli():\n    \"\"\"CLI entry point for uv script.\"\"\"\n    asyncio.run(main())\n\n\nif __name__ == \"__main__\":\n    cli()\n"
  },
  {
    "path": "examples/clients/simple-auth-client/pyproject.toml",
    "content": "[project]\nname = \"mcp-simple-auth-client\"\nversion = \"0.1.0\"\ndescription = \"A simple OAuth client for the MCP simple-auth server\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nkeywords = [\"mcp\", \"oauth\", \"client\", \"auth\"]\nlicense = { text = \"MIT\" }\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n]\ndependencies = [\"click>=8.2.0\", \"mcp\"]\n\n[project.scripts]\nmcp-simple-auth-client = \"mcp_simple_auth_client.main:cli\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_simple_auth_client\"]\n\n[tool.pyright]\ninclude = [\"mcp_simple_auth_client\"]\nvenvPath = \".\"\nvenv = \".venv\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\"]\nignore = []\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n[dependency-groups]\ndev = [\"pyright>=1.1.379\", \"pytest>=8.3.3\", \"ruff>=0.6.9\"]\n"
  },
  {
    "path": "examples/clients/simple-chatbot/README.MD",
    "content": "# MCP Simple Chatbot\n\nThis example demonstrates how to integrate the Model Context Protocol (MCP) into a simple CLI chatbot. The implementation showcases MCP's flexibility by supporting multiple tools through MCP servers and is compatible with any LLM provider that follows OpenAI API standards.\n\n## Requirements\n\n- Python 3.10\n- `python-dotenv`\n- `requests`\n- `mcp`\n- `uvicorn`\n\n## Installation\n\n1. **Install the dependencies:**\n\n   ```bash\n   pip install -r requirements.txt\n   ```\n\n2. **Set up environment variables:**\n\n   Create a `.env` file in the root directory and add your API key:\n\n   ```plaintext\n   LLM_API_KEY=your_api_key_here\n   ```\n\n   **Note:** The current implementation is configured to use the Groq API endpoint (`https://api.groq.com/openai/v1/chat/completions`) with the `llama-3.2-90b-vision-preview` model. If you plan to use a different LLM provider, you'll need to modify the `LLMClient` class in `main.py` to use the appropriate endpoint URL and model parameters.\n\n3. **Configure servers:**\n\n   The `servers_config.json` follows the same structure as Claude Desktop, allowing for easy integration of multiple servers.\n   Here's an example:\n\n   ```json\n   {\n     \"mcpServers\": {\n       \"sqlite\": {\n         \"command\": \"uvx\",\n         \"args\": [\"mcp-server-sqlite\", \"--db-path\", \"./test.db\"]\n       },\n       \"puppeteer\": {\n         \"command\": \"npx\",\n         \"args\": [\"-y\", \"@modelcontextprotocol/server-puppeteer\"]\n       }\n     }\n   }\n   ```\n\n   Environment variables are supported as well. Pass them as you would with the Claude Desktop App.\n\n   Example:\n\n   ```json\n   {\n     \"mcpServers\": {\n       \"server_name\": {\n         \"command\": \"uvx\",\n         \"args\": [\"mcp-server-name\", \"--additional-args\"],\n         \"env\": {\n           \"API_KEY\": \"your_api_key_here\"\n         }\n       }\n     }\n   }\n   ```\n\n## Usage\n\n1. **Run the client:**\n\n   ```bash\n   python main.py\n   ```\n\n2. **Interact with the assistant:**\n\n   The assistant will automatically detect available tools and can respond to queries based on the tools provided by the configured servers.\n\n3. **Exit the session:**\n\n   Type `quit` or `exit` to end the session.\n\n## Architecture\n\n- **Tool Discovery**: Tools are automatically discovered from configured servers.\n- **System Prompt**: Tools are dynamically included in the system prompt, allowing the LLM to understand available capabilities.\n- **Server Integration**: Supports any MCP-compatible server, tested with various server implementations including Uvicorn and Node.js.\n\n### Class Structure\n\n- **Configuration**: Manages environment variables and server configurations\n- **Server**: Handles MCP server initialization, tool discovery, and execution\n- **Tool**: Represents individual tools with their properties and formatting\n- **LLMClient**: Manages communication with the LLM provider\n- **ChatSession**: Orchestrates the interaction between user, LLM, and tools\n\n### Logic Flow\n\n1. **Tool Integration**:\n   - Tools are dynamically discovered from MCP servers\n   - Tool descriptions are automatically included in system prompt\n   - Tool execution is handled through standardized MCP protocol\n\n2. **Runtime Flow**:\n   - User input is received\n   - Input is sent to LLM with context of available tools\n   - LLM response is parsed:\n     - If it's a tool call → execute tool and return result\n     - If it's a direct response → return to user\n   - Tool results are sent back to LLM for interpretation\n   - Final response is presented to user\n"
  },
  {
    "path": "examples/clients/simple-chatbot/mcp_simple_chatbot/main.py",
    "content": "from __future__ import annotations\n\nimport asyncio\nimport json\nimport logging\nimport os\nimport shutil\nfrom contextlib import AsyncExitStack\nfrom typing import Any\n\nimport httpx\nfrom dotenv import load_dotenv\nfrom mcp import ClientSession, StdioServerParameters\nfrom mcp.client.stdio import stdio_client\n\n# Configure logging\nlogging.basicConfig(level=logging.INFO, format=\"%(asctime)s - %(levelname)s - %(message)s\")\n\n\nclass Configuration:\n    \"\"\"Manages configuration and environment variables for the MCP client.\"\"\"\n\n    def __init__(self) -> None:\n        \"\"\"Initialize configuration with environment variables.\"\"\"\n        self.load_env()\n        self.api_key = os.getenv(\"LLM_API_KEY\")\n\n    @staticmethod\n    def load_env() -> None:\n        \"\"\"Load environment variables from .env file.\"\"\"\n        load_dotenv()\n\n    @staticmethod\n    def load_config(file_path: str) -> dict[str, Any]:\n        \"\"\"Load server configuration from JSON file.\n\n        Args:\n            file_path: Path to the JSON configuration file.\n\n        Returns:\n            Dict containing server configuration.\n\n        Raises:\n            FileNotFoundError: If configuration file doesn't exist.\n            JSONDecodeError: If configuration file is invalid JSON.\n        \"\"\"\n        with open(file_path, \"r\") as f:\n            return json.load(f)\n\n    @property\n    def llm_api_key(self) -> str:\n        \"\"\"Get the LLM API key.\n\n        Returns:\n            The API key as a string.\n\n        Raises:\n            ValueError: If the API key is not found in environment variables.\n        \"\"\"\n        if not self.api_key:\n            raise ValueError(\"LLM_API_KEY not found in environment variables\")\n        return self.api_key\n\n\nclass Server:\n    \"\"\"Manages MCP server connections and tool execution.\"\"\"\n\n    def __init__(self, name: str, config: dict[str, Any]) -> None:\n        self.name: str = name\n        self.config: dict[str, Any] = config\n        self.stdio_context: Any | None = None\n        self.session: ClientSession | None = None\n        self._cleanup_lock: asyncio.Lock = asyncio.Lock()\n        self.exit_stack: AsyncExitStack = AsyncExitStack()\n\n    async def initialize(self) -> None:\n        \"\"\"Initialize the server connection.\"\"\"\n        command = shutil.which(\"npx\") if self.config[\"command\"] == \"npx\" else self.config[\"command\"]\n        if command is None:\n            raise ValueError(\"The command must be a valid string and cannot be None.\")\n\n        server_params = StdioServerParameters(\n            command=command,\n            args=self.config[\"args\"],\n            env={**os.environ, **self.config[\"env\"]} if self.config.get(\"env\") else None,\n        )\n        try:\n            stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))\n            read, write = stdio_transport\n            session = await self.exit_stack.enter_async_context(ClientSession(read, write))\n            await session.initialize()\n            self.session = session\n        except Exception as e:\n            logging.error(f\"Error initializing server {self.name}: {e}\")\n            await self.cleanup()\n            raise\n\n    async def list_tools(self) -> list[Tool]:\n        \"\"\"List available tools from the server.\n\n        Returns:\n            A list of available tools.\n\n        Raises:\n            RuntimeError: If the server is not initialized.\n        \"\"\"\n        if not self.session:\n            raise RuntimeError(f\"Server {self.name} not initialized\")\n\n        tools_response = await self.session.list_tools()\n        tools: list[Tool] = []\n\n        for item in tools_response:\n            if item[0] == \"tools\":\n                tools.extend(Tool(tool.name, tool.description, tool.input_schema, tool.title) for tool in item[1])\n\n        return tools\n\n    async def execute_tool(\n        self,\n        tool_name: str,\n        arguments: dict[str, Any],\n        retries: int = 2,\n        delay: float = 1.0,\n    ) -> Any:\n        \"\"\"Execute a tool with retry mechanism.\n\n        Args:\n            tool_name: Name of the tool to execute.\n            arguments: Tool arguments.\n            retries: Number of retry attempts.\n            delay: Delay between retries in seconds.\n\n        Returns:\n            Tool execution result.\n\n        Raises:\n            RuntimeError: If server is not initialized.\n            Exception: If tool execution fails after all retries.\n        \"\"\"\n        if not self.session:\n            raise RuntimeError(f\"Server {self.name} not initialized\")\n\n        attempt = 0\n        while attempt < retries:\n            try:\n                logging.info(f\"Executing {tool_name}...\")\n                result = await self.session.call_tool(tool_name, arguments)\n\n                return result\n\n            except Exception as e:\n                attempt += 1\n                logging.warning(f\"Error executing tool: {e}. Attempt {attempt} of {retries}.\")\n                if attempt < retries:\n                    logging.info(f\"Retrying in {delay} seconds...\")\n                    await asyncio.sleep(delay)\n                else:\n                    logging.error(\"Max retries reached. Failing.\")\n                    raise\n\n    async def cleanup(self) -> None:\n        \"\"\"Clean up server resources.\"\"\"\n        async with self._cleanup_lock:\n            try:\n                await self.exit_stack.aclose()\n                self.session = None\n                self.stdio_context = None\n            except Exception as e:\n                logging.error(f\"Error during cleanup of server {self.name}: {e}\")\n\n\nclass Tool:\n    \"\"\"Represents a tool with its properties and formatting.\"\"\"\n\n    def __init__(\n        self,\n        name: str,\n        description: str,\n        input_schema: dict[str, Any],\n        title: str | None = None,\n    ) -> None:\n        self.name: str = name\n        self.title: str | None = title\n        self.description: str = description\n        self.input_schema: dict[str, Any] = input_schema\n\n    def format_for_llm(self) -> str:\n        \"\"\"Format tool information for LLM.\n\n        Returns:\n            A formatted string describing the tool.\n        \"\"\"\n        args_desc: list[str] = []\n        if \"properties\" in self.input_schema:\n            for param_name, param_info in self.input_schema[\"properties\"].items():\n                arg_desc = f\"- {param_name}: {param_info.get('description', 'No description')}\"\n                if param_name in self.input_schema.get(\"required\", []):\n                    arg_desc += \" (required)\"\n                args_desc.append(arg_desc)\n\n        # Build the formatted output with title as a separate field\n        output = f\"Tool: {self.name}\\n\"\n\n        # Add human-readable title if available\n        if self.title:\n            output += f\"User-readable title: {self.title}\\n\"\n\n        output += f\"\"\"Description: {self.description}\nArguments:\n{chr(10).join(args_desc)}\n\"\"\"\n\n        return output\n\n\nclass LLMClient:\n    \"\"\"Manages communication with the LLM provider.\"\"\"\n\n    def __init__(self, api_key: str) -> None:\n        self.api_key: str = api_key\n\n    def get_response(self, messages: list[dict[str, str]]) -> str:\n        \"\"\"Get a response from the LLM.\n\n        Args:\n            messages: A list of message dictionaries.\n\n        Returns:\n            The LLM's response as a string.\n\n        Raises:\n            httpx.RequestError: If the request to the LLM fails.\n        \"\"\"\n        url = \"https://api.groq.com/openai/v1/chat/completions\"\n\n        headers = {\n            \"Content-Type\": \"application/json\",\n            \"Authorization\": f\"Bearer {self.api_key}\",\n        }\n        payload = {\n            \"messages\": messages,\n            \"model\": \"meta-llama/llama-4-scout-17b-16e-instruct\",\n            \"temperature\": 0.7,\n            \"max_tokens\": 4096,\n            \"top_p\": 1,\n            \"stream\": False,\n            \"stop\": None,\n        }\n\n        try:\n            with httpx.Client() as client:\n                response = client.post(url, headers=headers, json=payload)\n                response.raise_for_status()\n                data = response.json()\n                return data[\"choices\"][0][\"message\"][\"content\"]\n\n        except httpx.RequestError as e:\n            error_message = f\"Error getting LLM response: {str(e)}\"\n            logging.error(error_message)\n\n            if isinstance(e, httpx.HTTPStatusError):\n                status_code = e.response.status_code\n                logging.error(f\"Status code: {status_code}\")\n                logging.error(f\"Response details: {e.response.text}\")\n\n            return f\"I encountered an error: {error_message}. Please try again or rephrase your request.\"\n\n\nclass ChatSession:\n    \"\"\"Orchestrates the interaction between user, LLM, and tools.\"\"\"\n\n    def __init__(self, servers: list[Server], llm_client: LLMClient) -> None:\n        self.servers: list[Server] = servers\n        self.llm_client: LLMClient = llm_client\n\n    async def cleanup_servers(self) -> None:\n        \"\"\"Clean up all servers properly.\"\"\"\n        for server in reversed(self.servers):\n            try:\n                await server.cleanup()\n            except Exception as e:\n                logging.warning(f\"Warning during final cleanup: {e}\")\n\n    async def process_llm_response(self, llm_response: str) -> str:\n        \"\"\"Process the LLM response and execute tools if needed.\n\n        Args:\n            llm_response: The response from the LLM.\n\n        Returns:\n            The result of tool execution or the original response.\n        \"\"\"\n        import json\n\n        def _clean_json_string(json_string: str) -> str:\n            \"\"\"Remove ```json ... ``` or ``` ... ``` wrappers if the LLM response is fenced.\"\"\"\n            import re\n\n            pattern = r\"^```(?:\\s*json)?\\s*(.*?)\\s*```$\"\n            return re.sub(pattern, r\"\\1\", json_string, flags=re.DOTALL | re.IGNORECASE).strip()\n\n        try:\n            tool_call = json.loads(_clean_json_string(llm_response))\n            if \"tool\" in tool_call and \"arguments\" in tool_call:\n                logging.info(f\"Executing tool: {tool_call['tool']}\")\n                logging.info(f\"With arguments: {tool_call['arguments']}\")\n\n                for server in self.servers:\n                    tools = await server.list_tools()\n                    if any(tool.name == tool_call[\"tool\"] for tool in tools):\n                        try:\n                            result = await server.execute_tool(tool_call[\"tool\"], tool_call[\"arguments\"])\n\n                            if isinstance(result, dict) and \"progress\" in result:\n                                progress = result[\"progress\"]  # type: ignore\n                                total = result[\"total\"]  # type: ignore\n                                percentage = (progress / total) * 100  # type: ignore\n                                logging.info(f\"Progress: {progress}/{total} ({percentage:.1f}%)\")\n\n                            return f\"Tool execution result: {result}\"\n                        except Exception as e:\n                            error_msg = f\"Error executing tool: {str(e)}\"\n                            logging.error(error_msg)\n                            return error_msg\n\n                return f\"No server found with tool: {tool_call['tool']}\"\n            return llm_response\n        except json.JSONDecodeError:\n            return llm_response\n\n    async def start(self) -> None:\n        \"\"\"Main chat session handler.\"\"\"\n        try:\n            for server in self.servers:\n                try:\n                    await server.initialize()\n                except Exception as e:\n                    logging.error(f\"Failed to initialize server: {e}\")\n                    await self.cleanup_servers()\n                    return\n\n            all_tools: list[Tool] = []\n            for server in self.servers:\n                tools = await server.list_tools()\n                all_tools.extend(tools)\n\n            tools_description = \"\\n\".join([tool.format_for_llm() for tool in all_tools])\n\n            system_message = (\n                \"You are a helpful assistant with access to these tools:\\n\\n\"\n                f\"{tools_description}\\n\"\n                \"Choose the appropriate tool based on the user's question. \"\n                \"If no tool is needed, reply directly.\\n\\n\"\n                \"IMPORTANT: When you need to use a tool, you must ONLY respond with \"\n                \"the exact JSON object format below, nothing else:\\n\"\n                \"{\\n\"\n                '    \"tool\": \"tool-name\",\\n'\n                '    \"arguments\": {\\n'\n                '        \"argument-name\": \"value\"\\n'\n                \"    }\\n\"\n                \"}\\n\\n\"\n                \"After receiving a tool's response:\\n\"\n                \"1. Transform the raw data into a natural, conversational response\\n\"\n                \"2. Keep responses concise but informative\\n\"\n                \"3. Focus on the most relevant information\\n\"\n                \"4. Use appropriate context from the user's question\\n\"\n                \"5. Avoid simply repeating the raw data\\n\\n\"\n                \"Please use only the tools that are explicitly defined above.\"\n            )\n\n            messages = [{\"role\": \"system\", \"content\": system_message}]\n\n            while True:\n                try:\n                    user_input = input(\"You: \").strip().lower()\n                    if user_input in [\"quit\", \"exit\"]:\n                        logging.info(\"\\nExiting...\")\n                        break\n\n                    messages.append({\"role\": \"user\", \"content\": user_input})\n\n                    llm_response = self.llm_client.get_response(messages)\n                    logging.info(\"\\nAssistant: %s\", llm_response)\n\n                    result = await self.process_llm_response(llm_response)\n\n                    if result != llm_response:\n                        messages.append({\"role\": \"assistant\", \"content\": llm_response})\n                        messages.append({\"role\": \"system\", \"content\": result})\n\n                        final_response = self.llm_client.get_response(messages)\n                        logging.info(\"\\nFinal response: %s\", final_response)\n                        messages.append({\"role\": \"assistant\", \"content\": final_response})\n                    else:\n                        messages.append({\"role\": \"assistant\", \"content\": llm_response})\n\n                except KeyboardInterrupt:\n                    logging.info(\"\\nExiting...\")\n                    break\n\n        finally:\n            await self.cleanup_servers()\n\n\nasync def run() -> None:\n    \"\"\"Initialize and run the chat session.\"\"\"\n    config = Configuration()\n    server_config = config.load_config(\"servers_config.json\")\n    servers = [Server(name, srv_config) for name, srv_config in server_config[\"mcpServers\"].items()]\n    llm_client = LLMClient(config.llm_api_key)\n    chat_session = ChatSession(servers, llm_client)\n    await chat_session.start()\n\n\ndef main() -> None:\n    asyncio.run(run())\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/clients/simple-chatbot/mcp_simple_chatbot/requirements.txt",
    "content": "python-dotenv>=1.0.0\nrequests>=2.31.0\nmcp>=1.0.0\nuvicorn>=0.32.1\n"
  },
  {
    "path": "examples/clients/simple-chatbot/mcp_simple_chatbot/servers_config.json",
    "content": "{\n  \"mcpServers\": {\n    \"sqlite\": {\n      \"command\": \"uvx\",\n      \"args\": [\"mcp-server-sqlite\", \"--db-path\", \"./test.db\"]\n    },\n    \"puppeteer\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@modelcontextprotocol/server-puppeteer\"]\n    }\n  }\n}\n"
  },
  {
    "path": "examples/clients/simple-chatbot/pyproject.toml",
    "content": "[project]\nname = \"mcp-simple-chatbot\"\nversion = \"0.1.0\"\ndescription = \"A simple CLI chatbot using the Model Context Protocol (MCP)\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nkeywords = [\"mcp\", \"llm\", \"chatbot\", \"cli\"]\nlicense = { text = \"MIT\" }\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n]\ndependencies = [\n    \"python-dotenv>=1.0.0\",\n    \"mcp\",\n    \"uvicorn>=0.32.1\",\n]\n\n[project.scripts]\nmcp-simple-chatbot = \"mcp_simple_chatbot.main:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_simple_chatbot\"]\n\n[tool.pyright]\ninclude = [\"mcp_simple_chatbot\"]\nvenvPath = \".\"\nvenv = \".venv\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\"]\nignore = []\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n[dependency-groups]\ndev = [\"pyright>=1.1.379\", \"pytest>=8.3.3\", \"ruff>=0.6.9\"]\n"
  },
  {
    "path": "examples/clients/simple-task-client/README.md",
    "content": "# Simple Task Client\n\nA minimal MCP client demonstrating polling for task results over streamable HTTP.\n\n## Running\n\nFirst, start the simple-task server in another terminal:\n\n```bash\ncd examples/servers/simple-task\nuv run mcp-simple-task\n```\n\nThen run the client:\n\n```bash\ncd examples/clients/simple-task-client\nuv run mcp-simple-task-client\n```\n\nUse `--url` to connect to a different server.\n\n## What it does\n\n1. Connects to the server via streamable HTTP\n2. Calls the `long_running_task` tool as a task\n3. Polls the task status until completion\n4. Retrieves and prints the result\n\n## Expected output\n\n```text\nAvailable tools: ['long_running_task']\n\nCalling tool as a task...\nTask created: <task-id>\n  Status: working - Starting work...\n  Status: working - Processing step 1...\n  Status: working - Processing step 2...\n  Status: completed -\n\nResult: Task completed!\n```\n"
  },
  {
    "path": "examples/clients/simple-task-client/mcp_simple_task_client/__init__.py",
    "content": ""
  },
  {
    "path": "examples/clients/simple-task-client/mcp_simple_task_client/__main__.py",
    "content": "import sys\n\nfrom .main import main\n\nsys.exit(main())  # type: ignore[call-arg]\n"
  },
  {
    "path": "examples/clients/simple-task-client/mcp_simple_task_client/main.py",
    "content": "\"\"\"Simple task client demonstrating MCP tasks polling over streamable HTTP.\"\"\"\n\nimport asyncio\n\nimport click\nfrom mcp import ClientSession\nfrom mcp.client.streamable_http import streamable_http_client\nfrom mcp.types import CallToolResult, TextContent\n\n\nasync def run(url: str) -> None:\n    async with streamable_http_client(url) as (read, write):\n        async with ClientSession(read, write) as session:\n            await session.initialize()\n\n            # List tools\n            tools = await session.list_tools()\n            print(f\"Available tools: {[t.name for t in tools.tools]}\")\n\n            # Call the tool as a task\n            print(\"\\nCalling tool as a task...\")\n\n            result = await session.experimental.call_tool_as_task(\n                \"long_running_task\",\n                arguments={},\n                ttl=60000,\n            )\n            task_id = result.task.task_id\n            print(f\"Task created: {task_id}\")\n\n            status = None\n            # Poll until done (respects server's pollInterval hint)\n            async for status in session.experimental.poll_task(task_id):\n                print(f\"  Status: {status.status} - {status.status_message or ''}\")\n\n            # Check final status\n            if status and status.status != \"completed\":\n                print(f\"Task ended with status: {status.status}\")\n                return\n\n            # Get the result\n            task_result = await session.experimental.get_task_result(task_id, CallToolResult)\n            content = task_result.content[0]\n            if isinstance(content, TextContent):\n                print(f\"\\nResult: {content.text}\")\n\n\n@click.command()\n@click.option(\"--url\", default=\"http://localhost:8000/mcp\", help=\"Server URL\")\ndef main(url: str) -> int:\n    asyncio.run(run(url))\n    return 0\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/clients/simple-task-client/pyproject.toml",
    "content": "[project]\nname = \"mcp-simple-task-client\"\nversion = \"0.1.0\"\ndescription = \"A simple MCP client demonstrating task polling\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nkeywords = [\"mcp\", \"llm\", \"tasks\", \"client\"]\nlicense = { text = \"MIT\" }\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n]\ndependencies = [\"click>=8.0\", \"mcp\"]\n\n[project.scripts]\nmcp-simple-task-client = \"mcp_simple_task_client.main:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_simple_task_client\"]\n\n[tool.pyright]\ninclude = [\"mcp_simple_task_client\"]\nvenvPath = \".\"\nvenv = \".venv\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\"]\nignore = []\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n[dependency-groups]\ndev = [\"pyright>=1.1.378\", \"ruff>=0.6.9\"]\n"
  },
  {
    "path": "examples/clients/simple-task-interactive-client/README.md",
    "content": "# Simple Interactive Task Client\n\nA minimal MCP client demonstrating responses to interactive tasks (elicitation and sampling).\n\n## Running\n\nFirst, start the interactive task server in another terminal:\n\n```bash\ncd examples/servers/simple-task-interactive\nuv run mcp-simple-task-interactive\n```\n\nThen run the client:\n\n```bash\ncd examples/clients/simple-task-interactive-client\nuv run mcp-simple-task-interactive-client\n```\n\nUse `--url` to connect to a different server.\n\n## What it does\n\n1. Connects to the server via streamable HTTP\n2. Calls `confirm_delete` - server asks for confirmation, client responds via terminal\n3. Calls `write_haiku` - server requests LLM completion, client returns a hardcoded haiku\n\n## Key concepts\n\n### Elicitation callback\n\n```python\nasync def elicitation_callback(context, params) -> ElicitResult:\n    # Handle user input request from server\n    return ElicitResult(action=\"accept\", content={\"confirm\": True})\n```\n\n### Sampling callback\n\n```python\nasync def sampling_callback(context, params) -> CreateMessageResult:\n    # Handle LLM completion request from server\n    return CreateMessageResult(model=\"...\", role=\"assistant\", content=...)\n```\n\n### Using call_tool_as_task\n\n```python\n# Call a tool as a task (returns immediately with task reference)\nresult = await session.experimental.call_tool_as_task(\"tool_name\", {\"arg\": \"value\"})\ntask_id = result.task.task_id\n\n# Get result - this delivers elicitation/sampling requests and blocks until complete\nfinal = await session.experimental.get_task_result(task_id, CallToolResult)\n```\n\n**Important**: The `get_task_result()` call is what triggers the delivery of elicitation\nand sampling requests to your callbacks. It blocks until the task completes and returns\nthe final result.\n\n## Expected output\n\n```text\nAvailable tools: ['confirm_delete', 'write_haiku']\n\n--- Demo 1: Elicitation ---\nCalling confirm_delete tool...\nTask created: <task-id>\n\n[Elicitation] Server asks: Are you sure you want to delete 'important.txt'?\nYour response (y/n): y\n[Elicitation] Responding with: confirm=True\nResult: Deleted 'important.txt'\n\n--- Demo 2: Sampling ---\nCalling write_haiku tool...\nTask created: <task-id>\n\n[Sampling] Server requests LLM completion for: Write a haiku about autumn leaves\n[Sampling] Responding with haiku\nResult:\nHaiku:\nCherry blossoms fall\nSoftly on the quiet pond\nSpring whispers goodbye\n```\n"
  },
  {
    "path": "examples/clients/simple-task-interactive-client/mcp_simple_task_interactive_client/__init__.py",
    "content": ""
  },
  {
    "path": "examples/clients/simple-task-interactive-client/mcp_simple_task_interactive_client/__main__.py",
    "content": "import sys\n\nfrom .main import main\n\nsys.exit(main())  # type: ignore[call-arg]\n"
  },
  {
    "path": "examples/clients/simple-task-interactive-client/mcp_simple_task_interactive_client/main.py",
    "content": "\"\"\"Simple interactive task client demonstrating elicitation and sampling responses.\n\nThis example demonstrates the spec-compliant polling pattern:\n1. Poll tasks/get watching for status changes\n2. On input_required, call tasks/result to receive elicitation/sampling requests\n3. Continue until terminal status, then retrieve final result\n\"\"\"\n\nimport asyncio\n\nimport click\nfrom mcp import ClientSession\nfrom mcp.client.context import ClientRequestContext\nfrom mcp.client.streamable_http import streamable_http_client\nfrom mcp.types import (\n    CallToolResult,\n    CreateMessageRequestParams,\n    CreateMessageResult,\n    ElicitRequestParams,\n    ElicitResult,\n    TextContent,\n)\n\n\nasync def elicitation_callback(\n    context: ClientRequestContext,\n    params: ElicitRequestParams,\n) -> ElicitResult:\n    \"\"\"Handle elicitation requests from the server.\"\"\"\n    print(f\"\\n[Elicitation] Server asks: {params.message}\")\n\n    # Simple terminal prompt\n    response = input(\"Your response (y/n): \").strip().lower()\n    confirmed = response in (\"y\", \"yes\", \"true\", \"1\")\n\n    print(f\"[Elicitation] Responding with: confirm={confirmed}\")\n    return ElicitResult(action=\"accept\", content={\"confirm\": confirmed})\n\n\nasync def sampling_callback(\n    context: ClientRequestContext,\n    params: CreateMessageRequestParams,\n) -> CreateMessageResult:\n    \"\"\"Handle sampling requests from the server.\"\"\"\n    # Get the prompt from the first message\n    prompt = \"unknown\"\n    if params.messages:\n        content = params.messages[0].content\n        if isinstance(content, TextContent):\n            prompt = content.text\n\n    print(f\"\\n[Sampling] Server requests LLM completion for: {prompt}\")\n\n    # Return a hardcoded haiku (in real use, call your LLM here)\n    haiku = \"\"\"Cherry blossoms fall\nSoftly on the quiet pond\nSpring whispers goodbye\"\"\"\n\n    print(\"[Sampling] Responding with haiku\")\n    return CreateMessageResult(\n        model=\"mock-haiku-model\",\n        role=\"assistant\",\n        content=TextContent(type=\"text\", text=haiku),\n    )\n\n\ndef get_text(result: CallToolResult) -> str:\n    \"\"\"Extract text from a CallToolResult.\"\"\"\n    if result.content and isinstance(result.content[0], TextContent):\n        return result.content[0].text\n    return \"(no text)\"\n\n\nasync def run(url: str) -> None:\n    async with streamable_http_client(url) as (read, write):\n        async with ClientSession(\n            read,\n            write,\n            elicitation_callback=elicitation_callback,\n            sampling_callback=sampling_callback,\n        ) as session:\n            await session.initialize()\n\n            # List tools\n            tools = await session.list_tools()\n            print(f\"Available tools: {[t.name for t in tools.tools]}\")\n\n            # Demo 1: Elicitation (confirm_delete)\n            print(\"\\n--- Demo 1: Elicitation ---\")\n            print(\"Calling confirm_delete tool...\")\n\n            elicit_task = await session.experimental.call_tool_as_task(\"confirm_delete\", {\"filename\": \"important.txt\"})\n            elicit_task_id = elicit_task.task.task_id\n            print(f\"Task created: {elicit_task_id}\")\n\n            # Poll until terminal, calling tasks/result on input_required\n            async for status in session.experimental.poll_task(elicit_task_id):\n                print(f\"[Poll] Status: {status.status}\")\n                if status.status == \"input_required\":\n                    # Server needs input - tasks/result delivers the elicitation request\n                    elicit_result = await session.experimental.get_task_result(elicit_task_id, CallToolResult)\n                    break\n            else:\n                # poll_task exited due to terminal status\n                elicit_result = await session.experimental.get_task_result(elicit_task_id, CallToolResult)\n\n            print(f\"Result: {get_text(elicit_result)}\")\n\n            # Demo 2: Sampling (write_haiku)\n            print(\"\\n--- Demo 2: Sampling ---\")\n            print(\"Calling write_haiku tool...\")\n\n            sampling_task = await session.experimental.call_tool_as_task(\"write_haiku\", {\"topic\": \"autumn leaves\"})\n            sampling_task_id = sampling_task.task.task_id\n            print(f\"Task created: {sampling_task_id}\")\n\n            # Poll until terminal, calling tasks/result on input_required\n            async for status in session.experimental.poll_task(sampling_task_id):\n                print(f\"[Poll] Status: {status.status}\")\n                if status.status == \"input_required\":\n                    sampling_result = await session.experimental.get_task_result(sampling_task_id, CallToolResult)\n                    break\n            else:\n                sampling_result = await session.experimental.get_task_result(sampling_task_id, CallToolResult)\n\n            print(f\"Result:\\n{get_text(sampling_result)}\")\n\n\n@click.command()\n@click.option(\"--url\", default=\"http://localhost:8000/mcp\", help=\"Server URL\")\ndef main(url: str) -> int:\n    asyncio.run(run(url))\n    return 0\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/clients/simple-task-interactive-client/pyproject.toml",
    "content": "[project]\nname = \"mcp-simple-task-interactive-client\"\nversion = \"0.1.0\"\ndescription = \"A simple MCP client demonstrating interactive task responses\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nkeywords = [\"mcp\", \"llm\", \"tasks\", \"client\", \"elicitation\", \"sampling\"]\nlicense = { text = \"MIT\" }\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n]\ndependencies = [\"click>=8.0\", \"mcp\"]\n\n[project.scripts]\nmcp-simple-task-interactive-client = \"mcp_simple_task_interactive_client.main:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_simple_task_interactive_client\"]\n\n[tool.pyright]\ninclude = [\"mcp_simple_task_interactive_client\"]\nvenvPath = \".\"\nvenv = \".venv\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\"]\nignore = []\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n[dependency-groups]\ndev = [\"pyright>=1.1.378\", \"ruff>=0.6.9\"]\n"
  },
  {
    "path": "examples/clients/sse-polling-client/README.md",
    "content": "# MCP SSE Polling Demo Client\n\nDemonstrates client-side auto-reconnect for the SSE polling pattern (SEP-1699).\n\n## Features\n\n- Connects to SSE polling demo server\n- Automatically reconnects when server closes SSE stream\n- Resumes from Last-Event-ID to avoid missing messages\n- Respects server-provided retry interval\n\n## Usage\n\n```bash\n# First start the server:\nuv run mcp-sse-polling-demo --port 3000\n\n# Then run this client:\nuv run mcp-sse-polling-client --url http://localhost:3000/mcp\n\n# Custom options:\nuv run mcp-sse-polling-client --url http://localhost:3000/mcp --items 20 --checkpoint-every 5\n```\n\n## Options\n\n- `--url`: Server URL (default: <http://localhost:3000/mcp>)\n- `--items`: Number of items to process (default: 10)\n- `--checkpoint-every`: Checkpoint interval (default: 3)\n- `--log-level`: Logging level (default: DEBUG)\n"
  },
  {
    "path": "examples/clients/sse-polling-client/mcp_sse_polling_client/__init__.py",
    "content": "\"\"\"SSE Polling Demo Client - demonstrates auto-reconnect for long-running tasks.\"\"\"\n"
  },
  {
    "path": "examples/clients/sse-polling-client/mcp_sse_polling_client/main.py",
    "content": "\"\"\"SSE Polling Demo Client\n\nDemonstrates the client-side auto-reconnect for SSE polling pattern.\n\nThis client connects to the SSE Polling Demo server and calls process_batch,\nwhich triggers periodic server-side stream closes. The client automatically\nreconnects using Last-Event-ID and resumes receiving messages.\n\nRun with:\n    # First start the server:\n    uv run mcp-sse-polling-demo --port 3000\n\n    # Then run this client:\n    uv run mcp-sse-polling-client --url http://localhost:3000/mcp\n\"\"\"\n\nimport asyncio\nimport logging\n\nimport click\nfrom mcp import ClientSession\nfrom mcp.client.streamable_http import streamable_http_client\n\n\nasync def run_demo(url: str, items: int, checkpoint_every: int) -> None:\n    \"\"\"Run the SSE polling demo.\"\"\"\n    print(f\"\\n{'=' * 60}\")\n    print(\"SSE Polling Demo Client\")\n    print(f\"{'=' * 60}\")\n    print(f\"Server URL: {url}\")\n    print(f\"Processing {items} items with checkpoints every {checkpoint_every}\")\n    print(f\"{'=' * 60}\\n\")\n\n    async with streamable_http_client(url) as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            # Initialize the connection\n            print(\"Initializing connection...\")\n            await session.initialize()\n            print(\"Connected!\\n\")\n\n            # List available tools\n            tools = await session.list_tools()\n            print(f\"Available tools: {[t.name for t in tools.tools]}\\n\")\n\n            # Call the process_batch tool\n            print(f\"Calling process_batch(items={items}, checkpoint_every={checkpoint_every})...\\n\")\n            print(\"-\" * 40)\n\n            result = await session.call_tool(\n                \"process_batch\",\n                {\n                    \"items\": items,\n                    \"checkpoint_every\": checkpoint_every,\n                },\n            )\n\n            print(\"-\" * 40)\n            if result.content:\n                content = result.content[0]\n                text = getattr(content, \"text\", str(content))\n                print(f\"\\nResult: {text}\")\n            else:\n                print(\"\\nResult: No content\")\n            print(f\"{'=' * 60}\\n\")\n\n\n@click.command()\n@click.option(\n    \"--url\",\n    default=\"http://localhost:3000/mcp\",\n    help=\"Server URL\",\n)\n@click.option(\n    \"--items\",\n    default=10,\n    help=\"Number of items to process\",\n)\n@click.option(\n    \"--checkpoint-every\",\n    default=3,\n    help=\"Checkpoint interval\",\n)\n@click.option(\n    \"--log-level\",\n    default=\"INFO\",\n    help=\"Logging level\",\n)\ndef main(url: str, items: int, checkpoint_every: int, log_level: str) -> None:\n    \"\"\"Run the SSE Polling Demo client.\"\"\"\n    logging.basicConfig(\n        level=getattr(logging, log_level.upper()),\n        format=\"%(asctime)s - %(name)s - %(levelname)s - %(message)s\",\n    )\n    # Suppress noisy HTTP client logging\n    logging.getLogger(\"httpx\").setLevel(logging.WARNING)\n    logging.getLogger(\"httpcore\").setLevel(logging.WARNING)\n\n    asyncio.run(run_demo(url, items, checkpoint_every))\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/clients/sse-polling-client/pyproject.toml",
    "content": "[project]\nname = \"mcp-sse-polling-client\"\nversion = \"0.1.0\"\ndescription = \"Demo client for SSE polling with auto-reconnect\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nkeywords = [\"mcp\", \"sse\", \"polling\", \"client\"]\nlicense = { text = \"MIT\" }\ndependencies = [\"click>=8.2.0\", \"mcp\"]\n\n[project.scripts]\nmcp-sse-polling-client = \"mcp_sse_polling_client.main:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_sse_polling_client\"]\n\n[tool.pyright]\ninclude = [\"mcp_sse_polling_client\"]\nvenvPath = \".\"\nvenv = \".venv\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\"]\nignore = []\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n[dependency-groups]\ndev = [\"pyright>=1.1.378\", \"pytest>=8.3.3\", \"ruff>=0.6.9\"]\n"
  },
  {
    "path": "examples/mcpserver/complex_inputs.py",
    "content": "\"\"\"MCPServer Complex inputs Example\n\nDemonstrates validation via pydantic with complex models.\n\"\"\"\n\nfrom typing import Annotated\n\nfrom pydantic import BaseModel, Field\n\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(\"Shrimp Tank\")\n\n\nclass ShrimpTank(BaseModel):\n    class Shrimp(BaseModel):\n        name: Annotated[str, Field(max_length=10)]\n\n    shrimp: list[Shrimp]\n\n\n@mcp.tool()\ndef name_shrimp(\n    tank: ShrimpTank,\n    # You can use pydantic Field in function signatures for validation.\n    extra_names: Annotated[list[str], Field(max_length=10)],\n) -> list[str]:\n    \"\"\"List all shrimp names in the tank\"\"\"\n    return [shrimp.name for shrimp in tank.shrimp] + extra_names\n"
  },
  {
    "path": "examples/mcpserver/desktop.py",
    "content": "\"\"\"MCPServer Desktop Example\n\nA simple example that exposes the desktop directory as a resource.\n\"\"\"\n\nfrom pathlib import Path\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create server\nmcp = MCPServer(\"Demo\")\n\n\n@mcp.resource(\"dir://desktop\")\ndef desktop() -> list[str]:\n    \"\"\"List the files in the user's desktop\"\"\"\n    desktop = Path.home() / \"Desktop\"\n    return [str(f) for f in desktop.iterdir()]\n\n\n@mcp.tool()\ndef sum(a: int, b: int) -> int:\n    \"\"\"Add two numbers\"\"\"\n    return a + b\n"
  },
  {
    "path": "examples/mcpserver/direct_call_tool_result_return.py",
    "content": "\"\"\"MCPServer Echo Server with direct CallToolResult return\"\"\"\n\nfrom typing import Annotated\n\nfrom pydantic import BaseModel\n\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.types import CallToolResult, TextContent\n\nmcp = MCPServer(\"Echo Server\")\n\n\nclass EchoResponse(BaseModel):\n    text: str\n\n\n@mcp.tool()\ndef echo(text: str) -> Annotated[CallToolResult, EchoResponse]:\n    \"\"\"Echo the input text with structure and metadata\"\"\"\n    return CallToolResult(\n        content=[TextContent(type=\"text\", text=text)], structured_content={\"text\": text}, _meta={\"some\": \"metadata\"}\n    )\n"
  },
  {
    "path": "examples/mcpserver/echo.py",
    "content": "\"\"\"MCPServer Echo Server\"\"\"\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create server\nmcp = MCPServer(\"Echo Server\")\n\n\n@mcp.tool()\ndef echo_tool(text: str) -> str:\n    \"\"\"Echo the input text\"\"\"\n    return text\n\n\n@mcp.resource(\"echo://static\")\ndef echo_resource() -> str:\n    return \"Echo!\"\n\n\n@mcp.resource(\"echo://{text}\")\ndef echo_template(text: str) -> str:\n    \"\"\"Echo the input text\"\"\"\n    return f\"Echo: {text}\"\n\n\n@mcp.prompt(\"echo\")\ndef echo_prompt(text: str) -> str:\n    return text\n"
  },
  {
    "path": "examples/mcpserver/icons_demo.py",
    "content": "\"\"\"MCPServer Icons Demo Server\n\nDemonstrates using icons with tools, resources, prompts, and implementation.\n\"\"\"\n\nimport base64\nfrom pathlib import Path\n\nfrom mcp.server.mcpserver import Icon, MCPServer\n\n# Load the icon file and convert to data URI\nicon_path = Path(__file__).parent / \"mcp.png\"\nicon_data = base64.standard_b64encode(icon_path.read_bytes()).decode()\nicon_data_uri = f\"data:image/png;base64,{icon_data}\"\n\nicon_data = Icon(src=icon_data_uri, mime_type=\"image/png\", sizes=[\"64x64\"])\n\n# Create server with icons in implementation\nmcp = MCPServer(\n    \"Icons Demo Server\", website_url=\"https://github.com/modelcontextprotocol/python-sdk\", icons=[icon_data]\n)\n\n\n@mcp.tool(icons=[icon_data])\ndef demo_tool(message: str) -> str:\n    \"\"\"A demo tool with an icon.\"\"\"\n    return message\n\n\n@mcp.resource(\"demo://readme\", icons=[icon_data])\ndef readme_resource() -> str:\n    \"\"\"A demo resource with an icon\"\"\"\n    return \"This resource has an icon\"\n\n\n@mcp.prompt(\"prompt_with_icon\", icons=[icon_data])\ndef prompt_with_icon(text: str) -> str:\n    \"\"\"A demo prompt with an icon\"\"\"\n    return text\n\n\n@mcp.tool(\n    icons=[\n        Icon(src=icon_data_uri, mime_type=\"image/png\", sizes=[\"16x16\"]),\n        Icon(src=icon_data_uri, mime_type=\"image/png\", sizes=[\"32x32\"]),\n        Icon(src=icon_data_uri, mime_type=\"image/png\", sizes=[\"64x64\"]),\n    ]\n)\ndef multi_icon_tool(action: str) -> str:\n    \"\"\"A tool demonstrating multiple icons.\"\"\"\n    return \"multi_icon_tool\"\n\n\nif __name__ == \"__main__\":\n    # Run the server\n    mcp.run()\n"
  },
  {
    "path": "examples/mcpserver/logging_and_progress.py",
    "content": "\"\"\"MCPServer Echo Server that sends log messages and progress updates to the client\"\"\"\n\nimport asyncio\n\nfrom mcp.server.mcpserver import Context, MCPServer\n\n# Create server\nmcp = MCPServer(\"Echo Server with logging and progress updates\")\n\n\n@mcp.tool()\nasync def echo(text: str, ctx: Context) -> str:\n    \"\"\"Echo the input text sending log messages and progress updates during processing.\"\"\"\n    await ctx.report_progress(progress=0, total=100)\n    await ctx.info(\"Starting to process echo for input: \" + text)\n\n    await asyncio.sleep(2)\n\n    await ctx.info(\"Halfway through processing echo for input: \" + text)\n    await ctx.report_progress(progress=50, total=100)\n\n    await asyncio.sleep(2)\n\n    await ctx.info(\"Finished processing echo for input: \" + text)\n    await ctx.report_progress(progress=100, total=100)\n\n    # Progress notifications are process asynchronously by the client.\n    # A small delay here helps ensure the last notification is processed by the client.\n    await asyncio.sleep(0.1)\n\n    return text\n"
  },
  {
    "path": "examples/mcpserver/memory.py",
    "content": "# /// script\n# dependencies = [\"pydantic-ai-slim[openai]\", \"asyncpg\", \"numpy\", \"pgvector\"]\n# ///\n\n# uv pip install 'pydantic-ai-slim[openai]' asyncpg numpy pgvector\n\n\"\"\"Recursive memory system inspired by the human brain's clustering of memories.\nUses OpenAI's 'text-embedding-3-small' model and pgvector for efficient\nsimilarity search.\n\"\"\"\n\nimport asyncio\nimport math\nimport os\nfrom dataclasses import dataclass\nfrom datetime import datetime, timezone\nfrom pathlib import Path\nfrom typing import Annotated, Self, TypeVar\n\nimport asyncpg\nimport numpy as np\nfrom openai import AsyncOpenAI\nfrom pgvector.asyncpg import register_vector  # Import register_vector\nfrom pydantic import BaseModel, Field\nfrom pydantic_ai import Agent\n\nfrom mcp.server.mcpserver import MCPServer\n\nMAX_DEPTH = 5\nSIMILARITY_THRESHOLD = 0.7\nDECAY_FACTOR = 0.99\nREINFORCEMENT_FACTOR = 1.1\n\nDEFAULT_LLM_MODEL = \"openai:gpt-4o\"\nDEFAULT_EMBEDDING_MODEL = \"text-embedding-3-small\"\n\nT = TypeVar(\"T\")\n\nmcp = MCPServer(\"memory\")\n\nDB_DSN = \"postgresql://postgres:postgres@localhost:54320/memory_db\"\n# reset memory with rm ~/.mcp/{USER}/memory/*\nPROFILE_DIR = (Path.home() / \".mcp\" / os.environ.get(\"USER\", \"anon\") / \"memory\").resolve()\nPROFILE_DIR.mkdir(parents=True, exist_ok=True)\n\n\ndef cosine_similarity(a: list[float], b: list[float]) -> float:\n    a_array = np.array(a, dtype=np.float64)\n    b_array = np.array(b, dtype=np.float64)\n    return np.dot(a_array, b_array) / (np.linalg.norm(a_array) * np.linalg.norm(b_array))\n\n\nasync def do_ai(\n    user_prompt: str,\n    system_prompt: str,\n    result_type: type[T] | Annotated,\n    deps=None,\n) -> T:\n    agent = Agent(\n        DEFAULT_LLM_MODEL,\n        system_prompt=system_prompt,\n        result_type=result_type,\n    )\n    result = await agent.run(user_prompt, deps=deps)\n    return result.data\n\n\n@dataclass\nclass Deps:\n    openai: AsyncOpenAI\n    pool: asyncpg.Pool\n\n\nasync def get_db_pool() -> asyncpg.Pool:\n    async def init(conn):\n        await conn.execute(\"CREATE EXTENSION IF NOT EXISTS vector;\")\n        await register_vector(conn)\n\n    pool = await asyncpg.create_pool(DB_DSN, init=init)\n    return pool\n\n\nclass MemoryNode(BaseModel):\n    id: int | None = None\n    content: str\n    summary: str = \"\"\n    importance: float = 1.0\n    access_count: int = 0\n    timestamp: float = Field(default_factory=lambda: datetime.now(timezone.utc).timestamp())\n    embedding: list[float]\n\n    @classmethod\n    async def from_content(cls, content: str, deps: Deps):\n        embedding = await get_embedding(content, deps)\n        return cls(content=content, embedding=embedding)\n\n    async def save(self, deps: Deps):\n        async with deps.pool.acquire() as conn:\n            if self.id is None:\n                result = await conn.fetchrow(\n                    \"\"\"\n                    INSERT INTO memories (content, summary, importance, access_count,\n                        timestamp, embedding)\n                    VALUES ($1, $2, $3, $4, $5, $6)\n                    RETURNING id\n                    \"\"\",\n                    self.content,\n                    self.summary,\n                    self.importance,\n                    self.access_count,\n                    self.timestamp,\n                    self.embedding,\n                )\n                self.id = result[\"id\"]\n            else:\n                await conn.execute(\n                    \"\"\"\n                    UPDATE memories\n                    SET content = $1, summary = $2, importance = $3,\n                        access_count = $4, timestamp = $5, embedding = $6\n                    WHERE id = $7\n                    \"\"\",\n                    self.content,\n                    self.summary,\n                    self.importance,\n                    self.access_count,\n                    self.timestamp,\n                    self.embedding,\n                    self.id,\n                )\n\n    async def merge_with(self, other: Self, deps: Deps):\n        self.content = await do_ai(\n            f\"{self.content}\\n\\n{other.content}\",\n            \"Combine the following two texts into a single, coherent text.\",\n            str,\n            deps,\n        )\n        self.importance += other.importance\n        self.access_count += other.access_count\n        self.embedding = [(a + b) / 2 for a, b in zip(self.embedding, other.embedding)]\n        self.summary = await do_ai(self.content, \"Summarize the following text concisely.\", str, deps)\n        await self.save(deps)\n        # Delete the merged node from the database\n        if other.id is not None:\n            await delete_memory(other.id, deps)\n\n    def get_effective_importance(self):\n        return self.importance * (1 + math.log(self.access_count + 1))\n\n\nasync def get_embedding(text: str, deps: Deps) -> list[float]:\n    embedding_response = await deps.openai.embeddings.create(\n        input=text,\n        model=DEFAULT_EMBEDDING_MODEL,\n    )\n    return embedding_response.data[0].embedding\n\n\nasync def delete_memory(memory_id: int, deps: Deps):\n    async with deps.pool.acquire() as conn:\n        await conn.execute(\"DELETE FROM memories WHERE id = $1\", memory_id)\n\n\nasync def add_memory(content: str, deps: Deps):\n    new_memory = await MemoryNode.from_content(content, deps)\n    await new_memory.save(deps)\n\n    similar_memories = await find_similar_memories(new_memory.embedding, deps)\n    for memory in similar_memories:\n        if memory.id != new_memory.id:\n            await new_memory.merge_with(memory, deps)\n\n    await update_importance(new_memory.embedding, deps)\n\n    await prune_memories(deps)\n\n    return f\"Remembered: {content}\"\n\n\nasync def find_similar_memories(embedding: list[float], deps: Deps) -> list[MemoryNode]:\n    async with deps.pool.acquire() as conn:\n        rows = await conn.fetch(\n            \"\"\"\n            SELECT id, content, summary, importance, access_count, timestamp, embedding\n            FROM memories\n            ORDER BY embedding <-> $1\n            LIMIT 5\n            \"\"\",\n            embedding,\n        )\n    memories = [\n        MemoryNode(\n            id=row[\"id\"],\n            content=row[\"content\"],\n            summary=row[\"summary\"],\n            importance=row[\"importance\"],\n            access_count=row[\"access_count\"],\n            timestamp=row[\"timestamp\"],\n            embedding=row[\"embedding\"],\n        )\n        for row in rows\n    ]\n    return memories\n\n\nasync def update_importance(user_embedding: list[float], deps: Deps):\n    async with deps.pool.acquire() as conn:\n        rows = await conn.fetch(\"SELECT id, importance, access_count, embedding FROM memories\")\n        for row in rows:\n            memory_embedding = row[\"embedding\"]\n            similarity = cosine_similarity(user_embedding, memory_embedding)\n            if similarity > SIMILARITY_THRESHOLD:\n                new_importance = row[\"importance\"] * REINFORCEMENT_FACTOR\n                new_access_count = row[\"access_count\"] + 1\n            else:\n                new_importance = row[\"importance\"] * DECAY_FACTOR\n                new_access_count = row[\"access_count\"]\n            await conn.execute(\n                \"\"\"\n                UPDATE memories\n                SET importance = $1, access_count = $2\n                WHERE id = $3\n                \"\"\",\n                new_importance,\n                new_access_count,\n                row[\"id\"],\n            )\n\n\nasync def prune_memories(deps: Deps):\n    async with deps.pool.acquire() as conn:\n        rows = await conn.fetch(\n            \"\"\"\n            SELECT id, importance, access_count\n            FROM memories\n            ORDER BY importance DESC\n            OFFSET $1\n            \"\"\",\n            MAX_DEPTH,\n        )\n        for row in rows:\n            await conn.execute(\"DELETE FROM memories WHERE id = $1\", row[\"id\"])\n\n\nasync def display_memory_tree(deps: Deps) -> str:\n    async with deps.pool.acquire() as conn:\n        rows = await conn.fetch(\n            \"\"\"\n            SELECT content, summary, importance, access_count\n            FROM memories\n            ORDER BY importance DESC\n            LIMIT $1\n            \"\"\",\n            MAX_DEPTH,\n        )\n    result = \"\"\n    for row in rows:\n        effective_importance = row[\"importance\"] * (1 + math.log(row[\"access_count\"] + 1))\n        summary = row[\"summary\"] or row[\"content\"]\n        result += f\"- {summary} (Importance: {effective_importance:.2f})\\n\"\n    return result\n\n\n@mcp.tool()\nasync def remember(\n    contents: list[str] = Field(description=\"List of observations or memories to store\"),\n):\n    deps = Deps(openai=AsyncOpenAI(), pool=await get_db_pool())\n    try:\n        return \"\\n\".join(await asyncio.gather(*[add_memory(content, deps) for content in contents]))\n    finally:\n        await deps.pool.close()\n\n\n@mcp.tool()\nasync def read_profile() -> str:\n    deps = Deps(openai=AsyncOpenAI(), pool=await get_db_pool())\n    profile = await display_memory_tree(deps)\n    await deps.pool.close()\n    return profile\n\n\nasync def initialize_database():\n    pool = await asyncpg.create_pool(\"postgresql://postgres:postgres@localhost:54320/postgres\")\n    try:\n        async with pool.acquire() as conn:\n            await conn.execute(\"\"\"\n                SELECT pg_terminate_backend(pg_stat_activity.pid)\n                FROM pg_stat_activity\n                WHERE pg_stat_activity.datname = 'memory_db'\n                AND pid <> pg_backend_pid();\n            \"\"\")\n            await conn.execute(\"DROP DATABASE IF EXISTS memory_db;\")\n            await conn.execute(\"CREATE DATABASE memory_db;\")\n    finally:\n        await pool.close()\n\n    pool = await asyncpg.create_pool(DB_DSN)\n    try:\n        async with pool.acquire() as conn:\n            await conn.execute(\"CREATE EXTENSION IF NOT EXISTS vector;\")\n\n            await register_vector(conn)\n\n            await conn.execute(\"\"\"\n                CREATE TABLE IF NOT EXISTS memories (\n                    id SERIAL PRIMARY KEY,\n                    content TEXT NOT NULL,\n                    summary TEXT,\n                    importance REAL NOT NULL,\n                    access_count INT NOT NULL,\n                    timestamp DOUBLE PRECISION NOT NULL,\n                    embedding vector(1536) NOT NULL\n                );\n                CREATE INDEX IF NOT EXISTS idx_memories_embedding ON memories\n                    USING hnsw (embedding vector_l2_ops);\n            \"\"\")\n    finally:\n        await pool.close()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(initialize_database())\n"
  },
  {
    "path": "examples/mcpserver/parameter_descriptions.py",
    "content": "\"\"\"MCPServer Example showing parameter descriptions\"\"\"\n\nfrom pydantic import Field\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create server\nmcp = MCPServer(\"Parameter Descriptions Server\")\n\n\n@mcp.tool()\ndef greet_user(\n    name: str = Field(description=\"The name of the person to greet\"),\n    title: str = Field(description=\"Optional title like Mr/Ms/Dr\", default=\"\"),\n    times: int = Field(description=\"Number of times to repeat the greeting\", default=1),\n) -> str:\n    \"\"\"Greet a user with optional title and repetition\"\"\"\n    greeting = f\"Hello {title + ' ' if title else ''}{name}!\"\n    return \"\\n\".join([greeting] * times)\n"
  },
  {
    "path": "examples/mcpserver/readme-quickstart.py",
    "content": "from mcp.server.mcpserver import MCPServer\n\n# Create an MCP server\nmcp = MCPServer(\"Demo\")\n\n\n# Add an addition tool\n@mcp.tool()\ndef sum(a: int, b: int) -> int:\n    \"\"\"Add two numbers\"\"\"\n    return a + b\n\n\n# Add a dynamic greeting resource\n@mcp.resource(\"greeting://{name}\")\ndef get_greeting(name: str) -> str:\n    \"\"\"Get a personalized greeting\"\"\"\n    return f\"Hello, {name}!\"\n"
  },
  {
    "path": "examples/mcpserver/screenshot.py",
    "content": "\"\"\"MCPServer Screenshot Example\n\nGive Claude a tool to capture and view screenshots.\n\"\"\"\n\nimport io\n\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.server.mcpserver.utilities.types import Image\n\n# Create server\nmcp = MCPServer(\"Screenshot Demo\")\n\n\n@mcp.tool()\ndef take_screenshot() -> Image:\n    \"\"\"Take a screenshot of the user's screen and return it as an image. Use\n    this tool anytime the user wants you to look at something they're doing.\n    \"\"\"\n    import pyautogui\n\n    buffer = io.BytesIO()\n\n    # if the file exceeds ~1MB, it will be rejected by Claude\n    screenshot = pyautogui.screenshot()\n    screenshot.convert(\"RGB\").save(buffer, format=\"JPEG\", quality=60, optimize=True)\n    return Image(data=buffer.getvalue(), format=\"jpeg\")\n"
  },
  {
    "path": "examples/mcpserver/simple_echo.py",
    "content": "\"\"\"MCPServer Echo Server\"\"\"\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create server\nmcp = MCPServer(\"Echo Server\")\n\n\n@mcp.tool()\ndef echo(text: str) -> str:\n    \"\"\"Echo the input text\"\"\"\n    return text\n"
  },
  {
    "path": "examples/mcpserver/text_me.py",
    "content": "# /// script\n# dependencies = []\n# ///\n\n\"\"\"MCPServer Text Me Server\n--------------------------------\nThis defines a simple MCPServer server that sends a text message to a phone number via https://surgemsg.com/.\n\nTo run this example, create a `.env` file with the following values:\n\nSURGE_API_KEY=...\nSURGE_ACCOUNT_ID=...\nSURGE_MY_PHONE_NUMBER=...\nSURGE_MY_FIRST_NAME=...\nSURGE_MY_LAST_NAME=...\n\nVisit https://surgemsg.com/ and click \"Get Started\" to obtain these values.\n\"\"\"\n\nfrom typing import Annotated\n\nimport httpx\nfrom pydantic import BeforeValidator\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nfrom mcp.server.mcpserver import MCPServer\n\n\nclass SurgeSettings(BaseSettings):\n    model_config: SettingsConfigDict = SettingsConfigDict(env_prefix=\"SURGE_\", env_file=\".env\")\n\n    api_key: str\n    account_id: str\n    my_phone_number: Annotated[str, BeforeValidator(lambda v: \"+\" + v if not v.startswith(\"+\") else v)]\n    my_first_name: str\n    my_last_name: str\n\n\n# Create server\nmcp = MCPServer(\"Text me\")\nsurge_settings = SurgeSettings()  # type: ignore\n\n\n@mcp.tool(name=\"textme\", description=\"Send a text message to me\")\ndef text_me(text_content: str) -> str:\n    \"\"\"Send a text message to a phone number via https://surgemsg.com/\"\"\"\n    with httpx.Client() as client:\n        response = client.post(\n            \"https://api.surgemsg.com/messages\",\n            headers={\n                \"Authorization\": f\"Bearer {surge_settings.api_key}\",\n                \"Surge-Account\": surge_settings.account_id,\n                \"Content-Type\": \"application/json\",\n            },\n            json={\n                \"body\": text_content,\n                \"conversation\": {\n                    \"contact\": {\n                        \"first_name\": surge_settings.my_first_name,\n                        \"last_name\": surge_settings.my_last_name,\n                        \"phone_number\": surge_settings.my_phone_number,\n                    }\n                },\n            },\n        )\n        response.raise_for_status()\n        return f\"Message sent: {text_content}\"\n"
  },
  {
    "path": "examples/mcpserver/unicode_example.py",
    "content": "\"\"\"Example MCPServer server that uses Unicode characters in various places to help test\nUnicode handling in tools and inspectors.\n\"\"\"\n\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer()\n\n\n@mcp.tool(description=\"🌟 A tool that uses various Unicode characters in its description: á é í ó ú ñ 漢字 🎉\")\ndef hello_unicode(name: str = \"世界\", greeting: str = \"¡Hola\") -> str:\n    \"\"\"A simple tool that demonstrates Unicode handling in:\n    - Tool description (emojis, accents, CJK characters)\n    - Parameter defaults (CJK characters)\n    - Return values (Spanish punctuation, emojis)\n    \"\"\"\n    return f\"{greeting}, {name}! 👋\"\n\n\n@mcp.tool(description=\"🎨 Tool that returns a list of emoji categories\")\ndef list_emoji_categories() -> list[str]:\n    \"\"\"Returns a list of emoji categories with emoji examples.\"\"\"\n    return [\n        \"😀 Smileys & Emotion\",\n        \"👋 People & Body\",\n        \"🐶 Animals & Nature\",\n        \"🍎 Food & Drink\",\n        \"⚽ Activities\",\n        \"🌍 Travel & Places\",\n        \"💡 Objects\",\n        \"❤️ Symbols\",\n        \"🚩 Flags\",\n    ]\n\n\n@mcp.tool(description=\"🔤 Tool that returns text in different scripts\")\ndef multilingual_hello() -> str:\n    \"\"\"Returns hello in different scripts and writing systems.\"\"\"\n    return \"\\n\".join(\n        [\n            \"English: Hello!\",\n            \"Spanish: ¡Hola!\",\n            \"French: Bonjour!\",\n            \"German: Grüß Gott!\",\n            \"Russian: Привет!\",\n            \"Greek: Γεια σας!\",\n            \"Hebrew: !שָׁלוֹם\",\n            \"Arabic: !مرحبا\",\n            \"Hindi: नमस्ते!\",\n            \"Chinese: 你好!\",\n            \"Japanese: こんにちは!\",\n            \"Korean: 안녕하세요!\",\n            \"Thai: สวัสดี!\",\n        ]\n    )\n\n\nif __name__ == \"__main__\":\n    mcp.run()\n"
  },
  {
    "path": "examples/mcpserver/weather_structured.py",
    "content": "\"\"\"MCPServer Weather Example with Structured Output\n\nDemonstrates how to use structured output with tools to return\nwell-typed, validated data that clients can easily process.\n\"\"\"\n\nimport asyncio\nimport json\nimport sys\nfrom dataclasses import dataclass\nfrom datetime import datetime\nfrom typing import TypedDict\n\nfrom pydantic import BaseModel, Field\n\nfrom mcp.client import Client\nfrom mcp.server.mcpserver import MCPServer\n\n# Create server\nmcp = MCPServer(\"Weather Service\")\n\n\n# Example 1: Using a Pydantic model for structured output\nclass WeatherData(BaseModel):\n    \"\"\"Structured weather data response\"\"\"\n\n    temperature: float = Field(description=\"Temperature in Celsius\")\n    humidity: float = Field(description=\"Humidity percentage (0-100)\")\n    condition: str = Field(description=\"Weather condition (sunny, cloudy, rainy, etc.)\")\n    wind_speed: float = Field(description=\"Wind speed in km/h\")\n    location: str = Field(description=\"Location name\")\n    timestamp: datetime = Field(default_factory=datetime.now, description=\"Observation time\")\n\n\n@mcp.tool()\ndef get_weather(city: str) -> WeatherData:\n    \"\"\"Get current weather for a city with full structured data\"\"\"\n    # In a real implementation, this would fetch from a weather API\n    return WeatherData(temperature=22.5, humidity=65.0, condition=\"partly cloudy\", wind_speed=12.3, location=city)\n\n\n# Example 2: Using TypedDict for a simpler structure\nclass WeatherSummary(TypedDict):\n    \"\"\"Simple weather summary\"\"\"\n\n    city: str\n    temp_c: float\n    description: str\n\n\n@mcp.tool()\ndef get_weather_summary(city: str) -> WeatherSummary:\n    \"\"\"Get a brief weather summary for a city\"\"\"\n    return WeatherSummary(city=city, temp_c=22.5, description=\"Partly cloudy with light breeze\")\n\n\n# Example 3: Using dict[str, Any] for flexible schemas\n@mcp.tool()\ndef get_weather_metrics(cities: list[str]) -> dict[str, dict[str, float]]:\n    \"\"\"Get weather metrics for multiple cities\n\n    Returns a dictionary mapping city names to their metrics\n    \"\"\"\n    # Returns nested dictionaries with weather metrics\n    return {\n        city: {\"temperature\": 20.0 + i * 2, \"humidity\": 60.0 + i * 5, \"pressure\": 1013.0 + i * 0.5}\n        for i, city in enumerate(cities)\n    }\n\n\n# Example 4: Using dataclass for weather alerts\n@dataclass\nclass WeatherAlert:\n    \"\"\"Weather alert information\"\"\"\n\n    severity: str  # \"low\", \"medium\", \"high\"\n    title: str\n    description: str\n    affected_areas: list[str]\n    valid_until: datetime\n\n\n@mcp.tool()\ndef get_weather_alerts(region: str) -> list[WeatherAlert]:\n    \"\"\"Get active weather alerts for a region\"\"\"\n    # In production, this would fetch real alerts\n    if region.lower() == \"california\":\n        return [\n            WeatherAlert(\n                severity=\"high\",\n                title=\"Heat Wave Warning\",\n                description=\"Temperatures expected to exceed 40 degrees\",\n                affected_areas=[\"Los Angeles\", \"San Diego\", \"Riverside\"],\n                valid_until=datetime(2024, 7, 15, 18, 0),\n            ),\n            WeatherAlert(\n                severity=\"medium\",\n                title=\"Air Quality Advisory\",\n                description=\"Poor air quality due to wildfire smoke\",\n                affected_areas=[\"San Francisco Bay Area\"],\n                valid_until=datetime(2024, 7, 14, 12, 0),\n            ),\n        ]\n    return []\n\n\n# Example 5: Returning primitives with structured output\n@mcp.tool()\ndef get_temperature(city: str, unit: str = \"celsius\") -> float:\n    \"\"\"Get just the temperature for a city\n\n    When returning primitives as structured output,\n    the result is wrapped in {\"result\": value}\n    \"\"\"\n    base_temp = 22.5\n    if unit.lower() == \"fahrenheit\":\n        return base_temp * 9 / 5 + 32\n    return base_temp\n\n\n# Example 6: Weather statistics with nested models\nclass DailyStats(BaseModel):\n    \"\"\"Statistics for a single day\"\"\"\n\n    high: float\n    low: float\n    mean: float\n\n\nclass WeatherStats(BaseModel):\n    \"\"\"Weather statistics over a period\"\"\"\n\n    location: str\n    period_days: int\n    temperature: DailyStats\n    humidity: DailyStats\n    precipitation_mm: float = Field(description=\"Total precipitation in millimeters\")\n\n\n@mcp.tool()\ndef get_weather_stats(city: str, days: int = 7) -> WeatherStats:\n    \"\"\"Get weather statistics for the past N days\"\"\"\n    return WeatherStats(\n        location=city,\n        period_days=days,\n        temperature=DailyStats(high=28.5, low=15.2, mean=21.8),\n        humidity=DailyStats(high=85.0, low=45.0, mean=65.0),\n        precipitation_mm=12.4,\n    )\n\n\nif __name__ == \"__main__\":\n\n    async def test() -> None:\n        \"\"\"Test the tools by calling them through the server as a client would\"\"\"\n        print(\"Testing Weather Service Tools (via MCP protocol)\\n\")\n        print(\"=\" * 80)\n\n        async with Client(mcp) as client:\n            # Test get_weather\n            result = await client.call_tool(\"get_weather\", {\"city\": \"London\"})\n            print(\"\\nWeather in London:\")\n            print(json.dumps(result.structured_content, indent=2))\n\n            # Test get_weather_summary\n            result = await client.call_tool(\"get_weather_summary\", {\"city\": \"Paris\"})\n            print(\"\\nWeather summary for Paris:\")\n            print(json.dumps(result.structured_content, indent=2))\n\n            # Test get_weather_metrics\n            result = await client.call_tool(\"get_weather_metrics\", {\"cities\": [\"Tokyo\", \"Sydney\", \"Mumbai\"]})\n            print(\"\\nWeather metrics:\")\n            print(json.dumps(result.structured_content, indent=2))\n\n            # Test get_weather_alerts\n            result = await client.call_tool(\"get_weather_alerts\", {\"region\": \"California\"})\n            print(\"\\nWeather alerts for California:\")\n            print(json.dumps(result.structured_content, indent=2))\n\n            # Test get_temperature\n            result = await client.call_tool(\"get_temperature\", {\"city\": \"Berlin\", \"unit\": \"fahrenheit\"})\n            print(\"\\nTemperature in Berlin:\")\n            print(json.dumps(result.structured_content, indent=2))\n\n            # Test get_weather_stats\n            result = await client.call_tool(\"get_weather_stats\", {\"city\": \"Seattle\", \"days\": 30})\n            print(\"\\nWeather stats for Seattle (30 days):\")\n            print(json.dumps(result.structured_content, indent=2))\n\n            # Also show the text content for comparison\n            print(\"\\nText content for last result:\")\n            for content in result.content:\n                if content.type == \"text\":\n                    print(content.text)\n\n    async def print_schemas() -> None:\n        \"\"\"Print all tool schemas\"\"\"\n        print(\"Tool Schemas for Weather Service\\n\")\n        print(\"=\" * 80)\n\n        tools = await mcp.list_tools()\n        for tool in tools:\n            print(f\"\\nTool: {tool.name}\")\n            print(f\"Description: {tool.description}\")\n            print(\"Input Schema:\")\n            print(json.dumps(tool.input_schema, indent=2))\n\n            if tool.output_schema:\n                print(\"Output Schema:\")\n                print(json.dumps(tool.output_schema, indent=2))\n            else:\n                print(\"Output Schema: None (returns unstructured content)\")\n\n            print(\"-\" * 80)\n\n    # Check command line arguments\n    if len(sys.argv) > 1 and sys.argv[1] == \"--schemas\":\n        asyncio.run(print_schemas())\n    else:\n        print(\"Usage:\")\n        print(\"  python weather_structured.py          # Run tool tests\")\n        print(\"  python weather_structured.py --schemas # Print tool schemas\")\n        print()\n        asyncio.run(test())\n"
  },
  {
    "path": "examples/servers/everything-server/README.md",
    "content": "# MCP Everything Server\n\nA comprehensive MCP server implementing all protocol features for conformance testing.\n\n## Overview\n\nThe Everything Server is a reference implementation that demonstrates all features of the Model Context Protocol (MCP). It is designed to be used with the [MCP Conformance Test Framework](https://github.com/modelcontextprotocol/conformance) to validate MCP client and server implementations.\n\n## Installation\n\nFrom the python-sdk root directory:\n\n```bash\nuv sync --frozen\n```\n\n## Usage\n\n### Running the Server\n\nStart the server with default settings (port 3001):\n\n```bash\nuv run -m mcp_everything_server\n```\n\nOr with custom options:\n\n```bash\nuv run -m mcp_everything_server --port 3001 --log-level DEBUG\n```\n\nThe server will be available at: `http://localhost:3001/mcp`\n\n### Command-Line Options\n\n- `--port` - Port to listen on (default: 3001)\n- `--log-level` - Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL (default: INFO)\n\n## Running Conformance Tests\n\nSee the [MCP Conformance Test Framework](https://github.com/modelcontextprotocol/conformance) for instructions on running conformance tests against this server.\n"
  },
  {
    "path": "examples/servers/everything-server/mcp_everything_server/__init__.py",
    "content": "\"\"\"MCP Everything Server - Comprehensive conformance test server.\"\"\"\n\n__version__ = \"0.1.0\"\n"
  },
  {
    "path": "examples/servers/everything-server/mcp_everything_server/__main__.py",
    "content": "\"\"\"CLI entry point for the MCP Everything Server.\"\"\"\n\nfrom .server import main\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/servers/everything-server/mcp_everything_server/server.py",
    "content": "#!/usr/bin/env python3\n\"\"\"MCP Everything Server - Conformance Test Server\n\nServer implementing all MCP features for conformance testing based on Conformance Server Specification.\n\"\"\"\n\nimport asyncio\nimport base64\nimport json\nimport logging\n\nimport click\nfrom mcp.server import ServerRequestContext\nfrom mcp.server.mcpserver import Context, MCPServer\nfrom mcp.server.mcpserver.prompts.base import UserMessage\nfrom mcp.server.streamable_http import EventCallback, EventMessage, EventStore\nfrom mcp.types import (\n    AudioContent,\n    Completion,\n    CompletionArgument,\n    CompletionContext,\n    EmbeddedResource,\n    EmptyResult,\n    ImageContent,\n    JSONRPCMessage,\n    PromptReference,\n    ResourceTemplateReference,\n    SamplingMessage,\n    SetLevelRequestParams,\n    SubscribeRequestParams,\n    TextContent,\n    TextResourceContents,\n    UnsubscribeRequestParams,\n)\nfrom pydantic import BaseModel, Field\n\nlogger = logging.getLogger(__name__)\n\n# Type aliases for event store\nStreamId = str\nEventId = str\n\n\nclass InMemoryEventStore(EventStore):\n    \"\"\"Simple in-memory event store for SSE resumability testing.\"\"\"\n\n    def __init__(self) -> None:\n        self._events: list[tuple[StreamId, EventId, JSONRPCMessage | None]] = []\n        self._event_id_counter = 0\n\n    async def store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId:\n        \"\"\"Store an event and return its ID.\"\"\"\n        self._event_id_counter += 1\n        event_id = str(self._event_id_counter)\n        self._events.append((stream_id, event_id, message))\n        return event_id\n\n    async def replay_events_after(self, last_event_id: EventId, send_callback: EventCallback) -> StreamId | None:\n        \"\"\"Replay events after the specified ID.\"\"\"\n        target_stream_id = None\n        for stream_id, event_id, _ in self._events:\n            if event_id == last_event_id:\n                target_stream_id = stream_id\n                break\n        if target_stream_id is None:\n            return None\n        last_event_id_int = int(last_event_id)\n        for stream_id, event_id, message in self._events:\n            if stream_id == target_stream_id and int(event_id) > last_event_id_int:\n                # Skip priming events (None message)\n                if message is not None:\n                    await send_callback(EventMessage(message, event_id))\n        return target_stream_id\n\n\n# Test data\nTEST_IMAGE_BASE64 = \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==\"\nTEST_AUDIO_BASE64 = \"UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA=\"\n\n# Server state\nresource_subscriptions: set[str] = set()\nwatched_resource_content = \"Watched resource content\"\n\n# Create event store for SSE resumability (SEP-1699)\nevent_store = InMemoryEventStore()\n\nmcp = MCPServer(\n    name=\"mcp-conformance-test-server\",\n)\n\n\n# Tools\n@mcp.tool()\ndef test_simple_text() -> str:\n    \"\"\"Tests simple text content response\"\"\"\n    return \"This is a simple text response for testing.\"\n\n\n@mcp.tool()\ndef test_image_content() -> list[ImageContent]:\n    \"\"\"Tests image content response\"\"\"\n    return [ImageContent(type=\"image\", data=TEST_IMAGE_BASE64, mime_type=\"image/png\")]\n\n\n@mcp.tool()\ndef test_audio_content() -> list[AudioContent]:\n    \"\"\"Tests audio content response\"\"\"\n    return [AudioContent(type=\"audio\", data=TEST_AUDIO_BASE64, mime_type=\"audio/wav\")]\n\n\n@mcp.tool()\ndef test_embedded_resource() -> list[EmbeddedResource]:\n    \"\"\"Tests embedded resource content response\"\"\"\n    return [\n        EmbeddedResource(\n            type=\"resource\",\n            resource=TextResourceContents(\n                uri=\"test://embedded-resource\",\n                mime_type=\"text/plain\",\n                text=\"This is an embedded resource content.\",\n            ),\n        )\n    ]\n\n\n@mcp.tool()\ndef test_multiple_content_types() -> list[TextContent | ImageContent | EmbeddedResource]:\n    \"\"\"Tests response with multiple content types (text, image, resource)\"\"\"\n    return [\n        TextContent(type=\"text\", text=\"Multiple content types test:\"),\n        ImageContent(type=\"image\", data=TEST_IMAGE_BASE64, mime_type=\"image/png\"),\n        EmbeddedResource(\n            type=\"resource\",\n            resource=TextResourceContents(\n                uri=\"test://mixed-content-resource\",\n                mime_type=\"application/json\",\n                text='{\"test\": \"data\", \"value\": 123}',\n            ),\n        ),\n    ]\n\n\n@mcp.tool()\nasync def test_tool_with_logging(ctx: Context) -> str:\n    \"\"\"Tests tool that emits log messages during execution\"\"\"\n    await ctx.info(\"Tool execution started\")\n    await asyncio.sleep(0.05)\n\n    await ctx.info(\"Tool processing data\")\n    await asyncio.sleep(0.05)\n\n    await ctx.info(\"Tool execution completed\")\n    return \"Tool with logging executed successfully\"\n\n\n@mcp.tool()\nasync def test_tool_with_progress(ctx: Context) -> str:\n    \"\"\"Tests tool that reports progress notifications\"\"\"\n    await ctx.report_progress(progress=0, total=100, message=\"Completed step 0 of 100\")\n    await asyncio.sleep(0.05)\n\n    await ctx.report_progress(progress=50, total=100, message=\"Completed step 50 of 100\")\n    await asyncio.sleep(0.05)\n\n    await ctx.report_progress(progress=100, total=100, message=\"Completed step 100 of 100\")\n\n    # Return progress token as string\n    progress_token = (\n        ctx.request_context.meta.get(\"progress_token\") if ctx.request_context and ctx.request_context.meta else 0\n    )\n    return str(progress_token)\n\n\n@mcp.tool()\nasync def test_sampling(prompt: str, ctx: Context) -> str:\n    \"\"\"Tests server-initiated sampling (LLM completion request)\"\"\"\n    try:\n        # Request sampling from client\n        result = await ctx.session.create_message(\n            messages=[SamplingMessage(role=\"user\", content=TextContent(type=\"text\", text=prompt))],\n            max_tokens=100,\n        )\n\n        # Since we're not passing tools param, result.content is single content\n        if result.content.type == \"text\":\n            model_response = result.content.text\n        else:\n            model_response = \"No response\"\n\n        return f\"LLM response: {model_response}\"\n    except Exception as e:\n        return f\"Sampling not supported or error: {str(e)}\"\n\n\nclass UserResponse(BaseModel):\n    response: str = Field(description=\"User's response\")\n\n\n@mcp.tool()\nasync def test_elicitation(message: str, ctx: Context) -> str:\n    \"\"\"Tests server-initiated elicitation (user input request)\"\"\"\n    try:\n        # Request user input from client\n        result = await ctx.elicit(message=message, schema=UserResponse)\n\n        # Type-safe discriminated union narrowing using action field\n        if result.action == \"accept\":\n            content = result.data.model_dump_json()\n        else:  # decline or cancel\n            content = \"{}\"\n\n        return f\"User response: action={result.action}, content={content}\"\n    except Exception as e:\n        return f\"Elicitation not supported or error: {str(e)}\"\n\n\nclass SEP1034DefaultsSchema(BaseModel):\n    \"\"\"Schema for testing SEP-1034 elicitation with default values for all primitive types\"\"\"\n\n    name: str = Field(default=\"John Doe\", description=\"User name\")\n    age: int = Field(default=30, description=\"User age\")\n    score: float = Field(default=95.5, description=\"User score\")\n    status: str = Field(\n        default=\"active\",\n        description=\"User status\",\n        json_schema_extra={\"enum\": [\"active\", \"inactive\", \"pending\"]},\n    )\n    verified: bool = Field(default=True, description=\"Verification status\")\n\n\n@mcp.tool()\nasync def test_elicitation_sep1034_defaults(ctx: Context) -> str:\n    \"\"\"Tests elicitation with default values for all primitive types (SEP-1034)\"\"\"\n    try:\n        # Request user input with defaults for all primitive types\n        result = await ctx.elicit(message=\"Please provide user information\", schema=SEP1034DefaultsSchema)\n\n        # Type-safe discriminated union narrowing using action field\n        if result.action == \"accept\":\n            content = result.data.model_dump_json()\n        else:  # decline or cancel\n            content = \"{}\"\n\n        return f\"Elicitation result: action={result.action}, content={content}\"\n    except Exception as e:\n        return f\"Elicitation not supported or error: {str(e)}\"\n\n\nclass EnumSchemasTestSchema(BaseModel):\n    \"\"\"Schema for testing enum schema variations (SEP-1330)\"\"\"\n\n    untitledSingle: str = Field(\n        description=\"Simple enum without titles\", json_schema_extra={\"enum\": [\"active\", \"inactive\", \"pending\"]}\n    )\n    titledSingle: str = Field(\n        description=\"Enum with titled options (oneOf)\",\n        json_schema_extra={\n            \"oneOf\": [\n                {\"const\": \"low\", \"title\": \"Low Priority\"},\n                {\"const\": \"medium\", \"title\": \"Medium Priority\"},\n                {\"const\": \"high\", \"title\": \"High Priority\"},\n            ]\n        },\n    )\n    untitledMulti: list[str] = Field(\n        description=\"Multi-select without titles\",\n        json_schema_extra={\"items\": {\"type\": \"string\", \"enum\": [\"read\", \"write\", \"execute\"]}},\n    )\n    titledMulti: list[str] = Field(\n        description=\"Multi-select with titled options\",\n        json_schema_extra={\n            \"items\": {\n                \"anyOf\": [\n                    {\"const\": \"feature\", \"title\": \"New Feature\"},\n                    {\"const\": \"bug\", \"title\": \"Bug Fix\"},\n                    {\"const\": \"docs\", \"title\": \"Documentation\"},\n                ]\n            }\n        },\n    )\n    legacyEnum: str = Field(\n        description=\"Legacy enum with enumNames\",\n        json_schema_extra={\n            \"enum\": [\"small\", \"medium\", \"large\"],\n            \"enumNames\": [\"Small Size\", \"Medium Size\", \"Large Size\"],\n        },\n    )\n\n\n@mcp.tool()\nasync def test_elicitation_sep1330_enums(ctx: Context) -> str:\n    \"\"\"Tests elicitation with enum schema variations per SEP-1330\"\"\"\n    try:\n        result = await ctx.elicit(\n            message=\"Please select values using different enum schema types\", schema=EnumSchemasTestSchema\n        )\n\n        if result.action == \"accept\":\n            content = result.data.model_dump_json()\n        else:\n            content = \"{}\"\n\n        return f\"Elicitation completed: action={result.action}, content={content}\"\n    except Exception as e:\n        return f\"Elicitation not supported or error: {str(e)}\"\n\n\n@mcp.tool()\ndef test_error_handling() -> str:\n    \"\"\"Tests error response handling\"\"\"\n    raise RuntimeError(\"This tool intentionally returns an error for testing\")\n\n\n@mcp.tool()\nasync def test_reconnection(ctx: Context) -> str:\n    \"\"\"Tests SSE polling by closing stream mid-call (SEP-1699)\"\"\"\n    await ctx.info(\"Before disconnect\")\n\n    await ctx.close_sse_stream()\n\n    await asyncio.sleep(0.2)  # Wait for client to reconnect\n\n    await ctx.info(\"After reconnect\")\n    return \"Reconnection test completed\"\n\n\n# Resources\n@mcp.resource(\"test://static-text\")\ndef static_text_resource() -> str:\n    \"\"\"A static text resource for testing\"\"\"\n    return \"This is the content of the static text resource.\"\n\n\n@mcp.resource(\"test://static-binary\")\ndef static_binary_resource() -> bytes:\n    \"\"\"A static binary resource (image) for testing\"\"\"\n    return base64.b64decode(TEST_IMAGE_BASE64)\n\n\n@mcp.resource(\"test://template/{id}/data\")\ndef template_resource(id: str) -> str:\n    \"\"\"A resource template with parameter substitution\"\"\"\n    return json.dumps({\"id\": id, \"templateTest\": True, \"data\": f\"Data for ID: {id}\"})\n\n\n@mcp.resource(\"test://watched-resource\")\ndef watched_resource() -> str:\n    \"\"\"A resource that can be subscribed to for updates\"\"\"\n    return watched_resource_content\n\n\n# Prompts\n@mcp.prompt()\ndef test_simple_prompt() -> list[UserMessage]:\n    \"\"\"A simple prompt without arguments\"\"\"\n    return [UserMessage(role=\"user\", content=TextContent(type=\"text\", text=\"This is a simple prompt for testing.\"))]\n\n\n@mcp.prompt()\ndef test_prompt_with_arguments(arg1: str, arg2: str) -> list[UserMessage]:\n    \"\"\"A prompt with required arguments\"\"\"\n    return [\n        UserMessage(\n            role=\"user\", content=TextContent(type=\"text\", text=f\"Prompt with arguments: arg1='{arg1}', arg2='{arg2}'\")\n        )\n    ]\n\n\n@mcp.prompt()\ndef test_prompt_with_embedded_resource(resourceUri: str) -> list[UserMessage]:\n    \"\"\"A prompt that includes an embedded resource\"\"\"\n    return [\n        UserMessage(\n            role=\"user\",\n            content=EmbeddedResource(\n                type=\"resource\",\n                resource=TextResourceContents(\n                    uri=resourceUri,\n                    mime_type=\"text/plain\",\n                    text=\"Embedded resource content for testing.\",\n                ),\n            ),\n        ),\n        UserMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Please process the embedded resource above.\")),\n    ]\n\n\n@mcp.prompt()\ndef test_prompt_with_image() -> list[UserMessage]:\n    \"\"\"A prompt that includes image content\"\"\"\n    return [\n        UserMessage(role=\"user\", content=ImageContent(type=\"image\", data=TEST_IMAGE_BASE64, mime_type=\"image/png\")),\n        UserMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Please analyze the image above.\")),\n    ]\n\n\n# Custom request handlers\n# TODO(felix): Add public APIs to MCPServer for subscribe_resource, unsubscribe_resource,\n# and set_logging_level to avoid accessing protected _lowlevel_server attribute.\nasync def handle_set_logging_level(ctx: ServerRequestContext, params: SetLevelRequestParams) -> EmptyResult:\n    \"\"\"Handle logging level changes\"\"\"\n    logger.info(f\"Log level set to: {params.level}\")\n    return EmptyResult()\n\n\nasync def handle_subscribe(ctx: ServerRequestContext, params: SubscribeRequestParams) -> EmptyResult:\n    \"\"\"Handle resource subscription\"\"\"\n    resource_subscriptions.add(str(params.uri))\n    logger.info(f\"Subscribed to resource: {params.uri}\")\n    return EmptyResult()\n\n\nasync def handle_unsubscribe(ctx: ServerRequestContext, params: UnsubscribeRequestParams) -> EmptyResult:\n    \"\"\"Handle resource unsubscription\"\"\"\n    resource_subscriptions.discard(str(params.uri))\n    logger.info(f\"Unsubscribed from resource: {params.uri}\")\n    return EmptyResult()\n\n\nmcp._lowlevel_server._add_request_handler(\"logging/setLevel\", handle_set_logging_level)  # pyright: ignore[reportPrivateUsage]\nmcp._lowlevel_server._add_request_handler(\"resources/subscribe\", handle_subscribe)  # pyright: ignore[reportPrivateUsage]\nmcp._lowlevel_server._add_request_handler(\"resources/unsubscribe\", handle_unsubscribe)  # pyright: ignore[reportPrivateUsage]\n\n\n@mcp.completion()\nasync def _handle_completion(\n    ref: PromptReference | ResourceTemplateReference,\n    argument: CompletionArgument,\n    context: CompletionContext | None,\n) -> Completion:\n    \"\"\"Handle completion requests\"\"\"\n    # Basic completion support - returns empty array for conformance\n    # Real implementations would provide contextual suggestions\n    return Completion(values=[], total=0, has_more=False)\n\n\n# CLI\n@click.command()\n@click.option(\"--port\", default=3001, help=\"Port to listen on for HTTP\")\n@click.option(\n    \"--log-level\",\n    default=\"INFO\",\n    help=\"Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)\",\n)\ndef main(port: int, log_level: str) -> int:\n    \"\"\"Run the MCP Everything Server.\"\"\"\n    logging.basicConfig(\n        level=getattr(logging, log_level.upper()),\n        format=\"%(asctime)s - %(name)s - %(levelname)s - %(message)s\",\n    )\n\n    logger.info(f\"Starting MCP Everything Server on port {port}\")\n    logger.info(f\"Endpoint will be: http://localhost:{port}/mcp\")\n\n    mcp.run(\n        transport=\"streamable-http\",\n        port=port,\n        event_store=event_store,\n        retry_interval=100,  # 100ms retry interval for SSE polling\n    )\n\n    return 0\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/servers/everything-server/pyproject.toml",
    "content": "[project]\nname = \"mcp-everything-server\"\nversion = \"0.1.0\"\ndescription = \"Comprehensive MCP server implementing all protocol features for conformance testing\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nkeywords = [\"mcp\", \"llm\", \"automation\", \"conformance\", \"testing\"]\nlicense = { text = \"MIT\" }\ndependencies = [\"anyio>=4.5\", \"click>=8.2.0\", \"httpx>=0.27\", \"mcp\", \"starlette\", \"uvicorn\"]\n\n[project.scripts]\nmcp-everything-server = \"mcp_everything_server.server:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_everything_server\"]\n\n[tool.pyright]\ninclude = [\"mcp_everything_server\"]\nvenvPath = \".\"\nvenv = \".venv\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\"]\nignore = []\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n[dependency-groups]\ndev = [\"pyright>=1.1.378\", \"pytest>=8.3.3\", \"ruff>=0.6.9\"]\n"
  },
  {
    "path": "examples/servers/simple-auth/README.md",
    "content": "# MCP OAuth Authentication Demo\n\nThis example demonstrates OAuth 2.0 authentication with the Model Context Protocol using **separate Authorization Server (AS) and Resource Server (RS)** to comply with the new RFC 9728 specification.\n\n---\n\n## Running the Servers\n\n### Step 1: Start Authorization Server\n\n```bash\n# Navigate to the simple-auth directory\ncd examples/servers/simple-auth\n\n# Start Authorization Server on port 9000\nuv run mcp-simple-auth-as --port=9000\n```\n\n**What it provides:**\n\n- OAuth 2.0 flows (registration, authorization, token exchange)\n- Simple credential-based authentication (no external provider needed)  \n- Token introspection endpoint for Resource Servers (`/introspect`)\n\n---\n\n### Step 2: Start Resource Server (MCP Server)\n\n```bash\n# In another terminal, navigate to the simple-auth directory\ncd examples/servers/simple-auth\n\n# Start Resource Server on port 8001, connected to Authorization Server\nuv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http\n\n# With RFC 8707 strict resource validation (recommended for production)\nuv run mcp-simple-auth-rs --port=8001 --auth-server=http://localhost:9000 --transport=streamable-http --oauth-strict\n\n```\n\n### Step 3: Test with Client\n\n```bash\ncd examples/clients/simple-auth-client\n# Start client with streamable HTTP\nMCP_SERVER_PORT=8001 MCP_TRANSPORT_TYPE=streamable-http uv run mcp-simple-auth-client\n```\n\n## How It Works\n\n### RFC 9728 Discovery\n\n**Client → Resource Server:**\n\n```bash\ncurl http://localhost:8001/.well-known/oauth-protected-resource\n```\n\n```json\n{\n  \"resource\": \"http://localhost:8001\",\n  \"authorization_servers\": [\"http://localhost:9000\"]\n}\n```\n\n**Client → Authorization Server:**\n\n```bash\ncurl http://localhost:9000/.well-known/oauth-authorization-server\n```\n\n```json\n{\n  \"issuer\": \"http://localhost:9000\",\n  \"authorization_endpoint\": \"http://localhost:9000/authorize\",\n  \"token_endpoint\": \"http://localhost:9000/token\"\n}\n```\n\n## Legacy MCP Server as Authorization Server (Backwards Compatibility)\n\nFor backwards compatibility with older MCP implementations, a legacy server is provided that acts as an Authorization Server (following the old spec where MCP servers could optionally provide OAuth):\n\n### Running the Legacy Server\n\n```bash\n# Start legacy server on port 8000 (the default)\ncd examples/servers/simple-auth\nuv run mcp-simple-auth-legacy --port=8000 --transport=streamable-http\n```\n\n**Differences from the new architecture:**\n\n- **MCP server acts as AS:** The MCP server itself provides OAuth endpoints (old spec behavior)\n- **No separate RS:** The server handles both authentication and MCP tools\n- **Local token validation:** Tokens are validated internally without introspection\n- **No RFC 9728 support:** Does not provide `/.well-known/oauth-protected-resource`\n- **Direct OAuth discovery:** OAuth metadata is at the MCP server's URL\n\n### Testing with Legacy Server\n\n```bash\n# Test with client (will automatically fall back to legacy discovery)\ncd examples/clients/simple-auth-client\nMCP_SERVER_PORT=8000 MCP_TRANSPORT_TYPE=streamable-http uv run mcp-simple-auth-client\n```\n\nThe client will:\n\n1. Try RFC 9728 discovery at `/.well-known/oauth-protected-resource` (404 on legacy server)\n2. Fall back to direct OAuth discovery at `/.well-known/oauth-authorization-server`\n3. Complete authentication with the MCP server acting as its own AS\n\nThis ensures existing MCP servers (which could optionally act as Authorization Servers under the old spec) continue to work while the ecosystem transitions to the new architecture where MCP servers are Resource Servers only.\n\n## Manual Testing\n\n### Test Discovery\n\n```bash\n# Test Resource Server discovery endpoint (new architecture)\ncurl -v http://localhost:8001/.well-known/oauth-protected-resource\n\n# Test Authorization Server metadata\ncurl -v http://localhost:9000/.well-known/oauth-authorization-server\n```\n\n### Test Token Introspection\n\n```bash\n# After getting a token through OAuth flow:\ncurl -X POST http://localhost:9000/introspect \\\n  -H \"Content-Type: application/x-www-form-urlencoded\" \\\n  -d \"token=your_access_token\"\n```\n"
  },
  {
    "path": "examples/servers/simple-auth/mcp_simple_auth/__init__.py",
    "content": "\"\"\"Simple MCP server with GitHub OAuth authentication.\"\"\"\n"
  },
  {
    "path": "examples/servers/simple-auth/mcp_simple_auth/__main__.py",
    "content": "\"\"\"Main entry point for simple MCP server with GitHub OAuth authentication.\"\"\"\n\nimport sys\n\nfrom mcp_simple_auth.server import main\n\nsys.exit(main())  # type: ignore[call-arg]\n"
  },
  {
    "path": "examples/servers/simple-auth/mcp_simple_auth/auth_server.py",
    "content": "\"\"\"Authorization Server for MCP Split Demo.\n\nThis server handles OAuth flows, client registration, and token issuance.\nCan be replaced with enterprise authorization servers like Auth0, Entra ID, etc.\n\nNOTE: this is a simplified example for demonstration purposes.\nThis is not a production-ready implementation.\n\n\"\"\"\n\nimport asyncio\nimport logging\nimport time\n\nimport click\nfrom pydantic import AnyHttpUrl, BaseModel\nfrom starlette.applications import Starlette\nfrom starlette.exceptions import HTTPException\nfrom starlette.requests import Request\nfrom starlette.responses import JSONResponse, Response\nfrom starlette.routing import Route\nfrom uvicorn import Config, Server\n\nfrom mcp.server.auth.routes import cors_middleware, create_auth_routes\nfrom mcp.server.auth.settings import AuthSettings, ClientRegistrationOptions\n\nfrom .simple_auth_provider import SimpleAuthSettings, SimpleOAuthProvider\n\nlogger = logging.getLogger(__name__)\n\n\nclass AuthServerSettings(BaseModel):\n    \"\"\"Settings for the Authorization Server.\"\"\"\n\n    # Server settings\n    host: str = \"localhost\"\n    port: int = 9000\n    server_url: AnyHttpUrl = AnyHttpUrl(\"http://localhost:9000\")\n    auth_callback_path: str = \"http://localhost:9000/login/callback\"\n\n\nclass SimpleAuthProvider(SimpleOAuthProvider):\n    \"\"\"Authorization Server provider with simple demo authentication.\n\n    This provider:\n    1. Issues MCP tokens after simple credential authentication\n    2. Stores token state for introspection by Resource Servers\n    \"\"\"\n\n    def __init__(self, auth_settings: SimpleAuthSettings, auth_callback_path: str, server_url: str):\n        super().__init__(auth_settings, auth_callback_path, server_url)\n\n\ndef create_authorization_server(server_settings: AuthServerSettings, auth_settings: SimpleAuthSettings) -> Starlette:\n    \"\"\"Create the Authorization Server application.\"\"\"\n    oauth_provider = SimpleAuthProvider(\n        auth_settings, server_settings.auth_callback_path, str(server_settings.server_url)\n    )\n\n    mcp_auth_settings = AuthSettings(\n        issuer_url=server_settings.server_url,\n        client_registration_options=ClientRegistrationOptions(\n            enabled=True,\n            valid_scopes=[auth_settings.mcp_scope],\n            default_scopes=[auth_settings.mcp_scope],\n        ),\n        required_scopes=[auth_settings.mcp_scope],\n        resource_server_url=None,\n    )\n\n    # Create OAuth routes\n    routes = create_auth_routes(\n        provider=oauth_provider,\n        issuer_url=mcp_auth_settings.issuer_url,\n        service_documentation_url=mcp_auth_settings.service_documentation_url,\n        client_registration_options=mcp_auth_settings.client_registration_options,\n        revocation_options=mcp_auth_settings.revocation_options,\n    )\n\n    # Add login page route (GET)\n    async def login_page_handler(request: Request) -> Response:\n        \"\"\"Show login form.\"\"\"\n        state = request.query_params.get(\"state\")\n        if not state:\n            raise HTTPException(400, \"Missing state parameter\")\n        return await oauth_provider.get_login_page(state)\n\n    routes.append(Route(\"/login\", endpoint=login_page_handler, methods=[\"GET\"]))\n\n    # Add login callback route (POST)\n    async def login_callback_handler(request: Request) -> Response:\n        \"\"\"Handle simple authentication callback.\"\"\"\n        return await oauth_provider.handle_login_callback(request)\n\n    routes.append(Route(\"/login/callback\", endpoint=login_callback_handler, methods=[\"POST\"]))\n\n    # Add token introspection endpoint (RFC 7662) for Resource Servers\n    async def introspect_handler(request: Request) -> Response:\n        \"\"\"Token introspection endpoint for Resource Servers.\n\n        Resource Servers call this endpoint to validate tokens without\n        needing direct access to token storage.\n        \"\"\"\n        form = await request.form()\n        token = form.get(\"token\")\n        if not token or not isinstance(token, str):\n            return JSONResponse({\"active\": False}, status_code=400)\n\n        # Look up token in provider\n        access_token = await oauth_provider.load_access_token(token)\n        if not access_token:\n            return JSONResponse({\"active\": False})\n\n        return JSONResponse(\n            {\n                \"active\": True,\n                \"client_id\": access_token.client_id,\n                \"scope\": \" \".join(access_token.scopes),\n                \"exp\": access_token.expires_at,\n                \"iat\": int(time.time()),\n                \"token_type\": \"Bearer\",\n                \"aud\": access_token.resource,  # RFC 8707 audience claim\n            }\n        )\n\n    routes.append(\n        Route(\n            \"/introspect\",\n            endpoint=cors_middleware(introspect_handler, [\"POST\", \"OPTIONS\"]),\n            methods=[\"POST\", \"OPTIONS\"],\n        )\n    )\n\n    return Starlette(routes=routes)\n\n\nasync def run_server(server_settings: AuthServerSettings, auth_settings: SimpleAuthSettings):\n    \"\"\"Run the Authorization Server.\"\"\"\n    auth_server = create_authorization_server(server_settings, auth_settings)\n\n    config = Config(\n        auth_server,\n        host=server_settings.host,\n        port=server_settings.port,\n        log_level=\"info\",\n    )\n    server = Server(config)\n\n    logger.info(f\"🚀 MCP Authorization Server running on {server_settings.server_url}\")\n\n    await server.serve()\n\n\n@click.command()\n@click.option(\"--port\", default=9000, help=\"Port to listen on\")\ndef main(port: int) -> int:\n    \"\"\"Run the MCP Authorization Server.\n\n    This server handles OAuth flows and can be used by multiple Resource Servers.\n\n    Uses simple hardcoded credentials for demo purposes.\n    \"\"\"\n    logging.basicConfig(level=logging.INFO)\n\n    # Load simple auth settings\n    auth_settings = SimpleAuthSettings()\n\n    # Create server settings\n    host = \"localhost\"\n    server_url = f\"http://{host}:{port}\"\n    server_settings = AuthServerSettings(\n        host=host,\n        port=port,\n        server_url=AnyHttpUrl(server_url),\n        auth_callback_path=f\"{server_url}/login\",\n    )\n\n    asyncio.run(run_server(server_settings, auth_settings))\n    return 0\n\n\nif __name__ == \"__main__\":\n    main()  # type: ignore[call-arg]\n"
  },
  {
    "path": "examples/servers/simple-auth/mcp_simple_auth/legacy_as_server.py",
    "content": "\"\"\"Legacy Combined Authorization Server + Resource Server for MCP.\n\nThis server implements the old spec where MCP servers could act as both AS and RS.\nUsed for backwards compatibility testing with the new split AS/RS architecture.\n\nNOTE: this is a simplified example for demonstration purposes.\nThis is not a production-ready implementation.\n\n\"\"\"\n\nimport datetime\nimport logging\nfrom typing import Any, Literal\n\nimport click\nfrom pydantic import AnyHttpUrl, BaseModel\nfrom starlette.exceptions import HTTPException\nfrom starlette.requests import Request\nfrom starlette.responses import Response\n\nfrom mcp.server.auth.settings import AuthSettings, ClientRegistrationOptions\nfrom mcp.server.mcpserver.server import MCPServer\n\nfrom .simple_auth_provider import SimpleAuthSettings, SimpleOAuthProvider\n\nlogger = logging.getLogger(__name__)\n\n\nclass ServerSettings(BaseModel):\n    \"\"\"Settings for the simple auth MCP server.\"\"\"\n\n    # Server settings\n    host: str = \"localhost\"\n    port: int = 8000\n    server_url: AnyHttpUrl = AnyHttpUrl(\"http://localhost:8000\")\n    auth_callback_path: str = \"http://localhost:8000/login/callback\"\n\n\nclass LegacySimpleOAuthProvider(SimpleOAuthProvider):\n    \"\"\"Simple OAuth provider for legacy MCP server.\"\"\"\n\n    def __init__(self, auth_settings: SimpleAuthSettings, auth_callback_path: str, server_url: str):\n        super().__init__(auth_settings, auth_callback_path, server_url)\n\n\ndef create_simple_mcp_server(server_settings: ServerSettings, auth_settings: SimpleAuthSettings) -> MCPServer:\n    \"\"\"Create a simple MCPServer server with simple authentication.\"\"\"\n    oauth_provider = LegacySimpleOAuthProvider(\n        auth_settings, server_settings.auth_callback_path, str(server_settings.server_url)\n    )\n\n    mcp_auth_settings = AuthSettings(\n        issuer_url=server_settings.server_url,\n        client_registration_options=ClientRegistrationOptions(\n            enabled=True,\n            valid_scopes=[auth_settings.mcp_scope],\n            default_scopes=[auth_settings.mcp_scope],\n        ),\n        required_scopes=[auth_settings.mcp_scope],\n        # No resource_server_url parameter in legacy mode\n        resource_server_url=None,\n    )\n\n    app = MCPServer(\n        name=\"Simple Auth MCP Server\",\n        instructions=\"A simple MCP server with simple credential authentication\",\n        auth_server_provider=oauth_provider,\n        debug=True,\n        auth=mcp_auth_settings,\n    )\n    # Store server settings for later use in run()\n    app._server_settings = server_settings  # type: ignore[attr-defined]\n\n    @app.custom_route(\"/login\", methods=[\"GET\"])\n    async def login_page_handler(request: Request) -> Response:\n        \"\"\"Show login form.\"\"\"\n        state = request.query_params.get(\"state\")\n        if not state:\n            raise HTTPException(400, \"Missing state parameter\")\n        return await oauth_provider.get_login_page(state)\n\n    @app.custom_route(\"/login/callback\", methods=[\"POST\"])\n    async def login_callback_handler(request: Request) -> Response:\n        \"\"\"Handle simple authentication callback.\"\"\"\n        return await oauth_provider.handle_login_callback(request)\n\n    @app.tool()\n    async def get_time() -> dict[str, Any]:\n        \"\"\"Get the current server time.\n\n        This tool demonstrates that system information can be protected\n        by OAuth authentication. User must be authenticated to access it.\n        \"\"\"\n\n        now = datetime.datetime.now()\n\n        return {\n            \"current_time\": now.isoformat(),\n            \"timezone\": \"UTC\",  # Simplified for demo\n            \"timestamp\": now.timestamp(),\n            \"formatted\": now.strftime(\"%Y-%m-%d %H:%M:%S\"),\n        }\n\n    return app\n\n\n@click.command()\n@click.option(\"--port\", default=8000, help=\"Port to listen on\")\n@click.option(\n    \"--transport\",\n    default=\"streamable-http\",\n    type=click.Choice([\"sse\", \"streamable-http\"]),\n    help=\"Transport protocol to use ('sse' or 'streamable-http')\",\n)\ndef main(port: int, transport: Literal[\"sse\", \"streamable-http\"]) -> int:\n    \"\"\"Run the simple auth MCP server.\"\"\"\n    logging.basicConfig(level=logging.INFO)\n\n    auth_settings = SimpleAuthSettings()\n    # Create server settings\n    host = \"localhost\"\n    server_url = f\"http://{host}:{port}\"\n    server_settings = ServerSettings(\n        host=host,\n        port=port,\n        server_url=AnyHttpUrl(server_url),\n        auth_callback_path=f\"{server_url}/login\",\n    )\n\n    mcp_server = create_simple_mcp_server(server_settings, auth_settings)\n    logger.info(f\"🚀 MCP Legacy Server running on {server_url}\")\n    mcp_server.run(transport=transport, host=host, port=port)\n    return 0\n\n\nif __name__ == \"__main__\":\n    main()  # type: ignore[call-arg]\n"
  },
  {
    "path": "examples/servers/simple-auth/mcp_simple_auth/py.typed",
    "content": ""
  },
  {
    "path": "examples/servers/simple-auth/mcp_simple_auth/server.py",
    "content": "\"\"\"MCP Resource Server with Token Introspection.\n\nThis server validates tokens via Authorization Server introspection and serves MCP resources.\nDemonstrates RFC 9728 Protected Resource Metadata for AS/RS separation.\n\nNOTE: this is a simplified example for demonstration purposes.\nThis is not a production-ready implementation.\n\"\"\"\n\nimport datetime\nimport logging\nfrom typing import Any, Literal\n\nimport click\nfrom pydantic import AnyHttpUrl\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\nfrom mcp.server.auth.settings import AuthSettings\nfrom mcp.server.mcpserver.server import MCPServer\n\nfrom .token_verifier import IntrospectionTokenVerifier\n\nlogger = logging.getLogger(__name__)\n\n\nclass ResourceServerSettings(BaseSettings):\n    \"\"\"Settings for the MCP Resource Server.\"\"\"\n\n    model_config = SettingsConfigDict(env_prefix=\"MCP_RESOURCE_\")\n\n    # Server settings\n    host: str = \"localhost\"\n    port: int = 8001\n    server_url: AnyHttpUrl = AnyHttpUrl(\"http://localhost:8001/mcp\")\n\n    # Authorization Server settings\n    auth_server_url: AnyHttpUrl = AnyHttpUrl(\"http://localhost:9000\")\n    auth_server_introspection_endpoint: str = \"http://localhost:9000/introspect\"\n    # No user endpoint needed - we get user data from token introspection\n\n    # MCP settings\n    mcp_scope: str = \"user\"\n\n    # RFC 8707 resource validation\n    oauth_strict: bool = False\n\n\ndef create_resource_server(settings: ResourceServerSettings) -> MCPServer:\n    \"\"\"Create MCP Resource Server with token introspection.\n\n    This server:\n    1. Provides protected resource metadata (RFC 9728)\n    2. Validates tokens via Authorization Server introspection\n    3. Serves MCP tools and resources\n    \"\"\"\n    # Create token verifier for introspection with RFC 8707 resource validation\n    token_verifier = IntrospectionTokenVerifier(\n        introspection_endpoint=settings.auth_server_introspection_endpoint,\n        server_url=str(settings.server_url),\n        validate_resource=settings.oauth_strict,  # Only validate when --oauth-strict is set\n    )\n\n    # Create MCPServer server as a Resource Server\n    app = MCPServer(\n        name=\"MCP Resource Server\",\n        instructions=\"Resource Server that validates tokens via Authorization Server introspection\",\n        debug=True,\n        # Auth configuration for RS mode\n        token_verifier=token_verifier,\n        auth=AuthSettings(\n            issuer_url=settings.auth_server_url,\n            required_scopes=[settings.mcp_scope],\n            resource_server_url=settings.server_url,\n        ),\n    )\n    # Store settings for later use in run()\n    app._resource_server_settings = settings  # type: ignore[attr-defined]\n\n    @app.tool()\n    async def get_time() -> dict[str, Any]:\n        \"\"\"Get the current server time.\n\n        This tool demonstrates that system information can be protected\n        by OAuth authentication. User must be authenticated to access it.\n        \"\"\"\n\n        now = datetime.datetime.now()\n\n        return {\n            \"current_time\": now.isoformat(),\n            \"timezone\": \"UTC\",  # Simplified for demo\n            \"timestamp\": now.timestamp(),\n            \"formatted\": now.strftime(\"%Y-%m-%d %H:%M:%S\"),\n        }\n\n    return app\n\n\n@click.command()\n@click.option(\"--port\", default=8001, help=\"Port to listen on\")\n@click.option(\"--auth-server\", default=\"http://localhost:9000\", help=\"Authorization Server URL\")\n@click.option(\n    \"--transport\",\n    default=\"streamable-http\",\n    type=click.Choice([\"sse\", \"streamable-http\"]),\n    help=\"Transport protocol to use ('sse' or 'streamable-http')\",\n)\n@click.option(\n    \"--oauth-strict\",\n    is_flag=True,\n    help=\"Enable RFC 8707 resource validation\",\n)\ndef main(port: int, auth_server: str, transport: Literal[\"sse\", \"streamable-http\"], oauth_strict: bool) -> int:\n    \"\"\"Run the MCP Resource Server.\n\n    This server:\n    - Provides RFC 9728 Protected Resource Metadata\n    - Validates tokens via Authorization Server introspection\n    - Serves MCP tools requiring authentication\n\n    Must be used with a running Authorization Server.\n    \"\"\"\n    logging.basicConfig(level=logging.INFO)\n\n    try:\n        # Parse auth server URL\n        auth_server_url = AnyHttpUrl(auth_server)\n\n        # Create settings\n        host = \"localhost\"\n        server_url = f\"http://{host}:{port}/mcp\"\n        settings = ResourceServerSettings(\n            host=host,\n            port=port,\n            server_url=AnyHttpUrl(server_url),\n            auth_server_url=auth_server_url,\n            auth_server_introspection_endpoint=f\"{auth_server}/introspect\",\n            oauth_strict=oauth_strict,\n        )\n    except ValueError as e:\n        logger.error(f\"Configuration error: {e}\")\n        logger.error(\"Make sure to provide a valid Authorization Server URL\")\n        return 1\n\n    try:\n        mcp_server = create_resource_server(settings)\n\n        logger.info(f\"🚀 MCP Resource Server running on {settings.server_url}\")\n        logger.info(f\"🔑 Using Authorization Server: {settings.auth_server_url}\")\n\n        # Run the server - this should block and keep running\n        mcp_server.run(transport=transport, host=host, port=port)\n        logger.info(\"Server stopped\")\n        return 0\n    except Exception:\n        logger.exception(\"Server error\")\n        return 1\n\n\nif __name__ == \"__main__\":\n    main()  # type: ignore[call-arg]\n"
  },
  {
    "path": "examples/servers/simple-auth/mcp_simple_auth/simple_auth_provider.py",
    "content": "\"\"\"Simple OAuth provider for MCP servers.\n\nThis module contains a basic OAuth implementation using hardcoded user credentials\nfor demonstration purposes. No external authentication provider is required.\n\nNOTE: this is a simplified example for demonstration purposes.\nThis is not a production-ready implementation.\n\n\"\"\"\n\nimport secrets\nimport time\nfrom typing import Any\n\nfrom pydantic import AnyHttpUrl\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom starlette.exceptions import HTTPException\nfrom starlette.requests import Request\nfrom starlette.responses import HTMLResponse, RedirectResponse, Response\n\nfrom mcp.server.auth.provider import (\n    AccessToken,\n    AuthorizationCode,\n    AuthorizationParams,\n    OAuthAuthorizationServerProvider,\n    RefreshToken,\n    construct_redirect_uri,\n)\nfrom mcp.shared.auth import OAuthClientInformationFull, OAuthToken\n\n\nclass SimpleAuthSettings(BaseSettings):\n    \"\"\"Simple OAuth settings for demo purposes.\"\"\"\n\n    model_config = SettingsConfigDict(env_prefix=\"MCP_\")\n\n    # Demo user credentials\n    demo_username: str = \"demo_user\"\n    demo_password: str = \"demo_password\"\n\n    # MCP OAuth scope\n    mcp_scope: str = \"user\"\n\n\nclass SimpleOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]):\n    \"\"\"Simple OAuth provider for demo purposes.\n\n    This provider handles the OAuth flow by:\n    1. Providing a simple login form for demo credentials\n    2. Issuing MCP tokens after successful authentication\n    3. Maintaining token state for introspection\n    \"\"\"\n\n    def __init__(self, settings: SimpleAuthSettings, auth_callback_url: str, server_url: str):\n        self.settings = settings\n        self.auth_callback_url = auth_callback_url\n        self.server_url = server_url\n        self.clients: dict[str, OAuthClientInformationFull] = {}\n        self.auth_codes: dict[str, AuthorizationCode] = {}\n        self.tokens: dict[str, AccessToken] = {}\n        self.state_mapping: dict[str, dict[str, str | None]] = {}\n        # Store authenticated user information\n        self.user_data: dict[str, dict[str, Any]] = {}\n\n    async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:\n        \"\"\"Get OAuth client information.\"\"\"\n        return self.clients.get(client_id)\n\n    async def register_client(self, client_info: OAuthClientInformationFull):\n        \"\"\"Register a new OAuth client.\"\"\"\n        if not client_info.client_id:\n            raise ValueError(\"No client_id provided\")\n        self.clients[client_info.client_id] = client_info\n\n    async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:\n        \"\"\"Generate an authorization URL for simple login flow.\"\"\"\n        state = params.state or secrets.token_hex(16)\n\n        # Store state mapping for callback\n        self.state_mapping[state] = {\n            \"redirect_uri\": str(params.redirect_uri),\n            \"code_challenge\": params.code_challenge,\n            \"redirect_uri_provided_explicitly\": str(params.redirect_uri_provided_explicitly),\n            \"client_id\": client.client_id,\n            \"resource\": params.resource,  # RFC 8707\n        }\n\n        # Build simple login URL that points to login page\n        auth_url = f\"{self.auth_callback_url}?state={state}&client_id={client.client_id}\"\n\n        return auth_url\n\n    async def get_login_page(self, state: str) -> HTMLResponse:\n        \"\"\"Generate login page HTML for the given state.\"\"\"\n        if not state:\n            raise HTTPException(400, \"Missing state parameter\")\n\n        # Create simple login form HTML\n        html_content = f\"\"\"\n        <!DOCTYPE html>\n        <html>\n        <head>\n            <title>MCP Demo Authentication</title>\n            <style>\n                body {{ font-family: Arial, sans-serif; max-width: 500px; margin: 0 auto; padding: 20px; }}\n                .form-group {{ margin-bottom: 15px; }}\n                input {{ width: 100%; padding: 8px; margin-top: 5px; }}\n                button {{ background-color: #4CAF50; color: white; padding: 10px 15px; border: none; cursor: pointer; }}\n            </style>\n        </head>\n        <body>\n            <h2>MCP Demo Authentication</h2>\n            <p>This is a simplified authentication demo. Use the demo credentials below:</p>\n            <p><strong>Username:</strong> demo_user<br>\n            <strong>Password:</strong> demo_password</p>\n\n            <form action=\"{self.server_url.rstrip(\"/\")}/login/callback\" method=\"post\">\n                <input type=\"hidden\" name=\"state\" value=\"{state}\">\n                <div class=\"form-group\">\n                    <label>Username:</label>\n                    <input type=\"text\" name=\"username\" value=\"demo_user\" required>\n                </div>\n                <div class=\"form-group\">\n                    <label>Password:</label>\n                    <input type=\"password\" name=\"password\" value=\"demo_password\" required>\n                </div>\n                <button type=\"submit\">Sign In</button>\n            </form>\n        </body>\n        </html>\n        \"\"\"\n\n        return HTMLResponse(content=html_content)\n\n    async def handle_login_callback(self, request: Request) -> Response:\n        \"\"\"Handle login form submission callback.\"\"\"\n        form = await request.form()\n        username = form.get(\"username\")\n        password = form.get(\"password\")\n        state = form.get(\"state\")\n\n        if not username or not password or not state:\n            raise HTTPException(400, \"Missing username, password, or state parameter\")\n\n        # Ensure we have strings, not UploadFile objects\n        if not isinstance(username, str) or not isinstance(password, str) or not isinstance(state, str):\n            raise HTTPException(400, \"Invalid parameter types\")\n\n        redirect_uri = await self.handle_simple_callback(username, password, state)\n        return RedirectResponse(url=redirect_uri, status_code=302)\n\n    async def handle_simple_callback(self, username: str, password: str, state: str) -> str:\n        \"\"\"Handle simple authentication callback and return redirect URI.\"\"\"\n        state_data = self.state_mapping.get(state)\n        if not state_data:\n            raise HTTPException(400, \"Invalid state parameter\")\n\n        redirect_uri = state_data[\"redirect_uri\"]\n        code_challenge = state_data[\"code_challenge\"]\n        redirect_uri_provided_explicitly = state_data[\"redirect_uri_provided_explicitly\"] == \"True\"\n        client_id = state_data[\"client_id\"]\n        resource = state_data.get(\"resource\")  # RFC 8707\n\n        # These are required values from our own state mapping\n        assert redirect_uri is not None\n        assert code_challenge is not None\n        assert client_id is not None\n\n        # Validate demo credentials\n        if username != self.settings.demo_username or password != self.settings.demo_password:\n            raise HTTPException(401, \"Invalid credentials\")\n\n        # Create MCP authorization code\n        new_code = f\"mcp_{secrets.token_hex(16)}\"\n        auth_code = AuthorizationCode(\n            code=new_code,\n            client_id=client_id,\n            redirect_uri=AnyHttpUrl(redirect_uri),\n            redirect_uri_provided_explicitly=redirect_uri_provided_explicitly,\n            expires_at=time.time() + 300,\n            scopes=[self.settings.mcp_scope],\n            code_challenge=code_challenge,\n            resource=resource,  # RFC 8707\n        )\n        self.auth_codes[new_code] = auth_code\n\n        # Store user data\n        self.user_data[username] = {\n            \"username\": username,\n            \"user_id\": f\"user_{secrets.token_hex(8)}\",\n            \"authenticated_at\": time.time(),\n        }\n\n        del self.state_mapping[state]\n        return construct_redirect_uri(redirect_uri, code=new_code, state=state)\n\n    async def load_authorization_code(\n        self, client: OAuthClientInformationFull, authorization_code: str\n    ) -> AuthorizationCode | None:\n        \"\"\"Load an authorization code.\"\"\"\n        return self.auth_codes.get(authorization_code)\n\n    async def exchange_authorization_code(\n        self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode\n    ) -> OAuthToken:\n        \"\"\"Exchange authorization code for tokens.\"\"\"\n        if authorization_code.code not in self.auth_codes:\n            raise ValueError(\"Invalid authorization code\")\n        if not client.client_id:\n            raise ValueError(\"No client_id provided\")\n\n        # Generate MCP access token\n        mcp_token = f\"mcp_{secrets.token_hex(32)}\"\n\n        # Store MCP token\n        self.tokens[mcp_token] = AccessToken(\n            token=mcp_token,\n            client_id=client.client_id,\n            scopes=authorization_code.scopes,\n            expires_at=int(time.time()) + 3600,\n            resource=authorization_code.resource,  # RFC 8707\n        )\n\n        # Store user data mapping for this token\n        self.user_data[mcp_token] = {\n            \"username\": self.settings.demo_username,\n            \"user_id\": f\"user_{secrets.token_hex(8)}\",\n            \"authenticated_at\": time.time(),\n        }\n\n        del self.auth_codes[authorization_code.code]\n\n        return OAuthToken(\n            access_token=mcp_token,\n            token_type=\"Bearer\",\n            expires_in=3600,\n            scope=\" \".join(authorization_code.scopes),\n        )\n\n    async def load_access_token(self, token: str) -> AccessToken | None:\n        \"\"\"Load and validate an access token.\"\"\"\n        access_token = self.tokens.get(token)\n        if not access_token:\n            return None\n\n        # Check if expired\n        if access_token.expires_at and access_token.expires_at < time.time():\n            del self.tokens[token]\n            return None\n\n        return access_token\n\n    async def load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None:\n        \"\"\"Load a refresh token - not supported in this example.\"\"\"\n        return None\n\n    async def exchange_refresh_token(\n        self,\n        client: OAuthClientInformationFull,\n        refresh_token: RefreshToken,\n        scopes: list[str],\n    ) -> OAuthToken:\n        \"\"\"Exchange refresh token - not supported in this example.\"\"\"\n        raise NotImplementedError(\"Refresh tokens not supported\")\n\n    # TODO(Marcelo): The type hint is wrong. We need to fix, and test to check if it works.\n    async def revoke_token(self, token: str, token_type_hint: str | None = None) -> None:  # type: ignore\n        \"\"\"Revoke a token.\"\"\"\n        if token in self.tokens:\n            del self.tokens[token]\n"
  },
  {
    "path": "examples/servers/simple-auth/mcp_simple_auth/token_verifier.py",
    "content": "\"\"\"Example token verifier implementation using OAuth 2.0 Token Introspection (RFC 7662).\"\"\"\n\nimport logging\nfrom typing import Any\n\nfrom mcp.server.auth.provider import AccessToken, TokenVerifier\nfrom mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url\n\nlogger = logging.getLogger(__name__)\n\n\nclass IntrospectionTokenVerifier(TokenVerifier):\n    \"\"\"Example token verifier that uses OAuth 2.0 Token Introspection (RFC 7662).\n\n    This is a simple example implementation for demonstration purposes.\n    Production implementations should consider:\n    - Connection pooling and reuse\n    - More sophisticated error handling\n    - Rate limiting and retry logic\n    - Comprehensive configuration options\n    \"\"\"\n\n    def __init__(\n        self,\n        introspection_endpoint: str,\n        server_url: str,\n        validate_resource: bool = False,\n    ):\n        self.introspection_endpoint = introspection_endpoint\n        self.server_url = server_url\n        self.validate_resource = validate_resource\n        self.resource_url = resource_url_from_server_url(server_url)\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Verify token via introspection endpoint.\"\"\"\n        import httpx\n\n        # Validate URL to prevent SSRF attacks\n        if not self.introspection_endpoint.startswith((\"https://\", \"http://localhost\", \"http://127.0.0.1\")):\n            logger.warning(f\"Rejecting introspection endpoint with unsafe scheme: {self.introspection_endpoint}\")\n            return None\n\n        # Configure secure HTTP client\n        timeout = httpx.Timeout(10.0, connect=5.0)\n        limits = httpx.Limits(max_connections=10, max_keepalive_connections=5)\n\n        async with httpx.AsyncClient(\n            timeout=timeout,\n            limits=limits,\n            verify=True,  # Enforce SSL verification\n        ) as client:\n            try:\n                response = await client.post(\n                    self.introspection_endpoint,\n                    data={\"token\": token},\n                    headers={\"Content-Type\": \"application/x-www-form-urlencoded\"},\n                )\n\n                if response.status_code != 200:\n                    logger.debug(f\"Token introspection returned status {response.status_code}\")\n                    return None\n\n                data = response.json()\n                if not data.get(\"active\", False):\n                    return None\n\n                # RFC 8707 resource validation (only when --oauth-strict is set)\n                if self.validate_resource and not self._validate_resource(data):\n                    logger.warning(f\"Token resource validation failed. Expected: {self.resource_url}\")\n                    return None\n\n                return AccessToken(\n                    token=token,\n                    client_id=data.get(\"client_id\", \"unknown\"),\n                    scopes=data.get(\"scope\", \"\").split() if data.get(\"scope\") else [],\n                    expires_at=data.get(\"exp\"),\n                    resource=data.get(\"aud\"),  # Include resource in token\n                )\n            except Exception as e:\n                logger.warning(f\"Token introspection failed: {e}\")\n                return None\n\n    def _validate_resource(self, token_data: dict[str, Any]) -> bool:\n        \"\"\"Validate token was issued for this resource server.\"\"\"\n        if not self.server_url or not self.resource_url:\n            return False  # Fail if strict validation requested but URLs missing\n\n        # Check 'aud' claim first (standard JWT audience)\n        aud: list[str] | str | None = token_data.get(\"aud\")\n        if isinstance(aud, list):\n            for audience in aud:\n                if self._is_valid_resource(audience):\n                    return True\n            return False\n        elif aud:\n            return self._is_valid_resource(aud)\n\n        # No resource binding - invalid per RFC 8707\n        return False\n\n    def _is_valid_resource(self, resource: str) -> bool:\n        \"\"\"Check if resource matches this server using hierarchical matching.\"\"\"\n        if not self.resource_url:\n            return False\n\n        return check_resource_allowed(requested_resource=self.resource_url, configured_resource=resource)\n"
  },
  {
    "path": "examples/servers/simple-auth/pyproject.toml",
    "content": "[project]\nname = \"mcp-simple-auth\"\nversion = \"0.1.0\"\ndescription = \"A simple MCP server demonstrating OAuth authentication\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nlicense = { text = \"MIT\" }\ndependencies = [\n    \"anyio>=4.5\",\n    \"click>=8.2.0\",\n    \"httpx>=0.27\",\n    \"mcp\",\n    \"pydantic>=2.0\",\n    \"pydantic-settings>=2.5.2\",\n    \"sse-starlette>=1.6.1\",\n    \"uvicorn>=0.23.1; sys_platform != 'emscripten'\",\n]\n\n[project.scripts]\nmcp-simple-auth-rs = \"mcp_simple_auth.server:main\"\nmcp-simple-auth-as = \"mcp_simple_auth.auth_server:main\"\nmcp-simple-auth-legacy = \"mcp_simple_auth.legacy_as_server:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_simple_auth\"]\n\n[dependency-groups]\ndev = [\"pyright>=1.1.391\", \"pytest>=8.3.4\", \"ruff>=0.8.5\"]\n"
  },
  {
    "path": "examples/servers/simple-pagination/README.md",
    "content": "# MCP Simple Pagination\n\nA simple MCP server demonstrating pagination for tools, resources, and prompts using cursor-based pagination.\n\n## Usage\n\nStart the server using either stdio (default) or Streamable HTTP transport:\n\n```bash\n# Using stdio transport (default)\nuv run mcp-simple-pagination\n\n# Using Streamable HTTP transport on custom port\nuv run mcp-simple-pagination --transport streamable-http --port 8000\n```\n\nThe server exposes:\n\n- 25 tools (paginated, 5 per page)\n- 30 resources (paginated, 10 per page)\n- 20 prompts (paginated, 7 per page)\n\nEach paginated list returns a `nextCursor` when more pages are available. Use this cursor in subsequent requests to retrieve the next page.\n\n## Example\n\nUsing the MCP client, you can retrieve paginated items like this using the STDIO transport:\n\n```python\nimport asyncio\nfrom mcp.client.session import ClientSession\nfrom mcp.client.stdio import StdioServerParameters, stdio_client\n\n\nasync def main():\n    async with stdio_client(\n        StdioServerParameters(command=\"uv\", args=[\"run\", \"mcp-simple-pagination\"])\n    ) as (read, write):\n        async with ClientSession(read, write) as session:\n            await session.initialize()\n\n            # Get first page of tools\n            tools_page1 = await session.list_tools()\n            print(f\"First page: {len(tools_page1.tools)} tools\")\n            print(f\"Next cursor: {tools_page1.nextCursor}\")\n\n            # Get second page using cursor\n            if tools_page1.nextCursor:\n                tools_page2 = await session.list_tools(cursor=tools_page1.nextCursor)\n                print(f\"Second page: {len(tools_page2.tools)} tools\")\n\n            # Similarly for resources\n            resources_page1 = await session.list_resources()\n            print(f\"First page: {len(resources_page1.resources)} resources\")\n\n            # And for prompts\n            prompts_page1 = await session.list_prompts()\n            print(f\"First page: {len(prompts_page1.prompts)} prompts\")\n\n\nasyncio.run(main())\n```\n\n## Pagination Details\n\nThe server uses simple numeric indices as cursors for demonstration purposes. In production scenarios, you might use:\n\n- Database offsets or row IDs\n- Timestamps for time-based pagination\n- Opaque tokens encoding pagination state\n\nThe pagination implementation demonstrates:\n\n- Handling `None` cursor for the first page\n- Returning `nextCursor` when more data exists\n- Gracefully handling invalid cursors\n- Different page sizes for different resource types\n"
  },
  {
    "path": "examples/servers/simple-pagination/mcp_simple_pagination/__init__.py",
    "content": ""
  },
  {
    "path": "examples/servers/simple-pagination/mcp_simple_pagination/__main__.py",
    "content": "import sys\n\nfrom .server import main\n\nsys.exit(main())  # type: ignore[call-arg]\n"
  },
  {
    "path": "examples/servers/simple-pagination/mcp_simple_pagination/server.py",
    "content": "\"\"\"Simple MCP server demonstrating pagination for tools, resources, and prompts.\n\nThis example shows how to implement pagination with the low-level server API\nto handle large lists of items that need to be split across multiple pages.\n\"\"\"\n\nfrom typing import TypeVar\n\nimport anyio\nimport click\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\n\nT = TypeVar(\"T\")\n\n# Sample data - in real scenarios, this might come from a database\nSAMPLE_TOOLS = [\n    types.Tool(\n        name=f\"tool_{i}\",\n        title=f\"Tool {i}\",\n        description=f\"This is sample tool number {i}\",\n        input_schema={\"type\": \"object\", \"properties\": {\"input\": {\"type\": \"string\"}}},\n    )\n    for i in range(1, 26)  # 25 tools total\n]\n\nSAMPLE_RESOURCES = [\n    types.Resource(\n        uri=f\"file:///path/to/resource_{i}.txt\",\n        name=f\"resource_{i}\",\n        description=f\"This is sample resource number {i}\",\n    )\n    for i in range(1, 31)  # 30 resources total\n]\n\nSAMPLE_PROMPTS = [\n    types.Prompt(\n        name=f\"prompt_{i}\",\n        description=f\"This is sample prompt number {i}\",\n        arguments=[\n            types.PromptArgument(name=\"arg1\", description=\"First argument\", required=True),\n        ],\n    )\n    for i in range(1, 21)  # 20 prompts total\n]\n\n\ndef _paginate(cursor: str | None, items: list[T], page_size: int) -> tuple[list[T], str | None]:\n    \"\"\"Helper to paginate a list of items given a cursor.\"\"\"\n    if cursor is not None:\n        try:\n            start_idx = int(cursor)\n        except (ValueError, TypeError):\n            return [], None\n    else:\n        start_idx = 0\n\n    page = items[start_idx : start_idx + page_size]\n    next_cursor = str(start_idx + page_size) if start_idx + page_size < len(items) else None\n    return page, next_cursor\n\n\n# Paginated list_tools - returns 5 tools per page\nasync def handle_list_tools(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListToolsResult:\n    cursor = params.cursor if params is not None else None\n    page, next_cursor = _paginate(cursor, SAMPLE_TOOLS, page_size=5)\n    return types.ListToolsResult(tools=page, next_cursor=next_cursor)\n\n\n# Paginated list_resources - returns 10 resources per page\nasync def handle_list_resources(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListResourcesResult:\n    cursor = params.cursor if params is not None else None\n    page, next_cursor = _paginate(cursor, SAMPLE_RESOURCES, page_size=10)\n    return types.ListResourcesResult(resources=page, next_cursor=next_cursor)\n\n\n# Paginated list_prompts - returns 7 prompts per page\nasync def handle_list_prompts(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListPromptsResult:\n    cursor = params.cursor if params is not None else None\n    page, next_cursor = _paginate(cursor, SAMPLE_PROMPTS, page_size=7)\n    return types.ListPromptsResult(prompts=page, next_cursor=next_cursor)\n\n\nasync def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:\n    # Find the tool in our sample data\n    tool = next((t for t in SAMPLE_TOOLS if t.name == params.name), None)\n    if not tool:\n        raise ValueError(f\"Unknown tool: {params.name}\")\n\n    return types.CallToolResult(\n        content=[\n            types.TextContent(\n                type=\"text\",\n                text=f\"Called tool '{params.name}' with arguments: {params.arguments}\",\n            )\n        ]\n    )\n\n\nasync def handle_read_resource(\n    ctx: ServerRequestContext, params: types.ReadResourceRequestParams\n) -> types.ReadResourceResult:\n    resource = next((r for r in SAMPLE_RESOURCES if r.uri == str(params.uri)), None)\n    if not resource:\n        raise ValueError(f\"Unknown resource: {params.uri}\")\n\n    return types.ReadResourceResult(\n        contents=[\n            types.TextResourceContents(\n                uri=str(params.uri),\n                text=f\"Content of {resource.name}: This is sample content for the resource.\",\n                mime_type=\"text/plain\",\n            )\n        ]\n    )\n\n\nasync def handle_get_prompt(ctx: ServerRequestContext, params: types.GetPromptRequestParams) -> types.GetPromptResult:\n    prompt = next((p for p in SAMPLE_PROMPTS if p.name == params.name), None)\n    if not prompt:\n        raise ValueError(f\"Unknown prompt: {params.name}\")\n\n    message_text = f\"This is the prompt '{params.name}'\"\n    if params.arguments:\n        message_text += f\" with arguments: {params.arguments}\"\n\n    return types.GetPromptResult(\n        description=prompt.description,\n        messages=[\n            types.PromptMessage(\n                role=\"user\",\n                content=types.TextContent(type=\"text\", text=message_text),\n            )\n        ],\n    )\n\n\n@click.command()\n@click.option(\"--port\", default=8000, help=\"Port to listen on for HTTP\")\n@click.option(\n    \"--transport\",\n    type=click.Choice([\"stdio\", \"streamable-http\"]),\n    default=\"stdio\",\n    help=\"Transport type\",\n)\ndef main(port: int, transport: str) -> int:\n    app = Server(\n        \"mcp-simple-pagination\",\n        on_list_tools=handle_list_tools,\n        on_list_resources=handle_list_resources,\n        on_list_prompts=handle_list_prompts,\n        on_call_tool=handle_call_tool,\n        on_read_resource=handle_read_resource,\n        on_get_prompt=handle_get_prompt,\n    )\n\n    if transport == \"streamable-http\":\n        import uvicorn\n\n        uvicorn.run(app.streamable_http_app(), host=\"127.0.0.1\", port=port)\n    else:\n        from mcp.server.stdio import stdio_server\n\n        async def arun():\n            async with stdio_server() as streams:\n                await app.run(streams[0], streams[1], app.create_initialization_options())\n\n        anyio.run(arun)\n\n    return 0\n"
  },
  {
    "path": "examples/servers/simple-pagination/pyproject.toml",
    "content": "[project]\nname = \"mcp-simple-pagination\"\nversion = \"0.1.0\"\ndescription = \"A simple MCP server demonstrating pagination for tools, resources, and prompts\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nkeywords = [\"mcp\", \"llm\", \"automation\", \"pagination\", \"cursor\"]\nlicense = { text = \"MIT\" }\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n]\ndependencies = [\"anyio>=4.5\", \"click>=8.2.0\", \"httpx>=0.27\", \"mcp\"]\n\n[project.scripts]\nmcp-simple-pagination = \"mcp_simple_pagination.server:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_simple_pagination\"]\n\n[tool.pyright]\ninclude = [\"mcp_simple_pagination\"]\nvenvPath = \".\"\nvenv = \".venv\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\"]\nignore = []\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n[dependency-groups]\ndev = [\"pyright>=1.1.378\", \"pytest>=8.3.3\", \"ruff>=0.6.9\"]\n"
  },
  {
    "path": "examples/servers/simple-prompt/README.md",
    "content": "# MCP Simple Prompt\n\nA simple MCP server that exposes a customizable prompt template with optional context and topic parameters.\n\n## Usage\n\nStart the server using either stdio (default) or Streamable HTTP transport:\n\n```bash\n# Using stdio transport (default)\nuv run mcp-simple-prompt\n\n# Using Streamable HTTP transport on custom port\nuv run mcp-simple-prompt --transport streamable-http --port 8000\n```\n\nThe server exposes a prompt named \"simple\" that accepts two optional arguments:\n\n- `context`: Additional context to consider\n- `topic`: Specific topic to focus on\n\n## Example\n\nUsing the MCP client, you can retrieve the prompt like this using the STDIO transport:\n\n```python\nimport asyncio\nfrom mcp.client.session import ClientSession\nfrom mcp.client.stdio import StdioServerParameters, stdio_client\n\n\nasync def main():\n    async with stdio_client(\n        StdioServerParameters(command=\"uv\", args=[\"run\", \"mcp-simple-prompt\"])\n    ) as (read, write):\n        async with ClientSession(read, write) as session:\n            await session.initialize()\n\n            # List available prompts\n            prompts = await session.list_prompts()\n            print(prompts)\n\n            # Get the prompt with arguments\n            prompt = await session.get_prompt(\n                \"simple\",\n                {\n                    \"context\": \"User is a software developer\",\n                    \"topic\": \"Python async programming\",\n                },\n            )\n            print(prompt)\n\n\nasyncio.run(main())\n```\n"
  },
  {
    "path": "examples/servers/simple-prompt/mcp_simple_prompt/__init__.py",
    "content": ""
  },
  {
    "path": "examples/servers/simple-prompt/mcp_simple_prompt/__main__.py",
    "content": "import sys\n\nfrom .server import main\n\nsys.exit(main())  # type: ignore[call-arg]\n"
  },
  {
    "path": "examples/servers/simple-prompt/mcp_simple_prompt/server.py",
    "content": "import anyio\nimport click\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\n\n\ndef create_messages(context: str | None = None, topic: str | None = None) -> list[types.PromptMessage]:\n    \"\"\"Create the messages for the prompt.\"\"\"\n    messages: list[types.PromptMessage] = []\n\n    # Add context if provided\n    if context:\n        messages.append(\n            types.PromptMessage(\n                role=\"user\",\n                content=types.TextContent(type=\"text\", text=f\"Here is some relevant context: {context}\"),\n            )\n        )\n\n    # Add the main prompt\n    prompt = \"Please help me with \"\n    if topic:\n        prompt += f\"the following topic: {topic}\"\n    else:\n        prompt += \"whatever questions I may have.\"\n\n    messages.append(types.PromptMessage(role=\"user\", content=types.TextContent(type=\"text\", text=prompt)))\n\n    return messages\n\n\nasync def handle_list_prompts(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListPromptsResult:\n    return types.ListPromptsResult(\n        prompts=[\n            types.Prompt(\n                name=\"simple\",\n                title=\"Simple Assistant Prompt\",\n                description=\"A simple prompt that can take optional context and topic arguments\",\n                arguments=[\n                    types.PromptArgument(\n                        name=\"context\",\n                        description=\"Additional context to consider\",\n                        required=False,\n                    ),\n                    types.PromptArgument(\n                        name=\"topic\",\n                        description=\"Specific topic to focus on\",\n                        required=False,\n                    ),\n                ],\n            )\n        ]\n    )\n\n\nasync def handle_get_prompt(ctx: ServerRequestContext, params: types.GetPromptRequestParams) -> types.GetPromptResult:\n    if params.name != \"simple\":\n        raise ValueError(f\"Unknown prompt: {params.name}\")\n\n    arguments = params.arguments or {}\n\n    return types.GetPromptResult(\n        messages=create_messages(context=arguments.get(\"context\"), topic=arguments.get(\"topic\")),\n        description=\"A simple prompt with optional context and topic arguments\",\n    )\n\n\n@click.command()\n@click.option(\"--port\", default=8000, help=\"Port to listen on for HTTP\")\n@click.option(\n    \"--transport\",\n    type=click.Choice([\"stdio\", \"streamable-http\"]),\n    default=\"stdio\",\n    help=\"Transport type\",\n)\ndef main(port: int, transport: str) -> int:\n    app = Server(\n        \"mcp-simple-prompt\",\n        on_list_prompts=handle_list_prompts,\n        on_get_prompt=handle_get_prompt,\n    )\n\n    if transport == \"streamable-http\":\n        import uvicorn\n\n        uvicorn.run(app.streamable_http_app(), host=\"127.0.0.1\", port=port)\n    else:\n        from mcp.server.stdio import stdio_server\n\n        async def arun():\n            async with stdio_server() as streams:\n                await app.run(streams[0], streams[1], app.create_initialization_options())\n\n        anyio.run(arun)\n\n    return 0\n"
  },
  {
    "path": "examples/servers/simple-prompt/pyproject.toml",
    "content": "[project]\nname = \"mcp-simple-prompt\"\nversion = \"0.1.0\"\ndescription = \"A simple MCP server exposing a customizable prompt\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nkeywords = [\"mcp\", \"llm\", \"automation\", \"web\", \"fetch\"]\nlicense = { text = \"MIT\" }\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n]\ndependencies = [\"anyio>=4.5\", \"click>=8.2.0\", \"httpx>=0.27\", \"mcp\"]\n\n[project.scripts]\nmcp-simple-prompt = \"mcp_simple_prompt.server:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_simple_prompt\"]\n\n[tool.pyright]\ninclude = [\"mcp_simple_prompt\"]\nvenvPath = \".\"\nvenv = \".venv\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\"]\nignore = []\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n[dependency-groups]\ndev = [\"pyright>=1.1.378\", \"pytest>=8.3.3\", \"ruff>=0.6.9\"]\n"
  },
  {
    "path": "examples/servers/simple-resource/README.md",
    "content": "# MCP Simple Resource\n\nA simple MCP server that exposes sample text files as resources.\n\n## Usage\n\nStart the server using either stdio (default) or Streamable HTTP transport:\n\n```bash\n# Using stdio transport (default)\nuv run mcp-simple-resource\n\n# Using Streamable HTTP transport on custom port\nuv run mcp-simple-resource --transport streamable-http --port 8000\n```\n\nThe server exposes some basic text file resources that can be read by clients.\n\n## Example\n\nUsing the MCP client, you can retrieve resources like this using the STDIO transport:\n\n```python\nimport asyncio\nfrom mcp.types import AnyUrl\nfrom mcp.client.session import ClientSession\nfrom mcp.client.stdio import StdioServerParameters, stdio_client\n\n\nasync def main():\n    async with stdio_client(\n        StdioServerParameters(command=\"uv\", args=[\"run\", \"mcp-simple-resource\"])\n    ) as (read, write):\n        async with ClientSession(read, write) as session:\n            await session.initialize()\n\n            # List available resources\n            resources = await session.list_resources()\n            print(resources)\n\n            # Get a specific resource\n            resource = await session.read_resource(AnyUrl(\"file:///greeting.txt\"))\n            print(resource)\n\n\nasyncio.run(main())\n\n```\n"
  },
  {
    "path": "examples/servers/simple-resource/mcp_simple_resource/__init__.py",
    "content": ""
  },
  {
    "path": "examples/servers/simple-resource/mcp_simple_resource/__main__.py",
    "content": "import sys\n\nfrom .server import main\n\nsys.exit(main())  # type: ignore[call-arg]\n"
  },
  {
    "path": "examples/servers/simple-resource/mcp_simple_resource/server.py",
    "content": "from urllib.parse import urlparse\n\nimport anyio\nimport click\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\n\nSAMPLE_RESOURCES = {\n    \"greeting\": {\n        \"content\": \"Hello! This is a sample text resource.\",\n        \"title\": \"Welcome Message\",\n    },\n    \"help\": {\n        \"content\": \"This server provides a few sample text resources for testing.\",\n        \"title\": \"Help Documentation\",\n    },\n    \"about\": {\n        \"content\": \"This is the simple-resource MCP server implementation.\",\n        \"title\": \"About This Server\",\n    },\n}\n\n\nasync def handle_list_resources(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListResourcesResult:\n    return types.ListResourcesResult(\n        resources=[\n            types.Resource(\n                uri=f\"file:///{name}.txt\",\n                name=name,\n                title=SAMPLE_RESOURCES[name][\"title\"],\n                description=f\"A sample text resource named {name}\",\n                mime_type=\"text/plain\",\n            )\n            for name in SAMPLE_RESOURCES.keys()\n        ]\n    )\n\n\nasync def handle_read_resource(\n    ctx: ServerRequestContext, params: types.ReadResourceRequestParams\n) -> types.ReadResourceResult:\n    parsed = urlparse(str(params.uri))\n    if not parsed.path:\n        raise ValueError(f\"Invalid resource path: {params.uri}\")\n    name = parsed.path.replace(\".txt\", \"\").lstrip(\"/\")\n\n    if name not in SAMPLE_RESOURCES:\n        raise ValueError(f\"Unknown resource: {params.uri}\")\n\n    return types.ReadResourceResult(\n        contents=[\n            types.TextResourceContents(\n                uri=str(params.uri),\n                text=SAMPLE_RESOURCES[name][\"content\"],\n                mime_type=\"text/plain\",\n            )\n        ]\n    )\n\n\n@click.command()\n@click.option(\"--port\", default=8000, help=\"Port to listen on for HTTP\")\n@click.option(\n    \"--transport\",\n    type=click.Choice([\"stdio\", \"streamable-http\"]),\n    default=\"stdio\",\n    help=\"Transport type\",\n)\ndef main(port: int, transport: str) -> int:\n    app = Server(\n        \"mcp-simple-resource\",\n        on_list_resources=handle_list_resources,\n        on_read_resource=handle_read_resource,\n    )\n\n    if transport == \"streamable-http\":\n        import uvicorn\n\n        uvicorn.run(app.streamable_http_app(), host=\"127.0.0.1\", port=port)\n    else:\n        from mcp.server.stdio import stdio_server\n\n        async def arun():\n            async with stdio_server() as streams:\n                await app.run(streams[0], streams[1], app.create_initialization_options())\n\n        anyio.run(arun)\n\n    return 0\n"
  },
  {
    "path": "examples/servers/simple-resource/pyproject.toml",
    "content": "[project]\nname = \"mcp-simple-resource\"\nversion = \"0.1.0\"\ndescription = \"A simple MCP server exposing sample text resources\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nkeywords = [\"mcp\", \"llm\", \"automation\", \"web\", \"fetch\"]\nlicense = { text = \"MIT\" }\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n]\ndependencies = [\"anyio>=4.5\", \"click>=8.2.0\", \"httpx>=0.27\", \"mcp\"]\n\n[project.scripts]\nmcp-simple-resource = \"mcp_simple_resource.server:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_simple_resource\"]\n\n[tool.pyright]\ninclude = [\"mcp_simple_resource\"]\nvenvPath = \".\"\nvenv = \".venv\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\"]\nignore = []\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n[dependency-groups]\ndev = [\"pyright>=1.1.378\", \"pytest>=8.3.3\", \"ruff>=0.6.9\"]\n"
  },
  {
    "path": "examples/servers/simple-streamablehttp/README.md",
    "content": "# MCP Simple StreamableHttp Server Example\n\nA simple MCP server example demonstrating the StreamableHttp transport, which enables HTTP-based communication with MCP servers using streaming.\n\n## Features\n\n- Uses the StreamableHTTP transport for server-client communication\n- Supports REST API operations (POST, GET, DELETE) for `/mcp` endpoint\n- Ability to send multiple notifications over time to the client\n- Resumability support via InMemoryEventStore\n\n## Usage\n\nStart the server on the default or custom port:\n\n```bash\n\n# Using custom port\nuv run mcp-simple-streamablehttp --port 3000\n\n# Custom logging level\nuv run mcp-simple-streamablehttp --log-level DEBUG\n\n# Enable JSON responses instead of SSE streams\nuv run mcp-simple-streamablehttp --json-response\n```\n\nThe server exposes a tool named \"start-notification-stream\" that accepts three arguments:\n\n- `interval`: Time between notifications in seconds (e.g., 1.0)\n- `count`: Number of notifications to send (e.g., 5)\n- `caller`: Identifier string for the caller\n\n## Resumability Support\n\nThis server includes resumability support through the InMemoryEventStore. This enables clients to:\n\n- Reconnect to the server after a disconnection\n- Resume event streaming from where they left off using the Last-Event-ID header\n\nThe server will:\n\n- Generate unique event IDs for each SSE message\n- Store events in memory for later replay\n- Replay missed events when a client reconnects with a Last-Event-ID header\n\nNote: The InMemoryEventStore is designed for demonstration purposes only. For production use, consider implementing a persistent storage solution.\n\n## Client\n\nYou can connect to this server using an HTTP client, for now only Typescript SDK has streamable HTTP client examples or you can use [Inspector](https://github.com/modelcontextprotocol/inspector)\n"
  },
  {
    "path": "examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/__init__.py",
    "content": ""
  },
  {
    "path": "examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/__main__.py",
    "content": "from .server import main\n\nif __name__ == \"__main__\":\n    main()  # type: ignore[call-arg]\n"
  },
  {
    "path": "examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/event_store.py",
    "content": "\"\"\"In-memory event store for demonstrating resumability functionality.\n\nThis is a simple implementation intended for examples and testing,\nnot for production use where a persistent storage solution would be more appropriate.\n\"\"\"\n\nimport logging\nfrom collections import deque\nfrom dataclasses import dataclass\nfrom uuid import uuid4\n\nfrom mcp.server.streamable_http import EventCallback, EventId, EventMessage, EventStore, StreamId\nfrom mcp.types import JSONRPCMessage\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass\nclass EventEntry:\n    \"\"\"Represents an event entry in the event store.\"\"\"\n\n    event_id: EventId\n    stream_id: StreamId\n    message: JSONRPCMessage | None\n\n\nclass InMemoryEventStore(EventStore):\n    \"\"\"Simple in-memory implementation of the EventStore interface for resumability.\n    This is primarily intended for examples and testing, not for production use\n    where a persistent storage solution would be more appropriate.\n\n    This implementation keeps only the last N events per stream for memory efficiency.\n    \"\"\"\n\n    def __init__(self, max_events_per_stream: int = 100):\n        \"\"\"Initialize the event store.\n\n        Args:\n            max_events_per_stream: Maximum number of events to keep per stream\n        \"\"\"\n        self.max_events_per_stream = max_events_per_stream\n        # for maintaining last N events per stream\n        self.streams: dict[StreamId, deque[EventEntry]] = {}\n        # event_id -> EventEntry for quick lookup\n        self.event_index: dict[EventId, EventEntry] = {}\n\n    async def store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId:\n        \"\"\"Stores an event with a generated event ID.\"\"\"\n        event_id = str(uuid4())\n        event_entry = EventEntry(event_id=event_id, stream_id=stream_id, message=message)\n\n        # Get or create deque for this stream\n        if stream_id not in self.streams:\n            self.streams[stream_id] = deque(maxlen=self.max_events_per_stream)\n\n        # If deque is full, the oldest event will be automatically removed\n        # We need to remove it from the event_index as well\n        if len(self.streams[stream_id]) == self.max_events_per_stream:\n            oldest_event = self.streams[stream_id][0]\n            self.event_index.pop(oldest_event.event_id, None)\n\n        # Add new event\n        self.streams[stream_id].append(event_entry)\n        self.event_index[event_id] = event_entry\n\n        return event_id\n\n    async def replay_events_after(\n        self,\n        last_event_id: EventId,\n        send_callback: EventCallback,\n    ) -> StreamId | None:\n        \"\"\"Replays events that occurred after the specified event ID.\"\"\"\n        if last_event_id not in self.event_index:\n            logger.warning(f\"Event ID {last_event_id} not found in store\")\n            return None\n\n        # Get the stream and find events after the last one\n        last_event = self.event_index[last_event_id]\n        stream_id = last_event.stream_id\n        stream_events = self.streams.get(last_event.stream_id, deque())\n\n        # Events in deque are already in chronological order\n        found_last = False\n        for event in stream_events:\n            if found_last:\n                # Skip priming events (None message)\n                if event.message is not None:\n                    await send_callback(EventMessage(event.message, event.event_id))\n            elif event.event_id == last_event_id:\n                found_last = True\n\n        return stream_id\n"
  },
  {
    "path": "examples/servers/simple-streamablehttp/mcp_simple_streamablehttp/server.py",
    "content": "import logging\n\nimport anyio\nimport click\nimport uvicorn\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\nfrom starlette.middleware.cors import CORSMiddleware\n\nfrom .event_store import InMemoryEventStore\n\n# Configure logging\nlogger = logging.getLogger(__name__)\n\n\nasync def handle_list_tools(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListToolsResult:\n    return types.ListToolsResult(\n        tools=[\n            types.Tool(\n                name=\"start-notification-stream\",\n                description=\"Sends a stream of notifications with configurable count and interval\",\n                input_schema={\n                    \"type\": \"object\",\n                    \"required\": [\"interval\", \"count\", \"caller\"],\n                    \"properties\": {\n                        \"interval\": {\n                            \"type\": \"number\",\n                            \"description\": \"Interval between notifications in seconds\",\n                        },\n                        \"count\": {\n                            \"type\": \"number\",\n                            \"description\": \"Number of notifications to send\",\n                        },\n                        \"caller\": {\n                            \"type\": \"string\",\n                            \"description\": \"Identifier of the caller to include in notifications\",\n                        },\n                    },\n                },\n            )\n        ]\n    )\n\n\nasync def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:\n    arguments = params.arguments or {}\n    interval = arguments.get(\"interval\", 1.0)\n    count = arguments.get(\"count\", 5)\n    caller = arguments.get(\"caller\", \"unknown\")\n\n    # Send the specified number of notifications with the given interval\n    for i in range(count):\n        # Include more detailed message for resumability demonstration\n        notification_msg = f\"[{i + 1}/{count}] Event from '{caller}' - Use Last-Event-ID to resume if disconnected\"\n        await ctx.session.send_log_message(\n            level=\"info\",\n            data=notification_msg,\n            logger=\"notification_stream\",\n            # Associates this notification with the original request\n            # Ensures notifications are sent to the correct response stream\n            # Without this, notifications will either go to:\n            # - a standalone SSE stream (if GET request is supported)\n            # - nowhere (if GET request isn't supported)\n            related_request_id=ctx.request_id,\n        )\n        logger.debug(f\"Sent notification {i + 1}/{count} for caller: {caller}\")\n        if i < count - 1:  # Don't wait after the last notification\n            await anyio.sleep(interval)\n\n    # This will send a resource notification through standalone SSE\n    # established by GET request\n    await ctx.session.send_resource_updated(uri=\"http:///test_resource\")\n    return types.CallToolResult(\n        content=[\n            types.TextContent(\n                type=\"text\",\n                text=(f\"Sent {count} notifications with {interval}s interval for caller: {caller}\"),\n            )\n        ]\n    )\n\n\n@click.command()\n@click.option(\"--port\", default=3000, help=\"Port to listen on for HTTP\")\n@click.option(\n    \"--log-level\",\n    default=\"INFO\",\n    help=\"Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)\",\n)\n@click.option(\n    \"--json-response\",\n    is_flag=True,\n    default=False,\n    help=\"Enable JSON responses instead of SSE streams\",\n)\ndef main(\n    port: int,\n    log_level: str,\n    json_response: bool,\n) -> int:\n    # Configure logging\n    logging.basicConfig(\n        level=getattr(logging, log_level.upper()),\n        format=\"%(asctime)s - %(name)s - %(levelname)s - %(message)s\",\n    )\n\n    app = Server(\n        \"mcp-streamable-http-demo\",\n        on_list_tools=handle_list_tools,\n        on_call_tool=handle_call_tool,\n    )\n\n    # Create event store for resumability\n    # The InMemoryEventStore enables resumability support for StreamableHTTP transport.\n    # It stores SSE events with unique IDs, allowing clients to:\n    #   1. Receive event IDs for each SSE message\n    #   2. Resume streams by sending Last-Event-ID in GET requests\n    #   3. Replay missed events after reconnection\n    # Note: This in-memory implementation is for demonstration ONLY.\n    # For production, use a persistent storage solution.\n    event_store = InMemoryEventStore()\n\n    starlette_app = app.streamable_http_app(\n        event_store=event_store,\n        json_response=json_response,\n        debug=True,\n    )\n\n    # Wrap ASGI application with CORS middleware to expose Mcp-Session-Id header\n    # for browser-based clients (ensures 500 errors get proper CORS headers)\n    starlette_app = CORSMiddleware(\n        starlette_app,\n        allow_origins=[\"*\"],  # Note: streamable_http_app() enforces localhost-only Origin by default\n        allow_methods=[\"GET\", \"POST\", \"DELETE\"],  # MCP streamable HTTP methods\n        expose_headers=[\"Mcp-Session-Id\"],\n    )\n\n    uvicorn.run(starlette_app, host=\"127.0.0.1\", port=port)\n\n    return 0\n"
  },
  {
    "path": "examples/servers/simple-streamablehttp/pyproject.toml",
    "content": "[project]\nname = \"mcp-simple-streamablehttp\"\nversion = \"0.1.0\"\ndescription = \"A simple MCP server exposing a StreamableHttp transport for testing\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nkeywords = [\"mcp\", \"llm\", \"automation\", \"web\", \"fetch\", \"http\", \"streamable\"]\nlicense = { text = \"MIT\" }\ndependencies = [\"anyio>=4.5\", \"click>=8.2.0\", \"httpx>=0.27\", \"mcp\", \"starlette\", \"uvicorn\"]\n\n[project.scripts]\nmcp-simple-streamablehttp = \"mcp_simple_streamablehttp.server:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_simple_streamablehttp\"]\n\n[tool.pyright]\ninclude = [\"mcp_simple_streamablehttp\"]\nvenvPath = \".\"\nvenv = \".venv\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\"]\nignore = []\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n[dependency-groups]\ndev = [\"pyright>=1.1.378\", \"pytest>=8.3.3\", \"ruff>=0.6.9\"]\n"
  },
  {
    "path": "examples/servers/simple-streamablehttp-stateless/README.md",
    "content": "# MCP Simple StreamableHttp Stateless Server Example\n\nA stateless MCP server example demonstrating the StreamableHttp transport without maintaining session state. This example is ideal for understanding how to deploy MCP servers in multi-node environments where requests can be routed to any instance.\n\n## Features\n\n- Uses the StreamableHTTP transport in stateless mode (mcp_session_id=None)\n- Each request creates a new ephemeral connection\n- No session state maintained between requests\n- Suitable for deployment in multi-node environments\n\n## Usage\n\nStart the server:\n\n```bash\n# Using default port 3000\nuv run mcp-simple-streamablehttp-stateless\n\n# Using custom port\nuv run mcp-simple-streamablehttp-stateless --port 3000\n\n# Custom logging level\nuv run mcp-simple-streamablehttp-stateless --log-level DEBUG\n\n# Enable JSON responses instead of SSE streams\nuv run mcp-simple-streamablehttp-stateless --json-response\n```\n\nThe server exposes a tool named \"start-notification-stream\" that accepts three arguments:\n\n- `interval`: Time between notifications in seconds (e.g., 1.0)\n- `count`: Number of notifications to send (e.g., 5)\n- `caller`: Identifier string for the caller\n\n## Client\n\nYou can connect to this server using an HTTP client. For now, only the TypeScript SDK has streamable HTTP client examples, or you can use [Inspector](https://github.com/modelcontextprotocol/inspector) for testing.\n"
  },
  {
    "path": "examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/__init__.py",
    "content": ""
  },
  {
    "path": "examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/__main__.py",
    "content": "from .server import main\n\nif __name__ == \"__main__\":\n    # Click will handle CLI arguments\n    import sys\n\n    sys.exit(main())  # type: ignore[call-arg]\n"
  },
  {
    "path": "examples/servers/simple-streamablehttp-stateless/mcp_simple_streamablehttp_stateless/server.py",
    "content": "import logging\n\nimport anyio\nimport click\nimport uvicorn\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\nfrom starlette.middleware.cors import CORSMiddleware\n\nlogger = logging.getLogger(__name__)\n\n\nasync def handle_list_tools(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListToolsResult:\n    return types.ListToolsResult(\n        tools=[\n            types.Tool(\n                name=\"start-notification-stream\",\n                description=(\"Sends a stream of notifications with configurable count and interval\"),\n                input_schema={\n                    \"type\": \"object\",\n                    \"required\": [\"interval\", \"count\", \"caller\"],\n                    \"properties\": {\n                        \"interval\": {\n                            \"type\": \"number\",\n                            \"description\": \"Interval between notifications in seconds\",\n                        },\n                        \"count\": {\n                            \"type\": \"number\",\n                            \"description\": \"Number of notifications to send\",\n                        },\n                        \"caller\": {\n                            \"type\": \"string\",\n                            \"description\": (\"Identifier of the caller to include in notifications\"),\n                        },\n                    },\n                },\n            )\n        ]\n    )\n\n\nasync def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:\n    arguments = params.arguments or {}\n    interval = arguments.get(\"interval\", 1.0)\n    count = arguments.get(\"count\", 5)\n    caller = arguments.get(\"caller\", \"unknown\")\n\n    # Send the specified number of notifications with the given interval\n    for i in range(count):\n        await ctx.session.send_log_message(\n            level=\"info\",\n            data=f\"Notification {i + 1}/{count} from caller: {caller}\",\n            logger=\"notification_stream\",\n            related_request_id=ctx.request_id,\n        )\n        if i < count - 1:  # Don't wait after the last notification\n            await anyio.sleep(interval)\n\n    return types.CallToolResult(\n        content=[\n            types.TextContent(\n                type=\"text\",\n                text=(f\"Sent {count} notifications with {interval}s interval for caller: {caller}\"),\n            )\n        ]\n    )\n\n\n@click.command()\n@click.option(\"--port\", default=3000, help=\"Port to listen on for HTTP\")\n@click.option(\n    \"--log-level\",\n    default=\"INFO\",\n    help=\"Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)\",\n)\n@click.option(\n    \"--json-response\",\n    is_flag=True,\n    default=False,\n    help=\"Enable JSON responses instead of SSE streams\",\n)\ndef main(\n    port: int,\n    log_level: str,\n    json_response: bool,\n) -> None:\n    # Configure logging\n    logging.basicConfig(\n        level=getattr(logging, log_level.upper()),\n        format=\"%(asctime)s - %(name)s - %(levelname)s - %(message)s\",\n    )\n\n    app = Server(\n        \"mcp-streamable-http-stateless-demo\",\n        on_list_tools=handle_list_tools,\n        on_call_tool=handle_call_tool,\n    )\n\n    starlette_app = app.streamable_http_app(\n        stateless_http=True,\n        json_response=json_response,\n        debug=True,\n    )\n\n    # Wrap ASGI application with CORS middleware to expose Mcp-Session-Id header\n    # for browser-based clients (ensures 500 errors get proper CORS headers)\n    starlette_app = CORSMiddleware(\n        starlette_app,\n        allow_origins=[\"*\"],  # Note: streamable_http_app() enforces localhost-only Origin by default\n        allow_methods=[\"GET\", \"POST\", \"DELETE\"],  # MCP streamable HTTP methods\n        expose_headers=[\"Mcp-Session-Id\"],\n    )\n\n    uvicorn.run(starlette_app, host=\"127.0.0.1\", port=port)\n"
  },
  {
    "path": "examples/servers/simple-streamablehttp-stateless/pyproject.toml",
    "content": "[project]\nname = \"mcp-simple-streamablehttp-stateless\"\nversion = \"0.1.0\"\ndescription = \"A simple MCP server exposing a StreamableHttp transport in stateless mode\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nkeywords = [\"mcp\", \"llm\", \"automation\", \"web\", \"fetch\", \"http\", \"streamable\", \"stateless\"]\nlicense = { text = \"MIT\" }\ndependencies = [\"anyio>=4.5\", \"click>=8.2.0\", \"httpx>=0.27\", \"mcp\", \"starlette\", \"uvicorn\"]\n\n[project.scripts]\nmcp-simple-streamablehttp-stateless = \"mcp_simple_streamablehttp_stateless.server:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_simple_streamablehttp_stateless\"]\n\n[tool.pyright]\ninclude = [\"mcp_simple_streamablehttp_stateless\"]\nvenvPath = \".\"\nvenv = \".venv\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\"]\nignore = []\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n[dependency-groups]\ndev = [\"pyright>=1.1.378\", \"pytest>=8.3.3\", \"ruff>=0.6.9\"]\n"
  },
  {
    "path": "examples/servers/simple-task/README.md",
    "content": "# Simple Task Server\n\nA minimal MCP server demonstrating the experimental tasks feature over streamable HTTP.\n\n## Running\n\n```bash\ncd examples/servers/simple-task\nuv run mcp-simple-task\n```\n\nThe server starts on `http://localhost:8000/mcp` by default. Use `--port` to change.\n\n## What it does\n\nThis server exposes a single tool `long_running_task` that:\n\n1. Must be called as a task (with `task` metadata in the request)\n2. Takes ~3 seconds to complete\n3. Sends status updates during execution\n4. Returns a result when complete\n\n## Usage with the client\n\nIn one terminal, start the server:\n\n```bash\ncd examples/servers/simple-task\nuv run mcp-simple-task\n```\n\nIn another terminal, run the client:\n\n```bash\ncd examples/clients/simple-task-client\nuv run mcp-simple-task-client\n```\n"
  },
  {
    "path": "examples/servers/simple-task/mcp_simple_task/__init__.py",
    "content": ""
  },
  {
    "path": "examples/servers/simple-task/mcp_simple_task/__main__.py",
    "content": "import sys\n\nfrom .server import main\n\nsys.exit(main())  # type: ignore[call-arg]\n"
  },
  {
    "path": "examples/servers/simple-task/mcp_simple_task/server.py",
    "content": "\"\"\"Simple task server demonstrating MCP tasks over streamable HTTP.\"\"\"\n\nimport anyio\nimport click\nimport uvicorn\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.server.experimental.task_context import ServerTaskContext\n\n\nasync def handle_list_tools(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListToolsResult:\n    return types.ListToolsResult(\n        tools=[\n            types.Tool(\n                name=\"long_running_task\",\n                description=\"A task that takes a few seconds to complete with status updates\",\n                input_schema={\"type\": \"object\", \"properties\": {}},\n                execution=types.ToolExecution(task_support=types.TASK_REQUIRED),\n            )\n        ]\n    )\n\n\nasync def handle_call_tool(\n    ctx: ServerRequestContext, params: types.CallToolRequestParams\n) -> types.CallToolResult | types.CreateTaskResult:\n    \"\"\"Dispatch tool calls to their handlers.\"\"\"\n    if params.name == \"long_running_task\":\n        ctx.experimental.validate_task_mode(types.TASK_REQUIRED)\n\n        async def work(task: ServerTaskContext) -> types.CallToolResult:\n            await task.update_status(\"Starting work...\")\n            await anyio.sleep(1)\n\n            await task.update_status(\"Processing step 1...\")\n            await anyio.sleep(1)\n\n            await task.update_status(\"Processing step 2...\")\n            await anyio.sleep(1)\n\n            return types.CallToolResult(content=[types.TextContent(type=\"text\", text=\"Task completed!\")])\n\n        return await ctx.experimental.run_task(work)\n\n    return types.CallToolResult(\n        content=[types.TextContent(type=\"text\", text=f\"Unknown tool: {params.name}\")],\n        is_error=True,\n    )\n\n\nserver = Server(\n    \"simple-task-server\",\n    on_list_tools=handle_list_tools,\n    on_call_tool=handle_call_tool,\n)\n\n# One-line setup: auto-registers get_task, get_task_result, list_tasks, cancel_task\nserver.experimental.enable_tasks()\n\n\n@click.command()\n@click.option(\"--port\", default=8000, help=\"Port to listen on\")\ndef main(port: int) -> int:\n    starlette_app = server.streamable_http_app()\n\n    print(f\"Starting server on http://localhost:{port}/mcp\")\n    uvicorn.run(starlette_app, host=\"127.0.0.1\", port=port)\n    return 0\n"
  },
  {
    "path": "examples/servers/simple-task/pyproject.toml",
    "content": "[project]\nname = \"mcp-simple-task\"\nversion = \"0.1.0\"\ndescription = \"A simple MCP server demonstrating tasks\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nkeywords = [\"mcp\", \"llm\", \"tasks\"]\nlicense = { text = \"MIT\" }\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n]\ndependencies = [\"anyio>=4.5\", \"click>=8.0\", \"mcp\", \"starlette\", \"uvicorn\"]\n\n[project.scripts]\nmcp-simple-task = \"mcp_simple_task.server:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_simple_task\"]\n\n[tool.pyright]\ninclude = [\"mcp_simple_task\"]\nvenvPath = \".\"\nvenv = \".venv\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\"]\nignore = []\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n[dependency-groups]\ndev = [\"pyright>=1.1.378\", \"ruff>=0.6.9\"]\n"
  },
  {
    "path": "examples/servers/simple-task-interactive/README.md",
    "content": "# Simple Interactive Task Server\n\nA minimal MCP server demonstrating interactive tasks with elicitation and sampling.\n\n## Running\n\n```bash\ncd examples/servers/simple-task-interactive\nuv run mcp-simple-task-interactive\n```\n\nThe server starts on `http://localhost:8000/mcp` by default. Use `--port` to change.\n\n## What it does\n\nThis server exposes two tools:\n\n### `confirm_delete` (demonstrates elicitation)\n\nAsks the user for confirmation before \"deleting\" a file.\n\n- Uses `task.elicit()` to request user input\n- Shows the elicitation flow: task -> input_required -> response -> complete\n\n### `write_haiku` (demonstrates sampling)\n\nAsks the LLM to write a haiku about a topic.\n\n- Uses `task.create_message()` to request LLM completion\n- Shows the sampling flow: task -> input_required -> response -> complete\n\n## Usage with the client\n\nIn one terminal, start the server:\n\n```bash\ncd examples/servers/simple-task-interactive\nuv run mcp-simple-task-interactive\n```\n\nIn another terminal, run the interactive client:\n\n```bash\ncd examples/clients/simple-task-interactive-client\nuv run mcp-simple-task-interactive-client\n```\n\n## Expected server output\n\nWhen a client connects and calls the tools, you'll see:\n\n```text\nStarting server on http://localhost:8000/mcp\n\n[Server] confirm_delete called for 'important.txt'\n[Server] Task created: <task-id>\n[Server] Sending elicitation request to client...\n[Server] Received elicitation response: action=accept, content={'confirm': True}\n[Server] Completing task with result: Deleted 'important.txt'\n\n[Server] write_haiku called for topic 'autumn leaves'\n[Server] Task created: <task-id>\n[Server] Sending sampling request to client...\n[Server] Received sampling response: Cherry blossoms fall\nSoftly on the quiet pon...\n[Server] Completing task with haiku\n```\n\n## Key concepts\n\n1. **ServerTaskContext**: Provides `elicit()` and `create_message()` for user interaction\n2. **run_task()**: Spawns background work, auto-completes/fails, returns immediately\n3. **TaskResultHandler**: Delivers queued messages and routes responses\n4. **Response routing**: Responses are routed back to waiting resolvers\n"
  },
  {
    "path": "examples/servers/simple-task-interactive/mcp_simple_task_interactive/__init__.py",
    "content": ""
  },
  {
    "path": "examples/servers/simple-task-interactive/mcp_simple_task_interactive/__main__.py",
    "content": "import sys\n\nfrom .server import main\n\nsys.exit(main())  # type: ignore[call-arg]\n"
  },
  {
    "path": "examples/servers/simple-task-interactive/mcp_simple_task_interactive/server.py",
    "content": "\"\"\"Simple interactive task server demonstrating elicitation and sampling.\n\nThis example shows the simplified task API where:\n- server.experimental.enable_tasks() sets up all infrastructure\n- ctx.experimental.run_task() handles task lifecycle automatically\n- ServerTaskContext.elicit() and ServerTaskContext.create_message() queue requests properly\n\"\"\"\n\nfrom typing import Any\n\nimport click\nimport uvicorn\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.server.experimental.task_context import ServerTaskContext\n\n\nasync def handle_list_tools(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListToolsResult:\n    return types.ListToolsResult(\n        tools=[\n            types.Tool(\n                name=\"confirm_delete\",\n                description=\"Asks for confirmation before deleting (demonstrates elicitation)\",\n                input_schema={\n                    \"type\": \"object\",\n                    \"properties\": {\"filename\": {\"type\": \"string\"}},\n                },\n                execution=types.ToolExecution(task_support=types.TASK_REQUIRED),\n            ),\n            types.Tool(\n                name=\"write_haiku\",\n                description=\"Asks LLM to write a haiku (demonstrates sampling)\",\n                input_schema={\"type\": \"object\", \"properties\": {\"topic\": {\"type\": \"string\"}}},\n                execution=types.ToolExecution(task_support=types.TASK_REQUIRED),\n            ),\n        ]\n    )\n\n\nasync def handle_confirm_delete(ctx: ServerRequestContext, arguments: dict[str, Any]) -> types.CreateTaskResult:\n    \"\"\"Handle the confirm_delete tool - demonstrates elicitation.\"\"\"\n    ctx.experimental.validate_task_mode(types.TASK_REQUIRED)\n\n    filename = arguments.get(\"filename\", \"unknown.txt\")\n    print(f\"\\n[Server] confirm_delete called for '{filename}'\")\n\n    async def work(task: ServerTaskContext) -> types.CallToolResult:\n        print(f\"[Server] Task {task.task_id} starting elicitation...\")\n\n        result = await task.elicit(\n            message=f\"Are you sure you want to delete '{filename}'?\",\n            requested_schema={\n                \"type\": \"object\",\n                \"properties\": {\"confirm\": {\"type\": \"boolean\"}},\n                \"required\": [\"confirm\"],\n            },\n        )\n\n        print(f\"[Server] Received elicitation response: action={result.action}, content={result.content}\")\n\n        if result.action == \"accept\" and result.content:\n            confirmed = result.content.get(\"confirm\", False)\n            text = f\"Deleted '{filename}'\" if confirmed else \"Deletion cancelled\"\n        else:\n            text = \"Deletion cancelled\"\n\n        print(f\"[Server] Completing task with result: {text}\")\n        return types.CallToolResult(content=[types.TextContent(type=\"text\", text=text)])\n\n    return await ctx.experimental.run_task(work)\n\n\nasync def handle_write_haiku(ctx: ServerRequestContext, arguments: dict[str, Any]) -> types.CreateTaskResult:\n    \"\"\"Handle the write_haiku tool - demonstrates sampling.\"\"\"\n    ctx.experimental.validate_task_mode(types.TASK_REQUIRED)\n\n    topic = arguments.get(\"topic\", \"nature\")\n    print(f\"\\n[Server] write_haiku called for topic '{topic}'\")\n\n    async def work(task: ServerTaskContext) -> types.CallToolResult:\n        print(f\"[Server] Task {task.task_id} starting sampling...\")\n\n        result = await task.create_message(\n            messages=[\n                types.SamplingMessage(\n                    role=\"user\",\n                    content=types.TextContent(type=\"text\", text=f\"Write a haiku about {topic}\"),\n                )\n            ],\n            max_tokens=50,\n        )\n\n        haiku = \"No response\"\n        if isinstance(result.content, types.TextContent):\n            haiku = result.content.text\n\n        print(f\"[Server] Received sampling response: {haiku[:50]}...\")\n        return types.CallToolResult(content=[types.TextContent(type=\"text\", text=f\"Haiku:\\n{haiku}\")])\n\n    return await ctx.experimental.run_task(work)\n\n\nasync def handle_call_tool(\n    ctx: ServerRequestContext, params: types.CallToolRequestParams\n) -> types.CallToolResult | types.CreateTaskResult:\n    \"\"\"Dispatch tool calls to their handlers.\"\"\"\n    arguments = params.arguments or {}\n\n    if params.name == \"confirm_delete\":\n        return await handle_confirm_delete(ctx, arguments)\n    elif params.name == \"write_haiku\":\n        return await handle_write_haiku(ctx, arguments)\n\n    return types.CallToolResult(\n        content=[types.TextContent(type=\"text\", text=f\"Unknown tool: {params.name}\")],\n        is_error=True,\n    )\n\n\nserver = Server(\n    \"simple-task-interactive\",\n    on_list_tools=handle_list_tools,\n    on_call_tool=handle_call_tool,\n)\n\n# Enable task support - this auto-registers all handlers\nserver.experimental.enable_tasks()\n\n\n@click.command()\n@click.option(\"--port\", default=8000, help=\"Port to listen on\")\ndef main(port: int) -> int:\n    starlette_app = server.streamable_http_app()\n    print(f\"Starting server on http://localhost:{port}/mcp\")\n    uvicorn.run(starlette_app, host=\"127.0.0.1\", port=port)\n    return 0\n"
  },
  {
    "path": "examples/servers/simple-task-interactive/pyproject.toml",
    "content": "[project]\nname = \"mcp-simple-task-interactive\"\nversion = \"0.1.0\"\ndescription = \"A simple MCP server demonstrating interactive tasks (elicitation & sampling)\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nkeywords = [\"mcp\", \"llm\", \"tasks\", \"elicitation\", \"sampling\"]\nlicense = { text = \"MIT\" }\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n]\ndependencies = [\"anyio>=4.5\", \"click>=8.0\", \"mcp\", \"starlette\", \"uvicorn\"]\n\n[project.scripts]\nmcp-simple-task-interactive = \"mcp_simple_task_interactive.server:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_simple_task_interactive\"]\n\n[tool.pyright]\ninclude = [\"mcp_simple_task_interactive\"]\nvenvPath = \".\"\nvenv = \".venv\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\"]\nignore = []\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n[dependency-groups]\ndev = [\"pyright>=1.1.378\", \"ruff>=0.6.9\"]\n"
  },
  {
    "path": "examples/servers/simple-tool/README.md",
    "content": "\nA simple MCP server that exposes a website fetching tool.\n\n## Usage\n\nStart the server using either stdio (default) or Streamable HTTP transport:\n\n```bash\n# Using stdio transport (default)\nuv run mcp-simple-tool\n\n# Using Streamable HTTP transport on custom port\nuv run mcp-simple-tool --transport streamable-http --port 8000\n```\n\nThe server exposes a tool named \"fetch\" that accepts one required argument:\n\n- `url`: The URL of the website to fetch\n\n## Example\n\nUsing the MCP client, you can use the tool like this using the STDIO transport:\n\n```python\nimport asyncio\nfrom mcp.client.session import ClientSession\nfrom mcp.client.stdio import StdioServerParameters, stdio_client\n\n\nasync def main():\n    async with stdio_client(\n        StdioServerParameters(command=\"uv\", args=[\"run\", \"mcp-simple-tool\"])\n    ) as (read, write):\n        async with ClientSession(read, write) as session:\n            await session.initialize()\n\n            # List available tools\n            tools = await session.list_tools()\n            print(tools)\n\n            # Call the fetch tool\n            result = await session.call_tool(\"fetch\", {\"url\": \"https://example.com\"})\n            print(result)\n\n\nasyncio.run(main())\n\n```\n"
  },
  {
    "path": "examples/servers/simple-tool/mcp_simple_tool/__init__.py",
    "content": ""
  },
  {
    "path": "examples/servers/simple-tool/mcp_simple_tool/__main__.py",
    "content": "import sys\n\nfrom .server import main\n\nsys.exit(main())  # type: ignore[call-arg]\n"
  },
  {
    "path": "examples/servers/simple-tool/mcp_simple_tool/server.py",
    "content": "import anyio\nimport click\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.shared._httpx_utils import create_mcp_http_client\n\n\nasync def fetch_website(\n    url: str,\n) -> list[types.ContentBlock]:\n    headers = {\"User-Agent\": \"MCP Test Server (github.com/modelcontextprotocol/python-sdk)\"}\n    async with create_mcp_http_client(headers=headers) as client:\n        response = await client.get(url)\n        response.raise_for_status()\n        return [types.TextContent(type=\"text\", text=response.text)]\n\n\nasync def handle_list_tools(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListToolsResult:\n    return types.ListToolsResult(\n        tools=[\n            types.Tool(\n                name=\"fetch\",\n                title=\"Website Fetcher\",\n                description=\"Fetches a website and returns its content\",\n                input_schema={\n                    \"type\": \"object\",\n                    \"required\": [\"url\"],\n                    \"properties\": {\n                        \"url\": {\n                            \"type\": \"string\",\n                            \"description\": \"URL to fetch\",\n                        }\n                    },\n                },\n            )\n        ]\n    )\n\n\nasync def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:\n    if params.name != \"fetch\":\n        raise ValueError(f\"Unknown tool: {params.name}\")\n    arguments = params.arguments or {}\n    if \"url\" not in arguments:\n        raise ValueError(\"Missing required argument 'url'\")\n    content = await fetch_website(arguments[\"url\"])\n    return types.CallToolResult(content=content)\n\n\n@click.command()\n@click.option(\"--port\", default=8000, help=\"Port to listen on for HTTP\")\n@click.option(\n    \"--transport\",\n    type=click.Choice([\"stdio\", \"streamable-http\"]),\n    default=\"stdio\",\n    help=\"Transport type\",\n)\ndef main(port: int, transport: str) -> int:\n    app = Server(\n        \"mcp-website-fetcher\",\n        on_list_tools=handle_list_tools,\n        on_call_tool=handle_call_tool,\n    )\n\n    if transport == \"streamable-http\":\n        import uvicorn\n\n        uvicorn.run(app.streamable_http_app(), host=\"127.0.0.1\", port=port)\n    else:\n        from mcp.server.stdio import stdio_server\n\n        async def arun():\n            async with stdio_server() as streams:\n                await app.run(streams[0], streams[1], app.create_initialization_options())\n\n        anyio.run(arun)\n\n    return 0\n"
  },
  {
    "path": "examples/servers/simple-tool/pyproject.toml",
    "content": "[project]\nname = \"mcp-simple-tool\"\nversion = \"0.1.0\"\ndescription = \"A simple MCP server exposing a website fetching tool\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nkeywords = [\"mcp\", \"llm\", \"automation\", \"web\", \"fetch\"]\nlicense = { text = \"MIT\" }\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n]\ndependencies = [\"anyio>=4.5\", \"click>=8.2.0\", \"httpx>=0.27\", \"mcp\"]\n\n[project.scripts]\nmcp-simple-tool = \"mcp_simple_tool.server:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_simple_tool\"]\n\n[tool.pyright]\ninclude = [\"mcp_simple_tool\"]\nvenvPath = \".\"\nvenv = \".venv\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\"]\nignore = []\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n[dependency-groups]\ndev = [\"pyright>=1.1.378\", \"pytest>=8.3.3\", \"ruff>=0.6.9\"]\n"
  },
  {
    "path": "examples/servers/sse-polling-demo/README.md",
    "content": "# MCP SSE Polling Demo Server\n\nDemonstrates the SSE polling pattern with server-initiated stream close for long-running tasks (SEP-1699).\n\n## Features\n\n- Priming events (automatic with EventStore)\n- Server-initiated stream close via `close_sse_stream()` callback\n- Client auto-reconnect with Last-Event-ID\n- Progress notifications during long-running tasks\n- Configurable retry interval\n\n## Usage\n\n```bash\n# Start server on default port\nuv run mcp-sse-polling-demo --port 3000\n\n# Custom retry interval (milliseconds)\nuv run mcp-sse-polling-demo --port 3000 --retry-interval 100\n```\n\n## Tool: process_batch\n\nProcesses items with periodic checkpoints that trigger SSE stream closes:\n\n- `items`: Number of items to process (1-100, default: 10)\n- `checkpoint_every`: Close stream after this many items (1-20, default: 3)\n\n## Client\n\nUse the companion `mcp-sse-polling-client` to test:\n\n```bash\nuv run mcp-sse-polling-client --url http://localhost:3000/mcp\n```\n"
  },
  {
    "path": "examples/servers/sse-polling-demo/mcp_sse_polling_demo/__init__.py",
    "content": "\"\"\"SSE Polling Demo Server - demonstrates close_sse_stream for long-running tasks.\"\"\"\n"
  },
  {
    "path": "examples/servers/sse-polling-demo/mcp_sse_polling_demo/__main__.py",
    "content": "\"\"\"Entry point for the SSE Polling Demo server.\"\"\"\n\nfrom .server import main\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/servers/sse-polling-demo/mcp_sse_polling_demo/event_store.py",
    "content": "\"\"\"In-memory event store for demonstrating resumability functionality.\n\nThis is a simple implementation intended for examples and testing,\nnot for production use where a persistent storage solution would be more appropriate.\n\"\"\"\n\nimport logging\nfrom collections import deque\nfrom dataclasses import dataclass\nfrom uuid import uuid4\n\nfrom mcp.server.streamable_http import EventCallback, EventId, EventMessage, EventStore, StreamId\nfrom mcp.types import JSONRPCMessage\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass\nclass EventEntry:\n    \"\"\"Represents an event entry in the event store.\"\"\"\n\n    event_id: EventId\n    stream_id: StreamId\n    message: JSONRPCMessage | None  # None for priming events\n\n\nclass InMemoryEventStore(EventStore):\n    \"\"\"Simple in-memory implementation of the EventStore interface for resumability.\n    This is primarily intended for examples and testing, not for production use\n    where a persistent storage solution would be more appropriate.\n\n    This implementation keeps only the last N events per stream for memory efficiency.\n    \"\"\"\n\n    def __init__(self, max_events_per_stream: int = 100):\n        \"\"\"Initialize the event store.\n\n        Args:\n            max_events_per_stream: Maximum number of events to keep per stream\n        \"\"\"\n        self.max_events_per_stream = max_events_per_stream\n        # for maintaining last N events per stream\n        self.streams: dict[StreamId, deque[EventEntry]] = {}\n        # event_id -> EventEntry for quick lookup\n        self.event_index: dict[EventId, EventEntry] = {}\n\n    async def store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId:\n        \"\"\"Stores an event with a generated event ID.\n\n        Args:\n            stream_id: ID of the stream the event belongs to\n            message: The message to store, or None for priming events\n        \"\"\"\n        event_id = str(uuid4())\n        event_entry = EventEntry(event_id=event_id, stream_id=stream_id, message=message)\n\n        # Get or create deque for this stream\n        if stream_id not in self.streams:\n            self.streams[stream_id] = deque(maxlen=self.max_events_per_stream)\n\n        # If deque is full, the oldest event will be automatically removed\n        # We need to remove it from the event_index as well\n        if len(self.streams[stream_id]) == self.max_events_per_stream:\n            oldest_event = self.streams[stream_id][0]\n            self.event_index.pop(oldest_event.event_id, None)\n\n        # Add new event\n        self.streams[stream_id].append(event_entry)\n        self.event_index[event_id] = event_entry\n\n        return event_id\n\n    async def replay_events_after(\n        self,\n        last_event_id: EventId,\n        send_callback: EventCallback,\n    ) -> StreamId | None:\n        \"\"\"Replays events that occurred after the specified event ID.\"\"\"\n        if last_event_id not in self.event_index:\n            logger.warning(f\"Event ID {last_event_id} not found in store\")\n            return None\n\n        # Get the stream and find events after the last one\n        last_event = self.event_index[last_event_id]\n        stream_id = last_event.stream_id\n        stream_events = self.streams.get(last_event.stream_id, deque())\n\n        # Events in deque are already in chronological order\n        found_last = False\n        for event in stream_events:\n            if found_last:\n                # Skip priming events (None messages) during replay\n                if event.message is not None:\n                    await send_callback(EventMessage(event.message, event.event_id))\n            elif event.event_id == last_event_id:\n                found_last = True\n\n        return stream_id\n"
  },
  {
    "path": "examples/servers/sse-polling-demo/mcp_sse_polling_demo/server.py",
    "content": "\"\"\"SSE Polling Demo Server\n\nDemonstrates the SSE polling pattern with close_sse_stream() for long-running tasks.\n\nFeatures demonstrated:\n- Priming events (automatic with EventStore)\n- Server-initiated stream close via close_sse_stream callback\n- Client auto-reconnect with Last-Event-ID\n- Progress notifications during long-running tasks\n\nRun with:\n    uv run mcp-sse-polling-demo --port 3000\n\"\"\"\n\nimport logging\n\nimport anyio\nimport click\nimport uvicorn\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\n\nfrom .event_store import InMemoryEventStore\n\nlogger = logging.getLogger(__name__)\n\n\nasync def handle_list_tools(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListToolsResult:\n    \"\"\"List available tools.\"\"\"\n    return types.ListToolsResult(\n        tools=[\n            types.Tool(\n                name=\"process_batch\",\n                description=(\n                    \"Process a batch of items with periodic checkpoints. \"\n                    \"Demonstrates SSE polling where server closes stream periodically.\"\n                ),\n                input_schema={\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"items\": {\n                            \"type\": \"integer\",\n                            \"description\": \"Number of items to process (1-100)\",\n                            \"default\": 10,\n                        },\n                        \"checkpoint_every\": {\n                            \"type\": \"integer\",\n                            \"description\": \"Close stream after this many items (1-20)\",\n                            \"default\": 3,\n                        },\n                    },\n                },\n            )\n        ]\n    )\n\n\nasync def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:\n    \"\"\"Handle tool calls.\"\"\"\n    arguments = params.arguments or {}\n\n    if params.name == \"process_batch\":\n        items = arguments.get(\"items\", 10)\n        checkpoint_every = arguments.get(\"checkpoint_every\", 3)\n\n        if items < 1 or items > 100:\n            return types.CallToolResult(\n                content=[types.TextContent(type=\"text\", text=\"Error: items must be between 1 and 100\")]\n            )\n        if checkpoint_every < 1 or checkpoint_every > 20:\n            return types.CallToolResult(\n                content=[types.TextContent(type=\"text\", text=\"Error: checkpoint_every must be between 1 and 20\")]\n            )\n\n        await ctx.session.send_log_message(\n            level=\"info\",\n            data=f\"Starting batch processing of {items} items...\",\n            logger=\"process_batch\",\n            related_request_id=ctx.request_id,\n        )\n\n        for i in range(1, items + 1):\n            # Simulate work\n            await anyio.sleep(0.5)\n\n            # Report progress\n            await ctx.session.send_log_message(\n                level=\"info\",\n                data=f\"[{i}/{items}] Processing item {i}\",\n                logger=\"process_batch\",\n                related_request_id=ctx.request_id,\n            )\n\n            # Checkpoint: close stream to trigger client reconnect\n            if i % checkpoint_every == 0 and i < items:\n                await ctx.session.send_log_message(\n                    level=\"info\",\n                    data=f\"Checkpoint at item {i} - closing SSE stream for polling\",\n                    logger=\"process_batch\",\n                    related_request_id=ctx.request_id,\n                )\n                if ctx.close_sse_stream:\n                    logger.info(f\"Closing SSE stream at checkpoint {i}\")\n                    await ctx.close_sse_stream()\n                # Wait for client to reconnect (must be > retry_interval of 100ms)\n                await anyio.sleep(0.2)\n\n        return types.CallToolResult(\n            content=[\n                types.TextContent(\n                    type=\"text\",\n                    text=f\"Successfully processed {items} items with checkpoints every {checkpoint_every} items\",\n                )\n            ]\n        )\n\n    return types.CallToolResult(content=[types.TextContent(type=\"text\", text=f\"Unknown tool: {params.name}\")])\n\n\n@click.command()\n@click.option(\"--port\", default=3000, help=\"Port to listen on\")\n@click.option(\n    \"--log-level\",\n    default=\"INFO\",\n    help=\"Logging level (DEBUG, INFO, WARNING, ERROR)\",\n)\n@click.option(\n    \"--retry-interval\",\n    default=100,\n    help=\"SSE retry interval in milliseconds (sent to client)\",\n)\ndef main(port: int, log_level: str, retry_interval: int) -> int:\n    \"\"\"Run the SSE Polling Demo server.\"\"\"\n    logging.basicConfig(\n        level=getattr(logging, log_level.upper()),\n        format=\"%(asctime)s - %(name)s - %(levelname)s - %(message)s\",\n    )\n\n    app = Server(\n        \"sse-polling-demo\",\n        on_list_tools=handle_list_tools,\n        on_call_tool=handle_call_tool,\n    )\n\n    starlette_app = app.streamable_http_app(\n        event_store=InMemoryEventStore(),\n        retry_interval=retry_interval,\n        debug=True,\n    )\n\n    logger.info(f\"SSE Polling Demo server starting on port {port}\")\n    logger.info(\"Try: POST /mcp with tools/call for 'process_batch'\")\n    uvicorn.run(starlette_app, host=\"127.0.0.1\", port=port)\n    return 0\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/servers/sse-polling-demo/pyproject.toml",
    "content": "[project]\nname = \"mcp-sse-polling-demo\"\nversion = \"0.1.0\"\ndescription = \"Demo server showing SSE polling with close_sse_stream for long-running tasks\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nkeywords = [\"mcp\", \"sse\", \"polling\", \"streamable\", \"http\"]\nlicense = { text = \"MIT\" }\ndependencies = [\"anyio>=4.5\", \"click>=8.2.0\", \"httpx>=0.27\", \"mcp\", \"starlette\", \"uvicorn\"]\n\n[project.scripts]\nmcp-sse-polling-demo = \"mcp_sse_polling_demo.server:main\"\n\n[build-system]\nrequires = [\"hatchling\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"mcp_sse_polling_demo\"]\n\n[tool.pyright]\ninclude = [\"mcp_sse_polling_demo\"]\nvenvPath = \".\"\nvenv = \".venv\"\n\n[tool.ruff.lint]\nselect = [\"E\", \"F\", \"I\"]\nignore = []\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\n\n[dependency-groups]\ndev = [\"pyright>=1.1.378\", \"pytest>=8.3.3\", \"ruff>=0.6.9\"]\n"
  },
  {
    "path": "examples/servers/structured-output-lowlevel/mcp_structured_output_lowlevel/__init__.py",
    "content": "\"\"\"Example of structured output with low-level MCP server.\"\"\"\n"
  },
  {
    "path": "examples/servers/structured-output-lowlevel/mcp_structured_output_lowlevel/__main__.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Example low-level MCP server demonstrating structured output support.\n\nThis example shows how to use the low-level server API to return\nstructured data from tools.\n\"\"\"\n\nimport asyncio\nimport json\nimport random\nfrom datetime import datetime\n\nimport mcp.server.stdio\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\n\n\nasync def handle_list_tools(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListToolsResult:\n    \"\"\"List available tools with their schemas.\"\"\"\n    return types.ListToolsResult(\n        tools=[\n            types.Tool(\n                name=\"get_weather\",\n                description=\"Get weather information (simulated)\",\n                input_schema={\n                    \"type\": \"object\",\n                    \"properties\": {\"city\": {\"type\": \"string\", \"description\": \"City name\"}},\n                    \"required\": [\"city\"],\n                },\n                output_schema={\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"temperature\": {\"type\": \"number\"},\n                        \"conditions\": {\"type\": \"string\"},\n                        \"humidity\": {\"type\": \"integer\", \"minimum\": 0, \"maximum\": 100},\n                        \"wind_speed\": {\"type\": \"number\"},\n                        \"timestamp\": {\"type\": \"string\", \"format\": \"date-time\"},\n                    },\n                    \"required\": [\"temperature\", \"conditions\", \"humidity\", \"wind_speed\", \"timestamp\"],\n                },\n            ),\n        ]\n    )\n\n\nasync def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:\n    \"\"\"Handle tool call with structured output.\"\"\"\n\n    if params.name == \"get_weather\":\n        # Simulate weather data (in production, call a real weather API)\n        weather_conditions = [\"sunny\", \"cloudy\", \"rainy\", \"partly cloudy\", \"foggy\"]\n\n        weather_data = {\n            \"temperature\": round(random.uniform(0, 35), 1),\n            \"conditions\": random.choice(weather_conditions),\n            \"humidity\": random.randint(30, 90),\n            \"wind_speed\": round(random.uniform(0, 30), 1),\n            \"timestamp\": datetime.now().isoformat(),\n        }\n\n        return types.CallToolResult(\n            content=[types.TextContent(type=\"text\", text=json.dumps(weather_data, indent=2))],\n            structured_content=weather_data,\n        )\n\n    raise ValueError(f\"Unknown tool: {params.name}\")\n\n\nserver = Server(\n    \"structured-output-lowlevel-example\",\n    on_list_tools=handle_list_tools,\n    on_call_tool=handle_call_tool,\n)\n\n\nasync def run():\n    \"\"\"Run the low-level server using stdio transport.\"\"\"\n    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):\n        await server.run(\n            read_stream,\n            write_stream,\n            server.create_initialization_options(),\n        )\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run())\n"
  },
  {
    "path": "examples/servers/structured-output-lowlevel/pyproject.toml",
    "content": "[project]\nname = \"mcp-structured-output-lowlevel\"\nversion = \"0.1.0\"\ndescription = \"Example of structured output with low-level MCP server\"\nrequires-python = \">=3.10\"\ndependencies = [\"mcp\"]\n"
  },
  {
    "path": "examples/snippets/clients/__init__.py",
    "content": ""
  },
  {
    "path": "examples/snippets/clients/completion_client.py",
    "content": "\"\"\"cd to the `examples/snippets` directory and run:\nuv run completion-client\n\"\"\"\n\nimport asyncio\nimport os\n\nfrom mcp import ClientSession, StdioServerParameters\nfrom mcp.client.stdio import stdio_client\nfrom mcp.types import PromptReference, ResourceTemplateReference\n\n# Create server parameters for stdio connection\nserver_params = StdioServerParameters(\n    command=\"uv\",  # Using uv to run the server\n    args=[\"run\", \"server\", \"completion\", \"stdio\"],  # Server with completion support\n    env={\"UV_INDEX\": os.environ.get(\"UV_INDEX\", \"\")},\n)\n\n\nasync def run():\n    \"\"\"Run the completion client example.\"\"\"\n    async with stdio_client(server_params) as (read, write):\n        async with ClientSession(read, write) as session:\n            # Initialize the connection\n            await session.initialize()\n\n            # List available resource templates\n            templates = await session.list_resource_templates()\n            print(\"Available resource templates:\")\n            for template in templates.resource_templates:\n                print(f\"  - {template.uri_template}\")\n\n            # List available prompts\n            prompts = await session.list_prompts()\n            print(\"\\nAvailable prompts:\")\n            for prompt in prompts.prompts:\n                print(f\"  - {prompt.name}\")\n\n            # Complete resource template arguments\n            if templates.resource_templates:\n                template = templates.resource_templates[0]\n                print(f\"\\nCompleting arguments for resource template: {template.uri_template}\")\n\n                # Complete without context\n                result = await session.complete(\n                    ref=ResourceTemplateReference(type=\"ref/resource\", uri=template.uri_template),\n                    argument={\"name\": \"owner\", \"value\": \"model\"},\n                )\n                print(f\"Completions for 'owner' starting with 'model': {result.completion.values}\")\n\n                # Complete with context - repo suggestions based on owner\n                result = await session.complete(\n                    ref=ResourceTemplateReference(type=\"ref/resource\", uri=template.uri_template),\n                    argument={\"name\": \"repo\", \"value\": \"\"},\n                    context_arguments={\"owner\": \"modelcontextprotocol\"},\n                )\n                print(f\"Completions for 'repo' with owner='modelcontextprotocol': {result.completion.values}\")\n\n            # Complete prompt arguments\n            if prompts.prompts:\n                prompt_name = prompts.prompts[0].name\n                print(f\"\\nCompleting arguments for prompt: {prompt_name}\")\n\n                result = await session.complete(\n                    ref=PromptReference(type=\"ref/prompt\", name=prompt_name),\n                    argument={\"name\": \"style\", \"value\": \"\"},\n                )\n                print(f\"Completions for 'style' argument: {result.completion.values}\")\n\n\ndef main():\n    \"\"\"Entry point for the completion client.\"\"\"\n    asyncio.run(run())\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/snippets/clients/display_utilities.py",
    "content": "\"\"\"cd to the `examples/snippets` directory and run:\nuv run display-utilities-client\n\"\"\"\n\nimport asyncio\nimport os\n\nfrom mcp import ClientSession, StdioServerParameters\nfrom mcp.client.stdio import stdio_client\nfrom mcp.shared.metadata_utils import get_display_name\n\n# Create server parameters for stdio connection\nserver_params = StdioServerParameters(\n    command=\"uv\",  # Using uv to run the server\n    args=[\"run\", \"server\", \"mcpserver_quickstart\", \"stdio\"],\n    env={\"UV_INDEX\": os.environ.get(\"UV_INDEX\", \"\")},\n)\n\n\nasync def display_tools(session: ClientSession):\n    \"\"\"Display available tools with human-readable names\"\"\"\n    tools_response = await session.list_tools()\n\n    for tool in tools_response.tools:\n        # get_display_name() returns the title if available, otherwise the name\n        display_name = get_display_name(tool)\n        print(f\"Tool: {display_name}\")\n        if tool.description:\n            print(f\"   {tool.description}\")\n\n\nasync def display_resources(session: ClientSession):\n    \"\"\"Display available resources with human-readable names\"\"\"\n    resources_response = await session.list_resources()\n\n    for resource in resources_response.resources:\n        display_name = get_display_name(resource)\n        print(f\"Resource: {display_name} ({resource.uri})\")\n\n    templates_response = await session.list_resource_templates()\n    for template in templates_response.resource_templates:\n        display_name = get_display_name(template)\n        print(f\"Resource Template: {display_name}\")\n\n\nasync def run():\n    \"\"\"Run the display utilities example.\"\"\"\n    async with stdio_client(server_params) as (read, write):\n        async with ClientSession(read, write) as session:\n            # Initialize the connection\n            await session.initialize()\n\n            print(\"=== Available Tools ===\")\n            await display_tools(session)\n\n            print(\"\\n=== Available Resources ===\")\n            await display_resources(session)\n\n\ndef main():\n    \"\"\"Entry point for the display utilities client.\"\"\"\n    asyncio.run(run())\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/snippets/clients/oauth_client.py",
    "content": "\"\"\"Before running, specify running MCP RS server URL.\nTo spin up RS server locally, see\n    examples/servers/simple-auth/README.md\n\ncd to the `examples/snippets` directory and run:\n    uv run oauth-client\n\"\"\"\n\nimport asyncio\nfrom urllib.parse import parse_qs, urlparse\n\nimport httpx\nfrom pydantic import AnyUrl\n\nfrom mcp import ClientSession\nfrom mcp.client.auth import OAuthClientProvider, TokenStorage\nfrom mcp.client.streamable_http import streamable_http_client\nfrom mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken\n\n\nclass InMemoryTokenStorage(TokenStorage):\n    \"\"\"Demo In-memory token storage implementation.\"\"\"\n\n    def __init__(self):\n        self.tokens: OAuthToken | None = None\n        self.client_info: OAuthClientInformationFull | None = None\n\n    async def get_tokens(self) -> OAuthToken | None:\n        \"\"\"Get stored tokens.\"\"\"\n        return self.tokens\n\n    async def set_tokens(self, tokens: OAuthToken) -> None:\n        \"\"\"Store tokens.\"\"\"\n        self.tokens = tokens\n\n    async def get_client_info(self) -> OAuthClientInformationFull | None:\n        \"\"\"Get stored client information.\"\"\"\n        return self.client_info\n\n    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:\n        \"\"\"Store client information.\"\"\"\n        self.client_info = client_info\n\n\nasync def handle_redirect(auth_url: str) -> None:\n    print(f\"Visit: {auth_url}\")\n\n\nasync def handle_callback() -> tuple[str, str | None]:\n    callback_url = input(\"Paste callback URL: \")\n    params = parse_qs(urlparse(callback_url).query)\n    return params[\"code\"][0], params.get(\"state\", [None])[0]\n\n\nasync def main():\n    \"\"\"Run the OAuth client example.\"\"\"\n    oauth_auth = OAuthClientProvider(\n        server_url=\"http://localhost:8001\",\n        client_metadata=OAuthClientMetadata(\n            client_name=\"Example MCP Client\",\n            redirect_uris=[AnyUrl(\"http://localhost:3000/callback\")],\n            grant_types=[\"authorization_code\", \"refresh_token\"],\n            response_types=[\"code\"],\n            scope=\"user\",\n        ),\n        storage=InMemoryTokenStorage(),\n        redirect_handler=handle_redirect,\n        callback_handler=handle_callback,\n    )\n\n    async with httpx.AsyncClient(auth=oauth_auth, follow_redirects=True) as custom_client:\n        async with streamable_http_client(\"http://localhost:8001/mcp\", http_client=custom_client) as (read, write):\n            async with ClientSession(read, write) as session:\n                await session.initialize()\n\n                tools = await session.list_tools()\n                print(f\"Available tools: {[tool.name for tool in tools.tools]}\")\n\n                resources = await session.list_resources()\n                print(f\"Available resources: {[r.uri for r in resources.resources]}\")\n\n\ndef run():\n    asyncio.run(main())\n\n\nif __name__ == \"__main__\":\n    run()\n"
  },
  {
    "path": "examples/snippets/clients/pagination_client.py",
    "content": "\"\"\"Example of consuming paginated MCP endpoints from a client.\"\"\"\n\nimport asyncio\n\nfrom mcp.client.session import ClientSession\nfrom mcp.client.stdio import StdioServerParameters, stdio_client\nfrom mcp.types import PaginatedRequestParams, Resource\n\n\nasync def list_all_resources() -> None:\n    \"\"\"Fetch all resources using pagination.\"\"\"\n    async with stdio_client(StdioServerParameters(command=\"uv\", args=[\"run\", \"mcp-simple-pagination\"])) as (\n        read,\n        write,\n    ):\n        async with ClientSession(read, write) as session:\n            await session.initialize()\n\n            all_resources: list[Resource] = []\n            cursor = None\n\n            while True:\n                # Fetch a page of resources\n                result = await session.list_resources(params=PaginatedRequestParams(cursor=cursor))\n                all_resources.extend(result.resources)\n\n                print(f\"Fetched {len(result.resources)} resources\")\n\n                # Check if there are more pages\n                if result.next_cursor:\n                    cursor = result.next_cursor\n                else:\n                    break\n\n            print(f\"Total resources: {len(all_resources)}\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(list_all_resources())\n"
  },
  {
    "path": "examples/snippets/clients/parsing_tool_results.py",
    "content": "\"\"\"examples/snippets/clients/parsing_tool_results.py\"\"\"\n\nimport asyncio\n\nfrom mcp import ClientSession, StdioServerParameters, types\nfrom mcp.client.stdio import stdio_client\n\n\nasync def parse_tool_results():\n    \"\"\"Demonstrates how to parse different types of content in CallToolResult.\"\"\"\n    server_params = StdioServerParameters(command=\"python\", args=[\"path/to/mcp_server.py\"])\n\n    async with stdio_client(server_params) as (read, write):\n        async with ClientSession(read, write) as session:\n            await session.initialize()\n\n            # Example 1: Parsing text content\n            result = await session.call_tool(\"get_data\", {\"format\": \"text\"})\n            for content in result.content:\n                if isinstance(content, types.TextContent):\n                    print(f\"Text: {content.text}\")\n\n            # Example 2: Parsing structured content from JSON tools\n            result = await session.call_tool(\"get_user\", {\"id\": \"123\"})\n            if hasattr(result, \"structured_content\") and result.structured_content:\n                # Access structured data directly\n                user_data = result.structured_content\n                print(f\"User: {user_data.get('name')}, Age: {user_data.get('age')}\")\n\n            # Example 3: Parsing embedded resources\n            result = await session.call_tool(\"read_config\", {})\n            for content in result.content:\n                if isinstance(content, types.EmbeddedResource):\n                    resource = content.resource\n                    if isinstance(resource, types.TextResourceContents):\n                        print(f\"Config from {resource.uri}: {resource.text}\")\n                    else:\n                        print(f\"Binary data from {resource.uri}\")\n\n            # Example 4: Parsing image content\n            result = await session.call_tool(\"generate_chart\", {\"data\": [1, 2, 3]})\n            for content in result.content:\n                if isinstance(content, types.ImageContent):\n                    print(f\"Image ({content.mime_type}): {len(content.data)} bytes\")\n\n            # Example 5: Handling errors\n            result = await session.call_tool(\"failing_tool\", {})\n            if result.is_error:\n                print(\"Tool execution failed!\")\n                for content in result.content:\n                    if isinstance(content, types.TextContent):\n                        print(f\"Error: {content.text}\")\n\n\nasync def main():\n    await parse_tool_results()\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/snippets/clients/stdio_client.py",
    "content": "\"\"\"cd to the `examples/snippets/clients` directory and run:\nuv run client\n\"\"\"\n\nimport asyncio\nimport os\n\nfrom mcp import ClientSession, StdioServerParameters, types\nfrom mcp.client.context import ClientRequestContext\nfrom mcp.client.stdio import stdio_client\n\n# Create server parameters for stdio connection\nserver_params = StdioServerParameters(\n    command=\"uv\",  # Using uv to run the server\n    args=[\"run\", \"server\", \"mcpserver_quickstart\", \"stdio\"],  # We're already in snippets dir\n    env={\"UV_INDEX\": os.environ.get(\"UV_INDEX\", \"\")},\n)\n\n\n# Optional: create a sampling callback\nasync def handle_sampling_message(\n    context: ClientRequestContext, params: types.CreateMessageRequestParams\n) -> types.CreateMessageResult:\n    print(f\"Sampling request: {params.messages}\")\n    return types.CreateMessageResult(\n        role=\"assistant\",\n        content=types.TextContent(\n            type=\"text\",\n            text=\"Hello, world! from model\",\n        ),\n        model=\"gpt-3.5-turbo\",\n        stop_reason=\"endTurn\",\n    )\n\n\nasync def run():\n    async with stdio_client(server_params) as (read, write):\n        async with ClientSession(read, write, sampling_callback=handle_sampling_message) as session:\n            # Initialize the connection\n            await session.initialize()\n\n            # List available prompts\n            prompts = await session.list_prompts()\n            print(f\"Available prompts: {[p.name for p in prompts.prompts]}\")\n\n            # Get a prompt (greet_user prompt from mcpserver_quickstart)\n            if prompts.prompts:\n                prompt = await session.get_prompt(\"greet_user\", arguments={\"name\": \"Alice\", \"style\": \"friendly\"})\n                print(f\"Prompt result: {prompt.messages[0].content}\")\n\n            # List available resources\n            resources = await session.list_resources()\n            print(f\"Available resources: {[r.uri for r in resources.resources]}\")\n\n            # List available tools\n            tools = await session.list_tools()\n            print(f\"Available tools: {[t.name for t in tools.tools]}\")\n\n            # Read a resource (greeting resource from mcpserver_quickstart)\n            resource_content = await session.read_resource(\"greeting://World\")\n            content_block = resource_content.contents[0]\n            if isinstance(content_block, types.TextContent):\n                print(f\"Resource content: {content_block.text}\")\n\n            # Call a tool (add tool from mcpserver_quickstart)\n            result = await session.call_tool(\"add\", arguments={\"a\": 5, \"b\": 3})\n            result_unstructured = result.content[0]\n            if isinstance(result_unstructured, types.TextContent):\n                print(f\"Tool result: {result_unstructured.text}\")\n            result_structured = result.structured_content\n            print(f\"Structured tool result: {result_structured}\")\n\n\ndef main():\n    \"\"\"Entry point for the client script.\"\"\"\n    asyncio.run(run())\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/snippets/clients/streamable_basic.py",
    "content": "\"\"\"Run from the repository root:\nuv run examples/snippets/clients/streamable_basic.py\n\"\"\"\n\nimport asyncio\n\nfrom mcp import ClientSession\nfrom mcp.client.streamable_http import streamable_http_client\n\n\nasync def main():\n    # Connect to a streamable HTTP server\n    async with streamable_http_client(\"http://localhost:8000/mcp\") as (read_stream, write_stream):\n        # Create a session using the client streams\n        async with ClientSession(read_stream, write_stream) as session:\n            # Initialize the connection\n            await session.initialize()\n            # List available tools\n            tools = await session.list_tools()\n            print(f\"Available tools: {[tool.name for tool in tools.tools]}\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "examples/snippets/clients/url_elicitation_client.py",
    "content": "\"\"\"URL Elicitation Client Example.\n\nDemonstrates how clients handle URL elicitation requests from servers.\nThis is the Python equivalent of TypeScript SDK's elicitationUrlExample.ts,\nfocused on URL elicitation patterns without OAuth complexity.\n\nFeatures demonstrated:\n1. Client elicitation capability declaration\n2. Handling elicitation requests from servers via callback\n3. Catching UrlElicitationRequiredError from tool calls\n4. Browser interaction with security warnings\n5. Interactive CLI for testing\n\nRun with:\n    cd examples/snippets\n    uv run elicitation-client\n\nRequires a server with URL elicitation tools running. Start the elicitation\nserver first:\n    uv run server elicitation sse\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport webbrowser\nfrom typing import Any\nfrom urllib.parse import urlparse\n\nfrom mcp import ClientSession, types\nfrom mcp.client.context import ClientRequestContext\nfrom mcp.client.sse import sse_client\nfrom mcp.shared.exceptions import MCPError, UrlElicitationRequiredError\nfrom mcp.types import URL_ELICITATION_REQUIRED\n\n\nasync def handle_elicitation(\n    context: ClientRequestContext,\n    params: types.ElicitRequestParams,\n) -> types.ElicitResult | types.ErrorData:\n    \"\"\"Handle elicitation requests from the server.\n\n    This callback is invoked when the server sends an elicitation/request.\n    For URL mode, we prompt the user and optionally open their browser.\n    \"\"\"\n    if params.mode == \"url\":\n        return await handle_url_elicitation(params)\n    else:\n        # We only support URL mode in this example\n        return types.ErrorData(\n            code=types.INVALID_REQUEST,\n            message=f\"Unsupported elicitation mode: {params.mode}\",\n        )\n\n\nALLOWED_SCHEMES = {\"http\", \"https\"}\n\n\nasync def handle_url_elicitation(\n    params: types.ElicitRequestParams,\n) -> types.ElicitResult:\n    \"\"\"Handle URL mode elicitation - show security warning and optionally open browser.\n\n    This function demonstrates the security-conscious approach to URL elicitation:\n    1. Validate the URL scheme before prompting the user\n    2. Display the full URL and domain for user inspection\n    3. Show the server's reason for requesting this interaction\n    4. Require explicit user consent before opening any URL\n    \"\"\"\n    # Extract URL parameters - these are available on URL mode requests\n    url = getattr(params, \"url\", None)\n    elicitation_id = getattr(params, \"elicitationId\", None)\n    message = params.message\n\n    if not url:\n        print(\"Error: No URL provided in elicitation request\")\n        return types.ElicitResult(action=\"cancel\")\n\n    # Reject dangerous URL schemes before prompting the user\n    parsed = urlparse(str(url))\n    if parsed.scheme.lower() not in ALLOWED_SCHEMES:\n        print(f\"\\nRejecting URL with disallowed scheme '{parsed.scheme}': {url}\")\n        return types.ElicitResult(action=\"decline\")\n\n    # Extract domain for security display\n    domain = extract_domain(url)\n\n    # Security warning - always show the user what they're being asked to do\n    print(\"\\n\" + \"=\" * 60)\n    print(\"SECURITY WARNING: External URL Request\")\n    print(\"=\" * 60)\n    print(\"\\nThe server is requesting you to open an external URL.\")\n    print(f\"\\n  Domain:  {domain}\")\n    print(f\"  Full URL: {url}\")\n    print(\"\\n  Server's reason:\")\n    print(f\"    {message}\")\n    print(f\"\\n  Elicitation ID: {elicitation_id}\")\n    print(\"\\n\" + \"-\" * 60)\n\n    # Get explicit user consent\n    try:\n        response = input(\"\\nOpen this URL in your browser? (y/n): \").strip().lower()\n    except EOFError:\n        return types.ElicitResult(action=\"cancel\")\n\n    if response in (\"n\", \"no\"):\n        print(\"URL navigation declined.\")\n        return types.ElicitResult(action=\"decline\")\n    elif response not in (\"y\", \"yes\"):\n        print(\"Invalid response. Cancelling.\")\n        return types.ElicitResult(action=\"cancel\")\n\n    # Open the browser\n    print(f\"\\nOpening browser to: {url}\")\n    try:\n        webbrowser.open(url)\n    except Exception as e:\n        print(f\"Failed to open browser: {e}\")\n        print(f\"Please manually open: {url}\")\n\n    print(\"Waiting for you to complete the interaction in your browser...\")\n    print(\"(The server will continue once you've finished)\")\n\n    return types.ElicitResult(action=\"accept\")\n\n\ndef extract_domain(url: str) -> str:\n    \"\"\"Extract domain from URL for security display.\"\"\"\n    try:\n        return urlparse(url).netloc\n    except Exception:\n        return \"unknown\"\n\n\nasync def call_tool_with_error_handling(\n    session: ClientSession,\n    tool_name: str,\n    arguments: dict[str, Any],\n) -> types.CallToolResult | None:\n    \"\"\"Call a tool, handling UrlElicitationRequiredError if raised.\n\n    When a server tool needs URL elicitation before it can proceed,\n    it can either:\n    1. Send an elicitation request directly (handled by elicitation_callback)\n    2. Return an error with code -32042 (URL_ELICITATION_REQUIRED)\n\n    This function demonstrates handling case 2 - catching the error\n    and processing the required URL elicitations.\n    \"\"\"\n    try:\n        result = await session.call_tool(tool_name, arguments)\n\n        # Check if the tool returned an error in the result\n        if result.is_error:\n            print(f\"Tool returned error: {result.content}\")\n            return None\n\n        return result\n\n    except MCPError as e:\n        # Check if this is a URL elicitation required error\n        if e.code == URL_ELICITATION_REQUIRED:\n            print(\"\\n[Tool requires URL elicitation to proceed]\")\n\n            # Convert to typed error to access elicitations\n            url_error = UrlElicitationRequiredError.from_error(e.error)\n\n            # Process each required elicitation\n            for elicitation in url_error.elicitations:\n                await handle_url_elicitation(elicitation)\n\n            return None\n        else:\n            # Re-raise other MCP errors\n            print(f\"MCP Error: {e.error.message} (code: {e.error.code})\")\n            return None\n\n\ndef print_help() -> None:\n    \"\"\"Print available commands.\"\"\"\n    print(\"\\nAvailable commands:\")\n    print(\"  list-tools              - List available tools\")\n    print(\"  call <name> [json-args] - Call a tool with optional JSON arguments\")\n    print(\"  secure-payment          - Test URL elicitation via ctx.elicit_url()\")\n    print(\"  connect-service         - Test URL elicitation via UrlElicitationRequiredError\")\n    print(\"  help                    - Show this help\")\n    print(\"  quit                    - Exit the program\")\n\n\ndef print_tool_result(result: types.CallToolResult | None) -> None:\n    \"\"\"Print a tool call result.\"\"\"\n    if not result:\n        return\n    print(\"\\nTool result:\")\n    for content in result.content:\n        if isinstance(content, types.TextContent):\n            print(f\"  {content.text}\")\n        else:\n            print(f\"  [{content.type}]\")\n\n\nasync def handle_list_tools(session: ClientSession) -> None:\n    \"\"\"Handle the list-tools command.\"\"\"\n    tools = await session.list_tools()\n    if tools.tools:\n        print(\"\\nAvailable tools:\")\n        for tool in tools.tools:\n            print(f\"  - {tool.name}: {tool.description or 'No description'}\")\n    else:\n        print(\"No tools available\")\n\n\nasync def handle_call_command(session: ClientSession, command: str) -> None:\n    \"\"\"Handle the call command.\"\"\"\n    parts = command.split(maxsplit=2)\n    if len(parts) < 2:\n        print(\"Usage: call <tool-name> [json-args]\")\n        return\n\n    tool_name = parts[1]\n    args: dict[str, Any] = {}\n    if len(parts) > 2:\n        try:\n            args = json.loads(parts[2])\n        except json.JSONDecodeError as e:\n            print(f\"Invalid JSON arguments: {e}\")\n            return\n\n    print(f\"\\nCalling tool '{tool_name}' with args: {args}\")\n    result = await call_tool_with_error_handling(session, tool_name, args)\n    print_tool_result(result)\n\n\nasync def process_command(session: ClientSession, command: str) -> bool:\n    \"\"\"Process a single command. Returns False if should exit.\"\"\"\n    if command in {\"quit\", \"exit\"}:\n        print(\"Goodbye!\")\n        return False\n\n    if command == \"help\":\n        print_help()\n    elif command == \"list-tools\":\n        await handle_list_tools(session)\n    elif command.startswith(\"call \"):\n        await handle_call_command(session, command)\n    elif command == \"secure-payment\":\n        print(\"\\nTesting secure_payment tool (uses ctx.elicit_url())...\")\n        result = await call_tool_with_error_handling(session, \"secure_payment\", {\"amount\": 99.99})\n        print_tool_result(result)\n    elif command == \"connect-service\":\n        print(\"\\nTesting connect_service tool (raises UrlElicitationRequiredError)...\")\n        result = await call_tool_with_error_handling(session, \"connect_service\", {\"service_name\": \"github\"})\n        print_tool_result(result)\n    else:\n        print(f\"Unknown command: {command}\")\n        print(\"Type 'help' for available commands.\")\n\n    return True\n\n\nasync def run_command_loop(session: ClientSession) -> None:\n    \"\"\"Run the interactive command loop.\"\"\"\n    while True:\n        try:\n            command = input(\"> \").strip()\n        except EOFError:\n            break\n        except KeyboardInterrupt:\n            print(\"\\n\")\n            break\n\n        if not command:\n            continue\n\n        if not await process_command(session, command):\n            break\n\n\nasync def main() -> None:\n    \"\"\"Run the interactive URL elicitation client.\"\"\"\n    server_url = \"http://localhost:8000/sse\"\n\n    print(\"=\" * 60)\n    print(\"URL Elicitation Client Example\")\n    print(\"=\" * 60)\n    print(f\"\\nConnecting to: {server_url}\")\n    print(\"(Start server with: cd examples/snippets && uv run server elicitation sse)\")\n\n    try:\n        async with sse_client(server_url) as (read, write):\n            async with ClientSession(\n                read,\n                write,\n                elicitation_callback=handle_elicitation,\n            ) as session:\n                await session.initialize()\n                print(\"\\nConnected! Type 'help' for available commands.\\n\")\n                await run_command_loop(session)\n\n    except ConnectionRefusedError:\n        print(f\"\\nError: Could not connect to {server_url}\")\n        print(\"Make sure the elicitation server is running:\")\n        print(\"  cd examples/snippets && uv run server elicitation sse\")\n    except Exception as e:\n        print(f\"\\nError: {e}\")\n        raise\n\n\ndef run() -> None:\n    \"\"\"Entry point for the client script.\"\"\"\n    asyncio.run(main())\n\n\nif __name__ == \"__main__\":\n    run()\n"
  },
  {
    "path": "examples/snippets/pyproject.toml",
    "content": "[project]\nname = \"mcp-snippets\"\nversion = \"0.1.0\"\ndescription = \"MCP Example Snippets\"\nrequires-python = \">=3.10\"\ndependencies = [\n    \"mcp\",\n]\n\n[build-system]\nrequires = [\"setuptools\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[tool.setuptools]\npackages = [\"servers\", \"clients\"]\n\n[project.scripts]\nserver = \"servers:run_server\"\nclient = \"clients.stdio_client:main\"\ncompletion-client = \"clients.completion_client:main\"\ndirect-execution-server = \"servers.direct_execution:main\"\ndisplay-utilities-client = \"clients.display_utilities:main\"\noauth-client = \"clients.oauth_client:run\"\nelicitation-client = \"clients.url_elicitation_client:run\"\n"
  },
  {
    "path": "examples/snippets/servers/__init__.py",
    "content": "\"\"\"MCP Snippets.\n\nThis package contains simple examples of MCP server features.\nEach server demonstrates a single feature and can be run as a standalone server.\n\nTo run a server, use the command:\n    uv run server basic_tool sse\n\"\"\"\n\nimport importlib\nimport sys\nfrom typing import Literal, cast\n\n\ndef run_server():\n    \"\"\"Run a server by name with optional transport.\n\n    Usage: server <server-name> [transport]\n    Example: server basic_tool sse\n    \"\"\"\n    if len(sys.argv) < 2:\n        print(\"Usage: server <server-name> [transport]\")\n        print(\"Available servers: basic_tool, basic_resource, basic_prompt, tool_progress,\")\n        print(\"                   sampling, elicitation, completion, notifications,\")\n        print(\"                   mcpserver_quickstart, structured_output, images\")\n        print(\"Available transports: stdio (default), sse, streamable-http\")\n        sys.exit(1)\n\n    server_name = sys.argv[1]\n    transport = sys.argv[2] if len(sys.argv) > 2 else \"stdio\"\n\n    try:\n        module = importlib.import_module(f\".{server_name}\", package=__name__)\n        module.mcp.run(cast(Literal[\"stdio\", \"sse\", \"streamable-http\"], transport))\n    except ImportError:\n        print(f\"Error: Server '{server_name}' not found\")\n        sys.exit(1)\n"
  },
  {
    "path": "examples/snippets/servers/basic_prompt.py",
    "content": "from mcp.server.mcpserver import MCPServer\nfrom mcp.server.mcpserver.prompts import base\n\nmcp = MCPServer(name=\"Prompt Example\")\n\n\n@mcp.prompt(title=\"Code Review\")\ndef review_code(code: str) -> str:\n    return f\"Please review this code:\\n\\n{code}\"\n\n\n@mcp.prompt(title=\"Debug Assistant\")\ndef debug_error(error: str) -> list[base.Message]:\n    return [\n        base.UserMessage(\"I'm seeing this error:\"),\n        base.UserMessage(error),\n        base.AssistantMessage(\"I'll help debug that. What have you tried so far?\"),\n    ]\n"
  },
  {
    "path": "examples/snippets/servers/basic_resource.py",
    "content": "from mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(name=\"Resource Example\")\n\n\n@mcp.resource(\"file://documents/{name}\")\ndef read_document(name: str) -> str:\n    \"\"\"Read a document by name.\"\"\"\n    # This would normally read from disk\n    return f\"Content of {name}\"\n\n\n@mcp.resource(\"config://settings\")\ndef get_settings() -> str:\n    \"\"\"Get application settings.\"\"\"\n    return \"\"\"{\n  \"theme\": \"dark\",\n  \"language\": \"en\",\n  \"debug\": false\n}\"\"\"\n"
  },
  {
    "path": "examples/snippets/servers/basic_tool.py",
    "content": "from mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(name=\"Tool Example\")\n\n\n@mcp.tool()\ndef sum(a: int, b: int) -> int:\n    \"\"\"Add two numbers together.\"\"\"\n    return a + b\n\n\n@mcp.tool()\ndef get_weather(city: str, unit: str = \"celsius\") -> str:\n    \"\"\"Get weather for a city.\"\"\"\n    # This would normally call a weather API\n    return f\"Weather in {city}: 22degrees{unit[0].upper()}\"\n"
  },
  {
    "path": "examples/snippets/servers/completion.py",
    "content": "from mcp.server.mcpserver import MCPServer\nfrom mcp.types import (\n    Completion,\n    CompletionArgument,\n    CompletionContext,\n    PromptReference,\n    ResourceTemplateReference,\n)\n\nmcp = MCPServer(name=\"Example\")\n\n\n@mcp.resource(\"github://repos/{owner}/{repo}\")\ndef github_repo(owner: str, repo: str) -> str:\n    \"\"\"GitHub repository resource.\"\"\"\n    return f\"Repository: {owner}/{repo}\"\n\n\n@mcp.prompt(description=\"Code review prompt\")\ndef review_code(language: str, code: str) -> str:\n    \"\"\"Generate a code review.\"\"\"\n    return f\"Review this {language} code:\\n{code}\"\n\n\n@mcp.completion()\nasync def handle_completion(\n    ref: PromptReference | ResourceTemplateReference,\n    argument: CompletionArgument,\n    context: CompletionContext | None,\n) -> Completion | None:\n    \"\"\"Provide completions for prompts and resources.\"\"\"\n\n    # Complete programming languages for the prompt\n    if isinstance(ref, PromptReference):\n        if ref.name == \"review_code\" and argument.name == \"language\":\n            languages = [\"python\", \"javascript\", \"typescript\", \"go\", \"rust\"]\n            return Completion(\n                values=[lang for lang in languages if lang.startswith(argument.value)],\n                has_more=False,\n            )\n\n    # Complete repository names for GitHub resources\n    if isinstance(ref, ResourceTemplateReference):\n        if ref.uri == \"github://repos/{owner}/{repo}\" and argument.name == \"repo\":\n            if context and context.arguments and context.arguments.get(\"owner\") == \"modelcontextprotocol\":\n                repos = [\"python-sdk\", \"typescript-sdk\", \"specification\"]\n                return Completion(values=repos, has_more=False)\n\n    return None\n"
  },
  {
    "path": "examples/snippets/servers/direct_call_tool_result.py",
    "content": "\"\"\"Example showing direct CallToolResult return for advanced control.\"\"\"\n\nfrom typing import Annotated\n\nfrom pydantic import BaseModel\n\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.types import CallToolResult, TextContent\n\nmcp = MCPServer(\"CallToolResult Example\")\n\n\nclass ValidationModel(BaseModel):\n    \"\"\"Model for validating structured output.\"\"\"\n\n    status: str\n    data: dict[str, int]\n\n\n@mcp.tool()\ndef advanced_tool() -> CallToolResult:\n    \"\"\"Return CallToolResult directly for full control including _meta field.\"\"\"\n    return CallToolResult(\n        content=[TextContent(type=\"text\", text=\"Response visible to the model\")],\n        _meta={\"hidden\": \"data for client applications only\"},\n    )\n\n\n@mcp.tool()\ndef validated_tool() -> Annotated[CallToolResult, ValidationModel]:\n    \"\"\"Return CallToolResult with structured output validation.\"\"\"\n    return CallToolResult(\n        content=[TextContent(type=\"text\", text=\"Validated response\")],\n        structured_content={\"status\": \"success\", \"data\": {\"result\": 42}},\n        _meta={\"internal\": \"metadata\"},\n    )\n\n\n@mcp.tool()\ndef empty_result_tool() -> CallToolResult:\n    \"\"\"For empty results, return CallToolResult with empty content.\"\"\"\n    return CallToolResult(content=[])\n"
  },
  {
    "path": "examples/snippets/servers/direct_execution.py",
    "content": "\"\"\"Example showing direct execution of an MCP server.\n\nThis is the simplest way to run an MCP server directly.\ncd to the `examples/snippets` directory and run:\n    uv run direct-execution-server\n    or\n    python servers/direct_execution.py\n\"\"\"\n\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(\"My App\")\n\n\n@mcp.tool()\ndef hello(name: str = \"World\") -> str:\n    \"\"\"Say hello to someone.\"\"\"\n    return f\"Hello, {name}!\"\n\n\ndef main():\n    \"\"\"Entry point for the direct execution server.\"\"\"\n    mcp.run()\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "examples/snippets/servers/elicitation.py",
    "content": "\"\"\"Elicitation examples demonstrating form and URL mode elicitation.\n\nForm mode elicitation collects structured, non-sensitive data through a schema.\nURL mode elicitation directs users to external URLs for sensitive operations\nlike OAuth flows, credential collection, or payment processing.\n\"\"\"\n\nimport uuid\n\nfrom pydantic import BaseModel, Field\n\nfrom mcp.server.mcpserver import Context, MCPServer\nfrom mcp.shared.exceptions import UrlElicitationRequiredError\nfrom mcp.types import ElicitRequestURLParams\n\nmcp = MCPServer(name=\"Elicitation Example\")\n\n\nclass BookingPreferences(BaseModel):\n    \"\"\"Schema for collecting user preferences.\"\"\"\n\n    checkAlternative: bool = Field(description=\"Would you like to check another date?\")\n    alternativeDate: str = Field(\n        default=\"2024-12-26\",\n        description=\"Alternative date (YYYY-MM-DD)\",\n    )\n\n\n@mcp.tool()\nasync def book_table(date: str, time: str, party_size: int, ctx: Context) -> str:\n    \"\"\"Book a table with date availability check.\n\n    This demonstrates form mode elicitation for collecting non-sensitive user input.\n    \"\"\"\n    # Check if date is available\n    if date == \"2024-12-25\":\n        # Date unavailable - ask user for alternative\n        result = await ctx.elicit(\n            message=(f\"No tables available for {party_size} on {date}. Would you like to try another date?\"),\n            schema=BookingPreferences,\n        )\n\n        if result.action == \"accept\" and result.data:\n            if result.data.checkAlternative:\n                return f\"[SUCCESS] Booked for {result.data.alternativeDate}\"\n            return \"[CANCELLED] No booking made\"\n        return \"[CANCELLED] Booking cancelled\"\n\n    # Date available\n    return f\"[SUCCESS] Booked for {date} at {time}\"\n\n\n@mcp.tool()\nasync def secure_payment(amount: float, ctx: Context) -> str:\n    \"\"\"Process a secure payment requiring URL confirmation.\n\n    This demonstrates URL mode elicitation using ctx.elicit_url() for\n    operations that require out-of-band user interaction.\n    \"\"\"\n    elicitation_id = str(uuid.uuid4())\n\n    result = await ctx.elicit_url(\n        message=f\"Please confirm payment of ${amount:.2f}\",\n        url=f\"https://payments.example.com/confirm?amount={amount}&id={elicitation_id}\",\n        elicitation_id=elicitation_id,\n    )\n\n    if result.action == \"accept\":\n        # In a real app, the payment confirmation would happen out-of-band\n        # and you'd verify the payment status from your backend\n        return f\"Payment of ${amount:.2f} initiated - check your browser to complete\"\n    elif result.action == \"decline\":\n        return \"Payment declined by user\"\n    return \"Payment cancelled\"\n\n\n@mcp.tool()\nasync def connect_service(service_name: str, ctx: Context) -> str:\n    \"\"\"Connect to a third-party service requiring OAuth authorization.\n\n    This demonstrates the \"throw error\" pattern using UrlElicitationRequiredError.\n    Use this pattern when the tool cannot proceed without user authorization.\n    \"\"\"\n    elicitation_id = str(uuid.uuid4())\n\n    # Raise UrlElicitationRequiredError to signal that the client must complete\n    # a URL elicitation before this request can be processed.\n    # The MCP framework will convert this to a -32042 error response.\n    raise UrlElicitationRequiredError(\n        [\n            ElicitRequestURLParams(\n                mode=\"url\",\n                message=f\"Authorization required to connect to {service_name}\",\n                url=f\"https://{service_name}.example.com/oauth/authorize?elicit={elicitation_id}\",\n                elicitation_id=elicitation_id,\n            )\n        ]\n    )\n"
  },
  {
    "path": "examples/snippets/servers/images.py",
    "content": "\"\"\"Example showing image handling with MCPServer.\"\"\"\n\nfrom PIL import Image as PILImage\n\nfrom mcp.server.mcpserver import Image, MCPServer\n\nmcp = MCPServer(\"Image Example\")\n\n\n@mcp.tool()\ndef create_thumbnail(image_path: str) -> Image:\n    \"\"\"Create a thumbnail from an image\"\"\"\n    img = PILImage.open(image_path)\n    img.thumbnail((100, 100))\n    return Image(data=img.tobytes(), format=\"png\")\n"
  },
  {
    "path": "examples/snippets/servers/lifespan_example.py",
    "content": "\"\"\"Example showing lifespan support for startup/shutdown with strong typing.\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom dataclasses import dataclass\n\nfrom mcp.server.mcpserver import Context, MCPServer\n\n\n# Mock database class for example\nclass Database:\n    \"\"\"Mock database class for example.\"\"\"\n\n    @classmethod\n    async def connect(cls) -> \"Database\":\n        \"\"\"Connect to database.\"\"\"\n        return cls()\n\n    async def disconnect(self) -> None:\n        \"\"\"Disconnect from database.\"\"\"\n        pass\n\n    def query(self) -> str:\n        \"\"\"Execute a query.\"\"\"\n        return \"Query result\"\n\n\n@dataclass\nclass AppContext:\n    \"\"\"Application context with typed dependencies.\"\"\"\n\n    db: Database\n\n\n@asynccontextmanager\nasync def app_lifespan(server: MCPServer) -> AsyncIterator[AppContext]:\n    \"\"\"Manage application lifecycle with type-safe context.\"\"\"\n    # Initialize on startup\n    db = await Database.connect()\n    try:\n        yield AppContext(db=db)\n    finally:\n        # Cleanup on shutdown\n        await db.disconnect()\n\n\n# Pass lifespan to server\nmcp = MCPServer(\"My App\", lifespan=app_lifespan)\n\n\n# Access type-safe lifespan context in tools\n@mcp.tool()\ndef query_db(ctx: Context[AppContext]) -> str:\n    \"\"\"Tool that uses initialized resources.\"\"\"\n    db = ctx.request_context.lifespan_context.db\n    return db.query()\n"
  },
  {
    "path": "examples/snippets/servers/lowlevel/__init__.py",
    "content": "\"\"\"Low-level server examples for MCP Python SDK.\"\"\"\n"
  },
  {
    "path": "examples/snippets/servers/lowlevel/basic.py",
    "content": "\"\"\"Run from the repository root:\nuv run examples/snippets/servers/lowlevel/basic.py\n\"\"\"\n\nimport asyncio\n\nimport mcp.server.stdio\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\n\n\nasync def handle_list_prompts(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListPromptsResult:\n    \"\"\"List available prompts.\"\"\"\n    return types.ListPromptsResult(\n        prompts=[\n            types.Prompt(\n                name=\"example-prompt\",\n                description=\"An example prompt template\",\n                arguments=[types.PromptArgument(name=\"arg1\", description=\"Example argument\", required=True)],\n            )\n        ]\n    )\n\n\nasync def handle_get_prompt(ctx: ServerRequestContext, params: types.GetPromptRequestParams) -> types.GetPromptResult:\n    \"\"\"Get a specific prompt by name.\"\"\"\n    if params.name != \"example-prompt\":\n        raise ValueError(f\"Unknown prompt: {params.name}\")\n\n    arg1_value = (params.arguments or {}).get(\"arg1\", \"default\")\n\n    return types.GetPromptResult(\n        description=\"Example prompt\",\n        messages=[\n            types.PromptMessage(\n                role=\"user\",\n                content=types.TextContent(type=\"text\", text=f\"Example prompt text with argument: {arg1_value}\"),\n            )\n        ],\n    )\n\n\nserver = Server(\n    \"example-server\",\n    on_list_prompts=handle_list_prompts,\n    on_get_prompt=handle_get_prompt,\n)\n\n\nasync def run():\n    \"\"\"Run the basic low-level server.\"\"\"\n    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):\n        await server.run(\n            read_stream,\n            write_stream,\n            server.create_initialization_options(),\n        )\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run())\n"
  },
  {
    "path": "examples/snippets/servers/lowlevel/direct_call_tool_result.py",
    "content": "\"\"\"Run from the repository root:\nuv run examples/snippets/servers/lowlevel/direct_call_tool_result.py\n\"\"\"\n\nimport asyncio\n\nimport mcp.server.stdio\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\n\n\nasync def handle_list_tools(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListToolsResult:\n    \"\"\"List available tools.\"\"\"\n    return types.ListToolsResult(\n        tools=[\n            types.Tool(\n                name=\"advanced_tool\",\n                description=\"Tool with full control including _meta field\",\n                input_schema={\n                    \"type\": \"object\",\n                    \"properties\": {\"message\": {\"type\": \"string\"}},\n                    \"required\": [\"message\"],\n                },\n            )\n        ]\n    )\n\n\nasync def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:\n    \"\"\"Handle tool calls by returning CallToolResult directly.\"\"\"\n    if params.name == \"advanced_tool\":\n        message = (params.arguments or {}).get(\"message\", \"\")\n        return types.CallToolResult(\n            content=[types.TextContent(type=\"text\", text=f\"Processed: {message}\")],\n            structured_content={\"result\": \"success\", \"message\": message},\n            _meta={\"hidden\": \"data for client applications only\"},\n        )\n\n    raise ValueError(f\"Unknown tool: {params.name}\")\n\n\nserver = Server(\n    \"example-server\",\n    on_list_tools=handle_list_tools,\n    on_call_tool=handle_call_tool,\n)\n\n\nasync def run():\n    \"\"\"Run the server.\"\"\"\n    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):\n        await server.run(\n            read_stream,\n            write_stream,\n            server.create_initialization_options(),\n        )\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run())\n"
  },
  {
    "path": "examples/snippets/servers/lowlevel/lifespan.py",
    "content": "\"\"\"Run from the repository root:\nuv run examples/snippets/servers/lowlevel/lifespan.py\n\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom typing import TypedDict\n\nimport mcp.server.stdio\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\n\n\n# Mock database class for example\nclass Database:\n    \"\"\"Mock database class for example.\"\"\"\n\n    @classmethod\n    async def connect(cls) -> \"Database\":\n        \"\"\"Connect to database.\"\"\"\n        print(\"Database connected\")\n        return cls()\n\n    async def disconnect(self) -> None:\n        \"\"\"Disconnect from database.\"\"\"\n        print(\"Database disconnected\")\n\n    async def query(self, query_str: str) -> list[dict[str, str]]:\n        \"\"\"Execute a query.\"\"\"\n        # Simulate database query\n        return [{\"id\": \"1\", \"name\": \"Example\", \"query\": query_str}]\n\n\nclass AppContext(TypedDict):\n    db: Database\n\n\n@asynccontextmanager\nasync def server_lifespan(_server: Server[AppContext]) -> AsyncIterator[AppContext]:\n    \"\"\"Manage server startup and shutdown lifecycle.\"\"\"\n    db = await Database.connect()\n    try:\n        yield {\"db\": db}\n    finally:\n        await db.disconnect()\n\n\nasync def handle_list_tools(\n    ctx: ServerRequestContext[AppContext], params: types.PaginatedRequestParams | None\n) -> types.ListToolsResult:\n    \"\"\"List available tools.\"\"\"\n    return types.ListToolsResult(\n        tools=[\n            types.Tool(\n                name=\"query_db\",\n                description=\"Query the database\",\n                input_schema={\n                    \"type\": \"object\",\n                    \"properties\": {\"query\": {\"type\": \"string\", \"description\": \"SQL query to execute\"}},\n                    \"required\": [\"query\"],\n                },\n            )\n        ]\n    )\n\n\nasync def handle_call_tool(\n    ctx: ServerRequestContext[AppContext], params: types.CallToolRequestParams\n) -> types.CallToolResult:\n    \"\"\"Handle database query tool call.\"\"\"\n    if params.name != \"query_db\":\n        raise ValueError(f\"Unknown tool: {params.name}\")\n\n    db = ctx.lifespan_context[\"db\"]\n    results = await db.query((params.arguments or {})[\"query\"])\n\n    return types.CallToolResult(content=[types.TextContent(type=\"text\", text=f\"Query results: {results}\")])\n\n\nserver = Server(\n    \"example-server\",\n    lifespan=server_lifespan,\n    on_list_tools=handle_list_tools,\n    on_call_tool=handle_call_tool,\n)\n\n\nasync def run():\n    \"\"\"Run the server with lifespan management.\"\"\"\n    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):\n        await server.run(\n            read_stream,\n            write_stream,\n            server.create_initialization_options(),\n        )\n\n\nif __name__ == \"__main__\":\n    import asyncio\n\n    asyncio.run(run())\n"
  },
  {
    "path": "examples/snippets/servers/lowlevel/structured_output.py",
    "content": "\"\"\"Run from the repository root:\nuv run examples/snippets/servers/lowlevel/structured_output.py\n\"\"\"\n\nimport asyncio\nimport json\n\nimport mcp.server.stdio\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\n\n\nasync def handle_list_tools(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListToolsResult:\n    \"\"\"List available tools with structured output schemas.\"\"\"\n    return types.ListToolsResult(\n        tools=[\n            types.Tool(\n                name=\"get_weather\",\n                description=\"Get current weather for a city\",\n                input_schema={\n                    \"type\": \"object\",\n                    \"properties\": {\"city\": {\"type\": \"string\", \"description\": \"City name\"}},\n                    \"required\": [\"city\"],\n                },\n                output_schema={\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"temperature\": {\"type\": \"number\", \"description\": \"Temperature in Celsius\"},\n                        \"condition\": {\"type\": \"string\", \"description\": \"Weather condition\"},\n                        \"humidity\": {\"type\": \"number\", \"description\": \"Humidity percentage\"},\n                        \"city\": {\"type\": \"string\", \"description\": \"City name\"},\n                    },\n                    \"required\": [\"temperature\", \"condition\", \"humidity\", \"city\"],\n                },\n            )\n        ]\n    )\n\n\nasync def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:\n    \"\"\"Handle tool calls with structured output.\"\"\"\n    if params.name == \"get_weather\":\n        city = (params.arguments or {})[\"city\"]\n\n        weather_data = {\n            \"temperature\": 22.5,\n            \"condition\": \"partly cloudy\",\n            \"humidity\": 65,\n            \"city\": city,\n        }\n\n        return types.CallToolResult(\n            content=[types.TextContent(type=\"text\", text=json.dumps(weather_data, indent=2))],\n            structured_content=weather_data,\n        )\n\n    raise ValueError(f\"Unknown tool: {params.name}\")\n\n\nserver = Server(\n    \"example-server\",\n    on_list_tools=handle_list_tools,\n    on_call_tool=handle_call_tool,\n)\n\n\nasync def run():\n    \"\"\"Run the structured output server.\"\"\"\n    async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):\n        await server.run(\n            read_stream,\n            write_stream,\n            server.create_initialization_options(),\n        )\n\n\nif __name__ == \"__main__\":\n    asyncio.run(run())\n"
  },
  {
    "path": "examples/snippets/servers/mcpserver_quickstart.py",
    "content": "\"\"\"MCPServer quickstart example.\n\nRun from the repository root:\n    uv run examples/snippets/servers/mcpserver_quickstart.py\n\"\"\"\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create an MCP server\nmcp = MCPServer(\"Demo\")\n\n\n# Add an addition tool\n@mcp.tool()\ndef add(a: int, b: int) -> int:\n    \"\"\"Add two numbers\"\"\"\n    return a + b\n\n\n# Add a dynamic greeting resource\n@mcp.resource(\"greeting://{name}\")\ndef get_greeting(name: str) -> str:\n    \"\"\"Get a personalized greeting\"\"\"\n    return f\"Hello, {name}!\"\n\n\n# Add a prompt\n@mcp.prompt()\ndef greet_user(name: str, style: str = \"friendly\") -> str:\n    \"\"\"Generate a greeting prompt\"\"\"\n    styles = {\n        \"friendly\": \"Please write a warm, friendly greeting\",\n        \"formal\": \"Please write a formal, professional greeting\",\n        \"casual\": \"Please write a casual, relaxed greeting\",\n    }\n\n    return f\"{styles.get(style, styles['friendly'])} for someone named {name}.\"\n\n\n# Run with streamable HTTP transport\nif __name__ == \"__main__\":\n    mcp.run(transport=\"streamable-http\", json_response=True)\n"
  },
  {
    "path": "examples/snippets/servers/notifications.py",
    "content": "from mcp.server.mcpserver import Context, MCPServer\n\nmcp = MCPServer(name=\"Notifications Example\")\n\n\n@mcp.tool()\nasync def process_data(data: str, ctx: Context) -> str:\n    \"\"\"Process data with logging.\"\"\"\n    # Different log levels\n    await ctx.debug(f\"Debug: Processing '{data}'\")\n    await ctx.info(\"Info: Starting processing\")\n    await ctx.warning(\"Warning: This is experimental\")\n    await ctx.error(\"Error: (This is just a demo)\")\n\n    # Notify about resource changes\n    await ctx.session.send_resource_list_changed()\n\n    return f\"Processed: {data}\"\n"
  },
  {
    "path": "examples/snippets/servers/oauth_server.py",
    "content": "\"\"\"Run from the repository root:\nuv run examples/snippets/servers/oauth_server.py\n\"\"\"\n\nfrom pydantic import AnyHttpUrl\n\nfrom mcp.server.auth.provider import AccessToken, TokenVerifier\nfrom mcp.server.auth.settings import AuthSettings\nfrom mcp.server.mcpserver import MCPServer\n\n\nclass SimpleTokenVerifier(TokenVerifier):\n    \"\"\"Simple token verifier for demonstration.\"\"\"\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        pass  # This is where you would implement actual token validation\n\n\n# Create MCPServer instance as a Resource Server\nmcp = MCPServer(\n    \"Weather Service\",\n    # Token verifier for authentication\n    token_verifier=SimpleTokenVerifier(),\n    # Auth settings for RFC 9728 Protected Resource Metadata\n    auth=AuthSettings(\n        issuer_url=AnyHttpUrl(\"https://auth.example.com\"),  # Authorization Server URL\n        resource_server_url=AnyHttpUrl(\"http://localhost:3001\"),  # This server's URL\n        required_scopes=[\"user\"],\n    ),\n)\n\n\n@mcp.tool()\nasync def get_weather(city: str = \"London\") -> dict[str, str]:\n    \"\"\"Get weather data for a city\"\"\"\n    return {\n        \"city\": city,\n        \"temperature\": \"22\",\n        \"condition\": \"Partly cloudy\",\n        \"humidity\": \"65%\",\n    }\n\n\nif __name__ == \"__main__\":\n    mcp.run(transport=\"streamable-http\", json_response=True)\n"
  },
  {
    "path": "examples/snippets/servers/pagination_example.py",
    "content": "\"\"\"Example of implementing pagination with the low-level MCP server.\"\"\"\n\nfrom mcp import types\nfrom mcp.server import Server, ServerRequestContext\n\n# Sample data to paginate\nITEMS = [f\"Item {i}\" for i in range(1, 101)]  # 100 items\n\n\nasync def handle_list_resources(\n    ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n) -> types.ListResourcesResult:\n    \"\"\"List resources with pagination support.\"\"\"\n    page_size = 10\n\n    # Extract cursor from request params\n    cursor = params.cursor if params is not None else None\n\n    # Parse cursor to get offset\n    start = 0 if cursor is None else int(cursor)\n    end = start + page_size\n\n    # Get page of resources\n    page_items = [\n        types.Resource(uri=f\"resource://items/{item}\", name=item, description=f\"Description for {item}\")\n        for item in ITEMS[start:end]\n    ]\n\n    # Determine next cursor\n    next_cursor = str(end) if end < len(ITEMS) else None\n\n    return types.ListResourcesResult(resources=page_items, next_cursor=next_cursor)\n\n\nserver = Server(\"paginated-server\", on_list_resources=handle_list_resources)\n"
  },
  {
    "path": "examples/snippets/servers/sampling.py",
    "content": "from mcp.server.mcpserver import Context, MCPServer\nfrom mcp.types import SamplingMessage, TextContent\n\nmcp = MCPServer(name=\"Sampling Example\")\n\n\n@mcp.tool()\nasync def generate_poem(topic: str, ctx: Context) -> str:\n    \"\"\"Generate a poem using LLM sampling.\"\"\"\n    prompt = f\"Write a short poem about {topic}\"\n\n    result = await ctx.session.create_message(\n        messages=[\n            SamplingMessage(\n                role=\"user\",\n                content=TextContent(type=\"text\", text=prompt),\n            )\n        ],\n        max_tokens=100,\n    )\n\n    # Since we're not passing tools param, result.content is single content\n    if result.content.type == \"text\":\n        return result.content.text\n    return str(result.content)\n"
  },
  {
    "path": "examples/snippets/servers/streamable_config.py",
    "content": "\"\"\"Run from the repository root:\nuv run examples/snippets/servers/streamable_config.py\n\"\"\"\n\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(\"StatelessServer\")\n\n\n# Add a simple tool to demonstrate the server\n@mcp.tool()\ndef greet(name: str = \"World\") -> str:\n    \"\"\"Greet someone by name.\"\"\"\n    return f\"Hello, {name}!\"\n\n\n# Run server with streamable_http transport\n# Transport-specific options (stateless_http, json_response) are passed to run()\nif __name__ == \"__main__\":\n    # Stateless server with JSON responses (recommended)\n    mcp.run(transport=\"streamable-http\", stateless_http=True, json_response=True)\n\n    # Other configuration options:\n    # Stateless server with SSE streaming responses\n    # mcp.run(transport=\"streamable-http\", stateless_http=True)\n\n    # Stateful server with session persistence\n    # mcp.run(transport=\"streamable-http\")\n"
  },
  {
    "path": "examples/snippets/servers/streamable_http_basic_mounting.py",
    "content": "\"\"\"Basic example showing how to mount StreamableHTTP server in Starlette.\n\nRun from the repository root:\n    uvicorn examples.snippets.servers.streamable_http_basic_mounting:app --reload\n\"\"\"\n\nimport contextlib\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create MCP server\nmcp = MCPServer(\"My App\")\n\n\n@mcp.tool()\ndef hello() -> str:\n    \"\"\"A simple hello tool\"\"\"\n    return \"Hello from MCP!\"\n\n\n# Create a lifespan context manager to run the session manager\n@contextlib.asynccontextmanager\nasync def lifespan(app: Starlette):\n    async with mcp.session_manager.run():\n        yield\n\n\n# Mount the StreamableHTTP server to the existing ASGI server\n# Transport-specific options are passed to streamable_http_app()\napp = Starlette(\n    routes=[\n        Mount(\"/\", app=mcp.streamable_http_app(json_response=True)),\n    ],\n    lifespan=lifespan,\n)\n"
  },
  {
    "path": "examples/snippets/servers/streamable_http_host_mounting.py",
    "content": "\"\"\"Example showing how to mount StreamableHTTP server using Host-based routing.\n\nRun from the repository root:\n    uvicorn examples.snippets.servers.streamable_http_host_mounting:app --reload\n\"\"\"\n\nimport contextlib\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Host\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create MCP server\nmcp = MCPServer(\"MCP Host App\")\n\n\n@mcp.tool()\ndef domain_info() -> str:\n    \"\"\"Get domain-specific information\"\"\"\n    return \"This is served from mcp.acme.corp\"\n\n\n# Create a lifespan context manager to run the session manager\n@contextlib.asynccontextmanager\nasync def lifespan(app: Starlette):\n    async with mcp.session_manager.run():\n        yield\n\n\n# Mount using Host-based routing\n# Transport-specific options are passed to streamable_http_app()\napp = Starlette(\n    routes=[\n        Host(\"mcp.acme.corp\", app=mcp.streamable_http_app(json_response=True)),\n    ],\n    lifespan=lifespan,\n)\n"
  },
  {
    "path": "examples/snippets/servers/streamable_http_multiple_servers.py",
    "content": "\"\"\"Example showing how to mount multiple StreamableHTTP servers with path configuration.\n\nRun from the repository root:\n    uvicorn examples.snippets.servers.streamable_http_multiple_servers:app --reload\n\"\"\"\n\nimport contextlib\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create multiple MCP servers\napi_mcp = MCPServer(\"API Server\")\nchat_mcp = MCPServer(\"Chat Server\")\n\n\n@api_mcp.tool()\ndef api_status() -> str:\n    \"\"\"Get API status\"\"\"\n    return \"API is running\"\n\n\n@chat_mcp.tool()\ndef send_message(message: str) -> str:\n    \"\"\"Send a chat message\"\"\"\n    return f\"Message sent: {message}\"\n\n\n# Create a combined lifespan to manage both session managers\n@contextlib.asynccontextmanager\nasync def lifespan(app: Starlette):\n    async with contextlib.AsyncExitStack() as stack:\n        await stack.enter_async_context(api_mcp.session_manager.run())\n        await stack.enter_async_context(chat_mcp.session_manager.run())\n        yield\n\n\n# Mount the servers with transport-specific options passed to streamable_http_app()\n# streamable_http_path=\"/\" means endpoints will be at /api and /chat instead of /api/mcp and /chat/mcp\napp = Starlette(\n    routes=[\n        Mount(\"/api\", app=api_mcp.streamable_http_app(json_response=True, streamable_http_path=\"/\")),\n        Mount(\"/chat\", app=chat_mcp.streamable_http_app(json_response=True, streamable_http_path=\"/\")),\n    ],\n    lifespan=lifespan,\n)\n"
  },
  {
    "path": "examples/snippets/servers/streamable_http_path_config.py",
    "content": "\"\"\"Example showing path configuration when mounting MCPServer.\n\nRun from the repository root:\n    uvicorn examples.snippets.servers.streamable_http_path_config:app --reload\n\"\"\"\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create a simple MCPServer server\nmcp_at_root = MCPServer(\"My Server\")\n\n\n@mcp_at_root.tool()\ndef process_data(data: str) -> str:\n    \"\"\"Process some data\"\"\"\n    return f\"Processed: {data}\"\n\n\n# Mount at /process with streamable_http_path=\"/\" so the endpoint is /process (not /process/mcp)\n# Transport-specific options like json_response are passed to streamable_http_app()\napp = Starlette(\n    routes=[\n        Mount(\n            \"/process\",\n            app=mcp_at_root.streamable_http_app(json_response=True, streamable_http_path=\"/\"),\n        ),\n    ]\n)\n"
  },
  {
    "path": "examples/snippets/servers/streamable_starlette_mount.py",
    "content": "\"\"\"Run from the repository root:\nuvicorn examples.snippets.servers.streamable_starlette_mount:app --reload\n\"\"\"\n\nimport contextlib\n\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom mcp.server.mcpserver import MCPServer\n\n# Create the Echo server\necho_mcp = MCPServer(name=\"EchoServer\")\n\n\n@echo_mcp.tool()\ndef echo(message: str) -> str:\n    \"\"\"A simple echo tool\"\"\"\n    return f\"Echo: {message}\"\n\n\n# Create the Math server\nmath_mcp = MCPServer(name=\"MathServer\")\n\n\n@math_mcp.tool()\ndef add_two(n: int) -> int:\n    \"\"\"Tool to add two to the input\"\"\"\n    return n + 2\n\n\n# Create a combined lifespan to manage both session managers\n@contextlib.asynccontextmanager\nasync def lifespan(app: Starlette):\n    async with contextlib.AsyncExitStack() as stack:\n        await stack.enter_async_context(echo_mcp.session_manager.run())\n        await stack.enter_async_context(math_mcp.session_manager.run())\n        yield\n\n\n# Create the Starlette app and mount the MCP servers\napp = Starlette(\n    routes=[\n        Mount(\"/echo\", echo_mcp.streamable_http_app(stateless_http=True, json_response=True)),\n        Mount(\"/math\", math_mcp.streamable_http_app(stateless_http=True, json_response=True)),\n    ],\n    lifespan=lifespan,\n)\n\n# Note: Clients connect to http://localhost:8000/echo/mcp and http://localhost:8000/math/mcp\n# To mount at the root of each path (e.g., /echo instead of /echo/mcp):\n# echo_mcp.streamable_http_app(streamable_http_path=\"/\", stateless_http=True, json_response=True)\n# math_mcp.streamable_http_app(streamable_http_path=\"/\", stateless_http=True, json_response=True)\n"
  },
  {
    "path": "examples/snippets/servers/structured_output.py",
    "content": "\"\"\"Example showing structured output with tools.\"\"\"\n\nfrom typing import TypedDict\n\nfrom pydantic import BaseModel, Field\n\nfrom mcp.server.mcpserver import MCPServer\n\nmcp = MCPServer(\"Structured Output Example\")\n\n\n# Using Pydantic models for rich structured data\nclass WeatherData(BaseModel):\n    \"\"\"Weather information structure.\"\"\"\n\n    temperature: float = Field(description=\"Temperature in Celsius\")\n    humidity: float = Field(description=\"Humidity percentage\")\n    condition: str\n    wind_speed: float\n\n\n@mcp.tool()\ndef get_weather(city: str) -> WeatherData:\n    \"\"\"Get weather for a city - returns structured data.\"\"\"\n    # Simulated weather data\n    return WeatherData(\n        temperature=22.5,\n        humidity=45.0,\n        condition=\"sunny\",\n        wind_speed=5.2,\n    )\n\n\n# Using TypedDict for simpler structures\nclass LocationInfo(TypedDict):\n    latitude: float\n    longitude: float\n    name: str\n\n\n@mcp.tool()\ndef get_location(address: str) -> LocationInfo:\n    \"\"\"Get location coordinates\"\"\"\n    return LocationInfo(latitude=51.5074, longitude=-0.1278, name=\"London, UK\")\n\n\n# Using dict[str, Any] for flexible schemas\n@mcp.tool()\ndef get_statistics(data_type: str) -> dict[str, float]:\n    \"\"\"Get various statistics\"\"\"\n    return {\"mean\": 42.5, \"median\": 40.0, \"std_dev\": 5.2}\n\n\n# Ordinary classes with type hints work for structured output\nclass UserProfile:\n    name: str\n    age: int\n    email: str | None = None\n\n    def __init__(self, name: str, age: int, email: str | None = None):\n        self.name = name\n        self.age = age\n        self.email = email\n\n\n@mcp.tool()\ndef get_user(user_id: str) -> UserProfile:\n    \"\"\"Get user profile - returns structured data\"\"\"\n    return UserProfile(name=\"Alice\", age=30, email=\"alice@example.com\")\n\n\n# Classes WITHOUT type hints cannot be used for structured output\nclass UntypedConfig:\n    def __init__(self, setting1, setting2):  # type: ignore[reportMissingParameterType]\n        self.setting1 = setting1\n        self.setting2 = setting2\n\n\n@mcp.tool()\ndef get_config() -> UntypedConfig:\n    \"\"\"This returns unstructured output - no schema generated\"\"\"\n    return UntypedConfig(\"value1\", \"value2\")\n\n\n# Lists and other types are wrapped automatically\n@mcp.tool()\ndef list_cities() -> list[str]:\n    \"\"\"Get a list of cities\"\"\"\n    return [\"London\", \"Paris\", \"Tokyo\"]\n    # Returns: {\"result\": [\"London\", \"Paris\", \"Tokyo\"]}\n\n\n@mcp.tool()\ndef get_temperature(city: str) -> float:\n    \"\"\"Get temperature as a simple float\"\"\"\n    return 22.5\n    # Returns: {\"result\": 22.5}\n"
  },
  {
    "path": "examples/snippets/servers/tool_progress.py",
    "content": "from mcp.server.mcpserver import Context, MCPServer\n\nmcp = MCPServer(name=\"Progress Example\")\n\n\n@mcp.tool()\nasync def long_running_task(task_name: str, ctx: Context, steps: int = 5) -> str:\n    \"\"\"Execute a task with progress updates.\"\"\"\n    await ctx.info(f\"Starting: {task_name}\")\n\n    for i in range(steps):\n        progress = (i + 1) / steps\n        await ctx.report_progress(\n            progress=progress,\n            total=1.0,\n            message=f\"Step {i + 1}/{steps}\",\n        )\n        await ctx.debug(f\"Completed step {i + 1}\")\n\n    return f\"Task '{task_name}' completed\"\n"
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: MCP Server\nsite_description: MCP Server\nstrict: true\n\nrepo_name: modelcontextprotocol/python-sdk\nrepo_url: https://github.com/modelcontextprotocol/python-sdk\nedit_uri: edit/main/docs/\nsite_url: https://modelcontextprotocol.github.io/python-sdk\n\n# TODO(Marcelo): Add Anthropic copyright?\n# copyright: © Model Context Protocol 2025 to present\n\nnav:\n  - Introduction: index.md\n  - Installation: installation.md\n  - Migration Guide: migration.md\n  - Documentation:\n      - Concepts: concepts.md\n      - Low-Level Server: low-level-server.md\n      - Authorization: authorization.md\n      - Testing: testing.md\n  - Experimental:\n      - Overview: experimental/index.md\n      - Tasks:\n          - Introduction: experimental/tasks.md\n          - Server Implementation: experimental/tasks-server.md\n          - Client Usage: experimental/tasks-client.md\n  - API Reference: api/\n\ntheme:\n  name: \"material\"\n  palette:\n    - media: \"(prefers-color-scheme)\"\n      scheme: default\n      primary: black\n      accent: black\n      toggle:\n        icon: material/lightbulb\n        name: \"Switch to light mode\"\n    - media: \"(prefers-color-scheme: light)\"\n      scheme: default\n      primary: black\n      accent: black\n      toggle:\n        icon: material/lightbulb-outline\n        name: \"Switch to dark mode\"\n    - media: \"(prefers-color-scheme: dark)\"\n      scheme: slate\n      primary: white\n      accent: white\n      toggle:\n        icon: material/lightbulb-auto-outline\n        name: \"Switch to system preference\"\n  features:\n    - search.suggest\n    - search.highlight\n    - content.tabs.link\n    - content.code.annotate\n    - content.code.copy\n    - content.code.select\n    - navigation.path\n    - navigation.indexes\n    - navigation.sections\n    - navigation.tracking\n    - toc.follow\n  # logo: \"img/logo-white.svg\"\n  # TODO(Marcelo): Add a favicon.\n  # favicon: \"favicon.ico\"\n\n# https://www.mkdocs.org/user-guide/configuration/#validation\nvalidation:\n  omitted_files: warn\n  absolute_links: warn\n  unrecognized_links: warn\n  anchors: warn\n\nmarkdown_extensions:\n  - tables\n  - admonition\n  - attr_list\n  - md_in_html\n  - pymdownx.details\n  - pymdownx.caret\n  - pymdownx.critic\n  - pymdownx.mark\n  - pymdownx.superfences\n  - pymdownx.snippets\n  - pymdownx.tilde\n  - pymdownx.inlinehilite\n  - pymdownx.highlight:\n      pygments_lang_class: true\n  - pymdownx.extra:\n      pymdownx.superfences:\n        custom_fences:\n          - name: mermaid\n            class: mermaid\n            format: !!python/name:pymdownx.superfences.fence_code_format\n  - pymdownx.emoji:\n      emoji_index: !!python/name:material.extensions.emoji.twemoji\n      emoji_generator: !!python/name:material.extensions.emoji.to_svg\n      options:\n        custom_icons:\n          - docs/.overrides/.icons\n  - pymdownx.tabbed:\n      alternate_style: true\n  - pymdownx.tasklist:\n      custom_checkbox: true\n  - sane_lists # this means you can start a list from any number\n\nwatch:\n  - src/mcp\n\nplugins:\n  - search\n  - social:\n      enabled: !ENV [ENABLE_SOCIAL_CARDS, false]\n  - glightbox\n  - gen-files:\n      scripts:\n        - docs/hooks/gen_ref_pages.py\n  - literate-nav:\n      nav_file: SUMMARY.md\n  - mkdocstrings:\n      handlers:\n        python:\n          paths: [src]\n          options:\n            relative_crossrefs: true\n            members_order: source\n            separate_signature: true\n            show_signature_annotations: true\n            signature_crossrefs: true\n            group_by_category: false\n          inventories:\n            - url: https://docs.python.org/3/objects.inv\n            - url: https://docs.pydantic.dev/latest/objects.inv\n            - url: https://typing-extensions.readthedocs.io/en/latest/objects.inv\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[project]\nname = \"mcp\"\ndynamic = [\"version\"]\ndescription = \"Model Context Protocol SDK\"\nreadme = \"README.md\"\nrequires-python = \">=3.10\"\nauthors = [{ name = \"Model Context Protocol a Series of LF Projects, LLC.\" }]\nmaintainers = [\n    { name = \"David Soria Parra\", email = \"davidsp@anthropic.com\" },\n    { name = \"Marcelo Trylesinski\", email = \"marcelotryle@gmail.com\" },\n    { name = \"Max Isbey\", email = \"maxisbey@anthropic.com\" },\n    { name = \"Felix Weinberger\", email = \"fweinberger@anthropic.com\" },\n]\nkeywords = [\"git\", \"mcp\", \"llm\", \"automation\"]\nlicense = { text = \"MIT\" }\nclassifiers = [\n    \"Development Status :: 4 - Beta\",\n    \"Intended Audience :: Developers\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.14\",\n]\ndependencies = [\n    \"anyio>=4.9\",\n    \"httpx>=0.27.1\",\n    \"httpx-sse>=0.4\",\n    \"pydantic>=2.12.0\",\n    \"starlette>=0.48.0; python_version >= '3.14'\",\n    \"starlette>=0.27; python_version < '3.14'\",\n    \"python-multipart>=0.0.9\",\n    \"sse-starlette>=3.0.0\",\n    \"pydantic-settings>=2.5.2\",\n    \"uvicorn>=0.31.1; sys_platform != 'emscripten'\",\n    \"jsonschema>=4.20.0\",\n    \"pywin32>=311; sys_platform == 'win32'\",\n    \"pyjwt[crypto]>=2.10.1\",\n    \"typing-extensions>=4.13.0\",\n    \"typing-inspection>=0.4.1\",\n]\n\n[project.optional-dependencies]\nrich = [\"rich>=13.9.4\"]\ncli = [\"typer>=0.16.0\", \"python-dotenv>=1.0.0\"]\nws = [\"websockets>=15.0.1\"]\n\n[project.scripts]\nmcp = \"mcp.cli:app [cli]\"\n\n[tool.uv]\ndefault-groups = [\"dev\", \"docs\"]\nrequired-version = \">=0.9.5\"\n\n[dependency-groups]\ndev = [\n    # We add mcp[cli,ws] so `uv sync` considers the extras.\n    \"mcp[cli,ws]\",\n    \"pyright>=1.1.400\",\n    \"pytest>=8.3.4\",\n    \"ruff>=0.8.5\",\n    \"trio>=0.26.2\",\n    \"pytest-flakefinder>=1.1.0\",\n    \"pytest-xdist>=3.6.1\",\n    \"pytest-examples>=0.0.14\",\n    \"pytest-pretty>=1.2.0\",\n    \"inline-snapshot>=0.23.0\",\n    \"dirty-equals>=0.9.0\",\n    \"coverage[toml]>=7.10.7,<=7.13\",\n    \"pillow>=12.0\",\n    \"strict-no-cover\",\n]\ndocs = [\n    \"mkdocs>=1.6.1\",\n    \"mkdocs-gen-files>=0.5.0\",\n    \"mkdocs-glightbox>=0.4.0\",\n    \"mkdocs-literate-nav>=0.6.1\",\n    \"mkdocs-material[imaging]>=9.5.45\",\n    \"mkdocstrings-python>=2.0.1\",\n]\n\n[build-system]\nrequires = [\"hatchling\", \"uv-dynamic-versioning\"]\nbuild-backend = \"hatchling.build\"\n\n[tool.hatch.version]\nsource = \"uv-dynamic-versioning\"\n\n[tool.uv-dynamic-versioning]\nvcs = \"git\"\nstyle = \"pep440\"\nbump = true\n\n[project.urls]\nHomepage = \"https://modelcontextprotocol.io\"\nRepository = \"https://github.com/modelcontextprotocol/python-sdk\"\nIssues = \"https://github.com/modelcontextprotocol/python-sdk/issues\"\n\n[tool.hatch.build.targets.wheel]\npackages = [\"src/mcp\"]\n\n[tool.pyright]\ntypeCheckingMode = \"strict\"\ninclude = [\n    \"src/mcp\",\n    \"tests\",\n    \"examples/servers\",\n    \"examples/snippets\",\n    \"examples/clients\",\n]\nvenvPath = \".\"\nvenv = \".venv\"\n# The FastAPI style of using decorators in tests gives a `reportUnusedFunction` error.\n# See https://github.com/microsoft/pyright/issues/7771 for more details.\n# TODO(Marcelo): We should remove `reportPrivateUsage = false`. The idea is that we should test the workflow that uses\n# those private functions instead of testing the private functions directly. It makes it easier to maintain the code source\n# and refactor code that is not public.\nexecutionEnvironments = [\n    { root = \"tests\", extraPaths = [\n        \".\",\n    ], reportUnusedFunction = false, reportPrivateUsage = false },\n    { root = \"examples/servers\", reportUnusedFunction = false },\n]\n\n[tool.ruff]\nline-length = 120\ntarget-version = \"py310\"\nextend-exclude = [\"README.md\", \"README.v2.md\"]\n\n[tool.ruff.lint]\nselect = [\n    \"C4\",     # flake8-comprehensions\n    \"C90\",    # mccabe\n    \"D212\",   # pydocstyle: multi-line docstring summary should start at the first line\n    \"E\",      # pycodestyle\n    \"F\",      # pyflakes\n    \"I\",      # isort\n    \"PERF\",   # Perflint\n    \"UP\",     # pyupgrade\n    \"TID251\", # https://docs.astral.sh/ruff/rules/banned-api/\n]\nignore = [\"PERF203\"]\n\n[tool.ruff.lint.flake8-tidy-imports.banned-api]\n\"pydantic.RootModel\".msg = \"Use `pydantic.TypeAdapter` instead.\"\n\n\n[tool.ruff.lint.mccabe]\nmax-complexity = 24 # Default is 10\n\n[tool.ruff.lint.per-file-ignores]\n\"__init__.py\" = [\"F401\"]\n\"tests/server/mcpserver/test_func_metadata.py\" = [\"E501\"]\n\"tests/shared/test_progress_notifications.py\" = [\"PLW0603\"]\n\n[tool.ruff.lint.pylint]\nallow-magic-value-types = [\"bytes\", \"float\", \"int\", \"str\"]\nmax-args = 23                                              # Default is 5\nmax-branches = 23                                          # Default is 12\nmax-returns = 13                                           # Default is 6\nmax-statements = 102                                       # Default is 50\n\n[tool.uv.workspace]\nmembers = [\"examples/clients/*\", \"examples/servers/*\", \"examples/snippets\"]\n\n[tool.uv.sources]\nmcp = { workspace = true }\nstrict-no-cover = { git = \"https://github.com/pydantic/strict-no-cover\" }\n\n[tool.pytest.ini_options]\nlog_cli = true\nxfail_strict = true\naddopts = \"\"\"\n    --color=yes\n    --capture=fd\n    -p anyio\n    -p examples\n\"\"\"\nfilterwarnings = [\n    \"error\",\n    # This should be fixed on Uvicorn's side.\n    \"ignore::DeprecationWarning:websockets\",\n    \"ignore:websockets.server.WebSocketServerProtocol is deprecated:DeprecationWarning\",\n    \"ignore:Returning str or bytes.*:DeprecationWarning:mcp.server.lowlevel\",\n    # pywin32 internal deprecation warning\n    \"ignore:getargs.*The 'u' format is deprecated:DeprecationWarning\",\n]\n\n[tool.markdown.lint]\ndefault = true\nMD004 = false    # ul-style - Unordered list style\nMD007.indent = 2 # ul-indent - Unordered list indentation\nMD013 = false    # line-length - Line length\nMD029 = false    # ol-prefix - Ordered list item prefix\nMD033 = false    # no-inline-html Inline HTML\nMD041 = false    # first-line-heading/first-line-h1\nMD046 = false    # indented-code-blocks\nMD059 = false    # descriptive-link-text\n\n# https://coverage.readthedocs.io/en/latest/config.html#run\n[tool.coverage.run]\nbranch = true\npatch = [\"subprocess\"]\nconcurrency = [\"multiprocessing\", \"thread\"]\nsource = [\"src\", \"tests\"]\nomit = [\n    \"src/mcp/client/__main__.py\",\n    \"src/mcp/server/__main__.py\",\n    \"src/mcp/os/posix/utilities.py\",\n    \"src/mcp/os/win32/utilities.py\",\n]\n\n# https://coverage.readthedocs.io/en/latest/config.html#report\n[tool.coverage.report]\nfail_under = 100\nskip_covered = true\nshow_missing = true\nignore_errors = true\nprecision = 2\nexclude_lines = [\n    \"pragma: no cover\",\n    \"pragma: lax no cover\",\n    \"if TYPE_CHECKING:\",\n    \"@overload\",\n    \"raise NotImplementedError\",\n    \"^\\\\s*\\\\.\\\\.\\\\.\\\\s*$\",\n]\n\n# https://coverage.readthedocs.io/en/latest/config.html#paths\n[tool.coverage.paths]\nsource = [\n    \"src/\",\n    \"/home/runner/work/python-sdk/python-sdk/src/\",\n    'D:\\a\\python-sdk\\python-sdk\\src',\n]\n\n[tool.inline-snapshot]\ndefault-flags = [\"disable\"]\nformat-command = \"ruff format --stdin-filename {filename}\"\n"
  },
  {
    "path": "scripts/test",
    "content": "#!/bin/sh\n\nset -ex\n\nuv run --frozen coverage erase\nuv run --frozen coverage run -m pytest -n auto $@\nuv run --frozen coverage combine\nuv run --frozen coverage report\n# strict-no-cover spawns `uv run coverage json` internally without --frozen;\n# UV_FROZEN=1 propagates to that subprocess so it doesn't touch uv.lock.\nUV_FROZEN=1 uv run --frozen strict-no-cover\n"
  },
  {
    "path": "scripts/update_readme_snippets.py",
    "content": "#!/usr/bin/env python3\n\"\"\"Update README.md with live code snippets from example files.\n\nThis script finds specially marked code blocks in README.md and updates them\nwith the actual code from the referenced files.\n\nUsage:\n    python scripts/update_readme_snippets.py\n    python scripts/update_readme_snippets.py --check  # Check mode for CI\n\"\"\"\n\nimport argparse\nimport re\nimport sys\nfrom pathlib import Path\n\n\ndef get_github_url(file_path: str) -> str:\n    \"\"\"Generate a GitHub URL for the file.\n\n    Args:\n        file_path: Path to the file relative to repo root\n\n    Returns:\n        GitHub URL\n    \"\"\"\n    base_url = \"https://github.com/modelcontextprotocol/python-sdk/blob/main\"\n    return f\"{base_url}/{file_path}\"\n\n\ndef process_snippet_block(match: re.Match[str], check_mode: bool = False) -> str:\n    \"\"\"Process a single snippet-source block.\n\n    Args:\n        match: The regex match object\n        check_mode: If True, return original if no changes needed\n\n    Returns:\n        The updated block content\n    \"\"\"\n    full_match = match.group(0)\n    indent = match.group(1)\n    file_path = match.group(2)\n\n    try:\n        # Read the entire file\n        file = Path(file_path)\n        if not file.exists():\n            print(f\"Warning: File not found: {file_path}\")\n            return full_match\n\n        code = file.read_text().rstrip()\n        github_url = get_github_url(file_path)\n\n        # Build the replacement block\n        indented_code = code.replace(\"\\n\", f\"\\n{indent}\")\n        replacement = f\"\"\"{indent}<!-- snippet-source {file_path} -->\n{indent}```python\n{indent}{indented_code}\n{indent}```\n\n{indent}_Full example: [{file_path}]({github_url})_\n{indent}<!-- /snippet-source -->\"\"\"\n\n        # In check mode, only check if code has changed\n        if check_mode:\n            # Extract existing code from the match\n            existing_content = match.group(3)\n            if existing_content is not None:\n                existing_lines = existing_content.strip().split(\"\\n\")\n                # Find code between ```python and ```\n                code_lines = []\n                in_code = False\n                for line in existing_lines:\n                    if line.strip() == \"```python\":\n                        in_code = True\n                    elif line.strip() == \"```\":\n                        break\n                    elif in_code:\n                        code_lines.append(line)\n                existing_code = \"\\n\".join(code_lines).strip()\n                # Compare with the indented version we would generate\n                expected_code = code.replace(\"\\n\", f\"\\n{indent}\").strip()\n                if existing_code == expected_code:\n                    return full_match\n\n        return replacement\n\n    except Exception as e:\n        print(f\"Error processing {file_path}: {e}\")\n        return full_match\n\n\ndef update_readme_snippets(readme_path: Path = Path(\"README.md\"), check_mode: bool = False) -> bool:\n    \"\"\"Update code snippets in README.md with live code from source files.\n\n    Args:\n        readme_path: Path to the README file\n        check_mode: If True, only check if updates are needed without modifying\n\n    Returns:\n        True if file is up to date or was updated, False if check failed\n    \"\"\"\n    if not readme_path.exists():\n        print(f\"Error: README file not found: {readme_path}\")\n        return False\n\n    content = readme_path.read_text()\n    original_content = content\n\n    # Pattern to match snippet-source blocks\n    # Matches: <!-- snippet-source path/to/file.py -->\n    #          ... any content ...\n    #          <!-- /snippet-source -->\n    pattern = r\"^(\\s*)<!-- snippet-source ([^\\s]+) -->\\n\" r\"(.*?)\" r\"^\\1<!-- /snippet-source -->\"\n\n    # Process all snippet-source blocks\n    updated_content = re.sub(\n        pattern, lambda m: process_snippet_block(m, check_mode), content, flags=re.MULTILINE | re.DOTALL\n    )\n\n    if check_mode:\n        if updated_content != original_content:\n            print(\n                f\"Error: {readme_path} has outdated code snippets. \"\n                \"Run 'python scripts/update_readme_snippets.py' to update.\"\n            )\n            return False\n        else:\n            print(f\"✓ {readme_path} code snippets are up to date\")\n            return True\n    else:\n        if updated_content != original_content:\n            readme_path.write_text(updated_content)\n            print(f\"✓ Updated {readme_path}\")\n        else:\n            print(f\"✓ {readme_path} already up to date\")\n        return True\n\n\ndef main():\n    \"\"\"Main entry point.\"\"\"\n    parser = argparse.ArgumentParser(description=\"Update README code snippets from source files\")\n    parser.add_argument(\n        \"--check\", action=\"store_true\", help=\"Check mode - verify snippets are up to date without modifying\"\n    )\n    # TODO(v2): Drop the `--readme` argument when v2 is released, and set to `README.md`.\n    parser.add_argument(\"--readme\", default=\"README.v2.md\", help=\"Path to README file (default: README.v2.md)\")\n\n    args = parser.parse_args()\n\n    success = update_readme_snippets(Path(args.readme), check_mode=args.check)\n\n    if not success:\n        sys.exit(1)\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "src/mcp/__init__.py",
    "content": "from .client.client import Client\nfrom .client.session import ClientSession\nfrom .client.session_group import ClientSessionGroup\nfrom .client.stdio import StdioServerParameters, stdio_client\nfrom .server.session import ServerSession\nfrom .server.stdio import stdio_server\nfrom .shared.exceptions import MCPError, UrlElicitationRequiredError\nfrom .types import (\n    CallToolRequest,\n    ClientCapabilities,\n    ClientNotification,\n    ClientRequest,\n    ClientResult,\n    CompleteRequest,\n    CreateMessageRequest,\n    CreateMessageResult,\n    CreateMessageResultWithTools,\n    ErrorData,\n    GetPromptRequest,\n    GetPromptResult,\n    Implementation,\n    IncludeContext,\n    InitializedNotification,\n    InitializeRequest,\n    InitializeResult,\n    JSONRPCError,\n    JSONRPCRequest,\n    JSONRPCResponse,\n    ListPromptsRequest,\n    ListPromptsResult,\n    ListResourcesRequest,\n    ListResourcesResult,\n    ListToolsResult,\n    LoggingLevel,\n    LoggingMessageNotification,\n    Notification,\n    PingRequest,\n    ProgressNotification,\n    PromptsCapability,\n    ReadResourceRequest,\n    ReadResourceResult,\n    Resource,\n    ResourcesCapability,\n    ResourceUpdatedNotification,\n    RootsCapability,\n    SamplingCapability,\n    SamplingContent,\n    SamplingContextCapability,\n    SamplingMessage,\n    SamplingMessageContentBlock,\n    SamplingToolsCapability,\n    ServerCapabilities,\n    ServerNotification,\n    ServerRequest,\n    ServerResult,\n    SetLevelRequest,\n    StopReason,\n    SubscribeRequest,\n    Tool,\n    ToolChoice,\n    ToolResultContent,\n    ToolsCapability,\n    ToolUseContent,\n    UnsubscribeRequest,\n)\nfrom .types import Role as SamplingRole\n\n__all__ = [\n    \"CallToolRequest\",\n    \"Client\",\n    \"ClientCapabilities\",\n    \"ClientNotification\",\n    \"ClientRequest\",\n    \"ClientResult\",\n    \"ClientSession\",\n    \"ClientSessionGroup\",\n    \"CompleteRequest\",\n    \"CreateMessageRequest\",\n    \"CreateMessageResult\",\n    \"CreateMessageResultWithTools\",\n    \"ErrorData\",\n    \"GetPromptRequest\",\n    \"GetPromptResult\",\n    \"Implementation\",\n    \"IncludeContext\",\n    \"InitializeRequest\",\n    \"InitializeResult\",\n    \"InitializedNotification\",\n    \"JSONRPCError\",\n    \"JSONRPCRequest\",\n    \"JSONRPCResponse\",\n    \"ListPromptsRequest\",\n    \"ListPromptsResult\",\n    \"ListResourcesRequest\",\n    \"ListResourcesResult\",\n    \"ListToolsResult\",\n    \"LoggingLevel\",\n    \"LoggingMessageNotification\",\n    \"MCPError\",\n    \"Notification\",\n    \"PingRequest\",\n    \"ProgressNotification\",\n    \"PromptsCapability\",\n    \"ReadResourceRequest\",\n    \"ReadResourceResult\",\n    \"Resource\",\n    \"ResourcesCapability\",\n    \"ResourceUpdatedNotification\",\n    \"RootsCapability\",\n    \"SamplingCapability\",\n    \"SamplingContent\",\n    \"SamplingContextCapability\",\n    \"SamplingMessage\",\n    \"SamplingMessageContentBlock\",\n    \"SamplingRole\",\n    \"SamplingToolsCapability\",\n    \"ServerCapabilities\",\n    \"ServerNotification\",\n    \"ServerRequest\",\n    \"ServerResult\",\n    \"ServerSession\",\n    \"SetLevelRequest\",\n    \"StdioServerParameters\",\n    \"StopReason\",\n    \"SubscribeRequest\",\n    \"Tool\",\n    \"ToolChoice\",\n    \"ToolResultContent\",\n    \"ToolsCapability\",\n    \"ToolUseContent\",\n    \"UnsubscribeRequest\",\n    \"UrlElicitationRequiredError\",\n    \"stdio_client\",\n    \"stdio_server\",\n]\n"
  },
  {
    "path": "src/mcp/cli/__init__.py",
    "content": "\"\"\"MCP CLI package.\"\"\"\n\nfrom .cli import app\n\nif __name__ == \"__main__\":  # pragma: no cover\n    app()\n"
  },
  {
    "path": "src/mcp/cli/claude.py",
    "content": "\"\"\"Claude app integration utilities.\"\"\"\n\nimport json\nimport os\nimport shutil\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\nfrom mcp.server.mcpserver.utilities.logging import get_logger\n\nlogger = get_logger(__name__)\n\nMCP_PACKAGE = \"mcp[cli]\"\n\n\ndef get_claude_config_path() -> Path | None:  # pragma: no cover\n    \"\"\"Get the Claude config directory based on platform.\"\"\"\n    if sys.platform == \"win32\":\n        path = Path(Path.home(), \"AppData\", \"Roaming\", \"Claude\")\n    elif sys.platform == \"darwin\":\n        path = Path(Path.home(), \"Library\", \"Application Support\", \"Claude\")\n    elif sys.platform.startswith(\"linux\"):\n        path = Path(os.environ.get(\"XDG_CONFIG_HOME\", Path.home() / \".config\"), \"Claude\")\n    else:\n        return None\n\n    if path.exists():\n        return path\n    return None\n\n\ndef get_uv_path() -> str:\n    \"\"\"Get the full path to the uv executable.\"\"\"\n    uv_path = shutil.which(\"uv\")\n    if not uv_path:\n        logger.error(\n            \"uv executable not found in PATH, falling back to 'uv'. Please ensure uv is installed and in your PATH\"\n        )\n        return \"uv\"  # Fall back to just \"uv\" if not found\n    return uv_path\n\n\ndef update_claude_config(\n    file_spec: str,\n    server_name: str,\n    *,\n    with_editable: Path | None = None,\n    with_packages: list[str] | None = None,\n    env_vars: dict[str, str] | None = None,\n) -> bool:\n    \"\"\"Add or update an MCP server in Claude's configuration.\n\n    Args:\n        file_spec: Path to the server file, optionally with :object suffix\n        server_name: Name for the server in Claude's config\n        with_editable: Optional directory to install in editable mode\n        with_packages: Optional list of additional packages to install\n        env_vars: Optional dictionary of environment variables. These are merged with\n            any existing variables, with new values taking precedence.\n\n    Raises:\n        RuntimeError: If Claude Desktop's config directory is not found, indicating\n            Claude Desktop may not be installed or properly set up.\n    \"\"\"\n    config_dir = get_claude_config_path()\n    uv_path = get_uv_path()\n    if not config_dir:\n        raise RuntimeError(\n            \"Claude Desktop config directory not found. Please ensure Claude Desktop\"\n            \" is installed and has been run at least once to initialize its config.\"\n        )\n\n    config_file = config_dir / \"claude_desktop_config.json\"\n    if not config_file.exists():  # pragma: lax no cover\n        try:\n            config_file.write_text(\"{}\")\n        except Exception:\n            logger.exception(\n                \"Failed to create Claude config file\",\n                extra={\n                    \"config_file\": str(config_file),\n                },\n            )\n            return False\n\n    try:\n        config = json.loads(config_file.read_text())\n        if \"mcpServers\" not in config:\n            config[\"mcpServers\"] = {}\n\n        # Always preserve existing env vars and merge with new ones\n        if server_name in config[\"mcpServers\"] and \"env\" in config[\"mcpServers\"][server_name]:\n            existing_env = config[\"mcpServers\"][server_name][\"env\"]\n            if env_vars:\n                # New vars take precedence over existing ones\n                env_vars = {**existing_env, **env_vars}\n            else:\n                env_vars = existing_env\n\n        # Build uv run command\n        args = [\"run\", \"--frozen\"]\n\n        # Collect all packages in a set to deduplicate\n        packages = {MCP_PACKAGE}\n        if with_packages:\n            packages.update(pkg for pkg in with_packages if pkg)\n\n        # Add all packages with --with\n        for pkg in sorted(packages):\n            args.extend([\"--with\", pkg])\n\n        if with_editable:\n            args.extend([\"--with-editable\", str(with_editable)])\n\n        # Convert file path to absolute before adding to command\n        # Split off any :object suffix first\n        # First check if we have a Windows path (e.g., C:\\...)\n        has_windows_drive = len(file_spec) > 1 and file_spec[1] == \":\"\n\n        # Split on the last colon, but only if it's not part of the Windows drive letter\n        if \":\" in (file_spec[2:] if has_windows_drive else file_spec):\n            file_path, server_object = file_spec.rsplit(\":\", 1)\n            file_spec = f\"{Path(file_path).resolve()}:{server_object}\"\n        else:\n            file_spec = str(Path(file_spec).resolve())\n\n        # Add mcp run command\n        args.extend([\"mcp\", \"run\", file_spec])\n\n        server_config: dict[str, Any] = {\"command\": uv_path, \"args\": args}\n\n        # Add environment variables if specified\n        if env_vars:\n            server_config[\"env\"] = env_vars\n\n        config[\"mcpServers\"][server_name] = server_config\n\n        config_file.write_text(json.dumps(config, indent=2))\n        logger.info(\n            f\"Added server '{server_name}' to Claude config\",\n            extra={\"config_file\": str(config_file)},\n        )\n        return True\n    except Exception:  # pragma: no cover\n        logger.exception(\n            \"Failed to update Claude config\",\n            extra={\n                \"config_file\": str(config_file),\n            },\n        )\n        return False\n"
  },
  {
    "path": "src/mcp/cli/cli.py",
    "content": "\"\"\"MCP CLI tools.\"\"\"\n\nimport importlib.metadata\nimport importlib.util\nimport os\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import Annotated, Any\n\nfrom mcp.server import MCPServer\nfrom mcp.server import Server as LowLevelServer\n\ntry:\n    import typer\nexcept ImportError:  # pragma: no cover\n    print(\"Error: typer is required. Install with 'pip install mcp[cli]'\")\n    sys.exit(1)\n\ntry:\n    from mcp.cli import claude\n    from mcp.server.mcpserver.utilities.logging import get_logger\nexcept ImportError:  # pragma: no cover\n    print(\"Error: mcp.server is not installed or not in PYTHONPATH\")\n    sys.exit(1)\n\ntry:\n    import dotenv\nexcept ImportError:  # pragma: no cover\n    dotenv = None\n\nlogger = get_logger(\"cli\")\n\napp = typer.Typer(\n    name=\"mcp\",\n    help=\"MCP development tools\",\n    add_completion=False,\n    no_args_is_help=True,  # Show help if no args provided\n)\n\n\ndef _get_npx_command():\n    \"\"\"Get the correct npx command for the current platform.\"\"\"\n    if sys.platform == \"win32\":\n        # Try both npx.cmd and npx.exe on Windows\n        for cmd in [\"npx.cmd\", \"npx.exe\", \"npx\"]:\n            try:\n                subprocess.run([cmd, \"--version\"], check=True, capture_output=True, shell=True)\n                return cmd\n            except subprocess.CalledProcessError:\n                continue\n        return None\n    return \"npx\"  # On Unix-like systems, just use npx\n\n\ndef _parse_env_var(env_var: str) -> tuple[str, str]:  # pragma: no cover\n    \"\"\"Parse environment variable string in format KEY=VALUE.\"\"\"\n    if \"=\" not in env_var:\n        logger.error(f\"Invalid environment variable format: {env_var}. Must be KEY=VALUE\")\n        sys.exit(1)\n    key, value = env_var.split(\"=\", 1)\n    return key.strip(), value.strip()\n\n\ndef _build_uv_command(\n    file_spec: str,\n    with_editable: Path | None = None,\n    with_packages: list[str] | None = None,\n) -> list[str]:\n    \"\"\"Build the uv run command that runs an MCP server through mcp run.\"\"\"\n    cmd = [\"uv\"]\n\n    cmd.extend([\"run\", \"--with\", \"mcp\"])\n\n    if with_editable:\n        cmd.extend([\"--with-editable\", str(with_editable)])\n\n    if with_packages:\n        for pkg in with_packages:\n            if pkg:  # pragma: no branch\n                cmd.extend([\"--with\", pkg])\n\n    # Add mcp run command\n    cmd.extend([\"mcp\", \"run\", file_spec])\n    return cmd\n\n\ndef _parse_file_path(file_spec: str) -> tuple[Path, str | None]:\n    \"\"\"Parse a file path that may include a server object specification.\n\n    Args:\n        file_spec: Path to file, optionally with :object suffix\n\n    Returns:\n        Tuple of (file_path, server_object)\n    \"\"\"\n    # First check if we have a Windows path (e.g., C:\\...)\n    has_windows_drive = len(file_spec) > 1 and file_spec[1] == \":\"\n\n    # Split on the last colon, but only if it's not part of the Windows drive letter\n    # and there's actually another colon in the string after the drive letter\n    if \":\" in (file_spec[2:] if has_windows_drive else file_spec):\n        file_str, server_object = file_spec.rsplit(\":\", 1)\n    else:\n        file_str, server_object = file_spec, None\n\n    # Resolve the file path\n    file_path = Path(file_str).expanduser().resolve()\n    if not file_path.exists():\n        logger.error(f\"File not found: {file_path}\")\n        sys.exit(1)\n    if not file_path.is_file():\n        logger.error(f\"Not a file: {file_path}\")\n        sys.exit(1)\n\n    return file_path, server_object\n\n\ndef _import_server(file: Path, server_object: str | None = None):  # pragma: no cover\n    \"\"\"Import an MCP server from a file.\n\n    Args:\n        file: Path to the file\n        server_object: Optional object name in format \"module:object\" or just \"object\"\n\n    Returns:\n        The server object\n    \"\"\"\n    # Add parent directory to Python path so imports can be resolved\n    file_dir = str(file.parent)\n    if file_dir not in sys.path:\n        sys.path.insert(0, file_dir)\n\n    # Import the module\n    spec = importlib.util.spec_from_file_location(\"server_module\", file)\n    if not spec or not spec.loader:\n        logger.error(\"Could not load module\", extra={\"file\": str(file)})\n        sys.exit(1)\n\n    module = importlib.util.module_from_spec(spec)\n    spec.loader.exec_module(module)\n\n    def _check_server_object(server_object: Any, object_name: str):\n        \"\"\"Helper function to check that the server object is supported\n\n        Args:\n            server_object: The server object to check.\n\n        Returns:\n            True if it's supported.\n        \"\"\"\n        if not isinstance(server_object, MCPServer):\n            logger.error(f\"The server object {object_name} is of type {type(server_object)} (expecting {MCPServer}).\")\n            if isinstance(server_object, LowLevelServer):\n                logger.warning(\"Note that only MCPServer is supported. Low level Server class is not yet supported.\")\n            return False\n        return True\n\n    # If no object specified, try common server names\n    if not server_object:\n        # Look for the most common server object names\n        for name in [\"mcp\", \"server\", \"app\"]:\n            if hasattr(module, name):\n                if not _check_server_object(getattr(module, name), f\"{file}:{name}\"):\n                    logger.error(f\"Ignoring object '{file}:{name}' as it's not a valid server object\")\n                    continue\n                return getattr(module, name)\n\n        logger.error(\n            f\"No server object found in {file}. Please either:\\n\"\n            \"1. Use a standard variable name (mcp, server, or app)\\n\"\n            \"2. Specify the object name with file:object syntax\"\n            \"3. If the server creates the MCPServer object within main() \"\n            \"   or another function, refactor the MCPServer object to be a \"\n            \"   global variable named mcp, server, or app.\",\n            extra={\"file\": str(file)},\n        )\n        sys.exit(1)\n\n    # Handle module:object syntax\n    if \":\" in server_object:\n        module_name, object_name = server_object.split(\":\", 1)\n        try:\n            server_module = importlib.import_module(module_name)\n            server = getattr(server_module, object_name, None)\n        except ImportError:\n            logger.error(\n                f\"Could not import module '{module_name}'\",\n                extra={\"file\": str(file)},\n            )\n            sys.exit(1)\n    else:\n        # Just object name\n        server = getattr(module, server_object, None)\n\n    if server is None:\n        logger.error(\n            f\"Server object '{server_object}' not found\",\n            extra={\"file\": str(file)},\n        )\n        sys.exit(1)\n\n    if not _check_server_object(server, server_object):\n        sys.exit(1)\n\n    return server\n\n\n@app.command()\ndef version() -> None:  # pragma: no cover\n    \"\"\"Show the MCP version.\"\"\"\n    try:\n        version = importlib.metadata.version(\"mcp\")\n        print(f\"MCP version {version}\")\n    except importlib.metadata.PackageNotFoundError:\n        print(\"MCP version unknown (package not installed)\")\n        sys.exit(1)\n\n\n@app.command()\ndef dev(\n    file_spec: str = typer.Argument(\n        ...,\n        help=\"Python file to run, optionally with :object suffix\",\n    ),\n    with_editable: Annotated[\n        Path | None,\n        typer.Option(\n            \"--with-editable\",\n            \"-e\",\n            help=\"Directory containing pyproject.toml to install in editable mode\",\n            exists=True,\n            file_okay=False,\n            resolve_path=True,\n        ),\n    ] = None,\n    with_packages: Annotated[\n        list[str],\n        typer.Option(\n            \"--with\",\n            help=\"Additional packages to install\",\n        ),\n    ] = [],\n) -> None:  # pragma: no cover\n    \"\"\"Run an MCP server with the MCP Inspector.\"\"\"\n    file, server_object = _parse_file_path(file_spec)\n\n    logger.debug(\n        \"Starting dev server\",\n        extra={\n            \"file\": str(file),\n            \"server_object\": server_object,\n            \"with_editable\": str(with_editable) if with_editable else None,\n            \"with_packages\": with_packages,\n        },\n    )\n\n    try:\n        # Import server to get dependencies\n        server = _import_server(file, server_object)\n        if hasattr(server, \"dependencies\"):\n            with_packages = list(set(with_packages + server.dependencies))\n\n        uv_cmd = _build_uv_command(file_spec, with_editable, with_packages)\n\n        # Get the correct npx command\n        npx_cmd = _get_npx_command()\n        if not npx_cmd:\n            logger.error(\n                \"npx not found. Please ensure Node.js and npm are properly installed and added to your system PATH.\"\n            )\n            sys.exit(1)\n\n        # Run the MCP Inspector command with shell=True on Windows\n        shell = sys.platform == \"win32\"\n        process = subprocess.run(\n            [npx_cmd, \"@modelcontextprotocol/inspector\"] + uv_cmd,\n            check=True,\n            shell=shell,\n            env=dict(os.environ.items()),  # Convert to list of tuples for env update\n        )\n        sys.exit(process.returncode)\n    except subprocess.CalledProcessError as e:\n        logger.error(\n            \"Dev server failed\",\n            extra={\n                \"file\": str(file),\n                \"error\": str(e),\n                \"returncode\": e.returncode,\n            },\n        )\n        sys.exit(e.returncode)\n    except FileNotFoundError:\n        logger.error(\n            \"npx not found. Please ensure Node.js and npm are properly installed \"\n            \"and added to your system PATH. You may need to restart your terminal \"\n            \"after installation.\",\n            extra={\"file\": str(file)},\n        )\n        sys.exit(1)\n\n\n@app.command()\ndef run(\n    file_spec: str = typer.Argument(\n        ...,\n        help=\"Python file to run, optionally with :object suffix\",\n    ),\n    transport: Annotated[\n        str | None,\n        typer.Option(\n            \"--transport\",\n            \"-t\",\n            help=\"Transport protocol to use (stdio or sse)\",\n        ),\n    ] = None,\n) -> None:  # pragma: no cover\n    \"\"\"Run an MCP server.\n\n    The server can be specified in two ways:\n    1. Module approach: server.py - runs the module directly, expecting a server.run() call.\n    2. Import approach: server.py:app - imports and runs the specified server object.\n\n    Note: This command runs the server directly. You are responsible for ensuring\n    all dependencies are available.\n    For dependency management, use `mcp install` or `mcp dev` instead.\n    \"\"\"  # noqa: E501\n    file, server_object = _parse_file_path(file_spec)\n\n    logger.debug(\n        \"Running server\",\n        extra={\n            \"file\": str(file),\n            \"server_object\": server_object,\n            \"transport\": transport,\n        },\n    )\n\n    try:\n        # Import and get server object\n        server = _import_server(file, server_object)\n\n        # Run the server\n        kwargs = {}\n        if transport:\n            kwargs[\"transport\"] = transport\n\n        server.run(**kwargs)\n\n    except Exception:\n        logger.exception(\n            \"Failed to run server\",\n            extra={\n                \"file\": str(file),\n            },\n        )\n        sys.exit(1)\n\n\n@app.command()\ndef install(\n    file_spec: str = typer.Argument(\n        ...,\n        help=\"Python file to run, optionally with :object suffix\",\n    ),\n    server_name: Annotated[\n        str | None,\n        typer.Option(\n            \"--name\",\n            \"-n\",\n            help=\"Custom name for the server (defaults to server's name attribute or file name)\",\n        ),\n    ] = None,\n    with_editable: Annotated[\n        Path | None,\n        typer.Option(\n            \"--with-editable\",\n            \"-e\",\n            help=\"Directory containing pyproject.toml to install in editable mode\",\n            exists=True,\n            file_okay=False,\n            resolve_path=True,\n        ),\n    ] = None,\n    with_packages: Annotated[\n        list[str],\n        typer.Option(\n            \"--with\",\n            help=\"Additional packages to install\",\n        ),\n    ] = [],\n    env_vars: Annotated[\n        list[str],\n        typer.Option(\n            \"--env-var\",\n            \"-v\",\n            help=\"Environment variables in KEY=VALUE format\",\n        ),\n    ] = [],\n    env_file: Annotated[\n        Path | None,\n        typer.Option(\n            \"--env-file\",\n            \"-f\",\n            help=\"Load environment variables from a .env file\",\n            exists=True,\n            file_okay=True,\n            dir_okay=False,\n            resolve_path=True,\n        ),\n    ] = None,\n) -> None:  # pragma: no cover\n    \"\"\"Install an MCP server in the Claude desktop app.\n\n    Environment variables are preserved once added and only updated if new values\n    are explicitly provided.\n    \"\"\"\n    file, server_object = _parse_file_path(file_spec)\n\n    logger.debug(\n        \"Installing server\",\n        extra={\n            \"file\": str(file),\n            \"server_name\": server_name,\n            \"server_object\": server_object,\n            \"with_editable\": str(with_editable) if with_editable else None,\n            \"with_packages\": with_packages,\n        },\n    )\n\n    if not claude.get_claude_config_path():\n        logger.error(\"Claude app not found\")\n        sys.exit(1)\n\n    # Try to import server to get its name, but fall back to file name if dependencies\n    # missing\n    name = server_name\n    server = None\n    if not name:\n        try:\n            server = _import_server(file, server_object)\n            name = server.name\n        except (ImportError, ModuleNotFoundError) as e:\n            logger.debug(\n                \"Could not import server (likely missing dependencies), using file name\",\n                extra={\"error\": str(e)},\n            )\n            name = file.stem\n\n    # Get server dependencies if available\n    server_dependencies = getattr(server, \"dependencies\", []) if server else []\n    if server_dependencies:\n        with_packages = list(set(with_packages + server_dependencies))\n\n    # Process environment variables if provided\n    env_dict: dict[str, str] | None = None\n    if env_file or env_vars:\n        env_dict = {}\n        # Load from .env file if specified\n        if env_file:\n            if dotenv:\n                try:\n                    env_dict |= {k: v for k, v in dotenv.dotenv_values(env_file).items() if v is not None}\n                except (OSError, ValueError):\n                    logger.exception(\"Failed to load .env file\")\n                    sys.exit(1)\n            else:\n                logger.error(\"python-dotenv is not installed. Cannot load .env file.\")\n                sys.exit(1)\n\n        # Add command line environment variables\n        for env_var in env_vars:\n            key, value = _parse_env_var(env_var)\n            env_dict[key] = value\n\n    if claude.update_claude_config(\n        file_spec,\n        name,\n        with_editable=with_editable,\n        with_packages=with_packages,\n        env_vars=env_dict,\n    ):\n        logger.info(f\"Successfully installed {name} in Claude app\")\n    else:\n        logger.error(f\"Failed to install {name} in Claude app\")\n        sys.exit(1)\n"
  },
  {
    "path": "src/mcp/client/__init__.py",
    "content": "\"\"\"MCP Client module.\"\"\"\n\nfrom mcp.client._transport import Transport\nfrom mcp.client.client import Client\nfrom mcp.client.context import ClientRequestContext\nfrom mcp.client.session import ClientSession\n\n__all__ = [\"Client\", \"ClientRequestContext\", \"ClientSession\", \"Transport\"]\n"
  },
  {
    "path": "src/mcp/client/__main__.py",
    "content": "import argparse\nimport logging\nimport sys\nimport warnings\nfrom functools import partial\nfrom urllib.parse import urlparse\n\nimport anyio\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\n\nfrom mcp import types\nfrom mcp.client.session import ClientSession\nfrom mcp.client.sse import sse_client\nfrom mcp.client.stdio import StdioServerParameters, stdio_client\nfrom mcp.shared.message import SessionMessage\nfrom mcp.shared.session import RequestResponder\n\nif not sys.warnoptions:\n    warnings.simplefilter(\"ignore\")\n\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(\"client\")\n\n\nasync def message_handler(\n    message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,\n) -> None:\n    if isinstance(message, Exception):\n        logger.error(\"Error: %s\", message)\n        return\n\n    logger.info(\"Received message from server: %s\", message)\n\n\nasync def run_session(\n    read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],\n    write_stream: MemoryObjectSendStream[SessionMessage],\n    client_info: types.Implementation | None = None,\n):\n    async with ClientSession(\n        read_stream,\n        write_stream,\n        message_handler=message_handler,\n        client_info=client_info,\n    ) as session:\n        logger.info(\"Initializing session\")\n        await session.initialize()\n        logger.info(\"Initialized\")\n\n\nasync def main(command_or_url: str, args: list[str], env: list[tuple[str, str]]):\n    env_dict = dict(env)\n\n    if urlparse(command_or_url).scheme in (\"http\", \"https\"):\n        # Use SSE client for HTTP(S) URLs\n        async with sse_client(command_or_url) as streams:\n            await run_session(*streams)\n    else:\n        # Use stdio client for commands\n        server_parameters = StdioServerParameters(command=command_or_url, args=args, env=env_dict)\n        async with stdio_client(server_parameters) as streams:\n            await run_session(*streams)\n\n\ndef cli():\n    parser = argparse.ArgumentParser()\n    parser.add_argument(\"command_or_url\", help=\"Command or URL to connect to\")\n    parser.add_argument(\"args\", nargs=\"*\", help=\"Additional arguments\")\n    parser.add_argument(\n        \"-e\",\n        \"--env\",\n        nargs=2,\n        action=\"append\",\n        metavar=(\"KEY\", \"VALUE\"),\n        help=\"Environment variables to set. Can be used multiple times.\",\n        default=[],\n    )\n\n    args = parser.parse_args()\n    anyio.run(partial(main, args.command_or_url, args.args, args.env), backend=\"trio\")\n\n\nif __name__ == \"__main__\":\n    cli()\n"
  },
  {
    "path": "src/mcp/client/_memory.py",
    "content": "\"\"\"In-memory transport for testing MCP servers without network overhead.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import AsyncIterator\nfrom contextlib import AbstractAsyncContextManager, asynccontextmanager\nfrom types import TracebackType\nfrom typing import Any\n\nimport anyio\n\nfrom mcp.client._transport import TransportStreams\nfrom mcp.server import Server\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.shared.memory import create_client_server_memory_streams\n\n\nclass InMemoryTransport:\n    \"\"\"In-memory transport for testing MCP servers without network overhead.\n\n    This transport starts the server in a background task and provides\n    streams for client-side communication. The server is automatically\n    stopped when the context manager exits.\n    \"\"\"\n\n    def __init__(self, server: Server[Any] | MCPServer, *, raise_exceptions: bool = False) -> None:\n        \"\"\"Initialize the in-memory transport.\n\n        Args:\n            server: The MCP server to connect to (Server or MCPServer instance)\n            raise_exceptions: Whether to raise exceptions from the server\n        \"\"\"\n        self._server = server\n        self._raise_exceptions = raise_exceptions\n        self._cm: AbstractAsyncContextManager[TransportStreams] | None = None\n\n    @asynccontextmanager\n    async def _connect(self) -> AsyncIterator[TransportStreams]:\n        \"\"\"Connect to the server and yield streams for communication.\"\"\"\n        # Unwrap MCPServer to get underlying Server\n        if isinstance(self._server, MCPServer):\n            # TODO(Marcelo): Make `lowlevel_server` public.\n            actual_server: Server[Any] = self._server._lowlevel_server  # type: ignore[reportPrivateUsage]\n        else:\n            actual_server = self._server\n\n        async with create_client_server_memory_streams() as (client_streams, server_streams):\n            client_read, client_write = client_streams\n            server_read, server_write = server_streams\n\n            async with anyio.create_task_group() as tg:\n                # Start server in background\n                tg.start_soon(\n                    lambda: actual_server.run(\n                        server_read,\n                        server_write,\n                        actual_server.create_initialization_options(),\n                        raise_exceptions=self._raise_exceptions,\n                    )\n                )\n\n                try:\n                    yield client_read, client_write\n                finally:\n                    tg.cancel_scope.cancel()\n\n    async def __aenter__(self) -> TransportStreams:\n        \"\"\"Connect to the server and return streams for communication.\"\"\"\n        self._cm = self._connect()\n        return await self._cm.__aenter__()\n\n    async def __aexit__(\n        self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: TracebackType | None\n    ) -> None:\n        \"\"\"Close the transport and stop the server.\"\"\"\n        if self._cm is not None:  # pragma: no branch\n            await self._cm.__aexit__(exc_type, exc_val, exc_tb)\n            self._cm = None\n"
  },
  {
    "path": "src/mcp/client/_transport.py",
    "content": "\"\"\"Transport protocol for MCP clients.\"\"\"\n\nfrom __future__ import annotations\n\nfrom contextlib import AbstractAsyncContextManager\nfrom typing import Protocol\n\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\n\nfrom mcp.shared.message import SessionMessage\n\nTransportStreams = tuple[MemoryObjectReceiveStream[SessionMessage | Exception], MemoryObjectSendStream[SessionMessage]]\n\n\nclass Transport(AbstractAsyncContextManager[TransportStreams], Protocol):\n    \"\"\"Protocol for MCP transports.\n\n    A transport is an async context manager that yields read and write streams\n    for bidirectional communication with an MCP server.\n    \"\"\"\n"
  },
  {
    "path": "src/mcp/client/auth/__init__.py",
    "content": "\"\"\"OAuth2 Authentication implementation for HTTPX.\n\nImplements authorization code flow with PKCE and automatic token refresh.\n\"\"\"\n\nfrom mcp.client.auth.exceptions import OAuthFlowError, OAuthRegistrationError, OAuthTokenError\nfrom mcp.client.auth.oauth2 import (\n    OAuthClientProvider,\n    PKCEParameters,\n    TokenStorage,\n)\n\n__all__ = [\n    \"OAuthClientProvider\",\n    \"OAuthFlowError\",\n    \"OAuthRegistrationError\",\n    \"OAuthTokenError\",\n    \"PKCEParameters\",\n    \"TokenStorage\",\n]\n"
  },
  {
    "path": "src/mcp/client/auth/exceptions.py",
    "content": "class OAuthFlowError(Exception):\n    \"\"\"Base exception for OAuth flow errors.\"\"\"\n\n\nclass OAuthTokenError(OAuthFlowError):\n    \"\"\"Raised when token operations fail.\"\"\"\n\n\nclass OAuthRegistrationError(OAuthFlowError):\n    \"\"\"Raised when client registration fails.\"\"\"\n"
  },
  {
    "path": "src/mcp/client/auth/extensions/__init__.py",
    "content": ""
  },
  {
    "path": "src/mcp/client/auth/extensions/client_credentials.py",
    "content": "\"\"\"OAuth client credential extensions for MCP.\n\nProvides OAuth providers for machine-to-machine authentication flows:\n- ClientCredentialsOAuthProvider: For client_credentials with client_id + client_secret\n- PrivateKeyJWTOAuthProvider: For client_credentials with private_key_jwt authentication\n  (typically using a pre-built JWT from workload identity federation)\n- RFC7523OAuthClientProvider: For jwt-bearer grant (RFC 7523 Section 2.1)\n\"\"\"\n\nimport time\nimport warnings\nfrom collections.abc import Awaitable, Callable\nfrom typing import Any, Literal\nfrom uuid import uuid4\n\nimport httpx\nimport jwt\nfrom pydantic import BaseModel, Field\n\nfrom mcp.client.auth import OAuthClientProvider, OAuthFlowError, OAuthTokenError, TokenStorage\nfrom mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata\n\n\nclass ClientCredentialsOAuthProvider(OAuthClientProvider):\n    \"\"\"OAuth provider for client_credentials grant with client_id + client_secret.\n\n    This provider sets client_info directly, bypassing dynamic client registration.\n    Use this when you already have client credentials (client_id and client_secret).\n\n    Example:\n        ```python\n        provider = ClientCredentialsOAuthProvider(\n            server_url=\"https://api.example.com\",\n            storage=my_token_storage,\n            client_id=\"my-client-id\",\n            client_secret=\"my-client-secret\",\n        )\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        server_url: str,\n        storage: TokenStorage,\n        client_id: str,\n        client_secret: str,\n        token_endpoint_auth_method: Literal[\"client_secret_basic\", \"client_secret_post\"] = \"client_secret_basic\",\n        scopes: str | None = None,\n    ) -> None:\n        \"\"\"Initialize client_credentials OAuth provider.\n\n        Args:\n            server_url: The MCP server URL.\n            storage: Token storage implementation.\n            client_id: The OAuth client ID.\n            client_secret: The OAuth client secret.\n            token_endpoint_auth_method: Authentication method for token endpoint.\n                Either \"client_secret_basic\" (default) or \"client_secret_post\".\n            scopes: Optional space-separated list of scopes to request.\n        \"\"\"\n        # Build minimal client_metadata for the base class\n        client_metadata = OAuthClientMetadata(\n            redirect_uris=None,\n            grant_types=[\"client_credentials\"],\n            token_endpoint_auth_method=token_endpoint_auth_method,\n            scope=scopes,\n        )\n        super().__init__(server_url, client_metadata, storage, None, None, 300.0)\n        # Store client_info to be set during _initialize - no dynamic registration needed\n        self._fixed_client_info = OAuthClientInformationFull(\n            redirect_uris=None,\n            client_id=client_id,\n            client_secret=client_secret,\n            grant_types=[\"client_credentials\"],\n            token_endpoint_auth_method=token_endpoint_auth_method,\n            scope=scopes,\n        )\n\n    async def _initialize(self) -> None:\n        \"\"\"Load stored tokens and set pre-configured client_info.\"\"\"\n        self.context.current_tokens = await self.context.storage.get_tokens()\n        self.context.client_info = self._fixed_client_info\n        self._initialized = True\n\n    async def _perform_authorization(self) -> httpx.Request:\n        \"\"\"Perform client_credentials authorization.\"\"\"\n        return await self._exchange_token_client_credentials()\n\n    async def _exchange_token_client_credentials(self) -> httpx.Request:\n        \"\"\"Build token exchange request for client_credentials grant.\"\"\"\n        token_data: dict[str, Any] = {\n            \"grant_type\": \"client_credentials\",\n        }\n\n        headers: dict[str, str] = {\"Content-Type\": \"application/x-www-form-urlencoded\"}\n\n        # Use standard auth methods (client_secret_basic, client_secret_post, none)\n        token_data, headers = self.context.prepare_token_auth(token_data, headers)\n\n        if self.context.should_include_resource_param(self.context.protocol_version):\n            token_data[\"resource\"] = self.context.get_resource_url()\n\n        if self.context.client_metadata.scope:\n            token_data[\"scope\"] = self.context.client_metadata.scope\n\n        token_url = self._get_token_endpoint()\n        return httpx.Request(\"POST\", token_url, data=token_data, headers=headers)\n\n\ndef static_assertion_provider(token: str) -> Callable[[str], Awaitable[str]]:\n    \"\"\"Create an assertion provider that returns a static JWT token.\n\n    Use this when you have a pre-built JWT (e.g., from workload identity federation)\n    that doesn't need the audience parameter.\n\n    Example:\n        ```python\n        provider = PrivateKeyJWTOAuthProvider(\n            server_url=\"https://api.example.com\",\n            storage=my_token_storage,\n            client_id=\"my-client-id\",\n            assertion_provider=static_assertion_provider(my_prebuilt_jwt),\n        )\n        ```\n\n    Args:\n        token: The pre-built JWT assertion string.\n\n    Returns:\n        An async callback suitable for use as an assertion_provider.\n    \"\"\"\n\n    async def provider(audience: str) -> str:\n        return token\n\n    return provider\n\n\nclass SignedJWTParameters(BaseModel):\n    \"\"\"Parameters for creating SDK-signed JWT assertions.\n\n    Use `create_assertion_provider()` to create an assertion provider callback\n    for use with `PrivateKeyJWTOAuthProvider`.\n\n    Example:\n        ```python\n        jwt_params = SignedJWTParameters(\n            issuer=\"my-client-id\",\n            subject=\"my-client-id\",\n            signing_key=private_key_pem,\n        )\n        provider = PrivateKeyJWTOAuthProvider(\n            server_url=\"https://api.example.com\",\n            storage=my_token_storage,\n            client_id=\"my-client-id\",\n            assertion_provider=jwt_params.create_assertion_provider(),\n        )\n        ```\n    \"\"\"\n\n    issuer: str = Field(description=\"Issuer for JWT assertions (typically client_id).\")\n    subject: str = Field(description=\"Subject identifier for JWT assertions (typically client_id).\")\n    signing_key: str = Field(description=\"Private key for JWT signing (PEM format).\")\n    signing_algorithm: str = Field(default=\"RS256\", description=\"Algorithm for signing JWT assertions.\")\n    lifetime_seconds: int = Field(default=300, description=\"Lifetime of generated JWT in seconds.\")\n    additional_claims: dict[str, Any] | None = Field(default=None, description=\"Additional claims.\")\n\n    def create_assertion_provider(self) -> Callable[[str], Awaitable[str]]:\n        \"\"\"Create an assertion provider callback for use with PrivateKeyJWTOAuthProvider.\n\n        Returns:\n            An async callback that takes the audience (authorization server issuer URL)\n            and returns a signed JWT assertion.\n        \"\"\"\n\n        async def provider(audience: str) -> str:\n            now = int(time.time())\n            claims: dict[str, Any] = {\n                \"iss\": self.issuer,\n                \"sub\": self.subject,\n                \"aud\": audience,\n                \"exp\": now + self.lifetime_seconds,\n                \"iat\": now,\n                \"jti\": str(uuid4()),\n            }\n            if self.additional_claims:\n                claims.update(self.additional_claims)\n\n            return jwt.encode(claims, self.signing_key, algorithm=self.signing_algorithm)\n\n        return provider\n\n\nclass PrivateKeyJWTOAuthProvider(OAuthClientProvider):\n    \"\"\"OAuth provider for client_credentials grant with private_key_jwt authentication.\n\n    Uses RFC 7523 Section 2.2 for client authentication via JWT assertion.\n\n    The JWT assertion's audience MUST be the authorization server's issuer identifier\n    (per RFC 7523bis security updates). The `assertion_provider` callback receives\n    this audience value and must return a JWT with that audience.\n\n    **Option 1: Pre-built JWT via Workload Identity Federation**\n\n    In production scenarios, the JWT assertion is typically obtained from a workload\n    identity provider (e.g., GCP, AWS IAM, Azure AD):\n\n        ```python\n        async def get_workload_identity_token(audience: str) -> str:\n            # Fetch JWT from your identity provider\n            # The JWT's audience must match the provided audience parameter\n            return await fetch_token_from_identity_provider(audience=audience)\n\n        provider = PrivateKeyJWTOAuthProvider(\n            server_url=\"https://api.example.com\",\n            storage=my_token_storage,\n            client_id=\"my-client-id\",\n            assertion_provider=get_workload_identity_token,\n        )\n        ```\n\n    **Option 2: Static pre-built JWT**\n\n    If you have a static JWT that doesn't need the audience parameter:\n\n        ```python\n        provider = PrivateKeyJWTOAuthProvider(\n            server_url=\"https://api.example.com\",\n            storage=my_token_storage,\n            client_id=\"my-client-id\",\n            assertion_provider=static_assertion_provider(my_prebuilt_jwt),\n        )\n        ```\n\n    **Option 3: SDK-signed JWT (for testing/simple setups)**\n\n    For testing or simple deployments, use `SignedJWTParameters.create_assertion_provider()`:\n\n        ```python\n        jwt_params = SignedJWTParameters(\n            issuer=\"my-client-id\",\n            subject=\"my-client-id\",\n            signing_key=private_key_pem,\n        )\n        provider = PrivateKeyJWTOAuthProvider(\n            server_url=\"https://api.example.com\",\n            storage=my_token_storage,\n            client_id=\"my-client-id\",\n            assertion_provider=jwt_params.create_assertion_provider(),\n        )\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        server_url: str,\n        storage: TokenStorage,\n        client_id: str,\n        assertion_provider: Callable[[str], Awaitable[str]],\n        scopes: str | None = None,\n    ) -> None:\n        \"\"\"Initialize private_key_jwt OAuth provider.\n\n        Args:\n            server_url: The MCP server URL.\n            storage: Token storage implementation.\n            client_id: The OAuth client ID.\n            assertion_provider: Async callback that takes the audience (authorization\n                server's issuer identifier) and returns a JWT assertion. Use\n                `SignedJWTParameters.create_assertion_provider()` for SDK-signed JWTs,\n                `static_assertion_provider()` for pre-built JWTs, or provide your own\n                callback for workload identity federation.\n            scopes: Optional space-separated list of scopes to request.\n        \"\"\"\n        # Build minimal client_metadata for the base class\n        client_metadata = OAuthClientMetadata(\n            redirect_uris=None,\n            grant_types=[\"client_credentials\"],\n            token_endpoint_auth_method=\"private_key_jwt\",\n            scope=scopes,\n        )\n        super().__init__(server_url, client_metadata, storage, None, None, 300.0)\n        self._assertion_provider = assertion_provider\n        # Store client_info to be set during _initialize - no dynamic registration needed\n        self._fixed_client_info = OAuthClientInformationFull(\n            redirect_uris=None,\n            client_id=client_id,\n            grant_types=[\"client_credentials\"],\n            token_endpoint_auth_method=\"private_key_jwt\",\n            scope=scopes,\n        )\n\n    async def _initialize(self) -> None:\n        \"\"\"Load stored tokens and set pre-configured client_info.\"\"\"\n        self.context.current_tokens = await self.context.storage.get_tokens()\n        self.context.client_info = self._fixed_client_info\n        self._initialized = True\n\n    async def _perform_authorization(self) -> httpx.Request:\n        \"\"\"Perform client_credentials authorization with private_key_jwt.\"\"\"\n        return await self._exchange_token_client_credentials()\n\n    async def _add_client_authentication_jwt(self, *, token_data: dict[str, Any]) -> None:\n        \"\"\"Add JWT assertion for client authentication to token endpoint parameters.\"\"\"\n        if not self.context.oauth_metadata:\n            raise OAuthFlowError(\"Missing OAuth metadata for private_key_jwt flow\")  # pragma: no cover\n\n        # Audience MUST be the issuer identifier of the authorization server\n        # https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rfc7523bis-01\n        audience = str(self.context.oauth_metadata.issuer)\n        assertion = await self._assertion_provider(audience)\n\n        # RFC 7523 Section 2.2: client authentication via JWT\n        token_data[\"client_assertion\"] = assertion\n        token_data[\"client_assertion_type\"] = \"urn:ietf:params:oauth:client-assertion-type:jwt-bearer\"\n\n    async def _exchange_token_client_credentials(self) -> httpx.Request:\n        \"\"\"Build token exchange request for client_credentials grant with private_key_jwt.\"\"\"\n        token_data: dict[str, Any] = {\n            \"grant_type\": \"client_credentials\",\n        }\n\n        headers: dict[str, str] = {\"Content-Type\": \"application/x-www-form-urlencoded\"}\n\n        # Add JWT client authentication (RFC 7523 Section 2.2)\n        await self._add_client_authentication_jwt(token_data=token_data)\n\n        if self.context.should_include_resource_param(self.context.protocol_version):\n            token_data[\"resource\"] = self.context.get_resource_url()\n\n        if self.context.client_metadata.scope:\n            token_data[\"scope\"] = self.context.client_metadata.scope\n\n        token_url = self._get_token_endpoint()\n        return httpx.Request(\"POST\", token_url, data=token_data, headers=headers)\n\n\nclass JWTParameters(BaseModel):\n    \"\"\"JWT parameters.\"\"\"\n\n    assertion: str | None = Field(\n        default=None,\n        description=\"JWT assertion for JWT authentication. \"\n        \"Will be used instead of generating a new assertion if provided.\",\n    )\n\n    issuer: str | None = Field(default=None, description=\"Issuer for JWT assertions.\")\n    subject: str | None = Field(default=None, description=\"Subject identifier for JWT assertions.\")\n    audience: str | None = Field(default=None, description=\"Audience for JWT assertions.\")\n    claims: dict[str, Any] | None = Field(default=None, description=\"Additional claims for JWT assertions.\")\n    jwt_signing_algorithm: str | None = Field(default=\"RS256\", description=\"Algorithm for signing JWT assertions.\")\n    jwt_signing_key: str | None = Field(default=None, description=\"Private key for JWT signing.\")\n    jwt_lifetime_seconds: int = Field(default=300, description=\"Lifetime of generated JWT in seconds.\")\n\n    def to_assertion(self, with_audience_fallback: str | None = None) -> str:\n        if self.assertion is not None:\n            # Prebuilt JWT (e.g. acquired out-of-band)\n            assertion = self.assertion\n        else:\n            if not self.jwt_signing_key:\n                raise OAuthFlowError(\"Missing signing key for JWT bearer grant\")  # pragma: no cover\n            if not self.issuer:\n                raise OAuthFlowError(\"Missing issuer for JWT bearer grant\")  # pragma: no cover\n            if not self.subject:\n                raise OAuthFlowError(\"Missing subject for JWT bearer grant\")  # pragma: no cover\n\n            audience = self.audience if self.audience else with_audience_fallback\n            if not audience:\n                raise OAuthFlowError(\"Missing audience for JWT bearer grant\")  # pragma: no cover\n\n            now = int(time.time())\n            claims: dict[str, Any] = {\n                \"iss\": self.issuer,\n                \"sub\": self.subject,\n                \"aud\": audience,\n                \"exp\": now + self.jwt_lifetime_seconds,\n                \"iat\": now,\n                \"jti\": str(uuid4()),\n            }\n            claims.update(self.claims or {})\n\n            assertion = jwt.encode(\n                claims,\n                self.jwt_signing_key,\n                algorithm=self.jwt_signing_algorithm or \"RS256\",\n            )\n        return assertion\n\n\nclass RFC7523OAuthClientProvider(OAuthClientProvider):\n    \"\"\"OAuth client provider for RFC 7523 jwt-bearer grant.\n\n    .. deprecated::\n        Use :class:`ClientCredentialsOAuthProvider` for client_credentials with\n        client_id + client_secret, or :class:`PrivateKeyJWTOAuthProvider` for\n        client_credentials with private_key_jwt authentication instead.\n\n    This provider supports the jwt-bearer authorization grant (RFC 7523 Section 2.1)\n    where the JWT itself is the authorization grant.\n    \"\"\"\n\n    def __init__(\n        self,\n        server_url: str,\n        client_metadata: OAuthClientMetadata,\n        storage: TokenStorage,\n        redirect_handler: Callable[[str], Awaitable[None]] | None = None,\n        callback_handler: Callable[[], Awaitable[tuple[str, str | None]]] | None = None,\n        timeout: float = 300.0,\n        jwt_parameters: JWTParameters | None = None,\n    ) -> None:\n        warnings.warn(\n            \"RFC7523OAuthClientProvider is deprecated. Use ClientCredentialsOAuthProvider \"\n            \"or PrivateKeyJWTOAuthProvider instead.\",\n            DeprecationWarning,\n            stacklevel=2,\n        )\n        super().__init__(server_url, client_metadata, storage, redirect_handler, callback_handler, timeout)\n        self.jwt_parameters = jwt_parameters\n\n    async def _exchange_token_authorization_code(\n        self, auth_code: str, code_verifier: str, *, token_data: dict[str, Any] | None = None\n    ) -> httpx.Request:  # pragma: no cover\n        \"\"\"Build token exchange request for authorization_code flow.\"\"\"\n        token_data = token_data or {}\n        if self.context.client_metadata.token_endpoint_auth_method == \"private_key_jwt\":\n            self._add_client_authentication_jwt(token_data=token_data)\n        return await super()._exchange_token_authorization_code(auth_code, code_verifier, token_data=token_data)\n\n    async def _perform_authorization(self) -> httpx.Request:  # pragma: no cover\n        \"\"\"Perform the authorization flow.\"\"\"\n        if \"urn:ietf:params:oauth:grant-type:jwt-bearer\" in self.context.client_metadata.grant_types:\n            token_request = await self._exchange_token_jwt_bearer()\n            return token_request\n        else:\n            return await super()._perform_authorization()\n\n    def _add_client_authentication_jwt(self, *, token_data: dict[str, Any]):  # pragma: no cover\n        \"\"\"Add JWT assertion for client authentication to token endpoint parameters.\"\"\"\n        if not self.jwt_parameters:\n            raise OAuthTokenError(\"Missing JWT parameters for private_key_jwt flow\")\n        if not self.context.oauth_metadata:\n            raise OAuthTokenError(\"Missing OAuth metadata for private_key_jwt flow\")\n\n        # We need to set the audience to the issuer identifier of the authorization server\n        # https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rfc7523bis-01#name-updates-to-rfc-7523\n        issuer = str(self.context.oauth_metadata.issuer)\n        assertion = self.jwt_parameters.to_assertion(with_audience_fallback=issuer)\n\n        # When using private_key_jwt, in a client_credentials flow, we use RFC 7523 Section 2.2\n        token_data[\"client_assertion\"] = assertion\n        token_data[\"client_assertion_type\"] = \"urn:ietf:params:oauth:client-assertion-type:jwt-bearer\"\n        # We need to set the audience to the resource server, the audience is different from the one in claims\n        # it represents the resource server that will validate the token\n        token_data[\"audience\"] = self.context.get_resource_url()\n\n    async def _exchange_token_jwt_bearer(self) -> httpx.Request:\n        \"\"\"Build token exchange request for JWT bearer grant.\"\"\"\n        if not self.context.client_info:\n            raise OAuthFlowError(\"Missing client info\")  # pragma: no cover\n        if not self.jwt_parameters:\n            raise OAuthFlowError(\"Missing JWT parameters\")  # pragma: no cover\n        if not self.context.oauth_metadata:\n            raise OAuthTokenError(\"Missing OAuth metadata\")  # pragma: no cover\n\n        # We need to set the audience to the issuer identifier of the authorization server\n        # https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rfc7523bis-01#name-updates-to-rfc-7523\n        issuer = str(self.context.oauth_metadata.issuer)\n        assertion = self.jwt_parameters.to_assertion(with_audience_fallback=issuer)\n\n        token_data = {\n            \"grant_type\": \"urn:ietf:params:oauth:grant-type:jwt-bearer\",\n            \"assertion\": assertion,\n        }\n\n        if self.context.should_include_resource_param(self.context.protocol_version):  # pragma: no branch\n            token_data[\"resource\"] = self.context.get_resource_url()\n\n        if self.context.client_metadata.scope:  # pragma: no branch\n            token_data[\"scope\"] = self.context.client_metadata.scope\n\n        token_url = self._get_token_endpoint()\n        return httpx.Request(\n            \"POST\", token_url, data=token_data, headers={\"Content-Type\": \"application/x-www-form-urlencoded\"}\n        )\n"
  },
  {
    "path": "src/mcp/client/auth/oauth2.py",
    "content": "\"\"\"OAuth2 Authentication implementation for HTTPX.\n\nImplements authorization code flow with PKCE and automatic token refresh.\n\"\"\"\n\nimport base64\nimport hashlib\nimport logging\nimport secrets\nimport string\nimport time\nfrom collections.abc import AsyncGenerator, Awaitable, Callable\nfrom dataclasses import dataclass, field\nfrom typing import Any, Protocol\nfrom urllib.parse import quote, urlencode, urljoin, urlparse\n\nimport anyio\nimport httpx\nfrom pydantic import BaseModel, Field, ValidationError\n\nfrom mcp.client.auth.exceptions import OAuthFlowError, OAuthTokenError\nfrom mcp.client.auth.utils import (\n    build_oauth_authorization_server_metadata_discovery_urls,\n    build_protected_resource_metadata_discovery_urls,\n    create_client_info_from_metadata_url,\n    create_client_registration_request,\n    create_oauth_metadata_request,\n    extract_field_from_www_auth,\n    extract_resource_metadata_from_www_auth,\n    extract_scope_from_www_auth,\n    get_client_metadata_scopes,\n    handle_auth_metadata_response,\n    handle_protected_resource_response,\n    handle_registration_response,\n    handle_token_response_scopes,\n    is_valid_client_metadata_url,\n    should_use_client_metadata_url,\n)\nfrom mcp.client.streamable_http import MCP_PROTOCOL_VERSION\nfrom mcp.shared.auth import (\n    OAuthClientInformationFull,\n    OAuthClientMetadata,\n    OAuthMetadata,\n    OAuthToken,\n    ProtectedResourceMetadata,\n)\nfrom mcp.shared.auth_utils import (\n    calculate_token_expiry,\n    check_resource_allowed,\n    resource_url_from_server_url,\n)\n\nlogger = logging.getLogger(__name__)\n\n\nclass PKCEParameters(BaseModel):\n    \"\"\"PKCE (Proof Key for Code Exchange) parameters.\"\"\"\n\n    code_verifier: str = Field(..., min_length=43, max_length=128)\n    code_challenge: str = Field(..., min_length=43, max_length=128)\n\n    @classmethod\n    def generate(cls) -> \"PKCEParameters\":\n        \"\"\"Generate new PKCE parameters.\"\"\"\n        code_verifier = \"\".join(secrets.choice(string.ascii_letters + string.digits + \"-._~\") for _ in range(128))\n        digest = hashlib.sha256(code_verifier.encode()).digest()\n        code_challenge = base64.urlsafe_b64encode(digest).decode().rstrip(\"=\")\n        return cls(code_verifier=code_verifier, code_challenge=code_challenge)\n\n\nclass TokenStorage(Protocol):\n    \"\"\"Protocol for token storage implementations.\"\"\"\n\n    async def get_tokens(self) -> OAuthToken | None:\n        \"\"\"Get stored tokens.\"\"\"\n        ...\n\n    async def set_tokens(self, tokens: OAuthToken) -> None:\n        \"\"\"Store tokens.\"\"\"\n        ...\n\n    async def get_client_info(self) -> OAuthClientInformationFull | None:\n        \"\"\"Get stored client information.\"\"\"\n        ...\n\n    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:\n        \"\"\"Store client information.\"\"\"\n        ...\n\n\n@dataclass\nclass OAuthContext:\n    \"\"\"OAuth flow context.\"\"\"\n\n    server_url: str\n    client_metadata: OAuthClientMetadata\n    storage: TokenStorage\n    redirect_handler: Callable[[str], Awaitable[None]] | None\n    callback_handler: Callable[[], Awaitable[tuple[str, str | None]]] | None\n    timeout: float = 300.0\n    client_metadata_url: str | None = None\n\n    # Discovered metadata\n    protected_resource_metadata: ProtectedResourceMetadata | None = None\n    oauth_metadata: OAuthMetadata | None = None\n    auth_server_url: str | None = None\n    protocol_version: str | None = None\n\n    # Client registration\n    client_info: OAuthClientInformationFull | None = None\n\n    # Token management\n    current_tokens: OAuthToken | None = None\n    token_expiry_time: float | None = None\n\n    # State\n    lock: anyio.Lock = field(default_factory=anyio.Lock)\n\n    def get_authorization_base_url(self, server_url: str) -> str:\n        \"\"\"Extract base URL by removing path component.\"\"\"\n        parsed = urlparse(server_url)\n        return f\"{parsed.scheme}://{parsed.netloc}\"\n\n    def update_token_expiry(self, token: OAuthToken) -> None:\n        \"\"\"Update token expiry time using shared util function.\"\"\"\n        self.token_expiry_time = calculate_token_expiry(token.expires_in)\n\n    def is_token_valid(self) -> bool:\n        \"\"\"Check if current token is valid.\"\"\"\n        return bool(\n            self.current_tokens\n            and self.current_tokens.access_token\n            and (not self.token_expiry_time or time.time() <= self.token_expiry_time)\n        )\n\n    def can_refresh_token(self) -> bool:\n        \"\"\"Check if token can be refreshed.\"\"\"\n        return bool(self.current_tokens and self.current_tokens.refresh_token and self.client_info)\n\n    def clear_tokens(self) -> None:\n        \"\"\"Clear current tokens.\"\"\"\n        self.current_tokens = None\n        self.token_expiry_time = None\n\n    def get_resource_url(self) -> str:\n        \"\"\"Get resource URL for RFC 8707.\n\n        Uses PRM resource if it's a valid parent, otherwise uses canonical server URL.\n        \"\"\"\n        resource = resource_url_from_server_url(self.server_url)\n\n        # If PRM provides a resource that's a valid parent, use it\n        if self.protected_resource_metadata and self.protected_resource_metadata.resource:\n            prm_resource = str(self.protected_resource_metadata.resource)\n            if check_resource_allowed(requested_resource=resource, configured_resource=prm_resource):\n                resource = prm_resource\n\n        return resource\n\n    def should_include_resource_param(self, protocol_version: str | None = None) -> bool:\n        \"\"\"Determine if the resource parameter should be included in OAuth requests.\n\n        Returns True if:\n        - Protected resource metadata is available, OR\n        - MCP-Protocol-Version header is 2025-06-18 or later\n        \"\"\"\n        # If we have protected resource metadata, include the resource param\n        if self.protected_resource_metadata is not None:\n            return True\n\n        # If no protocol version provided, don't include resource param\n        if not protocol_version:\n            return False\n\n        # Check if protocol version is 2025-06-18 or later\n        # Version format is YYYY-MM-DD, so string comparison works\n        return protocol_version >= \"2025-06-18\"\n\n    def prepare_token_auth(\n        self, data: dict[str, str], headers: dict[str, str] | None = None\n    ) -> tuple[dict[str, str], dict[str, str]]:\n        \"\"\"Prepare authentication for token requests.\n\n        Args:\n            data: The form data to send\n            headers: Optional headers dict to update\n\n        Returns:\n            Tuple of (updated_data, updated_headers)\n        \"\"\"\n        if headers is None:\n            headers = {}  # pragma: no cover\n\n        if not self.client_info:\n            return data, headers\n\n        auth_method = self.client_info.token_endpoint_auth_method\n\n        if auth_method == \"client_secret_basic\" and self.client_info.client_id and self.client_info.client_secret:\n            # URL-encode client ID and secret per RFC 6749 Section 2.3.1\n            encoded_id = quote(self.client_info.client_id, safe=\"\")\n            encoded_secret = quote(self.client_info.client_secret, safe=\"\")\n            credentials = f\"{encoded_id}:{encoded_secret}\"\n            encoded_credentials = base64.b64encode(credentials.encode()).decode()\n            headers[\"Authorization\"] = f\"Basic {encoded_credentials}\"\n            # Don't include client_secret in body for basic auth\n            data = {k: v for k, v in data.items() if k != \"client_secret\"}\n        elif auth_method == \"client_secret_post\" and self.client_info.client_id and self.client_info.client_secret:\n            # Include client_id and client_secret in request body (RFC 6749 §2.3.1)\n            data[\"client_id\"] = self.client_info.client_id\n            data[\"client_secret\"] = self.client_info.client_secret\n        # For auth_method == \"none\", don't add any client_secret\n\n        return data, headers\n\n\nclass OAuthClientProvider(httpx.Auth):\n    \"\"\"OAuth2 authentication for httpx.\n\n    Handles OAuth flow with automatic client registration and token storage.\n    \"\"\"\n\n    requires_response_body = True\n\n    def __init__(\n        self,\n        server_url: str,\n        client_metadata: OAuthClientMetadata,\n        storage: TokenStorage,\n        redirect_handler: Callable[[str], Awaitable[None]] | None = None,\n        callback_handler: Callable[[], Awaitable[tuple[str, str | None]]] | None = None,\n        timeout: float = 300.0,\n        client_metadata_url: str | None = None,\n        validate_resource_url: Callable[[str, str | None], Awaitable[None]] | None = None,\n    ):\n        \"\"\"Initialize OAuth2 authentication.\n\n        Args:\n            server_url: The MCP server URL.\n            client_metadata: OAuth client metadata for registration.\n            storage: Token storage implementation.\n            redirect_handler: Handler for authorization redirects.\n            callback_handler: Handler for authorization callbacks.\n            timeout: Timeout for the OAuth flow.\n            client_metadata_url: URL-based client ID. When provided and the server\n                advertises client_id_metadata_document_supported=True, this URL will be\n                used as the client_id instead of performing dynamic client registration.\n                Must be a valid HTTPS URL with a non-root pathname.\n            validate_resource_url: Optional callback to override resource URL validation.\n                Called with (server_url, prm_resource) where prm_resource is the resource\n                from Protected Resource Metadata (or None if not present). If not provided,\n                default validation rejects mismatched resources per RFC 8707.\n\n        Raises:\n            ValueError: If client_metadata_url is provided but not a valid HTTPS URL\n                with a non-root pathname.\n        \"\"\"\n        # Validate client_metadata_url if provided\n        if client_metadata_url is not None and not is_valid_client_metadata_url(client_metadata_url):\n            raise ValueError(\n                f\"client_metadata_url must be a valid HTTPS URL with a non-root pathname, got: {client_metadata_url}\"\n            )\n\n        self.context = OAuthContext(\n            server_url=server_url,\n            client_metadata=client_metadata,\n            storage=storage,\n            redirect_handler=redirect_handler,\n            callback_handler=callback_handler,\n            timeout=timeout,\n            client_metadata_url=client_metadata_url,\n        )\n        self._validate_resource_url_callback = validate_resource_url\n        self._initialized = False\n\n    async def _handle_protected_resource_response(self, response: httpx.Response) -> bool:\n        \"\"\"Handle protected resource metadata discovery response.\n\n        Per SEP-985, supports fallback when discovery fails at one URL.\n\n        Returns:\n            True if metadata was successfully discovered, False if we should try next URL\n        \"\"\"\n        if response.status_code == 200:\n            try:\n                content = await response.aread()\n                metadata = ProtectedResourceMetadata.model_validate_json(content)\n                self.context.protected_resource_metadata = metadata\n                if metadata.authorization_servers:  # pragma: no branch\n                    self.context.auth_server_url = str(metadata.authorization_servers[0])\n                return True\n\n            except ValidationError:  # pragma: no cover\n                # Invalid metadata - try next URL\n                logger.warning(f\"Invalid protected resource metadata at {response.request.url}\")\n                return False\n        elif response.status_code == 404:  # pragma: no cover\n            # Not found - try next URL in fallback chain\n            logger.debug(f\"Protected resource metadata not found at {response.request.url}, trying next URL\")\n            return False\n        else:\n            # Other error - fail immediately\n            raise OAuthFlowError(\n                f\"Protected Resource Metadata request failed: {response.status_code}\"\n            )  # pragma: no cover\n\n    async def _perform_authorization(self) -> httpx.Request:\n        \"\"\"Perform the authorization flow.\"\"\"\n        auth_code, code_verifier = await self._perform_authorization_code_grant()\n        token_request = await self._exchange_token_authorization_code(auth_code, code_verifier)\n        return token_request\n\n    async def _perform_authorization_code_grant(self) -> tuple[str, str]:\n        \"\"\"Perform the authorization redirect and get auth code.\"\"\"\n        if self.context.client_metadata.redirect_uris is None:\n            raise OAuthFlowError(\"No redirect URIs provided for authorization code grant\")  # pragma: no cover\n        if not self.context.redirect_handler:\n            raise OAuthFlowError(\"No redirect handler provided for authorization code grant\")  # pragma: no cover\n        if not self.context.callback_handler:\n            raise OAuthFlowError(\"No callback handler provided for authorization code grant\")  # pragma: no cover\n\n        if self.context.oauth_metadata and self.context.oauth_metadata.authorization_endpoint:\n            auth_endpoint = str(self.context.oauth_metadata.authorization_endpoint)  # pragma: no cover\n        else:\n            auth_base_url = self.context.get_authorization_base_url(self.context.server_url)\n            auth_endpoint = urljoin(auth_base_url, \"/authorize\")\n\n        if not self.context.client_info:\n            raise OAuthFlowError(\"No client info available for authorization\")  # pragma: no cover\n\n        # Generate PKCE parameters\n        pkce_params = PKCEParameters.generate()\n        state = secrets.token_urlsafe(32)\n\n        auth_params = {\n            \"response_type\": \"code\",\n            \"client_id\": self.context.client_info.client_id,\n            \"redirect_uri\": str(self.context.client_metadata.redirect_uris[0]),\n            \"state\": state,\n            \"code_challenge\": pkce_params.code_challenge,\n            \"code_challenge_method\": \"S256\",\n        }\n\n        # Only include resource param if conditions are met\n        if self.context.should_include_resource_param(self.context.protocol_version):\n            auth_params[\"resource\"] = self.context.get_resource_url()  # RFC 8707  # pragma: no cover\n\n        if self.context.client_metadata.scope:  # pragma: no branch\n            auth_params[\"scope\"] = self.context.client_metadata.scope\n\n        authorization_url = f\"{auth_endpoint}?{urlencode(auth_params)}\"\n        await self.context.redirect_handler(authorization_url)\n\n        # Wait for callback\n        auth_code, returned_state = await self.context.callback_handler()\n\n        if returned_state is None or not secrets.compare_digest(returned_state, state):\n            raise OAuthFlowError(f\"State parameter mismatch: {returned_state} != {state}\")  # pragma: no cover\n\n        if not auth_code:\n            raise OAuthFlowError(\"No authorization code received\")  # pragma: no cover\n\n        # Return auth code and code verifier for token exchange\n        return auth_code, pkce_params.code_verifier\n\n    def _get_token_endpoint(self) -> str:\n        if self.context.oauth_metadata and self.context.oauth_metadata.token_endpoint:\n            token_url = str(self.context.oauth_metadata.token_endpoint)\n        else:\n            auth_base_url = self.context.get_authorization_base_url(self.context.server_url)\n            token_url = urljoin(auth_base_url, \"/token\")\n        return token_url\n\n    async def _exchange_token_authorization_code(\n        self, auth_code: str, code_verifier: str, *, token_data: dict[str, Any] | None = {}\n    ) -> httpx.Request:\n        \"\"\"Build token exchange request for authorization_code flow.\"\"\"\n        if self.context.client_metadata.redirect_uris is None:\n            raise OAuthFlowError(\"No redirect URIs provided for authorization code grant\")  # pragma: no cover\n        if not self.context.client_info:\n            raise OAuthFlowError(\"Missing client info\")  # pragma: no cover\n\n        token_url = self._get_token_endpoint()\n        token_data = token_data or {}\n        token_data.update(\n            {\n                \"grant_type\": \"authorization_code\",\n                \"code\": auth_code,\n                \"redirect_uri\": str(self.context.client_metadata.redirect_uris[0]),\n                \"client_id\": self.context.client_info.client_id,\n                \"code_verifier\": code_verifier,\n            }\n        )\n\n        # Only include resource param if conditions are met\n        if self.context.should_include_resource_param(self.context.protocol_version):\n            token_data[\"resource\"] = self.context.get_resource_url()  # RFC 8707\n\n        # Prepare authentication based on preferred method\n        headers = {\"Content-Type\": \"application/x-www-form-urlencoded\"}\n        token_data, headers = self.context.prepare_token_auth(token_data, headers)\n\n        return httpx.Request(\"POST\", token_url, data=token_data, headers=headers)\n\n    async def _handle_token_response(self, response: httpx.Response) -> None:\n        \"\"\"Handle token exchange response.\"\"\"\n        if response.status_code not in {200, 201}:\n            body = await response.aread()  # pragma: no cover\n            body_text = body.decode(\"utf-8\")  # pragma: no cover\n            raise OAuthTokenError(f\"Token exchange failed ({response.status_code}): {body_text}\")  # pragma: no cover\n\n        # Parse and validate response with scope validation\n        token_response = await handle_token_response_scopes(response)\n\n        # Store tokens in context\n        self.context.current_tokens = token_response\n        self.context.update_token_expiry(token_response)\n        await self.context.storage.set_tokens(token_response)\n\n    async def _refresh_token(self) -> httpx.Request:\n        \"\"\"Build token refresh request.\"\"\"\n        if not self.context.current_tokens or not self.context.current_tokens.refresh_token:\n            raise OAuthTokenError(\"No refresh token available\")  # pragma: no cover\n\n        if not self.context.client_info or not self.context.client_info.client_id:\n            raise OAuthTokenError(\"No client info available\")  # pragma: no cover\n\n        if self.context.oauth_metadata and self.context.oauth_metadata.token_endpoint:\n            token_url = str(self.context.oauth_metadata.token_endpoint)\n        else:\n            auth_base_url = self.context.get_authorization_base_url(self.context.server_url)\n            token_url = urljoin(auth_base_url, \"/token\")\n\n        refresh_data: dict[str, str] = {\n            \"grant_type\": \"refresh_token\",\n            \"refresh_token\": self.context.current_tokens.refresh_token,\n            \"client_id\": self.context.client_info.client_id,\n        }\n\n        # Only include resource param if conditions are met\n        if self.context.should_include_resource_param(self.context.protocol_version):\n            refresh_data[\"resource\"] = self.context.get_resource_url()  # RFC 8707\n\n        # Prepare authentication based on preferred method\n        headers = {\"Content-Type\": \"application/x-www-form-urlencoded\"}\n        refresh_data, headers = self.context.prepare_token_auth(refresh_data, headers)\n\n        return httpx.Request(\"POST\", token_url, data=refresh_data, headers=headers)\n\n    async def _handle_refresh_response(self, response: httpx.Response) -> bool:  # pragma: no cover\n        \"\"\"Handle token refresh response. Returns True if successful.\"\"\"\n        if response.status_code != 200:\n            logger.warning(f\"Token refresh failed: {response.status_code}\")\n            self.context.clear_tokens()\n            return False\n\n        try:\n            content = await response.aread()\n            token_response = OAuthToken.model_validate_json(content)\n\n            self.context.current_tokens = token_response\n            self.context.update_token_expiry(token_response)\n            await self.context.storage.set_tokens(token_response)\n\n            return True\n        except ValidationError:\n            logger.exception(\"Invalid refresh response\")\n            self.context.clear_tokens()\n            return False\n\n    async def _initialize(self) -> None:  # pragma: no cover\n        \"\"\"Load stored tokens and client info.\"\"\"\n        self.context.current_tokens = await self.context.storage.get_tokens()\n        self.context.client_info = await self.context.storage.get_client_info()\n        self._initialized = True\n\n    def _add_auth_header(self, request: httpx.Request) -> None:\n        \"\"\"Add authorization header to request if we have valid tokens.\"\"\"\n        if self.context.current_tokens and self.context.current_tokens.access_token:  # pragma: no branch\n            request.headers[\"Authorization\"] = f\"Bearer {self.context.current_tokens.access_token}\"\n\n    async def _handle_oauth_metadata_response(self, response: httpx.Response) -> None:\n        content = await response.aread()\n        metadata = OAuthMetadata.model_validate_json(content)\n        self.context.oauth_metadata = metadata\n\n    async def _validate_resource_match(self, prm: ProtectedResourceMetadata) -> None:\n        \"\"\"Validate that PRM resource matches the server URL per RFC 8707.\"\"\"\n        prm_resource = str(prm.resource) if prm.resource else None\n\n        if self._validate_resource_url_callback is not None:\n            await self._validate_resource_url_callback(self.context.server_url, prm_resource)\n            return\n\n        if not prm_resource:\n            return  # pragma: no cover\n        default_resource = resource_url_from_server_url(self.context.server_url)\n        if not check_resource_allowed(requested_resource=default_resource, configured_resource=prm_resource):\n            raise OAuthFlowError(f\"Protected resource {prm_resource} does not match expected {default_resource}\")\n\n    async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]:\n        \"\"\"HTTPX auth flow integration.\"\"\"\n        async with self.context.lock:\n            if not self._initialized:\n                await self._initialize()  # pragma: no cover\n\n            # Capture protocol version from request headers\n            self.context.protocol_version = request.headers.get(MCP_PROTOCOL_VERSION)\n\n            if not self.context.is_token_valid() and self.context.can_refresh_token():\n                # Try to refresh token\n                refresh_request = await self._refresh_token()  # pragma: no cover\n                refresh_response = yield refresh_request  # pragma: no cover\n\n                if not await self._handle_refresh_response(refresh_response):  # pragma: no cover\n                    # Refresh failed, need full re-authentication\n                    self._initialized = False\n\n            if self.context.is_token_valid():\n                self._add_auth_header(request)\n\n            response = yield request\n\n            if response.status_code == 401:\n                # Perform full OAuth flow\n                try:\n                    # OAuth flow must be inline due to generator constraints\n                    www_auth_resource_metadata_url = extract_resource_metadata_from_www_auth(response)\n\n                    # Step 1: Discover protected resource metadata (SEP-985 with fallback support)\n                    prm_discovery_urls = build_protected_resource_metadata_discovery_urls(\n                        www_auth_resource_metadata_url, self.context.server_url\n                    )\n\n                    for url in prm_discovery_urls:  # pragma: no branch\n                        discovery_request = create_oauth_metadata_request(url)\n\n                        discovery_response = yield discovery_request  # sending request\n\n                        prm = await handle_protected_resource_response(discovery_response)\n                        if prm:\n                            # Validate PRM resource matches server URL (RFC 8707)\n                            await self._validate_resource_match(prm)\n                            self.context.protected_resource_metadata = prm\n\n                            # todo: try all authorization_servers to find the OASM\n                            assert (\n                                len(prm.authorization_servers) > 0\n                            )  # this is always true as authorization_servers has a min length of 1\n\n                            self.context.auth_server_url = str(prm.authorization_servers[0])\n                            break\n                        else:\n                            logger.debug(f\"Protected resource metadata discovery failed: {url}\")\n\n                    asm_discovery_urls = build_oauth_authorization_server_metadata_discovery_urls(\n                        self.context.auth_server_url, self.context.server_url\n                    )\n\n                    # Step 2: Discover OAuth Authorization Server Metadata (OASM) (with fallback for legacy servers)\n                    for url in asm_discovery_urls:  # pragma: no branch\n                        oauth_metadata_request = create_oauth_metadata_request(url)\n                        oauth_metadata_response = yield oauth_metadata_request\n\n                        ok, asm = await handle_auth_metadata_response(oauth_metadata_response)\n                        if not ok:\n                            break\n                        if ok and asm:\n                            self.context.oauth_metadata = asm\n                            break\n                        else:\n                            logger.debug(f\"OAuth metadata discovery failed: {url}\")\n\n                    # Step 3: Apply scope selection strategy\n                    self.context.client_metadata.scope = get_client_metadata_scopes(\n                        extract_scope_from_www_auth(response),\n                        self.context.protected_resource_metadata,\n                        self.context.oauth_metadata,\n                    )\n\n                    # Step 4: Register client or use URL-based client ID (CIMD)\n                    if not self.context.client_info:\n                        if should_use_client_metadata_url(\n                            self.context.oauth_metadata, self.context.client_metadata_url\n                        ):\n                            # Use URL-based client ID (CIMD)\n                            logger.debug(f\"Using URL-based client ID (CIMD): {self.context.client_metadata_url}\")\n                            client_information = create_client_info_from_metadata_url(\n                                self.context.client_metadata_url,  # type: ignore[arg-type]\n                                redirect_uris=self.context.client_metadata.redirect_uris,\n                            )\n                            self.context.client_info = client_information\n                            await self.context.storage.set_client_info(client_information)\n                        else:\n                            # Fallback to Dynamic Client Registration\n                            registration_request = create_client_registration_request(\n                                self.context.oauth_metadata,\n                                self.context.client_metadata,\n                                self.context.get_authorization_base_url(self.context.server_url),\n                            )\n                            registration_response = yield registration_request\n                            client_information = await handle_registration_response(registration_response)\n                            self.context.client_info = client_information\n                            await self.context.storage.set_client_info(client_information)\n\n                    # Step 5: Perform authorization and complete token exchange\n                    token_response = yield await self._perform_authorization()\n                    await self._handle_token_response(token_response)\n                except Exception:  # pragma: no cover\n                    logger.exception(\"OAuth flow error\")\n                    raise\n\n                # Retry with new tokens\n                self._add_auth_header(request)\n                yield request\n            elif response.status_code == 403:\n                # Step 1: Extract error field from WWW-Authenticate header\n                error = extract_field_from_www_auth(response, \"error\")\n\n                # Step 2: Check if we need to step-up authorization\n                if error == \"insufficient_scope\":  # pragma: no branch\n                    try:\n                        # Step 2a: Update the required scopes\n                        self.context.client_metadata.scope = get_client_metadata_scopes(\n                            extract_scope_from_www_auth(response), self.context.protected_resource_metadata\n                        )\n\n                        # Step 2b: Perform (re-)authorization and token exchange\n                        token_response = yield await self._perform_authorization()\n                        await self._handle_token_response(token_response)\n                    except Exception:  # pragma: no cover\n                        logger.exception(\"OAuth flow error\")\n                        raise\n\n                # Retry with new tokens\n                self._add_auth_header(request)\n                yield request\n"
  },
  {
    "path": "src/mcp/client/auth/utils.py",
    "content": "import re\nfrom urllib.parse import urljoin, urlparse\n\nfrom httpx import Request, Response\nfrom pydantic import AnyUrl, ValidationError\n\nfrom mcp.client.auth import OAuthRegistrationError, OAuthTokenError\nfrom mcp.client.streamable_http import MCP_PROTOCOL_VERSION\nfrom mcp.shared.auth import (\n    OAuthClientInformationFull,\n    OAuthClientMetadata,\n    OAuthMetadata,\n    OAuthToken,\n    ProtectedResourceMetadata,\n)\nfrom mcp.types import LATEST_PROTOCOL_VERSION\n\n\ndef extract_field_from_www_auth(response: Response, field_name: str) -> str | None:\n    \"\"\"Extract field from WWW-Authenticate header.\n\n    Returns:\n        Field value if found in WWW-Authenticate header, None otherwise\n    \"\"\"\n    www_auth_header = response.headers.get(\"WWW-Authenticate\")\n    if not www_auth_header:\n        return None\n\n    # Pattern matches: field_name=\"value\" or field_name=value (unquoted)\n    pattern = rf'{field_name}=(?:\"([^\"]+)\"|([^\\s,]+))'\n    match = re.search(pattern, www_auth_header)\n\n    if match:\n        # Return quoted value if present, otherwise unquoted value\n        return match.group(1) or match.group(2)\n\n    return None\n\n\ndef extract_scope_from_www_auth(response: Response) -> str | None:\n    \"\"\"Extract scope parameter from WWW-Authenticate header as per RFC 6750.\n\n    Returns:\n        Scope string if found in WWW-Authenticate header, None otherwise\n    \"\"\"\n    return extract_field_from_www_auth(response, \"scope\")\n\n\ndef extract_resource_metadata_from_www_auth(response: Response) -> str | None:\n    \"\"\"Extract protected resource metadata URL from WWW-Authenticate header as per RFC 9728.\n\n    Returns:\n        Resource metadata URL if found in WWW-Authenticate header, None otherwise\n    \"\"\"\n    if not response or response.status_code != 401:\n        return None  # pragma: no cover\n\n    return extract_field_from_www_auth(response, \"resource_metadata\")\n\n\ndef build_protected_resource_metadata_discovery_urls(www_auth_url: str | None, server_url: str) -> list[str]:\n    \"\"\"Build ordered list of URLs to try for protected resource metadata discovery.\n\n    Per SEP-985, the client MUST:\n    1. Try resource_metadata from WWW-Authenticate header (if present)\n    2. Fall back to path-based well-known URI: /.well-known/oauth-protected-resource/{path}\n    3. Fall back to root-based well-known URI: /.well-known/oauth-protected-resource\n\n    Args:\n        www_auth_url: Optional resource_metadata URL extracted from the WWW-Authenticate header\n        server_url: Server URL\n\n    Returns:\n        Ordered list of URLs to try for discovery\n    \"\"\"\n    urls: list[str] = []\n\n    # Priority 1: WWW-Authenticate header with resource_metadata parameter\n    if www_auth_url:\n        urls.append(www_auth_url)\n\n    # Priority 2-3: Well-known URIs (RFC 9728)\n    parsed = urlparse(server_url)\n    base_url = f\"{parsed.scheme}://{parsed.netloc}\"\n\n    # Priority 2: Path-based well-known URI (if server has a path component)\n    if parsed.path and parsed.path != \"/\":\n        path_based_url = urljoin(base_url, f\"/.well-known/oauth-protected-resource{parsed.path}\")\n        urls.append(path_based_url)\n\n    # Priority 3: Root-based well-known URI\n    root_based_url = urljoin(base_url, \"/.well-known/oauth-protected-resource\")\n    urls.append(root_based_url)\n\n    return urls\n\n\ndef get_client_metadata_scopes(\n    www_authenticate_scope: str | None,\n    protected_resource_metadata: ProtectedResourceMetadata | None,\n    authorization_server_metadata: OAuthMetadata | None = None,\n) -> str | None:\n    \"\"\"Select scopes as outlined in the 'Scope Selection Strategy' in the MCP spec.\"\"\"\n    # Per MCP spec, scope selection priority order:\n    # 1. Use scope from WWW-Authenticate header (if provided)\n    # 2. Use all scopes from PRM scopes_supported (if available)\n    # 3. Omit scope parameter if neither is available\n\n    if www_authenticate_scope is not None:\n        # Priority 1: WWW-Authenticate header scope\n        return www_authenticate_scope\n    elif protected_resource_metadata is not None and protected_resource_metadata.scopes_supported is not None:\n        # Priority 2: PRM scopes_supported\n        return \" \".join(protected_resource_metadata.scopes_supported)\n    elif authorization_server_metadata is not None and authorization_server_metadata.scopes_supported is not None:\n        return \" \".join(authorization_server_metadata.scopes_supported)  # pragma: no cover\n    else:\n        # Priority 3: Omit scope parameter\n        return None\n\n\ndef build_oauth_authorization_server_metadata_discovery_urls(auth_server_url: str | None, server_url: str) -> list[str]:\n    \"\"\"Generate an ordered list of URLs for authorization server metadata discovery.\n\n    Args:\n        auth_server_url: OAuth Authorization Server Metadata URL if found, otherwise None\n        server_url: URL for the MCP server, used as a fallback if auth_server_url is None\n    \"\"\"\n\n    if not auth_server_url:\n        # Legacy path using the 2025-03-26 spec:\n        # link: https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization\n        parsed = urlparse(server_url)\n        return [f\"{parsed.scheme}://{parsed.netloc}/.well-known/oauth-authorization-server\"]\n\n    urls: list[str] = []\n    parsed = urlparse(auth_server_url)\n    base_url = f\"{parsed.scheme}://{parsed.netloc}\"\n\n    # RFC 8414: Path-aware OAuth discovery\n    if parsed.path and parsed.path != \"/\":\n        oauth_path = f\"/.well-known/oauth-authorization-server{parsed.path.rstrip('/')}\"\n        urls.append(urljoin(base_url, oauth_path))\n\n        # RFC 8414 section 5: Path-aware OIDC discovery\n        # See https://www.rfc-editor.org/rfc/rfc8414.html#section-5\n        oidc_path = f\"/.well-known/openid-configuration{parsed.path.rstrip('/')}\"\n        urls.append(urljoin(base_url, oidc_path))\n\n        # https://openid.net/specs/openid-connect-discovery-1_0.html\n        oidc_path = f\"{parsed.path.rstrip('/')}/.well-known/openid-configuration\"\n        urls.append(urljoin(base_url, oidc_path))\n        return urls\n\n    # OAuth root\n    urls.append(urljoin(base_url, \"/.well-known/oauth-authorization-server\"))\n\n    # OIDC 1.0 fallback (appends to full URL per OIDC spec)\n    # https://openid.net/specs/openid-connect-discovery-1_0.html\n    urls.append(urljoin(base_url, \"/.well-known/openid-configuration\"))\n\n    return urls\n\n\nasync def handle_protected_resource_response(\n    response: Response,\n) -> ProtectedResourceMetadata | None:\n    \"\"\"Handle protected resource metadata discovery response.\n\n    Per SEP-985, supports fallback when discovery fails at one URL.\n\n    Returns:\n        ProtectedResourceMetadata if successfully discovered, None if we should try next URL\n    \"\"\"\n    if response.status_code == 200:\n        try:\n            content = await response.aread()\n            metadata = ProtectedResourceMetadata.model_validate_json(content)\n            return metadata\n\n        except ValidationError:  # pragma: no cover\n            # Invalid metadata - try next URL\n            return None\n    else:\n        # Not found - try next URL in fallback chain\n        return None\n\n\nasync def handle_auth_metadata_response(response: Response) -> tuple[bool, OAuthMetadata | None]:\n    if response.status_code == 200:\n        try:\n            content = await response.aread()\n            asm = OAuthMetadata.model_validate_json(content)\n            return True, asm\n        except ValidationError:  # pragma: no cover\n            return True, None\n    elif response.status_code < 400 or response.status_code >= 500:\n        return False, None  # Non-4XX error, stop trying\n    return True, None\n\n\ndef create_oauth_metadata_request(url: str) -> Request:\n    return Request(\"GET\", url, headers={MCP_PROTOCOL_VERSION: LATEST_PROTOCOL_VERSION})\n\n\ndef create_client_registration_request(\n    auth_server_metadata: OAuthMetadata | None, client_metadata: OAuthClientMetadata, auth_base_url: str\n) -> Request:\n    \"\"\"Build a client registration request.\"\"\"\n\n    if auth_server_metadata and auth_server_metadata.registration_endpoint:\n        registration_url = str(auth_server_metadata.registration_endpoint)\n    else:\n        registration_url = urljoin(auth_base_url, \"/register\")\n\n    registration_data = client_metadata.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n\n    return Request(\"POST\", registration_url, json=registration_data, headers={\"Content-Type\": \"application/json\"})\n\n\nasync def handle_registration_response(response: Response) -> OAuthClientInformationFull:\n    \"\"\"Handle registration response.\"\"\"\n    if response.status_code not in (200, 201):\n        await response.aread()\n        raise OAuthRegistrationError(f\"Registration failed: {response.status_code} {response.text}\")\n\n    try:\n        content = await response.aread()\n        client_info = OAuthClientInformationFull.model_validate_json(content)\n        return client_info\n        # self.context.client_info = client_info\n        # await self.context.storage.set_client_info(client_info)\n    except ValidationError as e:  # pragma: no cover\n        raise OAuthRegistrationError(f\"Invalid registration response: {e}\")\n\n\ndef is_valid_client_metadata_url(url: str | None) -> bool:\n    \"\"\"Validate that a URL is suitable for use as a client_id (CIMD).\n\n    The URL must be HTTPS with a non-root pathname.\n\n    Args:\n        url: The URL to validate\n\n    Returns:\n        True if the URL is a valid HTTPS URL with a non-root pathname\n    \"\"\"\n    if not url:\n        return False\n    try:\n        parsed = urlparse(url)\n        return parsed.scheme == \"https\" and parsed.path not in (\"\", \"/\")\n    except Exception:\n        return False\n\n\ndef should_use_client_metadata_url(\n    oauth_metadata: OAuthMetadata | None,\n    client_metadata_url: str | None,\n) -> bool:\n    \"\"\"Determine if URL-based client ID (CIMD) should be used instead of DCR.\n\n    URL-based client IDs should be used when:\n    1. The server advertises client_id_metadata_document_supported=True\n    2. The client has a valid client_metadata_url configured\n\n    Args:\n        oauth_metadata: OAuth authorization server metadata\n        client_metadata_url: URL-based client ID (already validated)\n\n    Returns:\n        True if CIMD should be used, False if DCR should be used\n    \"\"\"\n    if not client_metadata_url:\n        return False\n\n    if not oauth_metadata:\n        return False\n\n    return oauth_metadata.client_id_metadata_document_supported is True\n\n\ndef create_client_info_from_metadata_url(\n    client_metadata_url: str, redirect_uris: list[AnyUrl] | None = None\n) -> OAuthClientInformationFull:\n    \"\"\"Create client information using a URL-based client ID (CIMD).\n\n    When using URL-based client IDs, the URL itself becomes the client_id\n    and no client_secret is used (token_endpoint_auth_method=\"none\").\n\n    Args:\n        client_metadata_url: The URL to use as the client_id\n        redirect_uris: The redirect URIs from the client metadata (passed through for\n            compatibility with OAuthClientInformationFull which inherits from OAuthClientMetadata)\n\n    Returns:\n        OAuthClientInformationFull with the URL as client_id\n    \"\"\"\n    return OAuthClientInformationFull(\n        client_id=client_metadata_url,\n        token_endpoint_auth_method=\"none\",\n        redirect_uris=redirect_uris,\n    )\n\n\nasync def handle_token_response_scopes(\n    response: Response,\n) -> OAuthToken:\n    \"\"\"Parse and validate a token response.\n\n    Parses token response JSON. Callers should check response.status_code before calling.\n\n    Args:\n        response: HTTP response from token endpoint (status already checked by caller)\n\n    Returns:\n        Validated OAuthToken model\n\n    Raises:\n        OAuthTokenError: If response JSON is invalid\n    \"\"\"\n    try:\n        content = await response.aread()\n        token_response = OAuthToken.model_validate_json(content)\n        return token_response\n    except ValidationError as e:  # pragma: no cover\n        raise OAuthTokenError(f\"Invalid token response: {e}\")\n"
  },
  {
    "path": "src/mcp/client/client.py",
    "content": "\"\"\"Unified MCP Client that wraps ClientSession with transport management.\"\"\"\n\nfrom __future__ import annotations\n\nfrom contextlib import AsyncExitStack\nfrom dataclasses import KW_ONLY, dataclass, field\nfrom typing import Any\n\nfrom mcp.client._memory import InMemoryTransport\nfrom mcp.client._transport import Transport\nfrom mcp.client.session import ClientSession, ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT\nfrom mcp.client.streamable_http import streamable_http_client\nfrom mcp.server import Server\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.shared.session import ProgressFnT\nfrom mcp.types import (\n    CallToolResult,\n    CompleteResult,\n    EmptyResult,\n    GetPromptResult,\n    Implementation,\n    InitializeResult,\n    ListPromptsResult,\n    ListResourcesResult,\n    ListResourceTemplatesResult,\n    ListToolsResult,\n    LoggingLevel,\n    PaginatedRequestParams,\n    PromptReference,\n    ReadResourceResult,\n    RequestParamsMeta,\n    ResourceTemplateReference,\n)\n\n\n@dataclass\nclass Client:\n    \"\"\"A high-level MCP client for connecting to MCP servers.\n\n    Supports in-memory transport for testing (pass a Server or MCPServer instance),\n    Streamable HTTP transport (pass a URL string), or a custom Transport instance.\n\n    Example:\n        ```python\n        from mcp.client import Client\n        from mcp.server.mcpserver import MCPServer\n\n        server = MCPServer(\"test\")\n\n        @server.tool()\n        def add(a: int, b: int) -> int:\n            return a + b\n\n        async def main():\n            async with Client(server) as client:\n                result = await client.call_tool(\"add\", {\"a\": 1, \"b\": 2})\n\n        asyncio.run(main())\n        ```\n    \"\"\"\n\n    server: Server[Any] | MCPServer | Transport | str\n    \"\"\"The MCP server to connect to.\n\n    If the server is a `Server` or `MCPServer` instance, it will be wrapped in an `InMemoryTransport`.\n    If the server is a URL string, it will be used as the URL for a `streamable_http_client` transport.\n    If the server is a `Transport` instance, it will be used directly.\n    \"\"\"\n\n    _: KW_ONLY\n\n    # TODO(Marcelo): When do `raise_exceptions=True` actually raises?\n    raise_exceptions: bool = False\n    \"\"\"Whether to raise exceptions from the server.\"\"\"\n\n    read_timeout_seconds: float | None = None\n    \"\"\"Timeout for read operations.\"\"\"\n\n    sampling_callback: SamplingFnT | None = None\n    \"\"\"Callback for handling sampling requests.\"\"\"\n\n    list_roots_callback: ListRootsFnT | None = None\n    \"\"\"Callback for handling list roots requests.\"\"\"\n\n    logging_callback: LoggingFnT | None = None\n    \"\"\"Callback for handling logging notifications.\"\"\"\n\n    # TODO(Marcelo): Why do we have both \"callback\" and \"handler\"?\n    message_handler: MessageHandlerFnT | None = None\n    \"\"\"Callback for handling raw messages.\"\"\"\n\n    client_info: Implementation | None = None\n    \"\"\"Client implementation info to send to server.\"\"\"\n\n    elicitation_callback: ElicitationFnT | None = None\n    \"\"\"Callback for handling elicitation requests.\"\"\"\n\n    _session: ClientSession | None = field(init=False, default=None)\n    _exit_stack: AsyncExitStack | None = field(init=False, default=None)\n    _transport: Transport = field(init=False)\n\n    def __post_init__(self) -> None:\n        if isinstance(self.server, Server | MCPServer):\n            self._transport = InMemoryTransport(self.server, raise_exceptions=self.raise_exceptions)\n        elif isinstance(self.server, str):\n            self._transport = streamable_http_client(self.server)\n        else:\n            self._transport = self.server\n\n    async def __aenter__(self) -> Client:\n        \"\"\"Enter the async context manager.\"\"\"\n        if self._session is not None:\n            raise RuntimeError(\"Client is already entered; cannot reenter\")\n\n        async with AsyncExitStack() as exit_stack:\n            read_stream, write_stream = await exit_stack.enter_async_context(self._transport)\n\n            self._session = await exit_stack.enter_async_context(\n                ClientSession(\n                    read_stream=read_stream,\n                    write_stream=write_stream,\n                    read_timeout_seconds=self.read_timeout_seconds,\n                    sampling_callback=self.sampling_callback,\n                    list_roots_callback=self.list_roots_callback,\n                    logging_callback=self.logging_callback,\n                    message_handler=self.message_handler,\n                    client_info=self.client_info,\n                    elicitation_callback=self.elicitation_callback,\n                )\n            )\n\n            await self._session.initialize()\n\n            # Transfer ownership to self for __aexit__ to handle\n            self._exit_stack = exit_stack.pop_all()\n            return self\n\n    async def __aexit__(self, exc_type: type[BaseException] | None, exc_val: BaseException | None, exc_tb: Any) -> None:\n        \"\"\"Exit the async context manager.\"\"\"\n        if self._exit_stack:  # pragma: no branch\n            await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb)\n        self._session = None\n\n    @property\n    def session(self) -> ClientSession:\n        \"\"\"Get the underlying ClientSession.\n\n        This provides access to the full ClientSession API for advanced use cases.\n\n        Raises:\n            RuntimeError: If accessed before entering the context manager.\n        \"\"\"\n        if self._session is None:\n            raise RuntimeError(\"Client must be used within an async context manager\")\n        return self._session\n\n    @property\n    def initialize_result(self) -> InitializeResult:\n        \"\"\"The server's InitializeResult.\n\n        Contains server_info, capabilities, instructions, and the negotiated protocol_version.\n        Raises RuntimeError if accessed outside the context manager.\n        \"\"\"\n        result = self.session.initialize_result\n        if result is None:  # pragma: no cover\n            raise RuntimeError(\"Client must be used within an async context manager\")\n        return result\n\n    async def send_ping(self, *, meta: RequestParamsMeta | None = None) -> EmptyResult:\n        \"\"\"Send a ping request to the server.\"\"\"\n        return await self.session.send_ping(meta=meta)\n\n    async def send_progress_notification(\n        self,\n        progress_token: str | int,\n        progress: float,\n        total: float | None = None,\n        message: str | None = None,\n    ) -> None:\n        \"\"\"Send a progress notification to the server.\"\"\"\n        await self.session.send_progress_notification(\n            progress_token=progress_token,\n            progress=progress,\n            total=total,\n            message=message,\n        )\n\n    async def set_logging_level(self, level: LoggingLevel, *, meta: RequestParamsMeta | None = None) -> EmptyResult:\n        \"\"\"Set the logging level on the server.\"\"\"\n        return await self.session.set_logging_level(level=level, meta=meta)\n\n    async def list_resources(\n        self,\n        *,\n        cursor: str | None = None,\n        meta: RequestParamsMeta | None = None,\n    ) -> ListResourcesResult:\n        \"\"\"List available resources from the server.\"\"\"\n        return await self.session.list_resources(params=PaginatedRequestParams(cursor=cursor, _meta=meta))\n\n    async def list_resource_templates(\n        self,\n        *,\n        cursor: str | None = None,\n        meta: RequestParamsMeta | None = None,\n    ) -> ListResourceTemplatesResult:\n        \"\"\"List available resource templates from the server.\"\"\"\n        return await self.session.list_resource_templates(params=PaginatedRequestParams(cursor=cursor, _meta=meta))\n\n    async def read_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> ReadResourceResult:\n        \"\"\"Read a resource from the server.\n\n        Args:\n            uri: The URI of the resource to read.\n            meta: Additional metadata for the request.\n\n        Returns:\n            The resource content.\n        \"\"\"\n        return await self.session.read_resource(uri, meta=meta)\n\n    async def subscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> EmptyResult:\n        \"\"\"Subscribe to resource updates.\"\"\"\n        return await self.session.subscribe_resource(uri, meta=meta)\n\n    async def unsubscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> EmptyResult:\n        \"\"\"Unsubscribe from resource updates.\"\"\"\n        return await self.session.unsubscribe_resource(uri, meta=meta)\n\n    async def call_tool(\n        self,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        read_timeout_seconds: float | None = None,\n        progress_callback: ProgressFnT | None = None,\n        *,\n        meta: RequestParamsMeta | None = None,\n    ) -> CallToolResult:\n        \"\"\"Call a tool on the server.\n\n        Args:\n            name: The name of the tool to call\n            arguments: Arguments to pass to the tool\n            read_timeout_seconds: Timeout for the tool call\n            progress_callback: Callback for progress updates\n            meta: Additional metadata for the request\n\n        Returns:\n            The tool result.\n        \"\"\"\n        return await self.session.call_tool(\n            name=name,\n            arguments=arguments,\n            read_timeout_seconds=read_timeout_seconds,\n            progress_callback=progress_callback,\n            meta=meta,\n        )\n\n    async def list_prompts(\n        self,\n        *,\n        cursor: str | None = None,\n        meta: RequestParamsMeta | None = None,\n    ) -> ListPromptsResult:\n        \"\"\"List available prompts from the server.\"\"\"\n        return await self.session.list_prompts(params=PaginatedRequestParams(cursor=cursor, _meta=meta))\n\n    async def get_prompt(\n        self, name: str, arguments: dict[str, str] | None = None, *, meta: RequestParamsMeta | None = None\n    ) -> GetPromptResult:\n        \"\"\"Get a prompt from the server.\n\n        Args:\n            name: The name of the prompt\n            arguments: Arguments to pass to the prompt\n            meta: Additional metadata for the request\n\n        Returns:\n            The prompt content.\n        \"\"\"\n        return await self.session.get_prompt(name=name, arguments=arguments, meta=meta)\n\n    async def complete(\n        self,\n        ref: ResourceTemplateReference | PromptReference,\n        argument: dict[str, str],\n        context_arguments: dict[str, str] | None = None,\n    ) -> CompleteResult:\n        \"\"\"Get completions for a prompt or resource template argument.\n\n        Args:\n            ref: Reference to the prompt or resource template\n            argument: The argument to complete\n            context_arguments: Additional context arguments\n\n        Returns:\n            Completion suggestions.\n        \"\"\"\n        return await self.session.complete(ref=ref, argument=argument, context_arguments=context_arguments)\n\n    async def list_tools(self, *, cursor: str | None = None, meta: RequestParamsMeta | None = None) -> ListToolsResult:\n        \"\"\"List available tools from the server.\"\"\"\n        return await self.session.list_tools(params=PaginatedRequestParams(cursor=cursor, _meta=meta))\n\n    async def send_roots_list_changed(self) -> None:\n        \"\"\"Send a notification that the roots list has changed.\"\"\"\n        # TODO(Marcelo): Currently, there is no way for the server to handle this. We should add support.\n        await self.session.send_roots_list_changed()  # pragma: no cover\n"
  },
  {
    "path": "src/mcp/client/context.py",
    "content": "\"\"\"Request context for MCP client handlers.\"\"\"\n\nfrom mcp.client.session import ClientSession\nfrom mcp.shared._context import RequestContext\n\nClientRequestContext = RequestContext[ClientSession]\n\"\"\"Context for handling incoming requests in a client session.\n\nThis context is passed to client-side callbacks (sampling, elicitation, list_roots) when the server sends requests\nto the client.\n\nAttributes:\n    request_id: The unique identifier for this request.\n    meta: Optional metadata associated with the request.\n    session: The client session handling this request.\n\"\"\"\n"
  },
  {
    "path": "src/mcp/client/experimental/__init__.py",
    "content": "\"\"\"Experimental client features.\n\nWARNING: These APIs are experimental and may change without notice.\n\"\"\"\n\nfrom mcp.client.experimental.tasks import ExperimentalClientFeatures\n\n__all__ = [\"ExperimentalClientFeatures\"]\n"
  },
  {
    "path": "src/mcp/client/experimental/task_handlers.py",
    "content": "\"\"\"Experimental task handler protocols for server -> client requests.\n\nThis module provides Protocol types and default handlers for when servers\nsend task-related requests to clients (the reverse of normal client -> server flow).\n\nWARNING: These APIs are experimental and may change without notice.\n\nUse cases:\n- Server sends task-augmented sampling/elicitation request to client\n- Client creates a local task, spawns background work, returns CreateTaskResult\n- Server polls client's task status via tasks/get, tasks/result, etc.\n\"\"\"\n\nfrom __future__ import annotations\n\nfrom dataclasses import dataclass, field\nfrom typing import TYPE_CHECKING, Protocol\n\nfrom pydantic import TypeAdapter\n\nfrom mcp import types\nfrom mcp.shared._context import RequestContext\nfrom mcp.shared.session import RequestResponder\n\nif TYPE_CHECKING:\n    from mcp.client.session import ClientSession\n\n\nclass GetTaskHandlerFnT(Protocol):\n    \"\"\"Handler for tasks/get requests from server.\n\n    WARNING: This is experimental and may change without notice.\n    \"\"\"\n\n    async def __call__(\n        self,\n        context: RequestContext[ClientSession],\n        params: types.GetTaskRequestParams,\n    ) -> types.GetTaskResult | types.ErrorData: ...  # pragma: no branch\n\n\nclass GetTaskResultHandlerFnT(Protocol):\n    \"\"\"Handler for tasks/result requests from server.\n\n    WARNING: This is experimental and may change without notice.\n    \"\"\"\n\n    async def __call__(\n        self,\n        context: RequestContext[ClientSession],\n        params: types.GetTaskPayloadRequestParams,\n    ) -> types.GetTaskPayloadResult | types.ErrorData: ...  # pragma: no branch\n\n\nclass ListTasksHandlerFnT(Protocol):\n    \"\"\"Handler for tasks/list requests from server.\n\n    WARNING: This is experimental and may change without notice.\n    \"\"\"\n\n    async def __call__(\n        self,\n        context: RequestContext[ClientSession],\n        params: types.PaginatedRequestParams | None,\n    ) -> types.ListTasksResult | types.ErrorData: ...  # pragma: no branch\n\n\nclass CancelTaskHandlerFnT(Protocol):\n    \"\"\"Handler for tasks/cancel requests from server.\n\n    WARNING: This is experimental and may change without notice.\n    \"\"\"\n\n    async def __call__(\n        self,\n        context: RequestContext[ClientSession],\n        params: types.CancelTaskRequestParams,\n    ) -> types.CancelTaskResult | types.ErrorData: ...  # pragma: no branch\n\n\nclass TaskAugmentedSamplingFnT(Protocol):\n    \"\"\"Handler for task-augmented sampling/createMessage requests from server.\n\n    When server sends a CreateMessageRequest with task field, this callback\n    is invoked. The callback should create a task, spawn background work,\n    and return CreateTaskResult immediately.\n\n    WARNING: This is experimental and may change without notice.\n    \"\"\"\n\n    async def __call__(\n        self,\n        context: RequestContext[ClientSession],\n        params: types.CreateMessageRequestParams,\n        task_metadata: types.TaskMetadata,\n    ) -> types.CreateTaskResult | types.ErrorData: ...  # pragma: no branch\n\n\nclass TaskAugmentedElicitationFnT(Protocol):\n    \"\"\"Handler for task-augmented elicitation/create requests from server.\n\n    When server sends an ElicitRequest with task field, this callback\n    is invoked. The callback should create a task, spawn background work,\n    and return CreateTaskResult immediately.\n\n    WARNING: This is experimental and may change without notice.\n    \"\"\"\n\n    async def __call__(\n        self,\n        context: RequestContext[ClientSession],\n        params: types.ElicitRequestParams,\n        task_metadata: types.TaskMetadata,\n    ) -> types.CreateTaskResult | types.ErrorData: ...  # pragma: no branch\n\n\nasync def default_get_task_handler(\n    context: RequestContext[ClientSession],\n    params: types.GetTaskRequestParams,\n) -> types.GetTaskResult | types.ErrorData:\n    return types.ErrorData(\n        code=types.METHOD_NOT_FOUND,\n        message=\"tasks/get not supported\",\n    )\n\n\nasync def default_get_task_result_handler(\n    context: RequestContext[ClientSession],\n    params: types.GetTaskPayloadRequestParams,\n) -> types.GetTaskPayloadResult | types.ErrorData:\n    return types.ErrorData(\n        code=types.METHOD_NOT_FOUND,\n        message=\"tasks/result not supported\",\n    )\n\n\nasync def default_list_tasks_handler(\n    context: RequestContext[ClientSession],\n    params: types.PaginatedRequestParams | None,\n) -> types.ListTasksResult | types.ErrorData:\n    return types.ErrorData(\n        code=types.METHOD_NOT_FOUND,\n        message=\"tasks/list not supported\",\n    )\n\n\nasync def default_cancel_task_handler(\n    context: RequestContext[ClientSession],\n    params: types.CancelTaskRequestParams,\n) -> types.CancelTaskResult | types.ErrorData:\n    return types.ErrorData(\n        code=types.METHOD_NOT_FOUND,\n        message=\"tasks/cancel not supported\",\n    )\n\n\nasync def default_task_augmented_sampling(\n    context: RequestContext[ClientSession],\n    params: types.CreateMessageRequestParams,\n    task_metadata: types.TaskMetadata,\n) -> types.CreateTaskResult | types.ErrorData:\n    return types.ErrorData(\n        code=types.INVALID_REQUEST,\n        message=\"Task-augmented sampling not supported\",\n    )\n\n\nasync def default_task_augmented_elicitation(\n    context: RequestContext[ClientSession],\n    params: types.ElicitRequestParams,\n    task_metadata: types.TaskMetadata,\n) -> types.CreateTaskResult | types.ErrorData:\n    return types.ErrorData(\n        code=types.INVALID_REQUEST,\n        message=\"Task-augmented elicitation not supported\",\n    )\n\n\n@dataclass\nclass ExperimentalTaskHandlers:\n    \"\"\"Container for experimental task handlers.\n\n    Groups all task-related handlers that handle server -> client requests.\n    This includes both pure task requests (get, list, cancel, result) and\n    task-augmented request handlers (sampling, elicitation with task field).\n\n    WARNING: These APIs are experimental and may change without notice.\n\n    Example:\n        ```python\n        handlers = ExperimentalTaskHandlers(\n            get_task=my_get_task_handler,\n            list_tasks=my_list_tasks_handler,\n        )\n        session = ClientSession(..., experimental_task_handlers=handlers)\n        ```\n    \"\"\"\n\n    # Pure task request handlers\n    get_task: GetTaskHandlerFnT = field(default=default_get_task_handler)\n    get_task_result: GetTaskResultHandlerFnT = field(default=default_get_task_result_handler)\n    list_tasks: ListTasksHandlerFnT = field(default=default_list_tasks_handler)\n    cancel_task: CancelTaskHandlerFnT = field(default=default_cancel_task_handler)\n\n    # Task-augmented request handlers\n    augmented_sampling: TaskAugmentedSamplingFnT = field(default=default_task_augmented_sampling)\n    augmented_elicitation: TaskAugmentedElicitationFnT = field(default=default_task_augmented_elicitation)\n\n    def build_capability(self) -> types.ClientTasksCapability | None:\n        \"\"\"Build ClientTasksCapability from the configured handlers.\n\n        Returns a capability object that reflects which handlers are configured\n        (i.e., not using the default \"not supported\" handlers).\n\n        Returns:\n            ClientTasksCapability if any handlers are provided, None otherwise\n        \"\"\"\n        has_list = self.list_tasks is not default_list_tasks_handler\n        has_cancel = self.cancel_task is not default_cancel_task_handler\n        has_sampling = self.augmented_sampling is not default_task_augmented_sampling\n        has_elicitation = self.augmented_elicitation is not default_task_augmented_elicitation\n\n        # If no handlers are provided, return None\n        if not any([has_list, has_cancel, has_sampling, has_elicitation]):\n            return None\n\n        # Build requests capability if any request handlers are provided\n        requests_capability: types.ClientTasksRequestsCapability | None = None\n        if has_sampling or has_elicitation:\n            requests_capability = types.ClientTasksRequestsCapability(\n                sampling=types.TasksSamplingCapability(create_message=types.TasksCreateMessageCapability())\n                if has_sampling\n                else None,\n                elicitation=types.TasksElicitationCapability(create=types.TasksCreateElicitationCapability())\n                if has_elicitation\n                else None,\n            )\n\n        return types.ClientTasksCapability(\n            list=types.TasksListCapability() if has_list else None,\n            cancel=types.TasksCancelCapability() if has_cancel else None,\n            requests=requests_capability,\n        )\n\n    @staticmethod\n    def handles_request(request: types.ServerRequest) -> bool:\n        \"\"\"Check if this handler handles the given request type.\"\"\"\n        return isinstance(\n            request,\n            types.GetTaskRequest | types.GetTaskPayloadRequest | types.ListTasksRequest | types.CancelTaskRequest,\n        )\n\n    async def handle_request(\n        self,\n        ctx: RequestContext[ClientSession],\n        responder: RequestResponder[types.ServerRequest, types.ClientResult],\n    ) -> None:\n        \"\"\"Handle a task-related request from the server.\n\n        Call handles_request() first to check if this handler can handle the request.\n        \"\"\"\n        client_response_type: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter(\n            types.ClientResult | types.ErrorData\n        )\n\n        match responder.request:\n            case types.GetTaskRequest(params=params):\n                response = await self.get_task(ctx, params)\n                client_response = client_response_type.validate_python(response)\n                await responder.respond(client_response)\n\n            case types.GetTaskPayloadRequest(params=params):\n                response = await self.get_task_result(ctx, params)\n                client_response = client_response_type.validate_python(response)\n                await responder.respond(client_response)\n\n            case types.ListTasksRequest(params=params):\n                response = await self.list_tasks(ctx, params)\n                client_response = client_response_type.validate_python(response)\n                await responder.respond(client_response)\n\n            case types.CancelTaskRequest(params=params):\n                response = await self.cancel_task(ctx, params)\n                client_response = client_response_type.validate_python(response)\n                await responder.respond(client_response)\n\n            case _:  # pragma: no cover\n                raise ValueError(f\"Unhandled request type: {type(responder.request)}\")\n\n\n# Backwards compatibility aliases\ndefault_task_augmented_sampling_callback = default_task_augmented_sampling\ndefault_task_augmented_elicitation_callback = default_task_augmented_elicitation\n"
  },
  {
    "path": "src/mcp/client/experimental/tasks.py",
    "content": "\"\"\"Experimental client-side task support.\n\nThis module provides client methods for interacting with MCP tasks.\n\nWARNING: These APIs are experimental and may change without notice.\n\nExample:\n    ```python\n    # Call a tool as a task\n    result = await session.experimental.call_tool_as_task(\"tool_name\", {\"arg\": \"value\"})\n    task_id = result.task.task_id\n\n    # Get task status\n    status = await session.experimental.get_task(task_id)\n\n    # Get task result when complete\n    if status.status == \"completed\":\n        result = await session.experimental.get_task_result(task_id, CallToolResult)\n\n    # List all tasks\n    tasks = await session.experimental.list_tasks()\n\n    # Cancel a task\n    await session.experimental.cancel_task(task_id)\n    ```\n\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom typing import TYPE_CHECKING, Any, TypeVar\n\nfrom mcp import types\nfrom mcp.shared.experimental.tasks.polling import poll_until_terminal\nfrom mcp.types._types import RequestParamsMeta\n\nif TYPE_CHECKING:\n    from mcp.client.session import ClientSession\n\nResultT = TypeVar(\"ResultT\", bound=types.Result)\n\n\nclass ExperimentalClientFeatures:\n    \"\"\"Experimental client features for tasks and other experimental APIs.\n\n    WARNING: These APIs are experimental and may change without notice.\n\n    Access via session.experimental:\n        status = await session.experimental.get_task(task_id)\n    \"\"\"\n\n    def __init__(self, session: \"ClientSession\") -> None:\n        self._session = session\n\n    async def call_tool_as_task(\n        self,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        *,\n        ttl: int = 60000,\n        meta: RequestParamsMeta | None = None,\n    ) -> types.CreateTaskResult:\n        \"\"\"Call a tool as a task, returning a CreateTaskResult for polling.\n\n        This is a convenience method for calling tools that support task execution.\n        The server will return a task reference instead of the immediate result,\n        which can then be polled via `get_task()` and retrieved via `get_task_result()`.\n\n        Args:\n            name: The tool name\n            arguments: Tool arguments\n            ttl: Task time-to-live in milliseconds (default: 60000 = 1 minute)\n            meta: Optional metadata to include in the request\n\n        Returns:\n            CreateTaskResult containing the task reference\n\n        Example:\n            ```python\n            # Create task\n            result = await session.experimental.call_tool_as_task(\n                \"long_running_tool\", {\"input\": \"data\"}\n            )\n            task_id = result.task.task_id\n\n            # Poll for completion\n            while True:\n                status = await session.experimental.get_task(task_id)\n                if status.status == \"completed\":\n                    break\n                await anyio.sleep(0.5)\n\n            # Get result\n            final = await session.experimental.get_task_result(task_id, CallToolResult)\n            ```\n        \"\"\"\n        return await self._session.send_request(\n            types.CallToolRequest(\n                params=types.CallToolRequestParams(\n                    name=name,\n                    arguments=arguments,\n                    task=types.TaskMetadata(ttl=ttl),\n                    _meta=meta,\n                ),\n            ),\n            types.CreateTaskResult,\n        )\n\n    async def get_task(self, task_id: str) -> types.GetTaskResult:\n        \"\"\"Get the current status of a task.\n\n        Args:\n            task_id: The task identifier\n\n        Returns:\n            GetTaskResult containing the task status and metadata\n        \"\"\"\n        return await self._session.send_request(\n            types.GetTaskRequest(params=types.GetTaskRequestParams(task_id=task_id)),\n            types.GetTaskResult,\n        )\n\n    async def get_task_result(\n        self,\n        task_id: str,\n        result_type: type[ResultT],\n    ) -> ResultT:\n        \"\"\"Get the result of a completed task.\n\n        The result type depends on the original request type:\n        - tools/call tasks return CallToolResult\n        - Other request types return their corresponding result type\n\n        Args:\n            task_id: The task identifier\n            result_type: The expected result type (e.g., CallToolResult)\n\n        Returns:\n            The task result, validated against result_type\n        \"\"\"\n        return await self._session.send_request(\n            types.GetTaskPayloadRequest(\n                params=types.GetTaskPayloadRequestParams(task_id=task_id),\n            ),\n            result_type,\n        )\n\n    async def list_tasks(\n        self,\n        cursor: str | None = None,\n    ) -> types.ListTasksResult:\n        \"\"\"List all tasks.\n\n        Args:\n            cursor: Optional pagination cursor\n\n        Returns:\n            ListTasksResult containing tasks and optional next cursor\n        \"\"\"\n        params = types.PaginatedRequestParams(cursor=cursor) if cursor else None\n        return await self._session.send_request(\n            types.ListTasksRequest(params=params),\n            types.ListTasksResult,\n        )\n\n    async def cancel_task(self, task_id: str) -> types.CancelTaskResult:\n        \"\"\"Cancel a running task.\n\n        Args:\n            task_id: The task identifier\n\n        Returns:\n            CancelTaskResult with the updated task state\n        \"\"\"\n        return await self._session.send_request(\n            types.CancelTaskRequest(\n                params=types.CancelTaskRequestParams(task_id=task_id),\n            ),\n            types.CancelTaskResult,\n        )\n\n    async def poll_task(self, task_id: str) -> AsyncIterator[types.GetTaskResult]:\n        \"\"\"Poll a task until it reaches a terminal status.\n\n        Yields GetTaskResult for each poll, allowing the caller to react to\n        status changes (e.g., handle input_required). Exits when the task reaches\n        a terminal status (completed, failed, cancelled).\n\n        Respects the pollInterval hint from the server.\n\n        Args:\n            task_id: The task identifier\n\n        Yields:\n            GetTaskResult for each poll\n\n        Example:\n            ```python\n            async for status in session.experimental.poll_task(task_id):\n                print(f\"Status: {status.status}\")\n                if status.status == \"input_required\":\n                    # Handle elicitation request via tasks/result\n                    pass\n\n            # Task is now terminal, get the result\n            result = await session.experimental.get_task_result(task_id, CallToolResult)\n            ```\n        \"\"\"\n        async for status in poll_until_terminal(self.get_task, task_id):\n            yield status\n"
  },
  {
    "path": "src/mcp/client/session.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom typing import Any, Protocol\n\nimport anyio.lowlevel\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\nfrom pydantic import TypeAdapter\n\nfrom mcp import types\nfrom mcp.client.experimental import ExperimentalClientFeatures\nfrom mcp.client.experimental.task_handlers import ExperimentalTaskHandlers\nfrom mcp.shared._context import RequestContext\nfrom mcp.shared.message import SessionMessage\nfrom mcp.shared.session import BaseSession, ProgressFnT, RequestResponder\nfrom mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS\nfrom mcp.types._types import RequestParamsMeta\n\nDEFAULT_CLIENT_INFO = types.Implementation(name=\"mcp\", version=\"0.1.0\")\n\nlogger = logging.getLogger(\"client\")\n\n\nclass SamplingFnT(Protocol):\n    async def __call__(\n        self,\n        context: RequestContext[ClientSession],\n        params: types.CreateMessageRequestParams,\n    ) -> types.CreateMessageResult | types.CreateMessageResultWithTools | types.ErrorData: ...  # pragma: no branch\n\n\nclass ElicitationFnT(Protocol):\n    async def __call__(\n        self,\n        context: RequestContext[ClientSession],\n        params: types.ElicitRequestParams,\n    ) -> types.ElicitResult | types.ErrorData: ...  # pragma: no branch\n\n\nclass ListRootsFnT(Protocol):\n    async def __call__(\n        self, context: RequestContext[ClientSession]\n    ) -> types.ListRootsResult | types.ErrorData: ...  # pragma: no branch\n\n\nclass LoggingFnT(Protocol):\n    async def __call__(self, params: types.LoggingMessageNotificationParams) -> None: ...  # pragma: no branch\n\n\nclass MessageHandlerFnT(Protocol):\n    async def __call__(\n        self,\n        message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,\n    ) -> None: ...  # pragma: no branch\n\n\nasync def _default_message_handler(\n    message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,\n) -> None:\n    await anyio.lowlevel.checkpoint()\n\n\nasync def _default_sampling_callback(\n    context: RequestContext[ClientSession],\n    params: types.CreateMessageRequestParams,\n) -> types.CreateMessageResult | types.CreateMessageResultWithTools | types.ErrorData:\n    return types.ErrorData(\n        code=types.INVALID_REQUEST,\n        message=\"Sampling not supported\",\n    )\n\n\nasync def _default_elicitation_callback(\n    context: RequestContext[ClientSession],\n    params: types.ElicitRequestParams,\n) -> types.ElicitResult | types.ErrorData:\n    return types.ErrorData(  # pragma: no cover\n        code=types.INVALID_REQUEST,\n        message=\"Elicitation not supported\",\n    )\n\n\nasync def _default_list_roots_callback(\n    context: RequestContext[ClientSession],\n) -> types.ListRootsResult | types.ErrorData:\n    return types.ErrorData(\n        code=types.INVALID_REQUEST,\n        message=\"List roots not supported\",\n    )\n\n\nasync def _default_logging_callback(\n    params: types.LoggingMessageNotificationParams,\n) -> None:\n    pass\n\n\nClientResponse: TypeAdapter[types.ClientResult | types.ErrorData] = TypeAdapter(types.ClientResult | types.ErrorData)\n\n\nclass ClientSession(\n    BaseSession[\n        types.ClientRequest,\n        types.ClientNotification,\n        types.ClientResult,\n        types.ServerRequest,\n        types.ServerNotification,\n    ]\n):\n    def __init__(\n        self,\n        read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],\n        write_stream: MemoryObjectSendStream[SessionMessage],\n        read_timeout_seconds: float | None = None,\n        sampling_callback: SamplingFnT | None = None,\n        elicitation_callback: ElicitationFnT | None = None,\n        list_roots_callback: ListRootsFnT | None = None,\n        logging_callback: LoggingFnT | None = None,\n        message_handler: MessageHandlerFnT | None = None,\n        client_info: types.Implementation | None = None,\n        *,\n        sampling_capabilities: types.SamplingCapability | None = None,\n        experimental_task_handlers: ExperimentalTaskHandlers | None = None,\n    ) -> None:\n        super().__init__(read_stream, write_stream, read_timeout_seconds=read_timeout_seconds)\n        self._client_info = client_info or DEFAULT_CLIENT_INFO\n        self._sampling_callback = sampling_callback or _default_sampling_callback\n        self._sampling_capabilities = sampling_capabilities\n        self._elicitation_callback = elicitation_callback or _default_elicitation_callback\n        self._list_roots_callback = list_roots_callback or _default_list_roots_callback\n        self._logging_callback = logging_callback or _default_logging_callback\n        self._message_handler = message_handler or _default_message_handler\n        self._tool_output_schemas: dict[str, dict[str, Any] | None] = {}\n        self._initialize_result: types.InitializeResult | None = None\n        self._experimental_features: ExperimentalClientFeatures | None = None\n\n        # Experimental: Task handlers (use defaults if not provided)\n        self._task_handlers = experimental_task_handlers or ExperimentalTaskHandlers()\n\n    @property\n    def _receive_request_adapter(self) -> TypeAdapter[types.ServerRequest]:\n        return types.server_request_adapter\n\n    @property\n    def _receive_notification_adapter(self) -> TypeAdapter[types.ServerNotification]:\n        return types.server_notification_adapter\n\n    async def initialize(self) -> types.InitializeResult:\n        sampling = (\n            (self._sampling_capabilities or types.SamplingCapability())\n            if self._sampling_callback is not _default_sampling_callback\n            else None\n        )\n        elicitation = (\n            types.ElicitationCapability(form=types.FormElicitationCapability(), url=types.UrlElicitationCapability())\n            if self._elicitation_callback is not _default_elicitation_callback\n            else None\n        )\n        roots = (\n            # TODO: Should this be based on whether we\n            # _will_ send notifications, or only whether\n            # they're supported?\n            types.RootsCapability(list_changed=True)\n            if self._list_roots_callback is not _default_list_roots_callback\n            else None\n        )\n\n        result = await self.send_request(\n            types.InitializeRequest(\n                params=types.InitializeRequestParams(\n                    protocol_version=types.LATEST_PROTOCOL_VERSION,\n                    capabilities=types.ClientCapabilities(\n                        sampling=sampling,\n                        elicitation=elicitation,\n                        experimental=None,\n                        roots=roots,\n                        tasks=self._task_handlers.build_capability(),\n                    ),\n                    client_info=self._client_info,\n                ),\n            ),\n            types.InitializeResult,\n        )\n\n        if result.protocol_version not in SUPPORTED_PROTOCOL_VERSIONS:\n            raise RuntimeError(f\"Unsupported protocol version from the server: {result.protocol_version}\")\n\n        self._initialize_result = result\n\n        await self.send_notification(types.InitializedNotification())\n\n        return result\n\n    @property\n    def initialize_result(self) -> types.InitializeResult | None:\n        \"\"\"The server's InitializeResult. None until initialize() has been called.\n\n        Contains server_info, capabilities, instructions, and the negotiated protocol_version.\n        \"\"\"\n        return self._initialize_result\n\n    @property\n    def experimental(self) -> ExperimentalClientFeatures:\n        \"\"\"Experimental APIs for tasks and other features.\n\n        !!! warning\n            These APIs are experimental and may change without notice.\n\n        Example:\n            ```python\n            status = await session.experimental.get_task(task_id)\n            result = await session.experimental.get_task_result(task_id, CallToolResult)\n            ```\n        \"\"\"\n        if self._experimental_features is None:\n            self._experimental_features = ExperimentalClientFeatures(self)\n        return self._experimental_features\n\n    async def send_ping(self, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:\n        \"\"\"Send a ping request.\"\"\"\n        return await self.send_request(types.PingRequest(params=types.RequestParams(_meta=meta)), types.EmptyResult)\n\n    async def send_progress_notification(\n        self,\n        progress_token: str | int,\n        progress: float,\n        total: float | None = None,\n        message: str | None = None,\n        *,\n        meta: RequestParamsMeta | None = None,\n    ) -> None:\n        \"\"\"Send a progress notification.\"\"\"\n        await self.send_notification(\n            types.ProgressNotification(\n                params=types.ProgressNotificationParams(\n                    progress_token=progress_token,\n                    progress=progress,\n                    total=total,\n                    message=message,\n                    _meta=meta,\n                ),\n            )\n        )\n\n    async def set_logging_level(\n        self,\n        level: types.LoggingLevel,\n        *,\n        meta: RequestParamsMeta | None = None,\n    ) -> types.EmptyResult:\n        \"\"\"Send a logging/setLevel request.\"\"\"\n        return await self.send_request(\n            types.SetLevelRequest(params=types.SetLevelRequestParams(level=level, _meta=meta)),\n            types.EmptyResult,\n        )\n\n    async def list_resources(self, *, params: types.PaginatedRequestParams | None = None) -> types.ListResourcesResult:\n        \"\"\"Send a resources/list request.\n\n        Args:\n            params: Full pagination parameters including cursor and any future fields\n        \"\"\"\n        return await self.send_request(types.ListResourcesRequest(params=params), types.ListResourcesResult)\n\n    async def list_resource_templates(\n        self, *, params: types.PaginatedRequestParams | None = None\n    ) -> types.ListResourceTemplatesResult:\n        \"\"\"Send a resources/templates/list request.\n\n        Args:\n            params: Full pagination parameters including cursor and any future fields\n        \"\"\"\n        return await self.send_request(\n            types.ListResourceTemplatesRequest(params=params),\n            types.ListResourceTemplatesResult,\n        )\n\n    async def read_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.ReadResourceResult:\n        \"\"\"Send a resources/read request.\"\"\"\n        return await self.send_request(\n            types.ReadResourceRequest(params=types.ReadResourceRequestParams(uri=uri, _meta=meta)),\n            types.ReadResourceResult,\n        )\n\n    async def subscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:\n        \"\"\"Send a resources/subscribe request.\"\"\"\n        return await self.send_request(\n            types.SubscribeRequest(params=types.SubscribeRequestParams(uri=uri, _meta=meta)),\n            types.EmptyResult,\n        )\n\n    async def unsubscribe_resource(self, uri: str, *, meta: RequestParamsMeta | None = None) -> types.EmptyResult:\n        \"\"\"Send a resources/unsubscribe request.\"\"\"\n        return await self.send_request(\n            types.UnsubscribeRequest(params=types.UnsubscribeRequestParams(uri=uri, _meta=meta)),\n            types.EmptyResult,\n        )\n\n    async def call_tool(\n        self,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        read_timeout_seconds: float | None = None,\n        progress_callback: ProgressFnT | None = None,\n        *,\n        meta: RequestParamsMeta | None = None,\n    ) -> types.CallToolResult:\n        \"\"\"Send a tools/call request with optional progress callback support.\"\"\"\n\n        result = await self.send_request(\n            types.CallToolRequest(\n                params=types.CallToolRequestParams(name=name, arguments=arguments, _meta=meta),\n            ),\n            types.CallToolResult,\n            request_read_timeout_seconds=read_timeout_seconds,\n            progress_callback=progress_callback,\n        )\n\n        if not result.is_error:\n            await self._validate_tool_result(name, result)\n\n        return result\n\n    async def _validate_tool_result(self, name: str, result: types.CallToolResult) -> None:\n        \"\"\"Validate the structured content of a tool result against its output schema.\"\"\"\n        if name not in self._tool_output_schemas:\n            # refresh output schema cache\n            await self.list_tools()\n\n        output_schema = None\n        if name in self._tool_output_schemas:\n            output_schema = self._tool_output_schemas.get(name)\n        else:\n            logger.warning(f\"Tool {name} not listed by server, cannot validate any structured content\")\n\n        if output_schema is not None:\n            from jsonschema import SchemaError, ValidationError, validate\n\n            if result.structured_content is None:\n                raise RuntimeError(\n                    f\"Tool {name} has an output schema but did not return structured content\"\n                )  # pragma: no cover\n            try:\n                validate(result.structured_content, output_schema)\n            except ValidationError as e:\n                raise RuntimeError(f\"Invalid structured content returned by tool {name}: {e}\")\n            except SchemaError as e:  # pragma: no cover\n                raise RuntimeError(f\"Invalid schema for tool {name}: {e}\")  # pragma: no cover\n\n    async def list_prompts(self, *, params: types.PaginatedRequestParams | None = None) -> types.ListPromptsResult:\n        \"\"\"Send a prompts/list request.\n\n        Args:\n            params: Full pagination parameters including cursor and any future fields\n        \"\"\"\n        return await self.send_request(types.ListPromptsRequest(params=params), types.ListPromptsResult)\n\n    async def get_prompt(\n        self,\n        name: str,\n        arguments: dict[str, str] | None = None,\n        *,\n        meta: RequestParamsMeta | None = None,\n    ) -> types.GetPromptResult:\n        \"\"\"Send a prompts/get request.\"\"\"\n        return await self.send_request(\n            types.GetPromptRequest(params=types.GetPromptRequestParams(name=name, arguments=arguments, _meta=meta)),\n            types.GetPromptResult,\n        )\n\n    async def complete(\n        self,\n        ref: types.ResourceTemplateReference | types.PromptReference,\n        argument: dict[str, str],\n        context_arguments: dict[str, str] | None = None,\n    ) -> types.CompleteResult:\n        \"\"\"Send a completion/complete request.\"\"\"\n        context = None\n        if context_arguments is not None:\n            context = types.CompletionContext(arguments=context_arguments)\n\n        return await self.send_request(\n            types.CompleteRequest(\n                params=types.CompleteRequestParams(\n                    ref=ref,\n                    argument=types.CompletionArgument(**argument),\n                    context=context,\n                ),\n            ),\n            types.CompleteResult,\n        )\n\n    async def list_tools(self, *, params: types.PaginatedRequestParams | None = None) -> types.ListToolsResult:\n        \"\"\"Send a tools/list request.\n\n        Args:\n            params: Full pagination parameters including cursor and any future fields\n        \"\"\"\n        result = await self.send_request(\n            types.ListToolsRequest(params=params),\n            types.ListToolsResult,\n        )\n\n        # Cache tool output schemas for future validation\n        # Note: don't clear the cache, as we may be using a cursor\n        for tool in result.tools:\n            self._tool_output_schemas[tool.name] = tool.output_schema\n\n        return result\n\n    async def send_roots_list_changed(self) -> None:  # pragma: no cover\n        \"\"\"Send a roots/list_changed notification.\"\"\"\n        await self.send_notification(types.RootsListChangedNotification())\n\n    async def _received_request(self, responder: RequestResponder[types.ServerRequest, types.ClientResult]) -> None:\n        ctx = RequestContext[ClientSession](request_id=responder.request_id, meta=responder.request_meta, session=self)\n\n        # Delegate to experimental task handler if applicable\n        if self._task_handlers.handles_request(responder.request):\n            with responder:\n                await self._task_handlers.handle_request(ctx, responder)\n            return None\n\n        # Core request handling\n        match responder.request:\n            case types.CreateMessageRequest(params=params):\n                with responder:\n                    # Check if this is a task-augmented request\n                    if params.task is not None:\n                        response = await self._task_handlers.augmented_sampling(ctx, params, params.task)\n                    else:\n                        response = await self._sampling_callback(ctx, params)\n                    client_response = ClientResponse.validate_python(response)\n                    await responder.respond(client_response)\n\n            case types.ElicitRequest(params=params):\n                with responder:\n                    # Check if this is a task-augmented request\n                    if params.task is not None:\n                        response = await self._task_handlers.augmented_elicitation(ctx, params, params.task)\n                    else:\n                        response = await self._elicitation_callback(ctx, params)\n                    client_response = ClientResponse.validate_python(response)\n                    await responder.respond(client_response)\n\n            case types.ListRootsRequest():\n                with responder:\n                    response = await self._list_roots_callback(ctx)\n                    client_response = ClientResponse.validate_python(response)\n                    await responder.respond(client_response)\n\n            case types.PingRequest():  # pragma: no cover\n                with responder:\n                    return await responder.respond(types.EmptyResult())\n\n            case _:  # pragma: no cover\n                pass  # Task requests handled above by _task_handlers\n\n        return None\n\n    async def _handle_incoming(\n        self,\n        req: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,\n    ) -> None:\n        \"\"\"Handle incoming messages by forwarding to the message handler.\"\"\"\n        await self._message_handler(req)\n\n    async def _received_notification(self, notification: types.ServerNotification) -> None:\n        \"\"\"Handle notifications from the server.\"\"\"\n        # Process specific notification types\n        match notification:\n            case types.LoggingMessageNotification(params=params):\n                await self._logging_callback(params)\n            case types.ElicitCompleteNotification(params=params):\n                # Handle elicitation completion notification\n                # Clients MAY use this to retry requests or update UI\n                # The notification contains the elicitationId of the completed elicitation\n                pass\n            case _:\n                pass\n"
  },
  {
    "path": "src/mcp/client/session_group.py",
    "content": "\"\"\"SessionGroup concurrently manages multiple MCP session connections.\n\nTools, resources, and prompts are aggregated across servers. Servers may\nbe connected to or disconnected from at any point after initialization.\n\nThis abstraction can handle naming collisions using a custom user-provided hook.\n\"\"\"\n\nimport contextlib\nimport logging\nfrom collections.abc import Callable\nfrom dataclasses import dataclass\nfrom types import TracebackType\nfrom typing import Any, TypeAlias\n\nimport anyio\nimport httpx\nfrom pydantic import BaseModel, Field\nfrom typing_extensions import Self\n\nimport mcp\nfrom mcp import types\nfrom mcp.client.session import ElicitationFnT, ListRootsFnT, LoggingFnT, MessageHandlerFnT, SamplingFnT\nfrom mcp.client.sse import sse_client\nfrom mcp.client.stdio import StdioServerParameters\nfrom mcp.client.streamable_http import streamable_http_client\nfrom mcp.shared._httpx_utils import create_mcp_http_client\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.session import ProgressFnT\n\n\nclass SseServerParameters(BaseModel):\n    \"\"\"Parameters for initializing an sse_client.\"\"\"\n\n    # The endpoint URL.\n    url: str\n\n    # Optional headers to include in requests.\n    headers: dict[str, Any] | None = None\n\n    # HTTP timeout for regular operations (in seconds).\n    timeout: float = 5.0\n\n    # Timeout for SSE read operations (in seconds).\n    sse_read_timeout: float = 300.0\n\n\nclass StreamableHttpParameters(BaseModel):\n    \"\"\"Parameters for initializing a streamable_http_client.\"\"\"\n\n    # The endpoint URL.\n    url: str\n\n    # Optional headers to include in requests.\n    headers: dict[str, Any] | None = None\n\n    # HTTP timeout for regular operations (in seconds).\n    timeout: float = 30.0\n\n    # Timeout for SSE read operations (in seconds).\n    sse_read_timeout: float = 300.0\n\n    # Close the client session when the transport closes.\n    terminate_on_close: bool = True\n\n\nServerParameters: TypeAlias = StdioServerParameters | SseServerParameters | StreamableHttpParameters\n\n\n# Use dataclass instead of Pydantic BaseModel\n# because Pydantic BaseModel cannot handle Protocol fields.\n@dataclass\nclass ClientSessionParameters:\n    \"\"\"Parameters for establishing a client session to an MCP server.\"\"\"\n\n    read_timeout_seconds: float | None = None\n    sampling_callback: SamplingFnT | None = None\n    elicitation_callback: ElicitationFnT | None = None\n    list_roots_callback: ListRootsFnT | None = None\n    logging_callback: LoggingFnT | None = None\n    message_handler: MessageHandlerFnT | None = None\n    client_info: types.Implementation | None = None\n\n\nclass ClientSessionGroup:\n    \"\"\"Client for managing connections to multiple MCP servers.\n\n    This class is responsible for encapsulating management of server connections.\n    It aggregates tools, resources, and prompts from all connected servers.\n\n    For auxiliary handlers, such as resource subscription, this is delegated to\n    the client and can be accessed via the session.\n\n    Example:\n        ```python\n        name_fn = lambda name, server_info: f\"{(server_info.name)}_{name}\"\n        async with ClientSessionGroup(component_name_hook=name_fn) as group:\n            for server_param in server_params:\n                await group.connect_to_server(server_param)\n            ...\n        ```\n    \"\"\"\n\n    class _ComponentNames(BaseModel):\n        \"\"\"Used for reverse index to find components.\"\"\"\n\n        prompts: set[str] = Field(default_factory=set)\n        resources: set[str] = Field(default_factory=set)\n        tools: set[str] = Field(default_factory=set)\n\n    # Standard MCP components.\n    _prompts: dict[str, types.Prompt]\n    _resources: dict[str, types.Resource]\n    _tools: dict[str, types.Tool]\n\n    # Client-server connection management.\n    _sessions: dict[mcp.ClientSession, _ComponentNames]\n    _tool_to_session: dict[str, mcp.ClientSession]\n    _exit_stack: contextlib.AsyncExitStack\n    _session_exit_stacks: dict[mcp.ClientSession, contextlib.AsyncExitStack]\n\n    # Optional fn consuming (component_name, server_info) for custom names.\n    # This is to provide a means to mitigate naming conflicts across servers.\n    # Example: (tool_name, server_info) => \"{result.server_info.name}.{tool_name}\"\n    _ComponentNameHook: TypeAlias = Callable[[str, types.Implementation], str]\n    _component_name_hook: _ComponentNameHook | None\n\n    def __init__(\n        self,\n        exit_stack: contextlib.AsyncExitStack | None = None,\n        component_name_hook: _ComponentNameHook | None = None,\n    ) -> None:\n        \"\"\"Initializes the MCP client.\"\"\"\n\n        self._tools = {}\n        self._resources = {}\n        self._prompts = {}\n\n        self._sessions = {}\n        self._tool_to_session = {}\n        if exit_stack is None:\n            self._exit_stack = contextlib.AsyncExitStack()\n            self._owns_exit_stack = True\n        else:\n            self._exit_stack = exit_stack\n            self._owns_exit_stack = False\n        self._session_exit_stacks = {}\n        self._component_name_hook = component_name_hook\n\n    async def __aenter__(self) -> Self:  # pragma: no cover\n        # Enter the exit stack only if we created it ourselves\n        if self._owns_exit_stack:\n            await self._exit_stack.__aenter__()\n        return self\n\n    async def __aexit__(\n        self,\n        _exc_type: type[BaseException] | None,\n        _exc_val: BaseException | None,\n        _exc_tb: TracebackType | None,\n    ) -> bool | None:  # pragma: no cover\n        \"\"\"Closes session exit stacks and main exit stack upon completion.\"\"\"\n\n        # Only close the main exit stack if we created it\n        if self._owns_exit_stack:\n            await self._exit_stack.aclose()\n\n        # Concurrently close session stacks.\n        async with anyio.create_task_group() as tg:\n            for exit_stack in self._session_exit_stacks.values():\n                tg.start_soon(exit_stack.aclose)\n\n    @property\n    def sessions(self) -> list[mcp.ClientSession]:\n        \"\"\"Returns the list of sessions being managed.\"\"\"\n        return list(self._sessions.keys())  # pragma: no cover\n\n    @property\n    def prompts(self) -> dict[str, types.Prompt]:\n        \"\"\"Returns the prompts as a dictionary of names to prompts.\"\"\"\n        return self._prompts\n\n    @property\n    def resources(self) -> dict[str, types.Resource]:\n        \"\"\"Returns the resources as a dictionary of names to resources.\"\"\"\n        return self._resources\n\n    @property\n    def tools(self) -> dict[str, types.Tool]:\n        \"\"\"Returns the tools as a dictionary of names to tools.\"\"\"\n        return self._tools\n\n    async def call_tool(\n        self,\n        name: str,\n        arguments: dict[str, Any] | None = None,\n        read_timeout_seconds: float | None = None,\n        progress_callback: ProgressFnT | None = None,\n        *,\n        meta: types.RequestParamsMeta | None = None,\n    ) -> types.CallToolResult:\n        \"\"\"Executes a tool given its name and arguments.\"\"\"\n        session = self._tool_to_session[name]\n        session_tool_name = self.tools[name].name\n        return await session.call_tool(\n            session_tool_name,\n            arguments=arguments,\n            read_timeout_seconds=read_timeout_seconds,\n            progress_callback=progress_callback,\n            meta=meta,\n        )\n\n    async def disconnect_from_server(self, session: mcp.ClientSession) -> None:\n        \"\"\"Disconnects from a single MCP server.\"\"\"\n\n        session_known_for_components = session in self._sessions\n        session_known_for_stack = session in self._session_exit_stacks\n\n        if not session_known_for_components and not session_known_for_stack:\n            raise MCPError(\n                code=types.INVALID_PARAMS,\n                message=\"Provided session is not managed or already disconnected.\",\n            )\n\n        if session_known_for_components:  # pragma: no branch\n            component_names = self._sessions.pop(session)  # Pop from _sessions tracking\n\n            # Remove prompts associated with the session.\n            for name in component_names.prompts:\n                if name in self._prompts:  # pragma: no branch\n                    del self._prompts[name]\n            # Remove resources associated with the session.\n            for name in component_names.resources:\n                if name in self._resources:  # pragma: no branch\n                    del self._resources[name]\n            # Remove tools associated with the session.\n            for name in component_names.tools:\n                if name in self._tools:  # pragma: no branch\n                    del self._tools[name]\n                if name in self._tool_to_session:  # pragma: no branch\n                    del self._tool_to_session[name]\n\n        # Clean up the session's resources via its dedicated exit stack\n        if session_known_for_stack:\n            session_stack_to_close = self._session_exit_stacks.pop(session)  # pragma: no cover\n            await session_stack_to_close.aclose()  # pragma: no cover\n\n    async def connect_with_session(\n        self, server_info: types.Implementation, session: mcp.ClientSession\n    ) -> mcp.ClientSession:\n        \"\"\"Connects to a single MCP server.\"\"\"\n        await self._aggregate_components(server_info, session)\n        return session\n\n    async def connect_to_server(\n        self,\n        server_params: ServerParameters,\n        session_params: ClientSessionParameters | None = None,\n    ) -> mcp.ClientSession:\n        \"\"\"Connects to a single MCP server.\"\"\"\n        server_info, session = await self._establish_session(server_params, session_params or ClientSessionParameters())\n        return await self.connect_with_session(server_info, session)\n\n    async def _establish_session(\n        self,\n        server_params: ServerParameters,\n        session_params: ClientSessionParameters,\n    ) -> tuple[types.Implementation, mcp.ClientSession]:\n        \"\"\"Establish a client session to an MCP server.\"\"\"\n\n        session_stack = contextlib.AsyncExitStack()\n        try:\n            # Create read and write streams that facilitate io with the server.\n            if isinstance(server_params, StdioServerParameters):\n                client = mcp.stdio_client(server_params)\n                read, write = await session_stack.enter_async_context(client)\n            elif isinstance(server_params, SseServerParameters):\n                client = sse_client(\n                    url=server_params.url,\n                    headers=server_params.headers,\n                    timeout=server_params.timeout,\n                    sse_read_timeout=server_params.sse_read_timeout,\n                )\n                read, write = await session_stack.enter_async_context(client)\n            else:\n                httpx_client = create_mcp_http_client(\n                    headers=server_params.headers,\n                    timeout=httpx.Timeout(\n                        server_params.timeout,\n                        read=server_params.sse_read_timeout,\n                    ),\n                )\n                await session_stack.enter_async_context(httpx_client)\n\n                client = streamable_http_client(\n                    url=server_params.url,\n                    http_client=httpx_client,\n                    terminate_on_close=server_params.terminate_on_close,\n                )\n                read, write = await session_stack.enter_async_context(client)\n\n            session = await session_stack.enter_async_context(\n                mcp.ClientSession(\n                    read,\n                    write,\n                    read_timeout_seconds=session_params.read_timeout_seconds,\n                    sampling_callback=session_params.sampling_callback,\n                    elicitation_callback=session_params.elicitation_callback,\n                    list_roots_callback=session_params.list_roots_callback,\n                    logging_callback=session_params.logging_callback,\n                    message_handler=session_params.message_handler,\n                    client_info=session_params.client_info,\n                )\n            )\n\n            result = await session.initialize()\n\n            # Session successfully initialized.\n            # Store its stack and register the stack with the main group stack.\n            self._session_exit_stacks[session] = session_stack\n            # session_stack itself becomes a resource managed by the\n            # main _exit_stack.\n            await self._exit_stack.enter_async_context(session_stack)\n\n            return result.server_info, session\n        except Exception:  # pragma: no cover\n            # If anything during this setup fails, ensure the session-specific\n            # stack is closed.\n            await session_stack.aclose()\n            raise\n\n    async def _aggregate_components(self, server_info: types.Implementation, session: mcp.ClientSession) -> None:\n        \"\"\"Aggregates prompts, resources, and tools from a given session.\"\"\"\n\n        # Create a reverse index so we can find all prompts, resources, and\n        # tools belonging to this session. Used for removing components from\n        # the session group via self.disconnect_from_server.\n        component_names = self._ComponentNames()\n\n        # Temporary components dicts. We do not want to modify the aggregate\n        # lists in case of an intermediate failure.\n        prompts_temp: dict[str, types.Prompt] = {}\n        resources_temp: dict[str, types.Resource] = {}\n        tools_temp: dict[str, types.Tool] = {}\n        tool_to_session_temp: dict[str, mcp.ClientSession] = {}\n\n        # Query the server for its prompts and aggregate to list.\n        try:\n            prompts = (await session.list_prompts()).prompts\n            for prompt in prompts:\n                name = self._component_name(prompt.name, server_info)\n                prompts_temp[name] = prompt\n                component_names.prompts.add(name)\n        except MCPError as err:  # pragma: no cover\n            logging.warning(f\"Could not fetch prompts: {err}\")\n\n        # Query the server for its resources and aggregate to list.\n        try:\n            resources = (await session.list_resources()).resources\n            for resource in resources:\n                name = self._component_name(resource.name, server_info)\n                resources_temp[name] = resource\n                component_names.resources.add(name)\n        except MCPError as err:  # pragma: no cover\n            logging.warning(f\"Could not fetch resources: {err}\")\n\n        # Query the server for its tools and aggregate to list.\n        try:\n            tools = (await session.list_tools()).tools\n            for tool in tools:\n                name = self._component_name(tool.name, server_info)\n                tools_temp[name] = tool\n                tool_to_session_temp[name] = session\n                component_names.tools.add(name)\n        except MCPError as err:  # pragma: no cover\n            logging.warning(f\"Could not fetch tools: {err}\")\n\n        # Clean up exit stack for session if we couldn't retrieve anything\n        # from the server.\n        if not any((prompts_temp, resources_temp, tools_temp)):\n            del self._session_exit_stacks[session]  # pragma: no cover\n\n        # Check for duplicates.\n        matching_prompts = prompts_temp.keys() & self._prompts.keys()\n        if matching_prompts:\n            raise MCPError(  # pragma: no cover\n                code=types.INVALID_PARAMS,\n                message=f\"{matching_prompts} already exist in group prompts.\",\n            )\n        matching_resources = resources_temp.keys() & self._resources.keys()\n        if matching_resources:\n            raise MCPError(  # pragma: no cover\n                code=types.INVALID_PARAMS,\n                message=f\"{matching_resources} already exist in group resources.\",\n            )\n        matching_tools = tools_temp.keys() & self._tools.keys()\n        if matching_tools:\n            raise MCPError(code=types.INVALID_PARAMS, message=f\"{matching_tools} already exist in group tools.\")\n\n        # Aggregate components.\n        self._sessions[session] = component_names\n        self._prompts.update(prompts_temp)\n        self._resources.update(resources_temp)\n        self._tools.update(tools_temp)\n        self._tool_to_session.update(tool_to_session_temp)\n\n    def _component_name(self, name: str, server_info: types.Implementation) -> str:\n        if self._component_name_hook:\n            return self._component_name_hook(name, server_info)\n        return name\n"
  },
  {
    "path": "src/mcp/client/sse.py",
    "content": "import logging\nfrom collections.abc import Callable\nfrom contextlib import asynccontextmanager\nfrom typing import Any\nfrom urllib.parse import parse_qs, urljoin, urlparse\n\nimport anyio\nimport httpx\nfrom anyio.abc import TaskStatus\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\nfrom httpx_sse import aconnect_sse\nfrom httpx_sse._exceptions import SSEError\n\nfrom mcp import types\nfrom mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client\nfrom mcp.shared.message import SessionMessage\n\nlogger = logging.getLogger(__name__)\n\n\ndef remove_request_params(url: str) -> str:\n    return urljoin(url, urlparse(url).path)\n\n\ndef _extract_session_id_from_endpoint(endpoint_url: str) -> str | None:\n    query_params = parse_qs(urlparse(endpoint_url).query)\n    return query_params.get(\"sessionId\", [None])[0] or query_params.get(\"session_id\", [None])[0]\n\n\n@asynccontextmanager\nasync def sse_client(\n    url: str,\n    headers: dict[str, Any] | None = None,\n    timeout: float = 5.0,\n    sse_read_timeout: float = 300.0,\n    httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,\n    auth: httpx.Auth | None = None,\n    on_session_created: Callable[[str], None] | None = None,\n):\n    \"\"\"Client transport for SSE.\n\n    `sse_read_timeout` determines how long (in seconds) the client will wait for a new\n    event before disconnecting. All other HTTP operations are controlled by `timeout`.\n\n    Args:\n        url: The SSE endpoint URL.\n        headers: Optional headers to include in requests.\n        timeout: HTTP timeout for regular operations (in seconds).\n        sse_read_timeout: Timeout for SSE read operations (in seconds).\n        httpx_client_factory: Factory function for creating the HTTPX client.\n        auth: Optional HTTPX authentication handler.\n        on_session_created: Optional callback invoked with the session ID when received.\n    \"\"\"\n    read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]\n    read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]\n\n    write_stream: MemoryObjectSendStream[SessionMessage]\n    write_stream_reader: MemoryObjectReceiveStream[SessionMessage]\n\n    logger.debug(f\"Connecting to SSE endpoint: {remove_request_params(url)}\")\n    async with httpx_client_factory(\n        headers=headers, auth=auth, timeout=httpx.Timeout(timeout, read=sse_read_timeout)\n    ) as client:\n        async with aconnect_sse(client, \"GET\", url) as event_source:\n            event_source.response.raise_for_status()\n            logger.debug(\"SSE connection established\")\n\n            read_stream_writer, read_stream = anyio.create_memory_object_stream(0)\n            write_stream, write_stream_reader = anyio.create_memory_object_stream(0)\n\n            async def sse_reader(task_status: TaskStatus[str] = anyio.TASK_STATUS_IGNORED):\n                try:\n                    async for sse in event_source.aiter_sse():  # pragma: no branch\n                        logger.debug(f\"Received SSE event: {sse.event}\")\n                        match sse.event:\n                            case \"endpoint\":\n                                endpoint_url = urljoin(url, sse.data)\n                                logger.debug(f\"Received endpoint URL: {endpoint_url}\")\n\n                                url_parsed = urlparse(url)\n                                endpoint_parsed = urlparse(endpoint_url)\n                                if (  # pragma: no cover\n                                    url_parsed.netloc != endpoint_parsed.netloc\n                                    or url_parsed.scheme != endpoint_parsed.scheme\n                                ):\n                                    error_msg = (  # pragma: no cover\n                                        f\"Endpoint origin does not match connection origin: {endpoint_url}\"\n                                    )\n                                    logger.error(error_msg)  # pragma: no cover\n                                    raise ValueError(error_msg)  # pragma: no cover\n\n                                if on_session_created:\n                                    session_id = _extract_session_id_from_endpoint(endpoint_url)\n                                    if session_id:\n                                        on_session_created(session_id)\n\n                                task_status.started(endpoint_url)\n\n                            case \"message\":\n                                # Skip empty data (keep-alive pings)\n                                if not sse.data:\n                                    continue\n                                try:\n                                    message = types.jsonrpc_message_adapter.validate_json(sse.data, by_name=False)\n                                    logger.debug(f\"Received server message: {message}\")\n                                except Exception as exc:  # pragma: no cover\n                                    logger.exception(\"Error parsing server message\")  # pragma: no cover\n                                    await read_stream_writer.send(exc)  # pragma: no cover\n                                    continue  # pragma: no cover\n\n                                session_message = SessionMessage(message)\n                                await read_stream_writer.send(session_message)\n                            case _:  # pragma: no cover\n                                logger.warning(f\"Unknown SSE event: {sse.event}\")  # pragma: no cover\n                except SSEError as sse_exc:  # pragma: lax no cover\n                    logger.exception(\"Encountered SSE exception\")\n                    raise sse_exc\n                except Exception as exc:  # pragma: lax no cover\n                    logger.exception(\"Error in sse_reader\")\n                    await read_stream_writer.send(exc)\n                finally:\n                    await read_stream_writer.aclose()\n\n            async def post_writer(endpoint_url: str):\n                try:\n                    async with write_stream_reader, write_stream:\n                        async for session_message in write_stream_reader:\n                            logger.debug(f\"Sending client message: {session_message}\")\n                            response = await client.post(\n                                endpoint_url,\n                                json=session_message.message.model_dump(\n                                    by_alias=True,\n                                    mode=\"json\",\n                                    exclude_unset=True,\n                                ),\n                            )\n                            response.raise_for_status()\n                            logger.debug(f\"Client message sent successfully: {response.status_code}\")\n                except Exception:  # pragma: lax no cover\n                    logger.exception(\"Error in post_writer\")\n\n            # On Python 3.14, coverage.py reports a phantom branch arc on this\n            # line (->yield) when nested two async-with levels deep. The branch\n            # is the unreachable \"did __aexit__ suppress?\" arm for memory streams.\n            async with (  # pragma: no branch\n                read_stream_writer,\n                read_stream,\n                write_stream,\n                write_stream_reader,\n                anyio.create_task_group() as tg,\n            ):\n                endpoint_url = await tg.start(sse_reader)\n                logger.debug(f\"Starting post writer with endpoint URL: {endpoint_url}\")\n                tg.start_soon(post_writer, endpoint_url)\n\n                yield read_stream, write_stream\n                tg.cancel_scope.cancel()\n"
  },
  {
    "path": "src/mcp/client/stdio.py",
    "content": "import logging\nimport os\nimport sys\nfrom contextlib import asynccontextmanager\nfrom pathlib import Path\nfrom typing import Literal, TextIO\n\nimport anyio\nimport anyio.lowlevel\nfrom anyio.abc import Process\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\nfrom anyio.streams.text import TextReceiveStream\nfrom pydantic import BaseModel, Field\n\nfrom mcp import types\nfrom mcp.os.posix.utilities import terminate_posix_process_tree\nfrom mcp.os.win32.utilities import (\n    FallbackProcess,\n    create_windows_process,\n    get_windows_executable_command,\n    terminate_windows_process_tree,\n)\nfrom mcp.shared.message import SessionMessage\n\nlogger = logging.getLogger(__name__)\n\n# Environment variables to inherit by default\nDEFAULT_INHERITED_ENV_VARS = (\n    [\n        \"APPDATA\",\n        \"HOMEDRIVE\",\n        \"HOMEPATH\",\n        \"LOCALAPPDATA\",\n        \"PATH\",\n        \"PATHEXT\",\n        \"PROCESSOR_ARCHITECTURE\",\n        \"SYSTEMDRIVE\",\n        \"SYSTEMROOT\",\n        \"TEMP\",\n        \"USERNAME\",\n        \"USERPROFILE\",\n    ]\n    if sys.platform == \"win32\"\n    else [\"HOME\", \"LOGNAME\", \"PATH\", \"SHELL\", \"TERM\", \"USER\"]\n)\n\n# Timeout for process termination before falling back to force kill\nPROCESS_TERMINATION_TIMEOUT = 2.0\n\n\ndef get_default_environment() -> dict[str, str]:\n    \"\"\"Returns a default environment object including only environment variables deemed\n    safe to inherit.\n    \"\"\"\n    env: dict[str, str] = {}\n\n    for key in DEFAULT_INHERITED_ENV_VARS:\n        value = os.environ.get(key)\n        if value is None:  # pragma: lax no cover\n            continue\n\n        if value.startswith(\"()\"):  # pragma: no cover\n            # Skip functions, which are a security risk\n            continue  # pragma: no cover\n\n        env[key] = value\n\n    return env\n\n\nclass StdioServerParameters(BaseModel):\n    command: str\n    \"\"\"The executable to run to start the server.\"\"\"\n\n    args: list[str] = Field(default_factory=list)\n    \"\"\"Command line arguments to pass to the executable.\"\"\"\n\n    env: dict[str, str] | None = None\n    \"\"\"\n    The environment to use when spawning the process.\n\n    If not specified, the result of get_default_environment() will be used.\n    \"\"\"\n\n    cwd: str | Path | None = None\n    \"\"\"The working directory to use when spawning the process.\"\"\"\n\n    encoding: str = \"utf-8\"\n    \"\"\"\n    The text encoding used when sending/receiving messages to the server.\n\n    Defaults to utf-8.\n    \"\"\"\n\n    encoding_error_handler: Literal[\"strict\", \"ignore\", \"replace\"] = \"strict\"\n    \"\"\"\n    The text encoding error handler.\n\n    See https://docs.python.org/3/library/codecs.html#codec-base-classes for\n    explanations of possible values.\n    \"\"\"\n\n\n@asynccontextmanager\nasync def stdio_client(server: StdioServerParameters, errlog: TextIO = sys.stderr):\n    \"\"\"Client transport for stdio: this will connect to a server by spawning a\n    process and communicating with it over stdin/stdout.\n    \"\"\"\n    read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]\n    read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]\n\n    write_stream: MemoryObjectSendStream[SessionMessage]\n    write_stream_reader: MemoryObjectReceiveStream[SessionMessage]\n\n    read_stream_writer, read_stream = anyio.create_memory_object_stream(0)\n    write_stream, write_stream_reader = anyio.create_memory_object_stream(0)\n\n    try:\n        command = _get_executable_command(server.command)\n\n        # Open process with stderr piped for capture\n        process = await _create_platform_compatible_process(\n            command=command,\n            args=server.args,\n            env=({**get_default_environment(), **server.env} if server.env is not None else get_default_environment()),\n            errlog=errlog,\n            cwd=server.cwd,\n        )\n    except OSError:\n        # Clean up streams if process creation fails\n        await read_stream.aclose()\n        await write_stream.aclose()\n        await read_stream_writer.aclose()\n        await write_stream_reader.aclose()\n        raise\n\n    async def stdout_reader():\n        assert process.stdout, \"Opened process is missing stdout\"\n\n        try:\n            async with read_stream_writer:\n                buffer = \"\"\n                async for chunk in TextReceiveStream(\n                    process.stdout,\n                    encoding=server.encoding,\n                    errors=server.encoding_error_handler,\n                ):\n                    lines = (buffer + chunk).split(\"\\n\")\n                    buffer = lines.pop()\n\n                    for line in lines:\n                        try:\n                            message = types.jsonrpc_message_adapter.validate_json(line, by_name=False)\n                        except Exception as exc:  # pragma: no cover\n                            logger.exception(\"Failed to parse JSONRPC message from server\")\n                            await read_stream_writer.send(exc)\n                            continue\n\n                        session_message = SessionMessage(message)\n                        await read_stream_writer.send(session_message)\n        except anyio.ClosedResourceError:  # pragma: lax no cover\n            await anyio.lowlevel.checkpoint()\n\n    async def stdin_writer():\n        assert process.stdin, \"Opened process is missing stdin\"\n\n        try:\n            async with write_stream_reader:\n                async for session_message in write_stream_reader:\n                    json = session_message.message.model_dump_json(by_alias=True, exclude_unset=True)\n                    await process.stdin.send(\n                        (json + \"\\n\").encode(\n                            encoding=server.encoding,\n                            errors=server.encoding_error_handler,\n                        )\n                    )\n        except anyio.ClosedResourceError:  # pragma: no cover\n            await anyio.lowlevel.checkpoint()\n\n    async with anyio.create_task_group() as tg, process:\n        tg.start_soon(stdout_reader)\n        tg.start_soon(stdin_writer)\n        try:\n            yield read_stream, write_stream\n        finally:\n            # MCP spec: stdio shutdown sequence\n            # 1. Close input stream to server\n            # 2. Wait for server to exit, or send SIGTERM if it doesn't exit in time\n            # 3. Send SIGKILL if still not exited\n            if process.stdin:  # pragma: no branch\n                try:\n                    await process.stdin.aclose()\n                except Exception:  # pragma: no cover\n                    # stdin might already be closed, which is fine\n                    pass\n\n            try:\n                # Give the process time to exit gracefully after stdin closes\n                with anyio.fail_after(PROCESS_TERMINATION_TIMEOUT):\n                    await process.wait()\n            except TimeoutError:\n                # Process didn't exit from stdin closure, use platform-specific termination\n                # which handles SIGTERM -> SIGKILL escalation\n                await _terminate_process_tree(process)\n            except ProcessLookupError:  # pragma: no cover\n                # Process already exited, which is fine\n                pass\n            await read_stream.aclose()\n            await write_stream.aclose()\n            await read_stream_writer.aclose()\n            await write_stream_reader.aclose()\n\n\ndef _get_executable_command(command: str) -> str:\n    \"\"\"Get the correct executable command normalized for the current platform.\n\n    Args:\n        command: Base command (e.g., 'uvx', 'npx')\n\n    Returns:\n        str: Platform-appropriate command\n    \"\"\"\n    if sys.platform == \"win32\":  # pragma: no cover\n        return get_windows_executable_command(command)\n    else:  # pragma: lax no cover\n        return command\n\n\nasync def _create_platform_compatible_process(\n    command: str,\n    args: list[str],\n    env: dict[str, str] | None = None,\n    errlog: TextIO = sys.stderr,\n    cwd: Path | str | None = None,\n):\n    \"\"\"Creates a subprocess in a platform-compatible way.\n\n    Unix: Creates process in a new session/process group for killpg support\n    Windows: Creates process in a Job Object for reliable child termination\n    \"\"\"\n    if sys.platform == \"win32\":  # pragma: no cover\n        process = await create_windows_process(command, args, env, errlog, cwd)\n    else:  # pragma: lax no cover\n        process = await anyio.open_process(\n            [command, *args],\n            env=env,\n            stderr=errlog,\n            cwd=cwd,\n            start_new_session=True,\n        )\n\n    return process\n\n\nasync def _terminate_process_tree(process: Process | FallbackProcess, timeout_seconds: float = 2.0) -> None:\n    \"\"\"Terminate a process and all its children using platform-specific methods.\n\n    Unix: Uses os.killpg() for atomic process group termination\n    Windows: Uses Job Objects via pywin32 for reliable child process cleanup\n\n    Args:\n        process: The process to terminate\n        timeout_seconds: Timeout in seconds before force killing (default: 2.0)\n    \"\"\"\n    if sys.platform == \"win32\":  # pragma: no cover\n        await terminate_windows_process_tree(process, timeout_seconds)\n    else:  # pragma: lax no cover\n        # FallbackProcess should only be used for Windows compatibility\n        assert isinstance(process, Process)\n        await terminate_posix_process_tree(process, timeout_seconds)\n"
  },
  {
    "path": "src/mcp/client/streamable_http.py",
    "content": "\"\"\"Implements StreamableHTTP transport for MCP clients.\"\"\"\n\nfrom __future__ import annotations as _annotations\n\nimport contextlib\nimport logging\nfrom collections.abc import AsyncGenerator, Awaitable, Callable\nfrom contextlib import asynccontextmanager\nfrom dataclasses import dataclass\n\nimport anyio\nimport httpx\nfrom anyio.abc import TaskGroup\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\nfrom httpx_sse import EventSource, ServerSentEvent, aconnect_sse\nfrom pydantic import ValidationError\n\nfrom mcp.client._transport import TransportStreams\nfrom mcp.shared._httpx_utils import create_mcp_http_client\nfrom mcp.shared.message import ClientMessageMetadata, SessionMessage\nfrom mcp.types import (\n    INTERNAL_ERROR,\n    INVALID_REQUEST,\n    PARSE_ERROR,\n    ErrorData,\n    InitializeResult,\n    JSONRPCError,\n    JSONRPCMessage,\n    JSONRPCNotification,\n    JSONRPCRequest,\n    JSONRPCResponse,\n    RequestId,\n    jsonrpc_message_adapter,\n)\n\nlogger = logging.getLogger(__name__)\n\n\n# TODO(Marcelo): Put the TransportStreams in a module under shared, so we can import here.\nSessionMessageOrError = SessionMessage | Exception\nStreamWriter = MemoryObjectSendStream[SessionMessageOrError]\nStreamReader = MemoryObjectReceiveStream[SessionMessage]\n\nMCP_SESSION_ID = \"mcp-session-id\"\nMCP_PROTOCOL_VERSION = \"mcp-protocol-version\"\nLAST_EVENT_ID = \"last-event-id\"\n\n# Reconnection defaults\nDEFAULT_RECONNECTION_DELAY_MS = 1000  # 1 second fallback when server doesn't provide retry\nMAX_RECONNECTION_ATTEMPTS = 2  # Max retry attempts before giving up\n\n\nclass StreamableHTTPError(Exception):\n    \"\"\"Base exception for StreamableHTTP transport errors.\"\"\"\n\n\nclass ResumptionError(StreamableHTTPError):\n    \"\"\"Raised when resumption request is invalid.\"\"\"\n\n\n@dataclass\nclass RequestContext:\n    \"\"\"Context for a request operation.\"\"\"\n\n    client: httpx.AsyncClient\n    session_id: str | None\n    session_message: SessionMessage\n    metadata: ClientMessageMetadata | None\n    read_stream_writer: StreamWriter\n\n\nclass StreamableHTTPTransport:\n    \"\"\"StreamableHTTP client transport implementation.\"\"\"\n\n    def __init__(self, url: str) -> None:\n        \"\"\"Initialize the StreamableHTTP transport.\n\n        Args:\n            url: The endpoint URL.\n        \"\"\"\n        self.url = url\n        self.session_id: str | None = None\n        self.protocol_version: str | None = None\n\n    def _prepare_headers(self) -> dict[str, str]:\n        \"\"\"Build MCP-specific request headers.\n\n        These headers will be merged with the httpx.AsyncClient's default headers,\n        with these MCP-specific headers taking precedence.\n        \"\"\"\n        headers: dict[str, str] = {\n            \"accept\": \"application/json, text/event-stream\",\n            \"content-type\": \"application/json\",\n        }\n        # Add session headers if available\n        if self.session_id:\n            headers[MCP_SESSION_ID] = self.session_id\n        if self.protocol_version:\n            headers[MCP_PROTOCOL_VERSION] = self.protocol_version\n        return headers\n\n    def _is_initialization_request(self, message: JSONRPCMessage) -> bool:\n        \"\"\"Check if the message is an initialization request.\"\"\"\n        return isinstance(message, JSONRPCRequest) and message.method == \"initialize\"\n\n    def _is_initialized_notification(self, message: JSONRPCMessage) -> bool:\n        \"\"\"Check if the message is an initialized notification.\"\"\"\n        return isinstance(message, JSONRPCNotification) and message.method == \"notifications/initialized\"\n\n    def _maybe_extract_session_id_from_response(self, response: httpx.Response) -> None:\n        \"\"\"Extract and store session ID from response headers.\"\"\"\n        new_session_id = response.headers.get(MCP_SESSION_ID)\n        if new_session_id:\n            self.session_id = new_session_id\n            logger.info(f\"Received session ID: {self.session_id}\")\n\n    def _maybe_extract_protocol_version_from_message(self, message: JSONRPCMessage) -> None:\n        \"\"\"Extract protocol version from initialization response message.\"\"\"\n        if isinstance(message, JSONRPCResponse) and message.result:  # pragma: no branch\n            try:\n                # Parse the result as InitializeResult for type safety\n                init_result = InitializeResult.model_validate(message.result, by_name=False)\n                self.protocol_version = str(init_result.protocol_version)\n                logger.info(f\"Negotiated protocol version: {self.protocol_version}\")\n            except Exception:  # pragma: no cover\n                logger.warning(\"Failed to parse initialization response as InitializeResult\", exc_info=True)\n                logger.warning(f\"Raw result: {message.result}\")\n\n    async def _handle_sse_event(\n        self,\n        sse: ServerSentEvent,\n        read_stream_writer: StreamWriter,\n        original_request_id: RequestId | None = None,\n        resumption_callback: Callable[[str], Awaitable[None]] | None = None,\n        is_initialization: bool = False,\n    ) -> bool:\n        \"\"\"Handle an SSE event, returning True if the response is complete.\"\"\"\n        if sse.event == \"message\":\n            # Handle priming events (empty data with ID) for resumability\n            if not sse.data:\n                # Call resumption callback for priming events that have an ID\n                if sse.id and resumption_callback:\n                    await resumption_callback(sse.id)\n                return False\n            try:\n                message = jsonrpc_message_adapter.validate_json(sse.data, by_name=False)\n                logger.debug(f\"SSE message: {message}\")\n\n                # Extract protocol version from initialization response\n                if is_initialization:\n                    self._maybe_extract_protocol_version_from_message(message)\n\n                # If this is a response and we have original_request_id, replace it\n                if original_request_id is not None and isinstance(message, JSONRPCResponse | JSONRPCError):\n                    message.id = original_request_id\n\n                session_message = SessionMessage(message)\n                await read_stream_writer.send(session_message)\n\n                # Call resumption token callback if we have an ID\n                if sse.id and resumption_callback:\n                    await resumption_callback(sse.id)\n\n                # If this is a response or error return True indicating completion\n                # Otherwise, return False to continue listening\n                return isinstance(message, JSONRPCResponse | JSONRPCError)\n\n            except Exception as exc:  # pragma: no cover\n                logger.exception(\"Error parsing SSE message\")\n                if original_request_id is not None:\n                    error_data = ErrorData(code=PARSE_ERROR, message=f\"Failed to parse SSE message: {exc}\")\n                    error_msg = SessionMessage(JSONRPCError(jsonrpc=\"2.0\", id=original_request_id, error=error_data))\n                    await read_stream_writer.send(error_msg)\n                    return True\n                await read_stream_writer.send(exc)\n                return False\n        else:  # pragma: no cover\n            logger.warning(f\"Unknown SSE event: {sse.event}\")\n            return False\n\n    async def handle_get_stream(self, client: httpx.AsyncClient, read_stream_writer: StreamWriter) -> None:\n        \"\"\"Handle GET stream for server-initiated messages with auto-reconnect.\"\"\"\n        last_event_id: str | None = None\n        retry_interval_ms: int | None = None\n        attempt: int = 0\n\n        while attempt < MAX_RECONNECTION_ATTEMPTS:  # pragma: no branch\n            try:\n                if not self.session_id:\n                    return\n\n                headers = self._prepare_headers()\n                if last_event_id:\n                    headers[LAST_EVENT_ID] = last_event_id\n\n                async with aconnect_sse(client, \"GET\", self.url, headers=headers) as event_source:\n                    event_source.response.raise_for_status()\n                    logger.debug(\"GET SSE connection established\")\n\n                    async for sse in event_source.aiter_sse():\n                        # Track last event ID for reconnection\n                        if sse.id:\n                            last_event_id = sse.id\n                        # Track retry interval from server\n                        if sse.retry is not None:\n                            retry_interval_ms = sse.retry\n\n                        await self._handle_sse_event(sse, read_stream_writer)\n\n                    # Stream ended normally (server closed) - reset attempt counter\n                    attempt = 0\n\n            except Exception:  # pragma: lax no cover\n                logger.debug(\"GET stream error\", exc_info=True)\n                attempt += 1\n\n            if attempt >= MAX_RECONNECTION_ATTEMPTS:  # pragma: no cover\n                logger.debug(f\"GET stream max reconnection attempts ({MAX_RECONNECTION_ATTEMPTS}) exceeded\")\n                return\n\n            # Wait before reconnecting\n            delay_ms = retry_interval_ms if retry_interval_ms is not None else DEFAULT_RECONNECTION_DELAY_MS\n            logger.info(f\"GET stream disconnected, reconnecting in {delay_ms}ms...\")\n            await anyio.sleep(delay_ms / 1000.0)\n\n    async def _handle_resumption_request(self, ctx: RequestContext) -> None:\n        \"\"\"Handle a resumption request using GET with SSE.\"\"\"\n        headers = self._prepare_headers()\n        if ctx.metadata and ctx.metadata.resumption_token:\n            headers[LAST_EVENT_ID] = ctx.metadata.resumption_token\n        else:\n            raise ResumptionError(\"Resumption request requires a resumption token\")  # pragma: no cover\n\n        # Extract original request ID to map responses\n        original_request_id = None\n        if isinstance(ctx.session_message.message, JSONRPCRequest):  # pragma: no branch\n            original_request_id = ctx.session_message.message.id\n\n        async with aconnect_sse(ctx.client, \"GET\", self.url, headers=headers) as event_source:\n            event_source.response.raise_for_status()\n            logger.debug(\"Resumption GET SSE connection established\")\n\n            async for sse in event_source.aiter_sse():  # pragma: no branch\n                is_complete = await self._handle_sse_event(\n                    sse,\n                    ctx.read_stream_writer,\n                    original_request_id,\n                    ctx.metadata.on_resumption_token_update if ctx.metadata else None,\n                )\n                if is_complete:\n                    await event_source.response.aclose()\n                    break\n\n    async def _handle_post_request(self, ctx: RequestContext) -> None:\n        \"\"\"Handle a POST request with response processing.\"\"\"\n        headers = self._prepare_headers()\n        message = ctx.session_message.message\n        is_initialization = self._is_initialization_request(message)\n\n        async with ctx.client.stream(\n            \"POST\",\n            self.url,\n            json=message.model_dump(by_alias=True, mode=\"json\", exclude_unset=True),\n            headers=headers,\n        ) as response:\n            if response.status_code == 202:\n                logger.debug(\"Received 202 Accepted\")\n                return\n\n            if response.status_code == 404:  # pragma: no branch\n                if isinstance(message, JSONRPCRequest):  # pragma: no branch\n                    error_data = ErrorData(code=INVALID_REQUEST, message=\"Session terminated\")\n                    session_message = SessionMessage(JSONRPCError(jsonrpc=\"2.0\", id=message.id, error=error_data))\n                    await ctx.read_stream_writer.send(session_message)\n                return\n\n            if response.status_code >= 400:\n                if isinstance(message, JSONRPCRequest):\n                    error_data = ErrorData(code=INTERNAL_ERROR, message=\"Server returned an error response\")\n                    session_message = SessionMessage(JSONRPCError(jsonrpc=\"2.0\", id=message.id, error=error_data))\n                    await ctx.read_stream_writer.send(session_message)\n                return\n\n            if is_initialization:\n                self._maybe_extract_session_id_from_response(response)\n\n            # Per https://modelcontextprotocol.io/specification/2025-06-18/basic#notifications:\n            # The server MUST NOT send a response to notifications.\n            if isinstance(message, JSONRPCRequest):\n                content_type = response.headers.get(\"content-type\", \"\").lower()\n                if content_type.startswith(\"application/json\"):\n                    await self._handle_json_response(\n                        response, ctx.read_stream_writer, is_initialization, request_id=message.id\n                    )\n                elif content_type.startswith(\"text/event-stream\"):\n                    await self._handle_sse_response(response, ctx, is_initialization)\n                else:\n                    logger.error(f\"Unexpected content type: {content_type}\")\n                    error_data = ErrorData(code=INVALID_REQUEST, message=f\"Unexpected content type: {content_type}\")\n                    error_msg = SessionMessage(JSONRPCError(jsonrpc=\"2.0\", id=message.id, error=error_data))\n                    await ctx.read_stream_writer.send(error_msg)\n\n    async def _handle_json_response(\n        self,\n        response: httpx.Response,\n        read_stream_writer: StreamWriter,\n        is_initialization: bool = False,\n        *,\n        request_id: RequestId,\n    ) -> None:\n        \"\"\"Handle JSON response from the server.\"\"\"\n        try:\n            content = await response.aread()\n            message = jsonrpc_message_adapter.validate_json(content, by_name=False)\n\n            # Extract protocol version from initialization response\n            if is_initialization:\n                self._maybe_extract_protocol_version_from_message(message)\n\n            session_message = SessionMessage(message)\n            await read_stream_writer.send(session_message)\n        except (httpx.StreamError, ValidationError) as exc:\n            logger.exception(\"Error parsing JSON response\")\n            error_data = ErrorData(code=PARSE_ERROR, message=f\"Failed to parse JSON response: {exc}\")\n            error_msg = SessionMessage(JSONRPCError(jsonrpc=\"2.0\", id=request_id, error=error_data))\n            await read_stream_writer.send(error_msg)\n\n    async def _handle_sse_response(\n        self,\n        response: httpx.Response,\n        ctx: RequestContext,\n        is_initialization: bool = False,\n    ) -> None:\n        \"\"\"Handle SSE response from the server.\"\"\"\n        last_event_id: str | None = None\n        retry_interval_ms: int | None = None\n\n        # The caller (_handle_post_request) only reaches here inside\n        # isinstance(message, JSONRPCRequest), so this is always a JSONRPCRequest.\n        assert isinstance(ctx.session_message.message, JSONRPCRequest)\n        original_request_id = ctx.session_message.message.id\n\n        try:\n            event_source = EventSource(response)\n            async for sse in event_source.aiter_sse():  # pragma: no branch\n                # Track last event ID for potential reconnection\n                if sse.id:\n                    last_event_id = sse.id\n\n                # Track retry interval from server\n                if sse.retry is not None:\n                    retry_interval_ms = sse.retry\n\n                is_complete = await self._handle_sse_event(\n                    sse,\n                    ctx.read_stream_writer,\n                    original_request_id=original_request_id,\n                    resumption_callback=(ctx.metadata.on_resumption_token_update if ctx.metadata else None),\n                    is_initialization=is_initialization,\n                )\n                # If the SSE event indicates completion, like returning response/error\n                # break the loop\n                if is_complete:\n                    await response.aclose()\n                    return  # Normal completion, no reconnect needed\n        except Exception:\n            logger.debug(\"SSE stream ended\", exc_info=True)  # pragma: no cover\n\n        # Stream ended without response - reconnect if we received an event with ID\n        if last_event_id is not None:  # pragma: no branch\n            logger.info(\"SSE stream disconnected, reconnecting...\")\n            await self._handle_reconnection(ctx, last_event_id, retry_interval_ms)\n\n    async def _handle_reconnection(\n        self,\n        ctx: RequestContext,\n        last_event_id: str,\n        retry_interval_ms: int | None = None,\n        attempt: int = 0,\n    ) -> None:\n        \"\"\"Reconnect with Last-Event-ID to resume stream after server disconnect.\"\"\"\n        # Bail if max retries exceeded\n        if attempt >= MAX_RECONNECTION_ATTEMPTS:  # pragma: no cover\n            logger.debug(f\"Max reconnection attempts ({MAX_RECONNECTION_ATTEMPTS}) exceeded\")\n            return\n\n        # Always wait - use server value or default\n        delay_ms = retry_interval_ms if retry_interval_ms is not None else DEFAULT_RECONNECTION_DELAY_MS\n        await anyio.sleep(delay_ms / 1000.0)\n\n        headers = self._prepare_headers()\n        headers[LAST_EVENT_ID] = last_event_id\n\n        # Extract original request ID to map responses\n        original_request_id = None\n        if isinstance(ctx.session_message.message, JSONRPCRequest):  # pragma: no branch\n            original_request_id = ctx.session_message.message.id\n\n        try:\n            async with aconnect_sse(ctx.client, \"GET\", self.url, headers=headers) as event_source:\n                event_source.response.raise_for_status()\n                logger.info(\"Reconnected to SSE stream\")\n\n                # Track for potential further reconnection\n                reconnect_last_event_id: str = last_event_id\n                reconnect_retry_ms = retry_interval_ms\n\n                async for sse in event_source.aiter_sse():\n                    if sse.id:  # pragma: no branch\n                        reconnect_last_event_id = sse.id\n                    if sse.retry is not None:\n                        reconnect_retry_ms = sse.retry\n\n                    is_complete = await self._handle_sse_event(\n                        sse,\n                        ctx.read_stream_writer,\n                        original_request_id,\n                        ctx.metadata.on_resumption_token_update if ctx.metadata else None,\n                    )\n                    if is_complete:\n                        await event_source.response.aclose()\n                        return\n\n                # Stream ended again without response - reconnect again (reset attempt counter)\n                logger.info(\"SSE stream disconnected, reconnecting...\")\n                await self._handle_reconnection(ctx, reconnect_last_event_id, reconnect_retry_ms, 0)\n        except Exception as e:  # pragma: no cover\n            logger.debug(f\"Reconnection failed: {e}\")\n            # Try to reconnect again if we still have an event ID\n            await self._handle_reconnection(ctx, last_event_id, retry_interval_ms, attempt + 1)\n\n    async def post_writer(\n        self,\n        client: httpx.AsyncClient,\n        write_stream_reader: StreamReader,\n        read_stream_writer: StreamWriter,\n        write_stream: MemoryObjectSendStream[SessionMessage],\n        start_get_stream: Callable[[], None],\n        tg: TaskGroup,\n    ) -> None:\n        \"\"\"Handle writing requests to the server.\"\"\"\n        try:\n            async with write_stream_reader, read_stream_writer, write_stream:\n                async for session_message in write_stream_reader:\n                    message = session_message.message\n                    metadata = (\n                        session_message.metadata\n                        if isinstance(session_message.metadata, ClientMessageMetadata)\n                        else None\n                    )\n\n                    # Check if this is a resumption request\n                    is_resumption = bool(metadata and metadata.resumption_token)\n\n                    logger.debug(f\"Sending client message: {message}\")\n\n                    # Handle initialized notification\n                    if self._is_initialized_notification(message):\n                        start_get_stream()\n\n                    ctx = RequestContext(\n                        client=client,\n                        session_id=self.session_id,\n                        session_message=session_message,\n                        metadata=metadata,\n                        read_stream_writer=read_stream_writer,\n                    )\n\n                    async def handle_request_async():\n                        if is_resumption:\n                            await self._handle_resumption_request(ctx)\n                        else:\n                            await self._handle_post_request(ctx)\n\n                    # If this is a request, start a new task to handle it\n                    if isinstance(message, JSONRPCRequest):\n                        tg.start_soon(handle_request_async)\n                    else:\n                        await handle_request_async()\n\n        except Exception:  # pragma: lax no cover\n            logger.exception(\"Error in post_writer\")\n\n    async def terminate_session(self, client: httpx.AsyncClient) -> None:\n        \"\"\"Terminate the session by sending a DELETE request.\"\"\"\n        if not self.session_id:  # pragma: lax no cover\n            return\n\n        try:\n            headers = self._prepare_headers()\n            response = await client.delete(self.url, headers=headers)\n\n            if response.status_code == 405:  # pragma: lax no cover\n                logger.debug(\"Server does not allow session termination\")\n            elif response.status_code not in (200, 204):  # pragma: lax no cover\n                logger.warning(f\"Session termination failed: {response.status_code}\")\n        except Exception as exc:  # pragma: no cover\n            logger.warning(f\"Session termination failed: {exc}\")\n\n    # TODO(Marcelo): Check the TODO below, and cover this with tests if necessary.\n    def get_session_id(self) -> str | None:\n        \"\"\"Get the current session ID.\"\"\"\n        return self.session_id  # pragma: no cover\n\n\n# TODO(Marcelo): I've dropped the `get_session_id` callback because it breaks the Transport protocol. Is that needed?\n# It's a completely wrong abstraction, so removal is a good idea. But if we need the client to find the session ID,\n# we should think about a better way to do it. I believe we can achieve it with other means.\n@asynccontextmanager\nasync def streamable_http_client(\n    url: str,\n    *,\n    http_client: httpx.AsyncClient | None = None,\n    terminate_on_close: bool = True,\n) -> AsyncGenerator[TransportStreams, None]:\n    \"\"\"Client transport for StreamableHTTP.\n\n    Args:\n        url: The MCP server endpoint URL.\n        http_client: Optional pre-configured httpx.AsyncClient. If None, a default\n            client with recommended MCP timeouts will be created. To configure headers,\n            authentication, or other HTTP settings, create an httpx.AsyncClient and pass it here.\n        terminate_on_close: If True, send a DELETE request to terminate the session when the context exits.\n\n    Yields:\n        Tuple containing:\n            - read_stream: Stream for reading messages from the server\n            - write_stream: Stream for sending messages to the server\n\n    Example:\n        See examples/snippets/clients/ for usage patterns.\n    \"\"\"\n    # Determine if we need to create and manage the client\n    client_provided = http_client is not None\n    client = http_client\n\n    if client is None:\n        # Create default client with recommended MCP timeouts\n        client = create_mcp_http_client()\n\n    transport = StreamableHTTPTransport(url)\n\n    logger.debug(f\"Connecting to StreamableHTTP endpoint: {url}\")\n\n    async with contextlib.AsyncExitStack() as stack:\n        # Only manage client lifecycle if we created it\n        if not client_provided:\n            await stack.enter_async_context(client)\n\n        read_stream_writer, read_stream = anyio.create_memory_object_stream[SessionMessage | Exception](0)\n        write_stream, write_stream_reader = anyio.create_memory_object_stream[SessionMessage](0)\n\n        async with (\n            read_stream_writer,\n            read_stream,\n            write_stream,\n            write_stream_reader,\n            anyio.create_task_group() as tg,\n        ):\n\n            def start_get_stream() -> None:\n                tg.start_soon(transport.handle_get_stream, client, read_stream_writer)\n\n            tg.start_soon(\n                transport.post_writer,\n                client,\n                write_stream_reader,\n                read_stream_writer,\n                write_stream,\n                start_get_stream,\n                tg,\n            )\n\n            try:\n                yield read_stream, write_stream\n            finally:\n                if transport.session_id and terminate_on_close:\n                    await transport.terminate_session(client)\n                tg.cancel_scope.cancel()\n"
  },
  {
    "path": "src/mcp/client/websocket.py",
    "content": "import json\nfrom collections.abc import AsyncGenerator\nfrom contextlib import asynccontextmanager\n\nimport anyio\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\nfrom pydantic import ValidationError\nfrom websockets.asyncio.client import connect as ws_connect\nfrom websockets.typing import Subprotocol\n\nfrom mcp import types\nfrom mcp.shared.message import SessionMessage\n\n\n@asynccontextmanager\nasync def websocket_client(\n    url: str,\n) -> AsyncGenerator[\n    tuple[MemoryObjectReceiveStream[SessionMessage | Exception], MemoryObjectSendStream[SessionMessage]],\n    None,\n]:\n    \"\"\"WebSocket client transport for MCP, symmetrical to the server version.\n\n    Connects to 'url' using the 'mcp' subprotocol, then yields:\n        (read_stream, write_stream)\n\n    - read_stream: As you read from this stream, you'll receive either valid\n      SessionMessage objects or Exception objects (when validation fails).\n    - write_stream: Write SessionMessage objects to this stream to send them\n      over the WebSocket to the server.\n    \"\"\"\n\n    # Create two in-memory streams:\n    # - One for incoming messages (read_stream, written by ws_reader)\n    # - One for outgoing messages (write_stream, read by ws_writer)\n    read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]\n    read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]\n    write_stream: MemoryObjectSendStream[SessionMessage]\n    write_stream_reader: MemoryObjectReceiveStream[SessionMessage]\n\n    # Connect using websockets, requesting the \"mcp\" subprotocol\n    async with ws_connect(url, subprotocols=[Subprotocol(\"mcp\")]) as ws:\n        read_stream_writer, read_stream = anyio.create_memory_object_stream(0)\n        write_stream, write_stream_reader = anyio.create_memory_object_stream(0)\n\n        async def ws_reader():\n            \"\"\"Reads text messages from the WebSocket, parses them as JSON-RPC messages,\n            and sends them into read_stream_writer.\n            \"\"\"\n            async with read_stream_writer:\n                async for raw_text in ws:\n                    try:\n                        message = types.jsonrpc_message_adapter.validate_json(raw_text, by_name=False)\n                        session_message = SessionMessage(message)\n                        await read_stream_writer.send(session_message)\n                    except ValidationError as exc:  # pragma: no cover\n                        # If JSON parse or model validation fails, send the exception\n                        await read_stream_writer.send(exc)\n\n        async def ws_writer():\n            \"\"\"Reads JSON-RPC messages from write_stream_reader and\n            sends them to the server.\n            \"\"\"\n            async with write_stream_reader:\n                async for session_message in write_stream_reader:\n                    # Convert to a dict, then to JSON\n                    msg_dict = session_message.message.model_dump(by_alias=True, mode=\"json\", exclude_unset=True)\n                    await ws.send(json.dumps(msg_dict))\n\n        async with (\n            read_stream_writer,\n            read_stream,\n            write_stream,\n            write_stream_reader,\n            anyio.create_task_group() as tg,\n        ):\n            # Start reader and writer tasks\n            tg.start_soon(ws_reader)\n            tg.start_soon(ws_writer)\n\n            # Yield the receive/send streams\n            yield (read_stream, write_stream)\n\n            # Once the caller's 'async with' block exits, we shut down\n            tg.cancel_scope.cancel()\n"
  },
  {
    "path": "src/mcp/os/__init__.py",
    "content": "\"\"\"Platform-specific utilities for MCP.\"\"\"\n"
  },
  {
    "path": "src/mcp/os/posix/__init__.py",
    "content": "\"\"\"POSIX-specific utilities for MCP.\"\"\"\n"
  },
  {
    "path": "src/mcp/os/posix/utilities.py",
    "content": "\"\"\"POSIX-specific functionality for stdio client operations.\"\"\"\n\nimport logging\nimport os\nimport signal\n\nimport anyio\nfrom anyio.abc import Process\n\nlogger = logging.getLogger(__name__)\n\n\nasync def terminate_posix_process_tree(process: Process, timeout_seconds: float = 2.0) -> None:\n    \"\"\"Terminate a process and all its children on POSIX systems.\n\n    Uses os.killpg() for atomic process group termination.\n\n    Args:\n        process: The process to terminate\n        timeout_seconds: Timeout in seconds before force killing (default: 2.0)\n    \"\"\"\n    pid = getattr(process, \"pid\", None) or getattr(getattr(process, \"popen\", None), \"pid\", None)\n    if not pid:\n        # No PID means there's no process to terminate - it either never started,\n        # already exited, or we have an invalid process object\n        return\n\n    try:\n        pgid = os.getpgid(pid)\n        os.killpg(pgid, signal.SIGTERM)\n\n        with anyio.move_on_after(timeout_seconds):\n            while True:\n                try:\n                    # Check if process group still exists (signal 0 = check only)\n                    os.killpg(pgid, 0)\n                    await anyio.sleep(0.1)\n                except ProcessLookupError:\n                    return\n\n        try:\n            os.killpg(pgid, signal.SIGKILL)\n        except ProcessLookupError:\n            pass\n\n    except (ProcessLookupError, PermissionError, OSError) as e:\n        logger.warning(f\"Process group termination failed for PID {pid}: {e}, falling back to simple terminate\")\n        try:\n            process.terminate()\n            with anyio.fail_after(timeout_seconds):\n                await process.wait()\n        except Exception:\n            logger.warning(f\"Process termination failed for PID {pid}, attempting force kill\")\n            try:\n                process.kill()\n            except Exception:\n                logger.exception(f\"Failed to kill process {pid}\")\n"
  },
  {
    "path": "src/mcp/os/win32/__init__.py",
    "content": "\"\"\"Windows-specific utilities for MCP.\"\"\"\n"
  },
  {
    "path": "src/mcp/os/win32/utilities.py",
    "content": "\"\"\"Windows-specific functionality for stdio client operations.\"\"\"\n\nimport logging\nimport shutil\nimport subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import BinaryIO, TextIO, cast\n\nimport anyio\nfrom anyio import to_thread\nfrom anyio.abc import Process\nfrom anyio.streams.file import FileReadStream, FileWriteStream\nfrom typing_extensions import deprecated\n\nlogger = logging.getLogger(\"client.stdio.win32\")\n\n# Windows-specific imports for Job Objects\nif sys.platform == \"win32\":\n    import pywintypes\n    import win32api\n    import win32con\n    import win32job\nelse:\n    # Type stubs for non-Windows platforms\n    win32api = None\n    win32con = None\n    win32job = None\n    pywintypes = None\n\nJobHandle = int\n\n\ndef get_windows_executable_command(command: str) -> str:\n    \"\"\"Get the correct executable command normalized for Windows.\n\n    On Windows, commands might exist with specific extensions (.exe, .cmd, etc.)\n    that need to be located for proper execution.\n\n    Args:\n        command: Base command (e.g., 'uvx', 'npx')\n\n    Returns:\n        str: Windows-appropriate command path\n    \"\"\"\n    try:\n        # First check if command exists in PATH as-is\n        if command_path := shutil.which(command):\n            return command_path\n\n        # Check for Windows-specific extensions\n        for ext in [\".cmd\", \".bat\", \".exe\", \".ps1\"]:\n            ext_version = f\"{command}{ext}\"\n            if ext_path := shutil.which(ext_version):\n                return ext_path\n\n        # For regular commands or if we couldn't find special versions\n        return command\n    except OSError:\n        # Handle file system errors during path resolution\n        # (permissions, broken symlinks, etc.)\n        return command\n\n\nclass FallbackProcess:\n    \"\"\"A fallback process wrapper for Windows to handle async I/O\n    when using subprocess.Popen, which provides sync-only FileIO objects.\n\n    This wraps stdin and stdout into async-compatible\n    streams (FileReadStream, FileWriteStream),\n    so that MCP clients expecting async streams can work properly.\n    \"\"\"\n\n    def __init__(self, popen_obj: subprocess.Popen[bytes]):\n        self.popen: subprocess.Popen[bytes] = popen_obj\n        self.stdin_raw = popen_obj.stdin  # type: ignore[assignment]\n        self.stdout_raw = popen_obj.stdout  # type: ignore[assignment]\n        self.stderr = popen_obj.stderr  # type: ignore[assignment]\n\n        self.stdin = FileWriteStream(cast(BinaryIO, self.stdin_raw)) if self.stdin_raw else None\n        self.stdout = FileReadStream(cast(BinaryIO, self.stdout_raw)) if self.stdout_raw else None\n\n    async def __aenter__(self):\n        \"\"\"Support async context manager entry.\"\"\"\n        return self\n\n    async def __aexit__(\n        self,\n        exc_type: BaseException | None,\n        exc_val: BaseException | None,\n        exc_tb: object | None,\n    ) -> None:\n        \"\"\"Terminate and wait on process exit inside a thread.\"\"\"\n        self.popen.terminate()\n        await to_thread.run_sync(self.popen.wait)\n\n        # Close the file handles to prevent ResourceWarning\n        if self.stdin:\n            await self.stdin.aclose()\n        if self.stdout:\n            await self.stdout.aclose()\n        if self.stdin_raw:\n            self.stdin_raw.close()\n        if self.stdout_raw:\n            self.stdout_raw.close()\n        if self.stderr:\n            self.stderr.close()\n\n    async def wait(self):\n        \"\"\"Async wait for process completion.\"\"\"\n        return await to_thread.run_sync(self.popen.wait)\n\n    def terminate(self):\n        \"\"\"Terminate the subprocess immediately.\"\"\"\n        return self.popen.terminate()\n\n    def kill(self) -> None:\n        \"\"\"Kill the subprocess immediately (alias for terminate).\"\"\"\n        self.terminate()\n\n    @property\n    def pid(self) -> int:\n        \"\"\"Return the process ID.\"\"\"\n        return self.popen.pid\n\n    @property\n    def returncode(self) -> int | None:\n        \"\"\"Return the exit code, or ``None`` if the process has not yet terminated.\"\"\"\n        return self.popen.returncode\n\n\n# ------------------------\n# Updated function\n# ------------------------\n\n\nasync def create_windows_process(\n    command: str,\n    args: list[str],\n    env: dict[str, str] | None = None,\n    errlog: TextIO | None = sys.stderr,\n    cwd: Path | str | None = None,\n) -> Process | FallbackProcess:\n    \"\"\"Creates a subprocess in a Windows-compatible way with Job Object support.\n\n    Attempts to use anyio's open_process for async subprocess creation.\n    In some cases this will throw NotImplementedError on Windows, e.g.,\n    when using the SelectorEventLoop, which does not support async subprocesses.\n    In that case, we fall back to using subprocess.Popen.\n\n    The process is automatically added to a Job Object to ensure all child\n    processes are terminated when the parent is terminated.\n\n    Args:\n        command (str): The executable to run\n        args (list[str]): List of command line arguments\n        env (dict[str, str] | None): Environment variables\n        errlog (TextIO | None): Where to send stderr output (defaults to sys.stderr)\n        cwd (Path | str | None): Working directory for the subprocess\n\n    Returns:\n        Process | FallbackProcess: Async-compatible subprocess with stdin and stdout streams\n    \"\"\"\n    job = _create_job_object()\n    process = None\n\n    try:\n        # First try using anyio with Windows-specific flags to hide console window\n        process = await anyio.open_process(\n            [command, *args],\n            env=env,\n            # Ensure we don't create console windows for each process\n            creationflags=subprocess.CREATE_NO_WINDOW  # type: ignore\n            if hasattr(subprocess, \"CREATE_NO_WINDOW\")\n            else 0,\n            stderr=errlog,\n            cwd=cwd,\n        )\n    except NotImplementedError:\n        # If Windows doesn't support async subprocess creation, use fallback\n        process = await _create_windows_fallback_process(command, args, env, errlog, cwd)\n    except Exception:\n        # Try again without creation flags\n        process = await anyio.open_process(\n            [command, *args],\n            env=env,\n            stderr=errlog,\n            cwd=cwd,\n        )\n\n    _maybe_assign_process_to_job(process, job)\n    return process\n\n\nasync def _create_windows_fallback_process(\n    command: str,\n    args: list[str],\n    env: dict[str, str] | None = None,\n    errlog: TextIO | None = sys.stderr,\n    cwd: Path | str | None = None,\n) -> FallbackProcess:\n    \"\"\"Create a subprocess using subprocess.Popen as a fallback when anyio fails.\n\n    This function wraps the sync subprocess.Popen in an async-compatible interface.\n    \"\"\"\n    try:\n        # Try launching with creationflags to avoid opening a new console window\n        popen_obj = subprocess.Popen(\n            [command, *args],\n            stdin=subprocess.PIPE,\n            stdout=subprocess.PIPE,\n            stderr=errlog,\n            env=env,\n            cwd=cwd,\n            bufsize=0,  # Unbuffered output\n            creationflags=getattr(subprocess, \"CREATE_NO_WINDOW\", 0),\n        )\n    except Exception:\n        # If creationflags failed, fallback without them\n        popen_obj = subprocess.Popen(\n            [command, *args],\n            stdin=subprocess.PIPE,\n            stdout=subprocess.PIPE,\n            stderr=errlog,\n            env=env,\n            cwd=cwd,\n            bufsize=0,\n        )\n    return FallbackProcess(popen_obj)\n\n\ndef _create_job_object() -> int | None:\n    \"\"\"Create a Windows Job Object configured to terminate all processes when closed.\"\"\"\n    if sys.platform != \"win32\" or not win32job:\n        return None\n\n    try:\n        job = win32job.CreateJobObject(None, \"\")\n        extended_info = win32job.QueryInformationJobObject(job, win32job.JobObjectExtendedLimitInformation)\n\n        extended_info[\"BasicLimitInformation\"][\"LimitFlags\"] |= win32job.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE\n        win32job.SetInformationJobObject(job, win32job.JobObjectExtendedLimitInformation, extended_info)\n        return job\n    except Exception as e:\n        logger.warning(f\"Failed to create Job Object for process tree management: {e}\")\n        return None\n\n\ndef _maybe_assign_process_to_job(process: Process | FallbackProcess, job: JobHandle | None) -> None:\n    \"\"\"Try to assign a process to a job object.\n\n    If assignment fails for any reason, the job handle is closed.\n    \"\"\"\n    if not job:\n        return\n\n    if sys.platform != \"win32\" or not win32api or not win32con or not win32job:\n        return\n\n    try:\n        process_handle = win32api.OpenProcess(\n            win32con.PROCESS_SET_QUOTA | win32con.PROCESS_TERMINATE, False, process.pid\n        )\n        if not process_handle:\n            raise Exception(\"Failed to open process handle\")\n\n        try:\n            win32job.AssignProcessToJobObject(job, process_handle)\n            process._job_object = job\n        finally:\n            win32api.CloseHandle(process_handle)\n    except Exception as e:\n        logger.warning(f\"Failed to assign process {process.pid} to Job Object: {e}\")\n        if win32api:\n            win32api.CloseHandle(job)\n\n\nasync def terminate_windows_process_tree(process: Process | FallbackProcess, timeout_seconds: float = 2.0) -> None:\n    \"\"\"Terminate a process and all its children on Windows.\n\n    If the process has an associated job object, it will be terminated.\n    Otherwise, falls back to basic process termination.\n\n    Args:\n        process: The process to terminate\n        timeout_seconds: Timeout in seconds before force killing (default: 2.0)\n    \"\"\"\n    if sys.platform != \"win32\":\n        return\n\n    job = getattr(process, \"_job_object\", None)\n    if job and win32job:\n        try:\n            win32job.TerminateJobObject(job, 1)\n        except Exception:\n            # Job might already be terminated\n            pass\n        finally:\n            if win32api:\n                try:\n                    win32api.CloseHandle(job)\n                except Exception:\n                    pass\n\n    # Always try to terminate the process itself as well\n    try:\n        process.terminate()\n    except Exception:\n        pass\n\n\n@deprecated(\n    \"terminate_windows_process is deprecated and will be removed in a future version. \"\n    \"Process termination is now handled internally by the stdio_client context manager.\"\n)\nasync def terminate_windows_process(process: Process | FallbackProcess):\n    \"\"\"Terminate a Windows process.\n\n    Note: On Windows, terminating a process with process.terminate() doesn't\n    always guarantee immediate process termination.\n    If the process does not exit within 2 seconds, process.kill() is called\n    to send a SIGKILL-equivalent signal.\n\n    Args:\n        process: The process to terminate\n    \"\"\"\n    try:\n        process.terminate()\n        with anyio.fail_after(2.0):\n            await process.wait()\n    except TimeoutError:\n        # Force kill if it doesn't terminate\n        process.kill()\n"
  },
  {
    "path": "src/mcp/py.typed",
    "content": ""
  },
  {
    "path": "src/mcp/server/__init__.py",
    "content": "from .context import ServerRequestContext\nfrom .lowlevel import NotificationOptions, Server\nfrom .mcpserver import MCPServer\nfrom .models import InitializationOptions\n\n__all__ = [\"Server\", \"ServerRequestContext\", \"MCPServer\", \"NotificationOptions\", \"InitializationOptions\"]\n"
  },
  {
    "path": "src/mcp/server/__main__.py",
    "content": "import importlib.metadata\nimport logging\nimport sys\nimport warnings\n\nimport anyio\n\nfrom mcp.server.models import InitializationOptions\nfrom mcp.server.session import ServerSession\nfrom mcp.server.stdio import stdio_server\nfrom mcp.types import ServerCapabilities\n\nif not sys.warnoptions:\n    warnings.simplefilter(\"ignore\")\n\nlogging.basicConfig(level=logging.INFO)\nlogger = logging.getLogger(\"server\")\n\n\nasync def receive_loop(session: ServerSession):\n    logger.info(\"Starting receive loop\")\n    async for message in session.incoming_messages:\n        if isinstance(message, Exception):\n            logger.error(\"Error: %s\", message)\n            continue\n\n        logger.info(\"Received message from client: %s\", message)\n\n\nasync def main():\n    version = importlib.metadata.version(\"mcp\")\n    async with stdio_server() as (read_stream, write_stream):\n        async with (\n            ServerSession(\n                read_stream,\n                write_stream,\n                InitializationOptions(\n                    server_name=\"mcp\",\n                    server_version=version,\n                    capabilities=ServerCapabilities(),\n                ),\n            ) as session,\n            write_stream,\n        ):\n            await receive_loop(session)\n\n\nif __name__ == \"__main__\":\n    anyio.run(main, backend=\"trio\")\n"
  },
  {
    "path": "src/mcp/server/auth/__init__.py",
    "content": "\"\"\"MCP OAuth server authorization components.\"\"\"\n"
  },
  {
    "path": "src/mcp/server/auth/errors.py",
    "content": "from pydantic import ValidationError\n\n\ndef stringify_pydantic_error(validation_error: ValidationError) -> str:\n    return \"\\n\".join(f\"{'.'.join(str(loc) for loc in e['loc'])}: {e['msg']}\" for e in validation_error.errors())\n"
  },
  {
    "path": "src/mcp/server/auth/handlers/__init__.py",
    "content": "\"\"\"Request handlers for MCP authorization endpoints.\"\"\"\n"
  },
  {
    "path": "src/mcp/server/auth/handlers/authorize.py",
    "content": "import logging\nfrom dataclasses import dataclass\nfrom typing import Any, Literal\n\n# TODO(Marcelo): We should drop the `RootModel`.\nfrom pydantic import AnyUrl, BaseModel, Field, RootModel, ValidationError  # noqa: TID251\nfrom starlette.datastructures import FormData, QueryParams\nfrom starlette.requests import Request\nfrom starlette.responses import RedirectResponse, Response\n\nfrom mcp.server.auth.errors import stringify_pydantic_error\nfrom mcp.server.auth.json_response import PydanticJSONResponse\nfrom mcp.server.auth.provider import (\n    AuthorizationErrorCode,\n    AuthorizationParams,\n    AuthorizeError,\n    OAuthAuthorizationServerProvider,\n    construct_redirect_uri,\n)\nfrom mcp.shared.auth import InvalidRedirectUriError, InvalidScopeError\n\nlogger = logging.getLogger(__name__)\n\n\nclass AuthorizationRequest(BaseModel):\n    # See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1\n    client_id: str = Field(..., description=\"The client ID\")\n    redirect_uri: AnyUrl | None = Field(None, description=\"URL to redirect to after authorization\")\n\n    # see OAuthClientMetadata; we only support `code`\n    response_type: Literal[\"code\"] = Field(..., description=\"Must be 'code' for authorization code flow\")\n    code_challenge: str = Field(..., description=\"PKCE code challenge\")\n    code_challenge_method: Literal[\"S256\"] = Field(\"S256\", description=\"PKCE code challenge method, must be S256\")\n    state: str | None = Field(None, description=\"Optional state parameter\")\n    scope: str | None = Field(\n        None,\n        description=\"Optional scope; if specified, should be a space-separated list of scope strings\",\n    )\n    resource: str | None = Field(\n        None,\n        description=\"RFC 8707 resource indicator - the MCP server this token will be used with\",\n    )\n\n\nclass AuthorizationErrorResponse(BaseModel):\n    error: AuthorizationErrorCode\n    error_description: str | None\n    error_uri: AnyUrl | None = None\n    # must be set if provided in the request\n    state: str | None = None\n\n\ndef best_effort_extract_string(key: str, params: None | FormData | QueryParams) -> str | None:\n    if params is None:  # pragma: no cover\n        return None\n    value = params.get(key)\n    if isinstance(value, str):\n        return value\n    return None\n\n\nclass AnyUrlModel(RootModel[AnyUrl]):\n    root: AnyUrl\n\n\n@dataclass\nclass AuthorizationHandler:\n    provider: OAuthAuthorizationServerProvider[Any, Any, Any]\n\n    async def handle(self, request: Request) -> Response:\n        # implements authorization requests for grant_type=code;\n        # see https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.1\n\n        state = None\n        redirect_uri = None\n        client = None\n        params = None\n\n        async def error_response(\n            error: AuthorizationErrorCode,\n            error_description: str | None,\n            attempt_load_client: bool = True,\n        ):\n            # Error responses take two different formats:\n            # 1. The request has a valid client ID & redirect_uri: we issue a redirect\n            #    back to the redirect_uri with the error response fields as query\n            #    parameters. This allows the client to be notified of the error.\n            # 2. Otherwise, we return an error response directly to the end user;\n            #     we choose to do so in JSON, but this is left undefined in the\n            #     specification.\n            # See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1\n            #\n            # This logic is a bit awkward to handle, because the error might be thrown\n            # very early in request validation, before we've done the usual Pydantic\n            # validation, loaded the client, etc. To handle this, error_response()\n            # contains fallback logic which attempts to load the parameters directly\n            # from the request.\n\n            nonlocal client, redirect_uri, state\n            if client is None and attempt_load_client:\n                # make last-ditch attempt to load the client\n                client_id = best_effort_extract_string(\"client_id\", params)\n                client = await self.provider.get_client(client_id) if client_id else None\n            if redirect_uri is None and client:\n                # make last-ditch effort to load the redirect uri\n                try:\n                    if params is not None and \"redirect_uri\" not in params:\n                        raw_redirect_uri = None\n                    else:\n                        raw_redirect_uri = AnyUrlModel.model_validate(\n                            best_effort_extract_string(\"redirect_uri\", params)\n                        ).root\n                    redirect_uri = client.validate_redirect_uri(raw_redirect_uri)\n                except (ValidationError, InvalidRedirectUriError):\n                    # if the redirect URI is invalid, ignore it & just return the\n                    # initial error\n                    pass\n\n            # the error response MUST contain the state specified by the client, if any\n            if state is None:  # pragma: no cover\n                # make last-ditch effort to load state\n                state = best_effort_extract_string(\"state\", params)\n\n            error_resp = AuthorizationErrorResponse(\n                error=error,\n                error_description=error_description,\n                state=state,\n            )\n\n            if redirect_uri and client:\n                return RedirectResponse(\n                    url=construct_redirect_uri(str(redirect_uri), **error_resp.model_dump(exclude_none=True)),\n                    status_code=302,\n                    headers={\"Cache-Control\": \"no-store\"},\n                )\n            else:\n                return PydanticJSONResponse(\n                    status_code=400,\n                    content=error_resp,\n                    headers={\"Cache-Control\": \"no-store\"},\n                )\n\n        try:\n            # Parse request parameters\n            if request.method == \"GET\":\n                # Convert query_params to dict for pydantic validation\n                params = request.query_params\n            else:\n                # Parse form data for POST requests\n                params = await request.form()\n\n            # Save state if it exists, even before validation\n            state = best_effort_extract_string(\"state\", params)\n\n            try:\n                auth_request = AuthorizationRequest.model_validate(params)\n                state = auth_request.state  # Update with validated state\n            except ValidationError as validation_error:\n                error: AuthorizationErrorCode = \"invalid_request\"\n                for e in validation_error.errors():\n                    if e[\"loc\"] == (\"response_type\",) and e[\"type\"] == \"literal_error\":\n                        error = \"unsupported_response_type\"\n                        break\n                return await error_response(error, stringify_pydantic_error(validation_error))\n\n            # Get client information\n            client = await self.provider.get_client(\n                auth_request.client_id,\n            )\n            if not client:\n                # For client_id validation errors, return direct error (no redirect)\n                return await error_response(\n                    error=\"invalid_request\",\n                    error_description=f\"Client ID '{auth_request.client_id}' not found\",\n                    attempt_load_client=False,\n                )\n\n            # Validate redirect_uri against client's registered URIs\n            try:\n                redirect_uri = client.validate_redirect_uri(auth_request.redirect_uri)\n            except InvalidRedirectUriError as validation_error:\n                # For redirect_uri validation errors, return direct error (no redirect)\n                return await error_response(\n                    error=\"invalid_request\",\n                    error_description=validation_error.message,\n                )\n\n            # Validate scope - for scope errors, we can redirect\n            try:\n                scopes = client.validate_scope(auth_request.scope)\n            except InvalidScopeError as validation_error:\n                # For scope errors, redirect with error parameters\n                return await error_response(\n                    error=\"invalid_scope\",\n                    error_description=validation_error.message,\n                )\n\n            # Setup authorization parameters\n            auth_params = AuthorizationParams(\n                state=state,\n                scopes=scopes,\n                code_challenge=auth_request.code_challenge,\n                redirect_uri=redirect_uri,\n                redirect_uri_provided_explicitly=auth_request.redirect_uri is not None,\n                resource=auth_request.resource,  # RFC 8707\n            )\n\n            try:\n                # Let the provider pick the next URI to redirect to\n                return RedirectResponse(\n                    url=await self.provider.authorize(\n                        client,\n                        auth_params,\n                    ),\n                    status_code=302,\n                    headers={\"Cache-Control\": \"no-store\"},\n                )\n            except AuthorizeError as e:\n                # Handle authorization errors as defined in RFC 6749 Section 4.1.2.1\n                return await error_response(error=e.error, error_description=e.error_description)\n\n        except Exception as validation_error:  # pragma: no cover\n            # Catch-all for unexpected errors\n            logger.exception(\"Unexpected error in authorization_handler\", exc_info=validation_error)\n            return await error_response(error=\"server_error\", error_description=\"An unexpected error occurred\")\n"
  },
  {
    "path": "src/mcp/server/auth/handlers/metadata.py",
    "content": "from dataclasses import dataclass\n\nfrom starlette.requests import Request\nfrom starlette.responses import Response\n\nfrom mcp.server.auth.json_response import PydanticJSONResponse\nfrom mcp.shared.auth import OAuthMetadata, ProtectedResourceMetadata\n\n\n@dataclass\nclass MetadataHandler:\n    metadata: OAuthMetadata\n\n    async def handle(self, request: Request) -> Response:\n        return PydanticJSONResponse(\n            content=self.metadata,\n            headers={\"Cache-Control\": \"public, max-age=3600\"},  # Cache for 1 hour\n        )\n\n\n@dataclass\nclass ProtectedResourceMetadataHandler:\n    metadata: ProtectedResourceMetadata\n\n    async def handle(self, request: Request) -> Response:\n        return PydanticJSONResponse(\n            content=self.metadata,\n            headers={\"Cache-Control\": \"public, max-age=3600\"},  # Cache for 1 hour\n        )\n"
  },
  {
    "path": "src/mcp/server/auth/handlers/register.py",
    "content": "import secrets\nimport time\nfrom dataclasses import dataclass\nfrom typing import Any\nfrom uuid import uuid4\n\nfrom pydantic import BaseModel, ValidationError\nfrom starlette.requests import Request\nfrom starlette.responses import Response\n\nfrom mcp.server.auth.errors import stringify_pydantic_error\nfrom mcp.server.auth.json_response import PydanticJSONResponse\nfrom mcp.server.auth.provider import OAuthAuthorizationServerProvider, RegistrationError, RegistrationErrorCode\nfrom mcp.server.auth.settings import ClientRegistrationOptions\nfrom mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata\n\n# this alias is a no-op; it's just to separate out the types exposed to the\n# provider from what we use in the HTTP handler\nRegistrationRequest = OAuthClientMetadata\n\n\nclass RegistrationErrorResponse(BaseModel):\n    error: RegistrationErrorCode\n    error_description: str | None\n\n\n@dataclass\nclass RegistrationHandler:\n    provider: OAuthAuthorizationServerProvider[Any, Any, Any]\n    options: ClientRegistrationOptions\n\n    async def handle(self, request: Request) -> Response:\n        # Implements dynamic client registration as defined in https://datatracker.ietf.org/doc/html/rfc7591#section-3.1\n        try:\n            body = await request.body()\n            client_metadata = OAuthClientMetadata.model_validate_json(body)\n\n            # Scope validation is handled below\n        except ValidationError as validation_error:\n            return PydanticJSONResponse(\n                content=RegistrationErrorResponse(\n                    error=\"invalid_client_metadata\",\n                    error_description=stringify_pydantic_error(validation_error),\n                ),\n                status_code=400,\n            )\n\n        client_id = str(uuid4())\n\n        # If auth method is None, default to client_secret_post\n        if client_metadata.token_endpoint_auth_method is None:\n            client_metadata.token_endpoint_auth_method = \"client_secret_post\"\n\n        client_secret = None\n        if client_metadata.token_endpoint_auth_method != \"none\":  # pragma: no branch\n            # cryptographically secure random 32-byte hex string\n            client_secret = secrets.token_hex(32)\n\n        if client_metadata.scope is None and self.options.default_scopes is not None:\n            client_metadata.scope = \" \".join(self.options.default_scopes)\n        elif client_metadata.scope is not None and self.options.valid_scopes is not None:\n            requested_scopes = set(client_metadata.scope.split())\n            valid_scopes = set(self.options.valid_scopes)\n            if not requested_scopes.issubset(valid_scopes):  # pragma: no branch\n                return PydanticJSONResponse(\n                    content=RegistrationErrorResponse(\n                        error=\"invalid_client_metadata\",\n                        error_description=\"Requested scopes are not valid: \"\n                        f\"{', '.join(requested_scopes - valid_scopes)}\",\n                    ),\n                    status_code=400,\n                )\n        if \"authorization_code\" not in client_metadata.grant_types:\n            return PydanticJSONResponse(\n                content=RegistrationErrorResponse(\n                    error=\"invalid_client_metadata\",\n                    error_description=\"grant_types must include 'authorization_code'\",\n                ),\n                status_code=400,\n            )\n\n        # The MCP spec requires servers to use the authorization `code` flow\n        # with PKCE\n        if \"code\" not in client_metadata.response_types:\n            return PydanticJSONResponse(\n                content=RegistrationErrorResponse(\n                    error=\"invalid_client_metadata\",\n                    error_description=\"response_types must include 'code' for authorization_code grant\",\n                ),\n                status_code=400,\n            )\n\n        client_id_issued_at = int(time.time())\n        client_secret_expires_at = (\n            client_id_issued_at + self.options.client_secret_expiry_seconds\n            if self.options.client_secret_expiry_seconds is not None\n            else None\n        )\n\n        client_info = OAuthClientInformationFull(\n            client_id=client_id,\n            client_id_issued_at=client_id_issued_at,\n            client_secret=client_secret,\n            client_secret_expires_at=client_secret_expires_at,\n            # passthrough information from the client request\n            redirect_uris=client_metadata.redirect_uris,\n            token_endpoint_auth_method=client_metadata.token_endpoint_auth_method,\n            grant_types=client_metadata.grant_types,\n            response_types=client_metadata.response_types,\n            client_name=client_metadata.client_name,\n            client_uri=client_metadata.client_uri,\n            logo_uri=client_metadata.logo_uri,\n            scope=client_metadata.scope,\n            contacts=client_metadata.contacts,\n            tos_uri=client_metadata.tos_uri,\n            policy_uri=client_metadata.policy_uri,\n            jwks_uri=client_metadata.jwks_uri,\n            jwks=client_metadata.jwks,\n            software_id=client_metadata.software_id,\n            software_version=client_metadata.software_version,\n        )\n        try:\n            # Register client\n            await self.provider.register_client(client_info)\n\n            # Return client information\n            return PydanticJSONResponse(content=client_info, status_code=201)\n        except RegistrationError as e:\n            # Handle registration errors as defined in RFC 7591 Section 3.2.2\n            return PydanticJSONResponse(\n                content=RegistrationErrorResponse(error=e.error, error_description=e.error_description),\n                status_code=400,\n            )\n"
  },
  {
    "path": "src/mcp/server/auth/handlers/revoke.py",
    "content": "from dataclasses import dataclass\nfrom functools import partial\nfrom typing import Any, Literal\n\nfrom pydantic import BaseModel, ValidationError\nfrom starlette.requests import Request\nfrom starlette.responses import Response\n\nfrom mcp.server.auth.errors import (\n    stringify_pydantic_error,\n)\nfrom mcp.server.auth.json_response import PydanticJSONResponse\nfrom mcp.server.auth.middleware.client_auth import AuthenticationError, ClientAuthenticator\nfrom mcp.server.auth.provider import AccessToken, OAuthAuthorizationServerProvider, RefreshToken\n\n\nclass RevocationRequest(BaseModel):\n    \"\"\"See https://datatracker.ietf.org/doc/html/rfc7009#section-2.1\"\"\"\n\n    token: str\n    token_type_hint: Literal[\"access_token\", \"refresh_token\"] | None = None\n    client_id: str\n    client_secret: str | None\n\n\nclass RevocationErrorResponse(BaseModel):\n    error: Literal[\"invalid_request\", \"unauthorized_client\"]\n    error_description: str | None = None\n\n\n@dataclass\nclass RevocationHandler:\n    provider: OAuthAuthorizationServerProvider[Any, Any, Any]\n    client_authenticator: ClientAuthenticator\n\n    async def handle(self, request: Request) -> Response:\n        \"\"\"Handler for the OAuth 2.0 Token Revocation endpoint.\"\"\"\n        try:\n            client = await self.client_authenticator.authenticate_request(request)\n        except AuthenticationError as e:  # pragma: no cover\n            return PydanticJSONResponse(\n                status_code=401,\n                content=RevocationErrorResponse(\n                    error=\"unauthorized_client\",\n                    error_description=e.message,\n                ),\n            )\n\n        try:\n            form_data = await request.form()\n            revocation_request = RevocationRequest.model_validate(dict(form_data))\n        except ValidationError as e:\n            return PydanticJSONResponse(\n                status_code=400,\n                content=RevocationErrorResponse(\n                    error=\"invalid_request\",\n                    error_description=stringify_pydantic_error(e),\n                ),\n            )\n\n        loaders = [\n            self.provider.load_access_token,\n            partial(self.provider.load_refresh_token, client),\n        ]\n        if revocation_request.token_type_hint == \"refresh_token\":  # pragma: no cover\n            loaders = reversed(loaders)\n\n        token: None | AccessToken | RefreshToken = None\n        for loader in loaders:\n            token = await loader(revocation_request.token)\n            if token is not None:\n                break\n\n        # if token is not found, just return HTTP 200 per the RFC\n        if token and token.client_id == client.client_id:\n            # Revoke token; provider is not meant to be able to do validation\n            # at this point that would result in an error\n            await self.provider.revoke_token(token)\n\n        # Return successful empty response\n        return Response(\n            status_code=200,\n            headers={\n                \"Cache-Control\": \"no-store\",\n                \"Pragma\": \"no-cache\",\n            },\n        )\n"
  },
  {
    "path": "src/mcp/server/auth/handlers/token.py",
    "content": "import base64\nimport hashlib\nimport time\nfrom dataclasses import dataclass\nfrom typing import Annotated, Any, Literal\n\nfrom pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, TypeAdapter, ValidationError\nfrom starlette.requests import Request\n\nfrom mcp.server.auth.errors import stringify_pydantic_error\nfrom mcp.server.auth.json_response import PydanticJSONResponse\nfrom mcp.server.auth.middleware.client_auth import AuthenticationError, ClientAuthenticator\nfrom mcp.server.auth.provider import OAuthAuthorizationServerProvider, TokenError, TokenErrorCode\nfrom mcp.shared.auth import OAuthToken\n\n\nclass AuthorizationCodeRequest(BaseModel):\n    # See https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3\n    grant_type: Literal[\"authorization_code\"]\n    code: str = Field(..., description=\"The authorization code\")\n    redirect_uri: AnyUrl | None = Field(None, description=\"Must be the same as redirect URI provided in /authorize\")\n    client_id: str\n    # we use the client_secret param, per https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1\n    client_secret: str | None = None\n    # See https://datatracker.ietf.org/doc/html/rfc7636#section-4.5\n    code_verifier: str = Field(..., description=\"PKCE code verifier\")\n    # RFC 8707 resource indicator\n    resource: str | None = Field(None, description=\"Resource indicator for the token\")\n\n\nclass RefreshTokenRequest(BaseModel):\n    # See https://datatracker.ietf.org/doc/html/rfc6749#section-6\n    grant_type: Literal[\"refresh_token\"]\n    refresh_token: str = Field(..., description=\"The refresh token\")\n    scope: str | None = Field(None, description=\"Optional scope parameter\")\n    client_id: str\n    # we use the client_secret param, per https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1\n    client_secret: str | None = None\n    # RFC 8707 resource indicator\n    resource: str | None = Field(None, description=\"Resource indicator for the token\")\n\n\nTokenRequest = Annotated[AuthorizationCodeRequest | RefreshTokenRequest, Field(discriminator=\"grant_type\")]\ntoken_request_adapter = TypeAdapter[TokenRequest](TokenRequest)\n\n\nclass TokenErrorResponse(BaseModel):\n    \"\"\"See https://datatracker.ietf.org/doc/html/rfc6749#section-5.2\"\"\"\n\n    error: TokenErrorCode\n    error_description: str | None = None\n    error_uri: AnyHttpUrl | None = None\n\n\n# this is just an alias over OAuthToken; the only reason we do this\n# is to have some separation between the HTTP response type, and the\n# type returned by the provider\nTokenSuccessResponse = OAuthToken\n\n\n@dataclass\nclass TokenHandler:\n    provider: OAuthAuthorizationServerProvider[Any, Any, Any]\n    client_authenticator: ClientAuthenticator\n\n    def response(self, obj: TokenSuccessResponse | TokenErrorResponse):\n        status_code = 200\n        if isinstance(obj, TokenErrorResponse):\n            status_code = 400\n\n        return PydanticJSONResponse(\n            content=obj,\n            status_code=status_code,\n            headers={\n                \"Cache-Control\": \"no-store\",\n                \"Pragma\": \"no-cache\",\n            },\n        )\n\n    async def handle(self, request: Request):\n        try:\n            client_info = await self.client_authenticator.authenticate_request(request)\n        except AuthenticationError as e:\n            # Authentication failures should return 401\n            return PydanticJSONResponse(\n                content=TokenErrorResponse(\n                    error=\"invalid_client\",\n                    error_description=e.message,\n                ),\n                status_code=401,\n                headers={\n                    \"Cache-Control\": \"no-store\",\n                    \"Pragma\": \"no-cache\",\n                },\n            )\n\n        try:\n            form_data = await request.form()\n            # TODO(Marcelo): Can someone check if this `dict()` wrapper is necessary?\n            token_request = token_request_adapter.validate_python(dict(form_data))\n        except ValidationError as validation_error:  # pragma: no cover\n            return self.response(\n                TokenErrorResponse(\n                    error=\"invalid_request\",\n                    error_description=stringify_pydantic_error(validation_error),\n                )\n            )\n\n        if token_request.grant_type not in client_info.grant_types:  # pragma: no cover\n            return self.response(\n                TokenErrorResponse(\n                    error=\"unsupported_grant_type\",\n                    error_description=(f\"Unsupported grant type (supported grant types are {client_info.grant_types})\"),\n                )\n            )\n\n        tokens: OAuthToken\n\n        match token_request:\n            case AuthorizationCodeRequest():\n                auth_code = await self.provider.load_authorization_code(client_info, token_request.code)\n                if auth_code is None or auth_code.client_id != token_request.client_id:\n                    # if code belongs to different client, pretend it doesn't exist\n                    return self.response(\n                        TokenErrorResponse(\n                            error=\"invalid_grant\",\n                            error_description=\"authorization code does not exist\",\n                        )\n                    )\n\n                # make auth codes expire after a deadline\n                # see https://datatracker.ietf.org/doc/html/rfc6749#section-10.5\n                if auth_code.expires_at < time.time():\n                    return self.response(\n                        TokenErrorResponse(\n                            error=\"invalid_grant\",\n                            error_description=\"authorization code has expired\",\n                        )\n                    )\n\n                # verify redirect_uri doesn't change between /authorize and /tokens\n                # see https://datatracker.ietf.org/doc/html/rfc6749#section-10.6\n                if auth_code.redirect_uri_provided_explicitly:\n                    authorize_request_redirect_uri = auth_code.redirect_uri\n                else:  # pragma: no cover\n                    authorize_request_redirect_uri = None\n\n                # Convert both sides to strings for comparison to handle AnyUrl vs string issues\n                token_redirect_str = str(token_request.redirect_uri) if token_request.redirect_uri is not None else None\n                auth_redirect_str = (\n                    str(authorize_request_redirect_uri) if authorize_request_redirect_uri is not None else None\n                )\n\n                if token_redirect_str != auth_redirect_str:\n                    return self.response(\n                        TokenErrorResponse(\n                            error=\"invalid_request\",\n                            error_description=(\"redirect_uri did not match the one used when creating auth code\"),\n                        )\n                    )\n\n                # Verify PKCE code verifier\n                sha256 = hashlib.sha256(token_request.code_verifier.encode()).digest()\n                hashed_code_verifier = base64.urlsafe_b64encode(sha256).decode().rstrip(\"=\")\n\n                if hashed_code_verifier != auth_code.code_challenge:\n                    # see https://datatracker.ietf.org/doc/html/rfc7636#section-4.6\n                    return self.response(\n                        TokenErrorResponse(\n                            error=\"invalid_grant\",\n                            error_description=\"incorrect code_verifier\",\n                        )\n                    )\n\n                try:\n                    # Exchange authorization code for tokens\n                    tokens = await self.provider.exchange_authorization_code(client_info, auth_code)\n                except TokenError as e:\n                    return self.response(TokenErrorResponse(error=e.error, error_description=e.error_description))\n\n            case RefreshTokenRequest():  # pragma: no branch\n                refresh_token = await self.provider.load_refresh_token(client_info, token_request.refresh_token)\n                if refresh_token is None or refresh_token.client_id != token_request.client_id:\n                    # if token belongs to different client, pretend it doesn't exist\n                    return self.response(\n                        TokenErrorResponse(\n                            error=\"invalid_grant\",\n                            error_description=\"refresh token does not exist\",\n                        )\n                    )\n\n                if refresh_token.expires_at and refresh_token.expires_at < time.time():\n                    # if the refresh token has expired, pretend it doesn't exist\n                    return self.response(\n                        TokenErrorResponse(\n                            error=\"invalid_grant\",\n                            error_description=\"refresh token has expired\",\n                        )\n                    )\n\n                # Parse scopes if provided\n                scopes = token_request.scope.split(\" \") if token_request.scope else refresh_token.scopes\n\n                for scope in scopes:\n                    if scope not in refresh_token.scopes:\n                        return self.response(\n                            TokenErrorResponse(\n                                error=\"invalid_scope\",\n                                error_description=(f\"cannot request scope `{scope}` not provided by refresh token\"),\n                            )\n                        )\n\n                try:\n                    # Exchange refresh token for new tokens\n                    tokens = await self.provider.exchange_refresh_token(client_info, refresh_token, scopes)\n                except TokenError as e:\n                    return self.response(TokenErrorResponse(error=e.error, error_description=e.error_description))\n\n        return self.response(tokens)\n"
  },
  {
    "path": "src/mcp/server/auth/json_response.py",
    "content": "from typing import Any\n\nfrom starlette.responses import JSONResponse\n\n\nclass PydanticJSONResponse(JSONResponse):\n    # use pydantic json serialization instead of the stock `json.dumps`,\n    # so that we can handle serializing pydantic models like AnyHttpUrl\n    def render(self, content: Any) -> bytes:\n        return content.model_dump_json(exclude_none=True).encode(\"utf-8\")\n"
  },
  {
    "path": "src/mcp/server/auth/middleware/__init__.py",
    "content": "\"\"\"Middleware for MCP authorization.\"\"\"\n"
  },
  {
    "path": "src/mcp/server/auth/middleware/auth_context.py",
    "content": "import contextvars\n\nfrom starlette.types import ASGIApp, Receive, Scope, Send\n\nfrom mcp.server.auth.middleware.bearer_auth import AuthenticatedUser\nfrom mcp.server.auth.provider import AccessToken\n\n# Create a contextvar to store the authenticated user\n# The default is None, indicating no authenticated user is present\nauth_context_var = contextvars.ContextVar[AuthenticatedUser | None](\"auth_context\", default=None)\n\n\ndef get_access_token() -> AccessToken | None:\n    \"\"\"Get the access token from the current context.\n\n    Returns:\n        The access token if an authenticated user is available, None otherwise.\n    \"\"\"\n    auth_user = auth_context_var.get()\n    return auth_user.access_token if auth_user else None\n\n\nclass AuthContextMiddleware:\n    \"\"\"Middleware that extracts the authenticated user from the request\n    and sets it in a contextvar for easy access throughout the request lifecycle.\n\n    This middleware should be added after the AuthenticationMiddleware in the\n    middleware stack to ensure that the user is properly authenticated before\n    being stored in the context.\n    \"\"\"\n\n    def __init__(self, app: ASGIApp):\n        self.app = app\n\n    async def __call__(self, scope: Scope, receive: Receive, send: Send):\n        user = scope.get(\"user\")\n        if isinstance(user, AuthenticatedUser):\n            # Set the authenticated user in the contextvar\n            token = auth_context_var.set(user)\n            try:\n                await self.app(scope, receive, send)\n            finally:\n                auth_context_var.reset(token)\n        else:\n            # No authenticated user, just process the request\n            await self.app(scope, receive, send)\n"
  },
  {
    "path": "src/mcp/server/auth/middleware/bearer_auth.py",
    "content": "import json\nimport time\nfrom typing import Any\n\nfrom pydantic import AnyHttpUrl\nfrom starlette.authentication import AuthCredentials, AuthenticationBackend, SimpleUser\nfrom starlette.requests import HTTPConnection\nfrom starlette.types import Receive, Scope, Send\n\nfrom mcp.server.auth.provider import AccessToken, TokenVerifier\n\n\nclass AuthenticatedUser(SimpleUser):\n    \"\"\"User with authentication info.\"\"\"\n\n    def __init__(self, auth_info: AccessToken):\n        super().__init__(auth_info.client_id)\n        self.access_token = auth_info\n        self.scopes = auth_info.scopes\n\n\nclass BearerAuthBackend(AuthenticationBackend):\n    \"\"\"Authentication backend that validates Bearer tokens using a TokenVerifier.\"\"\"\n\n    def __init__(self, token_verifier: TokenVerifier):\n        self.token_verifier = token_verifier\n\n    async def authenticate(self, conn: HTTPConnection):\n        auth_header = next(\n            (conn.headers.get(key) for key in conn.headers if key.lower() == \"authorization\"),\n            None,\n        )\n        if not auth_header or not auth_header.lower().startswith(\"bearer \"):\n            return None\n\n        token = auth_header[7:]  # Remove \"Bearer \" prefix\n\n        # Validate the token with the verifier\n        auth_info = await self.token_verifier.verify_token(token)\n\n        if not auth_info:\n            return None\n\n        if auth_info.expires_at and auth_info.expires_at < int(time.time()):\n            return None\n\n        return AuthCredentials(auth_info.scopes), AuthenticatedUser(auth_info)\n\n\nclass RequireAuthMiddleware:\n    \"\"\"Middleware that requires a valid Bearer token in the Authorization header.\n\n    This will validate the token with the auth provider and store the resulting\n    auth info in the request state.\n    \"\"\"\n\n    def __init__(\n        self,\n        app: Any,\n        required_scopes: list[str],\n        resource_metadata_url: AnyHttpUrl | None = None,\n    ):\n        \"\"\"Initialize the middleware.\n\n        Args:\n            app: ASGI application\n            required_scopes: List of scopes that the token must have\n            resource_metadata_url: Optional protected resource metadata URL for WWW-Authenticate header\n        \"\"\"\n        self.app = app\n        self.required_scopes = required_scopes\n        self.resource_metadata_url = resource_metadata_url\n\n    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:\n        auth_user = scope.get(\"user\")\n        if not isinstance(auth_user, AuthenticatedUser):\n            await self._send_auth_error(\n                send, status_code=401, error=\"invalid_token\", description=\"Authentication required\"\n            )\n            return\n\n        auth_credentials = scope.get(\"auth\")\n\n        for required_scope in self.required_scopes:\n            # auth_credentials should always be provided; this is just paranoia\n            if auth_credentials is None or required_scope not in auth_credentials.scopes:\n                await self._send_auth_error(\n                    send, status_code=403, error=\"insufficient_scope\", description=f\"Required scope: {required_scope}\"\n                )\n                return\n\n        await self.app(scope, receive, send)\n\n    async def _send_auth_error(self, send: Send, status_code: int, error: str, description: str) -> None:\n        \"\"\"Send an authentication error response with WWW-Authenticate header.\"\"\"\n        # Build WWW-Authenticate header value\n        www_auth_parts = [f'error=\"{error}\"', f'error_description=\"{description}\"']\n        if self.resource_metadata_url:  # pragma: no cover\n            www_auth_parts.append(f'resource_metadata=\"{self.resource_metadata_url}\"')\n\n        www_authenticate = f\"Bearer {', '.join(www_auth_parts)}\"\n\n        # Send response\n        body = {\"error\": error, \"error_description\": description}\n        body_bytes = json.dumps(body).encode()\n\n        await send(\n            {\n                \"type\": \"http.response.start\",\n                \"status\": status_code,\n                \"headers\": [\n                    (b\"content-type\", b\"application/json\"),\n                    (b\"content-length\", str(len(body_bytes)).encode()),\n                    (b\"www-authenticate\", www_authenticate.encode()),\n                ],\n            }\n        )\n\n        await send(\n            {\n                \"type\": \"http.response.body\",\n                \"body\": body_bytes,\n            }\n        )\n"
  },
  {
    "path": "src/mcp/server/auth/middleware/client_auth.py",
    "content": "import base64\nimport binascii\nimport hmac\nimport time\nfrom typing import Any\nfrom urllib.parse import unquote\n\nfrom starlette.requests import Request\n\nfrom mcp.server.auth.provider import OAuthAuthorizationServerProvider\nfrom mcp.shared.auth import OAuthClientInformationFull\n\n\nclass AuthenticationError(Exception):\n    def __init__(self, message: str):\n        self.message = message\n\n\nclass ClientAuthenticator:\n    \"\"\"ClientAuthenticator is a callable which validates requests from a client\n    application, used to verify /token calls.\n\n    If, during registration, the client requested to be issued a secret, the\n    authenticator asserts that /token calls must be authenticated with\n    that same secret.\n\n    NOTE: clients can opt for no authentication during registration, in which case this\n    logic is skipped.\n    \"\"\"\n\n    def __init__(self, provider: OAuthAuthorizationServerProvider[Any, Any, Any]):\n        \"\"\"Initialize the authenticator.\n\n        Args:\n            provider: Provider to look up client information\n        \"\"\"\n        self.provider = provider\n\n    async def authenticate_request(self, request: Request) -> OAuthClientInformationFull:\n        \"\"\"Authenticate a client from an HTTP request.\n\n        Extracts client credentials from the appropriate location based on the\n        client's registered authentication method and validates them.\n\n        Args:\n            request: The HTTP request containing client credentials\n\n        Returns:\n            The authenticated client information\n\n        Raises:\n            AuthenticationError: If authentication fails\n        \"\"\"\n        form_data = await request.form()\n        client_id = form_data.get(\"client_id\")\n        if not client_id:\n            raise AuthenticationError(\"Missing client_id\")\n\n        client = await self.provider.get_client(str(client_id))\n        if not client:\n            raise AuthenticationError(\"Invalid client_id\")  # pragma: no cover\n\n        request_client_secret: str | None = None\n        auth_header = request.headers.get(\"Authorization\", \"\")\n\n        if client.token_endpoint_auth_method == \"client_secret_basic\":\n            if not auth_header.startswith(\"Basic \"):\n                raise AuthenticationError(\"Missing or invalid Basic authentication in Authorization header\")\n\n            try:\n                encoded_credentials = auth_header[6:]  # Remove \"Basic \" prefix\n                decoded = base64.b64decode(encoded_credentials).decode(\"utf-8\")\n                if \":\" not in decoded:\n                    raise ValueError(\"Invalid Basic auth format\")\n                basic_client_id, request_client_secret = decoded.split(\":\", 1)\n\n                # URL-decode both parts per RFC 6749 Section 2.3.1\n                basic_client_id = unquote(basic_client_id)\n                request_client_secret = unquote(request_client_secret)\n\n                if basic_client_id != client_id:\n                    raise AuthenticationError(\"Client ID mismatch in Basic auth\")\n            except (ValueError, UnicodeDecodeError, binascii.Error):\n                raise AuthenticationError(\"Invalid Basic authentication header\")\n\n        elif client.token_endpoint_auth_method == \"client_secret_post\":\n            raw_form_data = form_data.get(\"client_secret\")\n            # form_data.get() can return an UploadFile or None, so we need to check if it's a string\n            if isinstance(raw_form_data, str):\n                request_client_secret = str(raw_form_data)\n\n        elif client.token_endpoint_auth_method == \"none\":\n            request_client_secret = None\n        else:\n            raise AuthenticationError(  # pragma: no cover\n                f\"Unsupported auth method: {client.token_endpoint_auth_method}\"\n            )\n\n        # If client from the store expects a secret, validate that the request provides\n        # that secret\n        if client.client_secret:\n            if not request_client_secret:\n                raise AuthenticationError(\"Client secret is required\")\n\n            # hmac.compare_digest requires that both arguments are either bytes or a `str` containing\n            # only ASCII characters. Since we do not control `request_client_secret`, we encode both\n            # arguments to bytes.\n            if not hmac.compare_digest(client.client_secret.encode(), request_client_secret.encode()):\n                raise AuthenticationError(\"Invalid client_secret\")\n\n            if client.client_secret_expires_at and client.client_secret_expires_at < int(time.time()):\n                raise AuthenticationError(\"Client secret has expired\")  # pragma: no cover\n\n        return client\n"
  },
  {
    "path": "src/mcp/server/auth/provider.py",
    "content": "from dataclasses import dataclass\nfrom typing import Generic, Literal, Protocol, TypeVar\nfrom urllib.parse import parse_qs, urlencode, urlparse, urlunparse\n\nfrom pydantic import AnyUrl, BaseModel\n\nfrom mcp.shared.auth import OAuthClientInformationFull, OAuthToken\n\n\nclass AuthorizationParams(BaseModel):\n    state: str | None\n    scopes: list[str] | None\n    code_challenge: str\n    redirect_uri: AnyUrl\n    redirect_uri_provided_explicitly: bool\n    resource: str | None = None  # RFC 8707 resource indicator\n\n\nclass AuthorizationCode(BaseModel):\n    code: str\n    scopes: list[str]\n    expires_at: float\n    client_id: str\n    code_challenge: str\n    redirect_uri: AnyUrl\n    redirect_uri_provided_explicitly: bool\n    resource: str | None = None  # RFC 8707 resource indicator\n\n\nclass RefreshToken(BaseModel):\n    token: str\n    client_id: str\n    scopes: list[str]\n    expires_at: int | None = None\n\n\nclass AccessToken(BaseModel):\n    token: str\n    client_id: str\n    scopes: list[str]\n    expires_at: int | None = None\n    resource: str | None = None  # RFC 8707 resource indicator\n\n\nRegistrationErrorCode = Literal[\n    \"invalid_redirect_uri\",\n    \"invalid_client_metadata\",\n    \"invalid_software_statement\",\n    \"unapproved_software_statement\",\n]\n\n\n@dataclass(frozen=True)\nclass RegistrationError(Exception):\n    error: RegistrationErrorCode\n    error_description: str | None = None\n\n\nAuthorizationErrorCode = Literal[\n    \"invalid_request\",\n    \"unauthorized_client\",\n    \"access_denied\",\n    \"unsupported_response_type\",\n    \"invalid_scope\",\n    \"server_error\",\n    \"temporarily_unavailable\",\n]\n\n\n@dataclass(frozen=True)\nclass AuthorizeError(Exception):\n    error: AuthorizationErrorCode\n    error_description: str | None = None\n\n\nTokenErrorCode = Literal[\n    \"invalid_request\",\n    \"invalid_client\",\n    \"invalid_grant\",\n    \"unauthorized_client\",\n    \"unsupported_grant_type\",\n    \"invalid_scope\",\n]\n\n\n@dataclass(frozen=True)\nclass TokenError(Exception):\n    error: TokenErrorCode\n    error_description: str | None = None\n\n\nclass TokenVerifier(Protocol):\n    \"\"\"Protocol for verifying bearer tokens.\"\"\"\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Verify a bearer token and return access info if valid.\"\"\"\n\n\n# NOTE: MCPServer doesn't render any of these types in the user response, so it's\n# OK to add fields to subclasses which should not be exposed externally.\nAuthorizationCodeT = TypeVar(\"AuthorizationCodeT\", bound=AuthorizationCode)\nRefreshTokenT = TypeVar(\"RefreshTokenT\", bound=RefreshToken)\nAccessTokenT = TypeVar(\"AccessTokenT\", bound=AccessToken)\n\n\nclass OAuthAuthorizationServerProvider(Protocol, Generic[AuthorizationCodeT, RefreshTokenT, AccessTokenT]):\n    async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:\n        \"\"\"Retrieves client information by client ID.\n\n        Implementors MAY raise NotImplementedError if dynamic client registration is\n        disabled in ClientRegistrationOptions.\n\n        Args:\n            client_id: The ID of the client to retrieve.\n\n        Returns:\n            The client information, or None if the client does not exist.\n        \"\"\"\n\n    async def register_client(self, client_info: OAuthClientInformationFull) -> None:\n        \"\"\"Saves client information as part of registering it.\n\n        Implementors MAY raise NotImplementedError if dynamic client registration is\n        disabled in ClientRegistrationOptions.\n\n        Args:\n            client_info: The client metadata to register.\n\n        Raises:\n            RegistrationError: If the client metadata is invalid.\n        \"\"\"\n\n    async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:\n        \"\"\"Handle the /authorize endpoint and return a URL that the client\n        will be redirected to.\n\n        Many MCP implementations will redirect to a third-party provider to perform\n        a second OAuth exchange with that provider. In this sort of setup, the client\n        has an OAuth connection with the MCP server, and the MCP server has an OAuth\n        connection with the 3rd-party provider. At the end of this flow, the client\n        should be redirected to the redirect_uri from params.redirect_uri.\n\n        +--------+     +------------+     +-------------------+\n        |        |     |            |     |                   |\n        | Client | --> | MCP Server | --> | 3rd Party OAuth   |\n        |        |     |            |     | Server            |\n        +--------+     +------------+     +-------------------+\n                            |   ^                  |\n        +------------+      |   |                  |\n        |            |      |   |    Redirect      |\n        |redirect_uri|<-----+   +------------------+\n        |            |\n        +------------+\n\n        Implementations will need to define another handler on the MCP server's return\n        flow to perform the second redirect, and generate and store an authorization\n        code as part of completing the OAuth authorization step.\n\n        Implementations SHOULD generate an authorization code with at least 160 bits of\n        entropy,\n        and MUST generate an authorization code with at least 128 bits of entropy.\n        See https://datatracker.ietf.org/doc/html/rfc6749#section-10.10.\n\n        Args:\n            client: The client requesting authorization.\n            params: The parameters of the authorization request.\n\n        Returns:\n            A URL to redirect the client to for authorization.\n\n        Raises:\n            AuthorizeError: If the authorization request is invalid.\n        \"\"\"\n        ...\n\n    async def load_authorization_code(\n        self, client: OAuthClientInformationFull, authorization_code: str\n    ) -> AuthorizationCodeT | None:\n        \"\"\"Loads an AuthorizationCode by its code.\n\n        Args:\n            client: The client that requested the authorization code.\n            authorization_code: The authorization code to get the challenge for.\n\n        Returns:\n            The AuthorizationCode, or None if not found.\n        \"\"\"\n        ...\n\n    async def exchange_authorization_code(\n        self, client: OAuthClientInformationFull, authorization_code: AuthorizationCodeT\n    ) -> OAuthToken:\n        \"\"\"Exchanges an authorization code for an access token and refresh token.\n\n        Args:\n            client: The client exchanging the authorization code.\n            authorization_code: The authorization code to exchange.\n\n        Returns:\n            The OAuth token, containing access and refresh tokens.\n\n        Raises:\n            TokenError: If the request is invalid.\n        \"\"\"\n        ...\n\n    async def load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshTokenT | None:\n        \"\"\"Loads a RefreshToken by its token string.\n\n        Args:\n            client: The client that is requesting to load the refresh token.\n            refresh_token: The refresh token string to load.\n\n        Returns:\n            The RefreshToken object if found, or None if not found.\n        \"\"\"\n        ...\n\n    async def exchange_refresh_token(\n        self,\n        client: OAuthClientInformationFull,\n        refresh_token: RefreshTokenT,\n        scopes: list[str],\n    ) -> OAuthToken:\n        \"\"\"Exchanges a refresh token for an access token and refresh token.\n\n        Implementations SHOULD rotate both the access token and refresh token.\n\n        Args:\n            client: The client exchanging the refresh token.\n            refresh_token: The refresh token to exchange.\n            scopes: Optional scopes to request with the new access token.\n\n        Returns:\n            The OAuth token, containing access and refresh tokens.\n\n        Raises:\n            TokenError: If the request is invalid.\n        \"\"\"\n        ...\n\n    async def load_access_token(self, token: str) -> AccessTokenT | None:\n        \"\"\"Loads an access token by its token string.\n\n        Args:\n            token: The access token to verify.\n\n        Returns:\n            The access token, or None if the token is invalid.\n        \"\"\"\n\n    async def revoke_token(\n        self,\n        token: AccessTokenT | RefreshTokenT,\n    ) -> None:\n        \"\"\"Revokes an access or refresh token.\n\n        If the given token is invalid or already revoked, this method should do nothing.\n\n        Implementations SHOULD revoke both the access token and its corresponding\n        refresh token, regardless of which of the access token or refresh token is\n        provided.\n\n        Args:\n            token: The token to revoke.\n        \"\"\"\n\n\ndef construct_redirect_uri(redirect_uri_base: str, **params: str | None) -> str:\n    parsed_uri = urlparse(redirect_uri_base)\n    query_params = [(k, v) for k, vs in parse_qs(parsed_uri.query).items() for v in vs]\n    for k, v in params.items():\n        if v is not None:\n            query_params.append((k, v))\n\n    redirect_uri = urlunparse(parsed_uri._replace(query=urlencode(query_params)))\n    return redirect_uri\n\n\nclass ProviderTokenVerifier(TokenVerifier):\n    \"\"\"Token verifier that uses an OAuthAuthorizationServerProvider.\n\n    This is provided for backwards compatibility with existing auth_server_provider\n    configurations. For new implementations using AS/RS separation, consider using\n    the TokenVerifier protocol with a dedicated implementation like IntrospectionTokenVerifier.\n    \"\"\"\n\n    def __init__(self, provider: \"OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]\"):\n        self.provider = provider\n\n    async def verify_token(self, token: str) -> AccessToken | None:\n        \"\"\"Verify token using the provider's load_access_token method.\"\"\"\n        return await self.provider.load_access_token(token)\n"
  },
  {
    "path": "src/mcp/server/auth/routes.py",
    "content": "from collections.abc import Awaitable, Callable\nfrom typing import Any\nfrom urllib.parse import urlparse\n\nfrom pydantic import AnyHttpUrl\nfrom starlette.middleware.cors import CORSMiddleware\nfrom starlette.requests import Request\nfrom starlette.responses import Response\nfrom starlette.routing import Route, request_response  # type: ignore\nfrom starlette.types import ASGIApp\n\nfrom mcp.server.auth.handlers.authorize import AuthorizationHandler\nfrom mcp.server.auth.handlers.metadata import MetadataHandler, ProtectedResourceMetadataHandler\nfrom mcp.server.auth.handlers.register import RegistrationHandler\nfrom mcp.server.auth.handlers.revoke import RevocationHandler\nfrom mcp.server.auth.handlers.token import TokenHandler\nfrom mcp.server.auth.middleware.client_auth import ClientAuthenticator\nfrom mcp.server.auth.provider import OAuthAuthorizationServerProvider\nfrom mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions\nfrom mcp.server.streamable_http import MCP_PROTOCOL_VERSION_HEADER\nfrom mcp.shared.auth import OAuthMetadata, ProtectedResourceMetadata\n\n\ndef validate_issuer_url(url: AnyHttpUrl):\n    \"\"\"Validate that the issuer URL meets OAuth 2.0 requirements.\n\n    Args:\n        url: The issuer URL to validate.\n\n    Raises:\n        ValueError: If the issuer URL is invalid.\n    \"\"\"\n\n    # RFC 8414 requires HTTPS, but we allow loopback/localhost HTTP for testing\n    if url.scheme != \"https\" and url.host not in (\"localhost\", \"127.0.0.1\", \"[::1]\"):\n        raise ValueError(\"Issuer URL must be HTTPS\")\n\n    # No fragments or query parameters allowed\n    if url.fragment:\n        raise ValueError(\"Issuer URL must not have a fragment\")\n    if url.query:\n        raise ValueError(\"Issuer URL must not have a query string\")\n\n\nAUTHORIZATION_PATH = \"/authorize\"\nTOKEN_PATH = \"/token\"\nREGISTRATION_PATH = \"/register\"\nREVOCATION_PATH = \"/revoke\"\n\n\ndef cors_middleware(\n    handler: Callable[[Request], Response | Awaitable[Response]],\n    allow_methods: list[str],\n) -> ASGIApp:\n    cors_app = CORSMiddleware(\n        app=request_response(handler),\n        allow_origins=\"*\",\n        allow_methods=allow_methods,\n        allow_headers=[MCP_PROTOCOL_VERSION_HEADER],\n    )\n    return cors_app\n\n\ndef create_auth_routes(\n    provider: OAuthAuthorizationServerProvider[Any, Any, Any],\n    issuer_url: AnyHttpUrl,\n    service_documentation_url: AnyHttpUrl | None = None,\n    client_registration_options: ClientRegistrationOptions | None = None,\n    revocation_options: RevocationOptions | None = None,\n) -> list[Route]:\n    validate_issuer_url(issuer_url)\n\n    client_registration_options = client_registration_options or ClientRegistrationOptions()\n    revocation_options = revocation_options or RevocationOptions()\n    metadata = build_metadata(\n        issuer_url,\n        service_documentation_url,\n        client_registration_options,\n        revocation_options,\n    )\n    client_authenticator = ClientAuthenticator(provider)\n\n    # Create routes\n    # Allow CORS requests for endpoints meant to be hit by the OAuth client\n    # (with the client secret). This is intended to support things like MCP Inspector,\n    # where the client runs in a web browser.\n    routes = [\n        Route(\n            \"/.well-known/oauth-authorization-server\",\n            endpoint=cors_middleware(\n                MetadataHandler(metadata).handle,\n                [\"GET\", \"OPTIONS\"],\n            ),\n            methods=[\"GET\", \"OPTIONS\"],\n        ),\n        Route(\n            AUTHORIZATION_PATH,\n            # do not allow CORS for authorization endpoint;\n            # clients should just redirect to this\n            endpoint=AuthorizationHandler(provider).handle,\n            methods=[\"GET\", \"POST\"],\n        ),\n        Route(\n            TOKEN_PATH,\n            endpoint=cors_middleware(\n                TokenHandler(provider, client_authenticator).handle,\n                [\"POST\", \"OPTIONS\"],\n            ),\n            methods=[\"POST\", \"OPTIONS\"],\n        ),\n    ]\n\n    if client_registration_options.enabled:  # pragma: no branch\n        registration_handler = RegistrationHandler(\n            provider,\n            options=client_registration_options,\n        )\n        routes.append(\n            Route(\n                REGISTRATION_PATH,\n                endpoint=cors_middleware(\n                    registration_handler.handle,\n                    [\"POST\", \"OPTIONS\"],\n                ),\n                methods=[\"POST\", \"OPTIONS\"],\n            )\n        )\n\n    if revocation_options.enabled:  # pragma: no branch\n        revocation_handler = RevocationHandler(provider, client_authenticator)\n        routes.append(\n            Route(\n                REVOCATION_PATH,\n                endpoint=cors_middleware(\n                    revocation_handler.handle,\n                    [\"POST\", \"OPTIONS\"],\n                ),\n                methods=[\"POST\", \"OPTIONS\"],\n            )\n        )\n\n    return routes\n\n\ndef build_metadata(\n    issuer_url: AnyHttpUrl,\n    service_documentation_url: AnyHttpUrl | None,\n    client_registration_options: ClientRegistrationOptions,\n    revocation_options: RevocationOptions,\n) -> OAuthMetadata:\n    authorization_url = AnyHttpUrl(str(issuer_url).rstrip(\"/\") + AUTHORIZATION_PATH)\n    token_url = AnyHttpUrl(str(issuer_url).rstrip(\"/\") + TOKEN_PATH)\n\n    # Create metadata\n    metadata = OAuthMetadata(\n        issuer=issuer_url,\n        authorization_endpoint=authorization_url,\n        token_endpoint=token_url,\n        scopes_supported=client_registration_options.valid_scopes,\n        response_types_supported=[\"code\"],\n        response_modes_supported=None,\n        grant_types_supported=[\"authorization_code\", \"refresh_token\"],\n        token_endpoint_auth_methods_supported=[\"client_secret_post\", \"client_secret_basic\"],\n        token_endpoint_auth_signing_alg_values_supported=None,\n        service_documentation=service_documentation_url,\n        ui_locales_supported=None,\n        op_policy_uri=None,\n        op_tos_uri=None,\n        introspection_endpoint=None,\n        code_challenge_methods_supported=[\"S256\"],\n    )\n\n    # Add registration endpoint if supported\n    if client_registration_options.enabled:  # pragma: no branch\n        metadata.registration_endpoint = AnyHttpUrl(str(issuer_url).rstrip(\"/\") + REGISTRATION_PATH)\n\n    # Add revocation endpoint if supported\n    if revocation_options.enabled:  # pragma: no branch\n        metadata.revocation_endpoint = AnyHttpUrl(str(issuer_url).rstrip(\"/\") + REVOCATION_PATH)\n        metadata.revocation_endpoint_auth_methods_supported = [\"client_secret_post\", \"client_secret_basic\"]\n\n    return metadata\n\n\ndef build_resource_metadata_url(resource_server_url: AnyHttpUrl) -> AnyHttpUrl:\n    \"\"\"Build RFC 9728 compliant protected resource metadata URL.\n\n    Inserts /.well-known/oauth-protected-resource between host and resource path\n    as specified in RFC 9728 §3.1.\n\n    Args:\n        resource_server_url: The resource server URL (e.g., https://example.com/mcp)\n\n    Returns:\n        The metadata URL (e.g., https://example.com/.well-known/oauth-protected-resource/mcp)\n    \"\"\"\n    parsed = urlparse(str(resource_server_url))\n    # Handle trailing slash: if path is just \"/\", treat as empty\n    resource_path = parsed.path if parsed.path != \"/\" else \"\"\n    return AnyHttpUrl(f\"{parsed.scheme}://{parsed.netloc}/.well-known/oauth-protected-resource{resource_path}\")\n\n\ndef create_protected_resource_routes(\n    resource_url: AnyHttpUrl,\n    authorization_servers: list[AnyHttpUrl],\n    scopes_supported: list[str] | None = None,\n    resource_name: str | None = None,\n    resource_documentation: AnyHttpUrl | None = None,\n) -> list[Route]:\n    \"\"\"Create routes for OAuth 2.0 Protected Resource Metadata (RFC 9728).\n\n    Args:\n        resource_url: The URL of this resource server\n        authorization_servers: List of authorization servers that can issue tokens\n        scopes_supported: Optional list of scopes supported by this resource\n        resource_name: Optional human-readable name for this resource\n        resource_documentation: Optional URL to documentation for this resource\n\n    Returns:\n        List of Starlette routes for protected resource metadata\n    \"\"\"\n    metadata = ProtectedResourceMetadata(\n        resource=resource_url,\n        authorization_servers=authorization_servers,\n        scopes_supported=scopes_supported,\n        resource_name=resource_name,\n        resource_documentation=resource_documentation,\n        # bearer_methods_supported defaults to [\"header\"] in the model\n    )\n\n    handler = ProtectedResourceMetadataHandler(metadata)\n\n    # RFC 9728 §3.1: Register route at /.well-known/oauth-protected-resource + resource path\n    metadata_url = build_resource_metadata_url(resource_url)\n    # Extract just the path part for route registration\n    parsed = urlparse(str(metadata_url))\n    well_known_path = parsed.path\n\n    return [\n        Route(\n            well_known_path,\n            endpoint=cors_middleware(handler.handle, [\"GET\", \"OPTIONS\"]),\n            methods=[\"GET\", \"OPTIONS\"],\n        )\n    ]\n"
  },
  {
    "path": "src/mcp/server/auth/settings.py",
    "content": "from pydantic import AnyHttpUrl, BaseModel, Field\n\n\nclass ClientRegistrationOptions(BaseModel):\n    enabled: bool = False\n    client_secret_expiry_seconds: int | None = None\n    valid_scopes: list[str] | None = None\n    default_scopes: list[str] | None = None\n\n\nclass RevocationOptions(BaseModel):\n    enabled: bool = False\n\n\nclass AuthSettings(BaseModel):\n    issuer_url: AnyHttpUrl = Field(\n        ...,\n        description=\"OAuth authorization server URL that issues tokens for this resource server.\",\n    )\n    service_documentation_url: AnyHttpUrl | None = None\n    client_registration_options: ClientRegistrationOptions | None = None\n    revocation_options: RevocationOptions | None = None\n    required_scopes: list[str] | None = None\n\n    # Resource Server settings (when operating as RS only)\n    resource_server_url: AnyHttpUrl | None = Field(\n        ...,\n        description=\"The URL of the MCP server to be used as the resource identifier \"\n        \"and base route to look up OAuth Protected Resource Metadata.\",\n    )\n"
  },
  {
    "path": "src/mcp/server/context.py",
    "content": "from __future__ import annotations\n\nfrom dataclasses import dataclass\nfrom typing import Any, Generic\n\nfrom typing_extensions import TypeVar\n\nfrom mcp.server.experimental.request_context import Experimental\nfrom mcp.server.session import ServerSession\nfrom mcp.shared._context import RequestContext\nfrom mcp.shared.message import CloseSSEStreamCallback\n\nLifespanContextT = TypeVar(\"LifespanContextT\", default=dict[str, Any])\nRequestT = TypeVar(\"RequestT\", default=Any)\n\n\n@dataclass(kw_only=True)\nclass ServerRequestContext(RequestContext[ServerSession], Generic[LifespanContextT, RequestT]):\n    lifespan_context: LifespanContextT\n    experimental: Experimental\n    request: RequestT | None = None\n    close_sse_stream: CloseSSEStreamCallback | None = None\n    close_standalone_sse_stream: CloseSSEStreamCallback | None = None\n"
  },
  {
    "path": "src/mcp/server/elicitation.py",
    "content": "\"\"\"Elicitation utilities for MCP servers.\"\"\"\n\nfrom __future__ import annotations\n\nimport types\nfrom collections.abc import Sequence\nfrom typing import Generic, Literal, TypeVar, Union, get_args, get_origin\n\nfrom pydantic import BaseModel\n\nfrom mcp.server.session import ServerSession\nfrom mcp.types import RequestId\n\nElicitSchemaModelT = TypeVar(\"ElicitSchemaModelT\", bound=BaseModel)\n\n\nclass AcceptedElicitation(BaseModel, Generic[ElicitSchemaModelT]):\n    \"\"\"Result when user accepts the elicitation.\"\"\"\n\n    action: Literal[\"accept\"] = \"accept\"\n    data: ElicitSchemaModelT\n\n\nclass DeclinedElicitation(BaseModel):\n    \"\"\"Result when user declines the elicitation.\"\"\"\n\n    action: Literal[\"decline\"] = \"decline\"\n\n\nclass CancelledElicitation(BaseModel):\n    \"\"\"Result when user cancels the elicitation.\"\"\"\n\n    action: Literal[\"cancel\"] = \"cancel\"\n\n\nElicitationResult = AcceptedElicitation[ElicitSchemaModelT] | DeclinedElicitation | CancelledElicitation\n\n\nclass AcceptedUrlElicitation(BaseModel):\n    \"\"\"Result when user accepts a URL mode elicitation.\"\"\"\n\n    action: Literal[\"accept\"] = \"accept\"\n\n\nUrlElicitationResult = AcceptedUrlElicitation | DeclinedElicitation | CancelledElicitation\n\n\n# Primitive types allowed in elicitation schemas\n_ELICITATION_PRIMITIVE_TYPES = (str, int, float, bool)\n\n\ndef _validate_elicitation_schema(schema: type[BaseModel]) -> None:\n    \"\"\"Validate that a Pydantic model only contains primitive field types.\"\"\"\n    for field_name, field_info in schema.model_fields.items():\n        annotation = field_info.annotation\n\n        if annotation is None or annotation is types.NoneType:  # pragma: no cover\n            continue\n        elif _is_primitive_field(annotation):\n            continue\n        elif _is_string_sequence(annotation):\n            continue\n        else:\n            raise TypeError(\n                f\"Elicitation schema field '{field_name}' must be a primitive type \"\n                f\"{_ELICITATION_PRIMITIVE_TYPES}, a sequence of strings (list[str], etc.), \"\n                f\"or Optional of these types. Nested models and complex types are not allowed.\"\n            )\n\n\ndef _is_string_sequence(annotation: type) -> bool:\n    \"\"\"Check if annotation is a sequence of strings (list[str], Sequence[str], etc).\"\"\"\n    origin = get_origin(annotation)\n    # Check if it's a sequence-like type with str elements\n    if origin:\n        try:\n            if issubclass(origin, Sequence):\n                args = get_args(annotation)\n                # Should have single str type arg\n                return len(args) == 1 and args[0] is str\n        except TypeError:  # pragma: no cover\n            # origin is not a class, so it can't be a subclass of Sequence\n            pass\n    return False\n\n\ndef _is_primitive_field(annotation: type) -> bool:\n    \"\"\"Check if a field is a primitive type allowed in elicitation schemas.\"\"\"\n    # Handle basic primitive types\n    if annotation in _ELICITATION_PRIMITIVE_TYPES:\n        return True\n\n    # Handle Union types\n    origin = get_origin(annotation)\n    if origin is Union or origin is types.UnionType:\n        args = get_args(annotation)\n        # All args must be primitive types, None, or string sequences\n        return all(\n            arg is types.NoneType or arg in _ELICITATION_PRIMITIVE_TYPES or _is_string_sequence(arg) for arg in args\n        )\n\n    return False\n\n\nasync def elicit_with_validation(\n    session: ServerSession,\n    message: str,\n    schema: type[ElicitSchemaModelT],\n    related_request_id: RequestId | None = None,\n) -> ElicitationResult[ElicitSchemaModelT]:\n    \"\"\"Elicit information from the client/user with schema validation (form mode).\n\n    This method can be used to interactively ask for additional information from the\n    client within a tool's execution. The client might display the message to the\n    user and collect a response according to the provided schema. If the client\n    is an agent, it might decide how to handle the elicitation -- either by asking\n    the user or automatically generating a response.\n\n    For sensitive data like credentials or OAuth flows, use elicit_url() instead.\n    \"\"\"\n    # Validate that schema only contains primitive types and fail loudly if not\n    _validate_elicitation_schema(schema)\n\n    json_schema = schema.model_json_schema()\n\n    result = await session.elicit_form(\n        message=message,\n        requested_schema=json_schema,\n        related_request_id=related_request_id,\n    )\n\n    if result.action == \"accept\" and result.content is not None:\n        # Validate and parse the content using the schema\n        validated_data = schema.model_validate(result.content)\n        return AcceptedElicitation(data=validated_data)\n    elif result.action == \"decline\":\n        return DeclinedElicitation()\n    elif result.action == \"cancel\":  # pragma: no cover\n        return CancelledElicitation()\n    else:  # pragma: no cover\n        # This should never happen, but handle it just in case\n        raise ValueError(f\"Unexpected elicitation action: {result.action}\")\n\n\nasync def elicit_url(\n    session: ServerSession,\n    message: str,\n    url: str,\n    elicitation_id: str,\n    related_request_id: RequestId | None = None,\n) -> UrlElicitationResult:\n    \"\"\"Elicit information from the user via out-of-band URL navigation (URL mode).\n\n    This method directs the user to an external URL where sensitive interactions can\n    occur without passing data through the MCP client. Use this for:\n    - Collecting sensitive credentials (API keys, passwords)\n    - OAuth authorization flows with third-party services\n    - Payment and subscription flows\n    - Any interaction where data should not pass through the LLM context\n\n    The response indicates whether the user consented to navigate to the URL.\n    The actual interaction happens out-of-band. When the elicitation completes,\n    the server should send an ElicitCompleteNotification to notify the client.\n\n    Args:\n        session: The server session\n        message: Human-readable explanation of why the interaction is needed\n        url: The URL the user should navigate to\n        elicitation_id: Unique identifier for tracking this elicitation\n        related_request_id: Optional ID of the request that triggered this elicitation\n\n    Returns:\n        UrlElicitationResult indicating accept, decline, or cancel\n    \"\"\"\n    result = await session.elicit_url(\n        message=message,\n        url=url,\n        elicitation_id=elicitation_id,\n        related_request_id=related_request_id,\n    )\n\n    if result.action == \"accept\":\n        return AcceptedUrlElicitation()\n    elif result.action == \"decline\":\n        return DeclinedElicitation()\n    elif result.action == \"cancel\":\n        return CancelledElicitation()\n    else:  # pragma: no cover\n        # This should never happen, but handle it just in case\n        raise ValueError(f\"Unexpected elicitation action: {result.action}\")\n"
  },
  {
    "path": "src/mcp/server/experimental/__init__.py",
    "content": "\"\"\"Server-side experimental features.\n\nWARNING: These APIs are experimental and may change without notice.\n\nImport directly from submodules:\n- mcp.server.experimental.task_context.ServerTaskContext\n- mcp.server.experimental.task_support.TaskSupport\n- mcp.server.experimental.task_result_handler.TaskResultHandler\n- mcp.server.experimental.request_context.Experimental\n\"\"\"\n"
  },
  {
    "path": "src/mcp/server/experimental/request_context.py",
    "content": "\"\"\"Experimental request context features.\n\nThis module provides the Experimental class which gives access to experimental\nfeatures within a request context, such as task-augmented request handling.\n\nWARNING: These APIs are experimental and may change without notice.\n\"\"\"\n\nfrom collections.abc import Awaitable, Callable\nfrom dataclasses import dataclass, field\nfrom typing import Any\n\nfrom mcp.server.experimental.task_context import ServerTaskContext\nfrom mcp.server.experimental.task_support import TaskSupport\nfrom mcp.server.session import ServerSession\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.experimental.tasks.helpers import MODEL_IMMEDIATE_RESPONSE_KEY, is_terminal\nfrom mcp.types import (\n    METHOD_NOT_FOUND,\n    TASK_FORBIDDEN,\n    TASK_REQUIRED,\n    ClientCapabilities,\n    CreateTaskResult,\n    ErrorData,\n    Result,\n    TaskExecutionMode,\n    TaskMetadata,\n    Tool,\n)\n\n\n@dataclass\nclass Experimental:\n    \"\"\"Experimental features context for task-augmented requests.\n\n    Provides helpers for validating task execution compatibility and\n    running tasks with automatic lifecycle management.\n\n    WARNING: This API is experimental and may change without notice.\n    \"\"\"\n\n    task_metadata: TaskMetadata | None = None\n    _client_capabilities: ClientCapabilities | None = field(default=None, repr=False)\n    _session: ServerSession | None = field(default=None, repr=False)\n    _task_support: TaskSupport | None = field(default=None, repr=False)\n\n    @property\n    def is_task(self) -> bool:\n        \"\"\"Check if this request is task-augmented.\"\"\"\n        return self.task_metadata is not None\n\n    @property\n    def client_supports_tasks(self) -> bool:\n        \"\"\"Check if the client declared task support.\"\"\"\n        if self._client_capabilities is None:\n            return False\n        return self._client_capabilities.tasks is not None\n\n    def validate_task_mode(\n        self, tool_task_mode: TaskExecutionMode | None, *, raise_error: bool = True\n    ) -> ErrorData | None:\n        \"\"\"Validate that the request is compatible with the tool's task execution mode.\n\n        Per MCP spec:\n        - \"required\": Clients MUST invoke as a task. Server returns -32601 if not.\n        - \"forbidden\" (or None): Clients MUST NOT invoke as a task. Server returns -32601 if they do.\n        - \"optional\": Either is acceptable.\n\n        Args:\n            tool_task_mode: The tool's execution.taskSupport value\n                (\"forbidden\", \"optional\", \"required\", or None)\n            raise_error: If True, raises MCPError on validation failure. If False, returns ErrorData.\n\n        Returns:\n            None if valid, ErrorData if invalid and raise_error=False\n\n        Raises:\n            MCPError: If invalid and raise_error=True\n        \"\"\"\n\n        mode = tool_task_mode or TASK_FORBIDDEN\n\n        error: ErrorData | None = None\n\n        if mode == TASK_REQUIRED and not self.is_task:\n            error = ErrorData(code=METHOD_NOT_FOUND, message=\"This tool requires task-augmented invocation\")\n        elif mode == TASK_FORBIDDEN and self.is_task:\n            error = ErrorData(code=METHOD_NOT_FOUND, message=\"This tool does not support task-augmented invocation\")\n\n        if error is not None and raise_error:\n            raise MCPError.from_error_data(error)\n\n        return error\n\n    def validate_for_tool(self, tool: Tool, *, raise_error: bool = True) -> ErrorData | None:\n        \"\"\"Validate that the request is compatible with the given tool.\n\n        Convenience wrapper around validate_task_mode that extracts the mode from a Tool.\n\n        Args:\n            tool: The Tool definition\n            raise_error: If True, raises MCPError on validation failure.\n\n        Returns:\n            None if valid, ErrorData if invalid and raise_error=False\n        \"\"\"\n        mode = tool.execution.task_support if tool.execution else None\n        return self.validate_task_mode(mode, raise_error=raise_error)\n\n    def can_use_tool(self, tool_task_mode: TaskExecutionMode | None) -> bool:\n        \"\"\"Check if this client can use a tool with the given task mode.\n\n        Useful for filtering tool lists or providing warnings.\n        Returns False if the tool's task mode is \"required\" but the client doesn't support tasks.\n\n        Args:\n            tool_task_mode: The tool's execution.taskSupport value\n\n        Returns:\n            True if the client can use this tool, False otherwise\n        \"\"\"\n        mode = tool_task_mode or TASK_FORBIDDEN\n        if mode == TASK_REQUIRED and not self.client_supports_tasks:\n            return False\n        return True\n\n    async def run_task(\n        self,\n        work: Callable[[ServerTaskContext], Awaitable[Result]],\n        *,\n        task_id: str | None = None,\n        model_immediate_response: str | None = None,\n    ) -> CreateTaskResult:\n        \"\"\"Create a task, spawn background work, and return CreateTaskResult immediately.\n\n        This is the recommended way to handle task-augmented tool calls. It:\n        1. Creates a task in the store\n        2. Spawns the work function in a background task\n        3. Returns CreateTaskResult immediately\n\n        The work function receives a ServerTaskContext with:\n        - elicit() for sending elicitation requests\n        - create_message() for sampling requests\n        - update_status() for progress updates\n        - complete()/fail() for finishing the task\n\n        When work() returns a Result, the task is auto-completed with that result.\n        If work() raises an exception, the task is auto-failed.\n\n        Args:\n            work: Async function that does the actual work\n            task_id: Optional task ID (generated if not provided)\n            model_immediate_response: Optional string to include in _meta as\n                io.modelcontextprotocol/model-immediate-response\n\n        Returns:\n            CreateTaskResult to return to the client\n\n        Raises:\n            RuntimeError: If task support is not enabled or task_metadata is missing\n\n        Example:\n            ```python\n            async def handle_tool(ctx: RequestContext, params: CallToolRequestParams) -> CallToolResult:\n                async def work(task: ServerTaskContext) -> CallToolResult:\n                    result = await task.elicit(\n                        message=\"Are you sure?\",\n                        requested_schema={\"type\": \"object\", ...}\n                    )\n                    confirmed = result.content.get(\"confirm\", False)\n                    return CallToolResult(content=[TextContent(text=\"Done\" if confirmed else \"Cancelled\")])\n\n                return await ctx.experimental.run_task(work)\n            ```\n\n        WARNING: This API is experimental and may change without notice.\n        \"\"\"\n        if self._task_support is None:\n            raise RuntimeError(\"Task support not enabled. Call server.experimental.enable_tasks() first.\")\n        if self._session is None:\n            raise RuntimeError(\"Session not available.\")\n        if self.task_metadata is None:\n            raise RuntimeError(\n                \"Request is not task-augmented (no task field in params). \"\n                \"The client must send a task-augmented request.\"\n            )\n\n        support = self._task_support\n        # Access task_group via TaskSupport - raises if not in run() context\n        task_group = support.task_group\n\n        task = await support.store.create_task(self.task_metadata, task_id)\n\n        task_ctx = ServerTaskContext(\n            task=task,\n            store=support.store,\n            session=self._session,\n            queue=support.queue,\n            handler=support.handler,\n        )\n\n        async def execute() -> None:\n            try:\n                result = await work(task_ctx)\n                if not is_terminal(task_ctx.task.status):\n                    await task_ctx.complete(result)\n            except Exception as e:\n                if not is_terminal(task_ctx.task.status):\n                    await task_ctx.fail(str(e))\n\n        task_group.start_soon(execute)\n\n        meta: dict[str, Any] | None = None\n        if model_immediate_response is not None:\n            meta = {MODEL_IMMEDIATE_RESPONSE_KEY: model_immediate_response}\n\n        return CreateTaskResult(task=task, **{\"_meta\": meta} if meta else {})\n"
  },
  {
    "path": "src/mcp/server/experimental/session_features.py",
    "content": "\"\"\"Experimental server session features for server→client task operations.\n\nThis module provides the server-side equivalent of ExperimentalClientFeatures,\nallowing the server to send task-augmented requests to the client and poll for results.\n\nWARNING: These APIs are experimental and may change without notice.\n\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom typing import TYPE_CHECKING, Any, TypeVar\n\nfrom mcp import types\nfrom mcp.server.validation import validate_sampling_tools, validate_tool_use_result_messages\nfrom mcp.shared.experimental.tasks.capabilities import (\n    require_task_augmented_elicitation,\n    require_task_augmented_sampling,\n)\nfrom mcp.shared.experimental.tasks.polling import poll_until_terminal\n\nif TYPE_CHECKING:\n    from mcp.server.session import ServerSession\n\nResultT = TypeVar(\"ResultT\", bound=types.Result)\n\n\nclass ExperimentalServerSessionFeatures:\n    \"\"\"Experimental server session features for server→client task operations.\n\n    This provides the server-side equivalent of ExperimentalClientFeatures,\n    allowing the server to send task-augmented requests to the client and\n    poll for results.\n\n    WARNING: These APIs are experimental and may change without notice.\n\n    Access via session.experimental:\n        result = await session.experimental.elicit_as_task(...)\n    \"\"\"\n\n    def __init__(self, session: \"ServerSession\") -> None:\n        self._session = session\n\n    async def get_task(self, task_id: str) -> types.GetTaskResult:\n        \"\"\"Send tasks/get to the client to get task status.\n\n        Args:\n            task_id: The task identifier\n\n        Returns:\n            GetTaskResult containing the task status\n        \"\"\"\n        return await self._session.send_request(\n            types.GetTaskRequest(params=types.GetTaskRequestParams(task_id=task_id)),\n            types.GetTaskResult,\n        )\n\n    async def get_task_result(\n        self,\n        task_id: str,\n        result_type: type[ResultT],\n    ) -> ResultT:\n        \"\"\"Send tasks/result to the client to retrieve the final result.\n\n        Args:\n            task_id: The task identifier\n            result_type: The expected result type\n\n        Returns:\n            The task result, validated against result_type\n        \"\"\"\n        return await self._session.send_request(\n            types.GetTaskPayloadRequest(params=types.GetTaskPayloadRequestParams(task_id=task_id)),\n            result_type,\n        )\n\n    async def poll_task(self, task_id: str) -> AsyncIterator[types.GetTaskResult]:\n        \"\"\"Poll a client task until it reaches terminal status.\n\n        Yields GetTaskResult for each poll, allowing the caller to react to\n        status changes. Exits when task reaches a terminal status.\n\n        Respects the pollInterval hint from the client.\n\n        Args:\n            task_id: The task identifier\n\n        Yields:\n            GetTaskResult for each poll\n        \"\"\"\n        async for status in poll_until_terminal(self.get_task, task_id):\n            yield status\n\n    async def elicit_as_task(\n        self,\n        message: str,\n        requested_schema: types.ElicitRequestedSchema,\n        *,\n        ttl: int = 60000,\n    ) -> types.ElicitResult:\n        \"\"\"Send a task-augmented elicitation to the client and poll until complete.\n\n        The client will create a local task, process the elicitation asynchronously,\n        and return the result when ready. This method handles the full flow:\n        1. Send elicitation with task field\n        2. Receive CreateTaskResult from client\n        3. Poll client's task until terminal\n        4. Retrieve and return the final ElicitResult\n\n        Args:\n            message: The message to present to the user\n            requested_schema: Schema defining the expected response\n            ttl: Task time-to-live in milliseconds\n\n        Returns:\n            The client's elicitation response\n\n        Raises:\n            MCPError: If client doesn't support task-augmented elicitation\n        \"\"\"\n        client_caps = self._session.client_params.capabilities if self._session.client_params else None\n        require_task_augmented_elicitation(client_caps)\n\n        create_result = await self._session.send_request(\n            types.ElicitRequest(\n                params=types.ElicitRequestFormParams(\n                    message=message,\n                    requested_schema=requested_schema,\n                    task=types.TaskMetadata(ttl=ttl),\n                )\n            ),\n            types.CreateTaskResult,\n        )\n\n        task_id = create_result.task.task_id\n\n        async for _ in self.poll_task(task_id):\n            pass\n\n        return await self.get_task_result(task_id, types.ElicitResult)\n\n    async def create_message_as_task(\n        self,\n        messages: list[types.SamplingMessage],\n        *,\n        max_tokens: int,\n        ttl: int = 60000,\n        system_prompt: str | None = None,\n        include_context: types.IncludeContext | None = None,\n        temperature: float | None = None,\n        stop_sequences: list[str] | None = None,\n        metadata: dict[str, Any] | None = None,\n        model_preferences: types.ModelPreferences | None = None,\n        tools: list[types.Tool] | None = None,\n        tool_choice: types.ToolChoice | None = None,\n    ) -> types.CreateMessageResult:\n        \"\"\"Send a task-augmented sampling request and poll until complete.\n\n        The client will create a local task, process the sampling request\n        asynchronously, and return the result when ready.\n\n        Args:\n            messages: The conversation messages for sampling\n            max_tokens: Maximum tokens in the response\n            ttl: Task time-to-live in milliseconds\n            system_prompt: Optional system prompt\n            include_context: Context inclusion strategy\n            temperature: Sampling temperature\n            stop_sequences: Stop sequences\n            metadata: Additional metadata\n            model_preferences: Model selection preferences\n            tools: Optional list of tools the LLM can use during sampling\n            tool_choice: Optional control over tool usage behavior\n\n        Returns:\n            The sampling result from the client\n\n        Raises:\n            MCPError: If client doesn't support task-augmented sampling or tools\n            ValueError: If tool_use or tool_result message structure is invalid\n        \"\"\"\n        client_caps = self._session.client_params.capabilities if self._session.client_params else None\n        require_task_augmented_sampling(client_caps)\n        validate_sampling_tools(client_caps, tools, tool_choice)\n        validate_tool_use_result_messages(messages)\n\n        create_result = await self._session.send_request(\n            types.CreateMessageRequest(\n                params=types.CreateMessageRequestParams(\n                    messages=messages,\n                    max_tokens=max_tokens,\n                    system_prompt=system_prompt,\n                    include_context=include_context,\n                    temperature=temperature,\n                    stop_sequences=stop_sequences,\n                    metadata=metadata,\n                    model_preferences=model_preferences,\n                    tools=tools,\n                    tool_choice=tool_choice,\n                    task=types.TaskMetadata(ttl=ttl),\n                )\n            ),\n            types.CreateTaskResult,\n        )\n\n        task_id = create_result.task.task_id\n\n        async for _ in self.poll_task(task_id):\n            pass\n\n        return await self.get_task_result(task_id, types.CreateMessageResult)\n"
  },
  {
    "path": "src/mcp/server/experimental/task_context.py",
    "content": "\"\"\"ServerTaskContext - Server-integrated task context with elicitation and sampling.\n\nThis wraps the pure TaskContext and adds server-specific functionality:\n- Elicitation (task.elicit())\n- Sampling (task.create_message())\n- Status notifications\n\"\"\"\n\nfrom typing import Any\n\nimport anyio\n\nfrom mcp.server.experimental.task_result_handler import TaskResultHandler\nfrom mcp.server.session import ServerSession\nfrom mcp.server.validation import validate_sampling_tools, validate_tool_use_result_messages\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.experimental.tasks.capabilities import (\n    require_task_augmented_elicitation,\n    require_task_augmented_sampling,\n)\nfrom mcp.shared.experimental.tasks.context import TaskContext\nfrom mcp.shared.experimental.tasks.message_queue import QueuedMessage, TaskMessageQueue\nfrom mcp.shared.experimental.tasks.resolver import Resolver\nfrom mcp.shared.experimental.tasks.store import TaskStore\nfrom mcp.types import (\n    INVALID_REQUEST,\n    TASK_STATUS_INPUT_REQUIRED,\n    TASK_STATUS_WORKING,\n    ClientCapabilities,\n    CreateMessageResult,\n    CreateTaskResult,\n    ElicitationCapability,\n    ElicitRequestedSchema,\n    ElicitResult,\n    IncludeContext,\n    ModelPreferences,\n    RequestId,\n    Result,\n    SamplingCapability,\n    SamplingMessage,\n    Task,\n    TaskMetadata,\n    TaskStatusNotification,\n    TaskStatusNotificationParams,\n    Tool,\n    ToolChoice,\n)\n\n\nclass ServerTaskContext:\n    \"\"\"Server-integrated task context with elicitation and sampling.\n\n    This wraps a pure TaskContext and adds server-specific functionality:\n    - elicit() for sending elicitation requests to the client\n    - create_message() for sampling requests\n    - Status notifications via the session\n\n    Example:\n        ```python\n        async def my_task_work(task: ServerTaskContext) -> CallToolResult:\n            await task.update_status(\"Starting...\")\n\n            result = await task.elicit(\n                message=\"Continue?\",\n                requested_schema={\"type\": \"object\", \"properties\": {\"ok\": {\"type\": \"boolean\"}}}\n            )\n\n            if result.content.get(\"ok\"):\n                return CallToolResult(content=[TextContent(text=\"Done!\")])\n            else:\n                return CallToolResult(content=[TextContent(text=\"Cancelled\")])\n        ```\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        task: Task,\n        store: TaskStore,\n        session: ServerSession,\n        queue: TaskMessageQueue,\n        handler: TaskResultHandler | None = None,\n    ):\n        \"\"\"Create a ServerTaskContext.\n\n        Args:\n            task: The Task object\n            store: The task store\n            session: The server session\n            queue: The message queue for elicitation/sampling\n            handler: The result handler for response routing (required for elicit/create_message)\n        \"\"\"\n        self._ctx = TaskContext(task=task, store=store)\n        self._session = session\n        self._queue = queue\n        self._handler = handler\n        self._store = store\n\n    # Delegate pure properties to inner context\n\n    @property\n    def task_id(self) -> str:\n        \"\"\"The task identifier.\"\"\"\n        return self._ctx.task_id\n\n    @property\n    def task(self) -> Task:\n        \"\"\"The current task state.\"\"\"\n        return self._ctx.task\n\n    @property\n    def is_cancelled(self) -> bool:\n        \"\"\"Whether cancellation has been requested.\"\"\"\n        return self._ctx.is_cancelled\n\n    def request_cancellation(self) -> None:\n        \"\"\"Request cancellation of this task.\"\"\"\n        self._ctx.request_cancellation()\n\n    # Enhanced methods with notifications\n\n    async def update_status(self, message: str, *, notify: bool = True) -> None:\n        \"\"\"Update the task's status message.\n\n        Args:\n            message: The new status message\n            notify: Whether to send a notification to the client\n        \"\"\"\n        await self._ctx.update_status(message)\n        if notify:\n            await self._send_notification()\n\n    async def complete(self, result: Result, *, notify: bool = True) -> None:\n        \"\"\"Mark the task as completed with the given result.\n\n        Args:\n            result: The task result\n            notify: Whether to send a notification to the client\n        \"\"\"\n        await self._ctx.complete(result)\n        if notify:\n            await self._send_notification()\n\n    async def fail(self, error: str, *, notify: bool = True) -> None:\n        \"\"\"Mark the task as failed with an error message.\n\n        Args:\n            error: The error message\n            notify: Whether to send a notification to the client\n        \"\"\"\n        await self._ctx.fail(error)\n        if notify:\n            await self._send_notification()\n\n    async def _send_notification(self) -> None:\n        \"\"\"Send a task status notification to the client.\"\"\"\n        task = self._ctx.task\n        await self._session.send_notification(\n            TaskStatusNotification(\n                params=TaskStatusNotificationParams(\n                    task_id=task.task_id,\n                    status=task.status,\n                    status_message=task.status_message,\n                    created_at=task.created_at,\n                    last_updated_at=task.last_updated_at,\n                    ttl=task.ttl,\n                    poll_interval=task.poll_interval,\n                )\n            )\n        )\n\n    # Server-specific methods: elicitation and sampling\n\n    def _check_elicitation_capability(self) -> None:\n        \"\"\"Check if the client supports elicitation.\"\"\"\n        if not self._session.check_client_capability(ClientCapabilities(elicitation=ElicitationCapability())):\n            raise MCPError(code=INVALID_REQUEST, message=\"Client does not support elicitation capability\")\n\n    def _check_sampling_capability(self) -> None:\n        \"\"\"Check if the client supports sampling.\"\"\"\n        if not self._session.check_client_capability(ClientCapabilities(sampling=SamplingCapability())):\n            raise MCPError(code=INVALID_REQUEST, message=\"Client does not support sampling capability\")\n\n    async def elicit(\n        self,\n        message: str,\n        requested_schema: ElicitRequestedSchema,\n    ) -> ElicitResult:\n        \"\"\"Send an elicitation request via the task message queue.\n\n        This method:\n        1. Checks client capability\n        2. Updates task status to \"input_required\"\n        3. Queues the elicitation request\n        4. Waits for the response (delivered via tasks/result round-trip)\n        5. Updates task status back to \"working\"\n        6. Returns the result\n\n        Args:\n            message: The message to present to the user\n            requested_schema: Schema defining the expected response structure\n\n        Returns:\n            The client's response\n\n        Raises:\n            MCPError: If client doesn't support elicitation capability\n        \"\"\"\n        self._check_elicitation_capability()\n\n        if self._handler is None:\n            raise RuntimeError(\"handler is required for elicit(). Pass handler= to ServerTaskContext.\")\n\n        # Update status to input_required\n        await self._store.update_task(self.task_id, status=TASK_STATUS_INPUT_REQUIRED)\n\n        # Build the request using session's helper\n        request = self._session._build_elicit_form_request(  # pyright: ignore[reportPrivateUsage]\n            message=message,\n            requested_schema=requested_schema,\n            related_task_id=self.task_id,\n        )\n        request_id: RequestId = request.id\n\n        resolver: Resolver[dict[str, Any]] = Resolver()\n        self._handler._pending_requests[request_id] = resolver  # pyright: ignore[reportPrivateUsage]\n\n        queued = QueuedMessage(\n            type=\"request\",\n            message=request,\n            resolver=resolver,\n            original_request_id=request_id,\n        )\n        await self._queue.enqueue(self.task_id, queued)\n\n        try:\n            # Wait for response (routed back via TaskResultHandler)\n            response_data = await resolver.wait()\n            await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)\n            return ElicitResult.model_validate(response_data)\n        except anyio.get_cancelled_exc_class():\n            # This path is tested in test_elicit_restores_status_on_cancellation\n            # which verifies status is restored to \"working\" after cancellation.\n            await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)\n            raise\n\n    async def elicit_url(\n        self,\n        message: str,\n        url: str,\n        elicitation_id: str,\n    ) -> ElicitResult:\n        \"\"\"Send a URL mode elicitation request via the task message queue.\n\n        This directs the user to an external URL for out-of-band interactions\n        like OAuth flows, credential collection, or payment processing.\n\n        This method:\n        1. Checks client capability\n        2. Updates task status to \"input_required\"\n        3. Queues the elicitation request\n        4. Waits for the response (delivered via tasks/result round-trip)\n        5. Updates task status back to \"working\"\n        6. Returns the result\n\n        Args:\n            message: Human-readable explanation of why the interaction is needed\n            url: The URL the user should navigate to\n            elicitation_id: Unique identifier for tracking this elicitation\n\n        Returns:\n            The client's response indicating acceptance, decline, or cancellation\n\n        Raises:\n            MCPError: If client doesn't support elicitation capability\n            RuntimeError: If handler is not configured\n        \"\"\"\n        self._check_elicitation_capability()\n\n        if self._handler is None:\n            raise RuntimeError(\"handler is required for elicit_url(). Pass handler= to ServerTaskContext.\")\n\n        # Update status to input_required\n        await self._store.update_task(self.task_id, status=TASK_STATUS_INPUT_REQUIRED)\n\n        # Build the request using session's helper\n        request = self._session._build_elicit_url_request(  # pyright: ignore[reportPrivateUsage]\n            message=message,\n            url=url,\n            elicitation_id=elicitation_id,\n            related_task_id=self.task_id,\n        )\n        request_id: RequestId = request.id\n\n        resolver: Resolver[dict[str, Any]] = Resolver()\n        self._handler._pending_requests[request_id] = resolver  # pyright: ignore[reportPrivateUsage]\n\n        queued = QueuedMessage(\n            type=\"request\",\n            message=request,\n            resolver=resolver,\n            original_request_id=request_id,\n        )\n        await self._queue.enqueue(self.task_id, queued)\n\n        try:\n            # Wait for response (routed back via TaskResultHandler)\n            response_data = await resolver.wait()\n            await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)\n            return ElicitResult.model_validate(response_data)\n        except anyio.get_cancelled_exc_class():  # pragma: no cover\n            await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)\n            raise\n\n    async def create_message(\n        self,\n        messages: list[SamplingMessage],\n        *,\n        max_tokens: int,\n        system_prompt: str | None = None,\n        include_context: IncludeContext | None = None,\n        temperature: float | None = None,\n        stop_sequences: list[str] | None = None,\n        metadata: dict[str, Any] | None = None,\n        model_preferences: ModelPreferences | None = None,\n        tools: list[Tool] | None = None,\n        tool_choice: ToolChoice | None = None,\n    ) -> CreateMessageResult:\n        \"\"\"Send a sampling request via the task message queue.\n\n        This method:\n        1. Checks client capability\n        2. Updates task status to \"input_required\"\n        3. Queues the sampling request\n        4. Waits for the response (delivered via tasks/result round-trip)\n        5. Updates task status back to \"working\"\n        6. Returns the result\n\n        Args:\n            messages: The conversation messages for sampling\n            max_tokens: Maximum tokens in the response\n            system_prompt: Optional system prompt\n            include_context: Context inclusion strategy\n            temperature: Sampling temperature\n            stop_sequences: Stop sequences\n            metadata: Additional metadata\n            model_preferences: Model selection preferences\n            tools: Optional list of tools the LLM can use during sampling\n            tool_choice: Optional control over tool usage behavior\n\n        Returns:\n            The sampling result from the client\n\n        Raises:\n            MCPError: If client doesn't support sampling capability or tools\n            ValueError: If tool_use or tool_result message structure is invalid\n        \"\"\"\n        self._check_sampling_capability()\n        client_caps = self._session.client_params.capabilities if self._session.client_params else None\n        validate_sampling_tools(client_caps, tools, tool_choice)\n        validate_tool_use_result_messages(messages)\n\n        if self._handler is None:\n            raise RuntimeError(\"handler is required for create_message(). Pass handler= to ServerTaskContext.\")\n\n        # Update status to input_required\n        await self._store.update_task(self.task_id, status=TASK_STATUS_INPUT_REQUIRED)\n\n        # Build the request using session's helper\n        request = self._session._build_create_message_request(  # pyright: ignore[reportPrivateUsage]\n            messages=messages,\n            max_tokens=max_tokens,\n            system_prompt=system_prompt,\n            include_context=include_context,\n            temperature=temperature,\n            stop_sequences=stop_sequences,\n            metadata=metadata,\n            model_preferences=model_preferences,\n            tools=tools,\n            tool_choice=tool_choice,\n            related_task_id=self.task_id,\n        )\n        request_id: RequestId = request.id\n\n        resolver: Resolver[dict[str, Any]] = Resolver()\n        self._handler._pending_requests[request_id] = resolver  # pyright: ignore[reportPrivateUsage]\n\n        queued = QueuedMessage(\n            type=\"request\",\n            message=request,\n            resolver=resolver,\n            original_request_id=request_id,\n        )\n        await self._queue.enqueue(self.task_id, queued)\n\n        try:\n            # Wait for response (routed back via TaskResultHandler)\n            response_data = await resolver.wait()\n            await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)\n            return CreateMessageResult.model_validate(response_data)\n        except anyio.get_cancelled_exc_class():\n            # This path is tested in test_create_message_restores_status_on_cancellation\n            # which verifies status is restored to \"working\" after cancellation.\n            await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)\n            raise\n\n    async def elicit_as_task(\n        self,\n        message: str,\n        requested_schema: ElicitRequestedSchema,\n        *,\n        ttl: int = 60000,\n    ) -> ElicitResult:\n        \"\"\"Send a task-augmented elicitation via the queue, then poll client.\n\n        This is for use inside a task-augmented tool call when you want the client\n        to handle the elicitation as its own task. The elicitation request is queued\n        and delivered when the client calls tasks/result. After the client responds\n        with CreateTaskResult, we poll the client's task until complete.\n\n        Args:\n            message: The message to present to the user\n            requested_schema: Schema defining the expected response structure\n            ttl: Task time-to-live in milliseconds for the client's task\n\n        Returns:\n            The client's elicitation response\n\n        Raises:\n            MCPError: If client doesn't support task-augmented elicitation\n            RuntimeError: If handler is not configured\n        \"\"\"\n        client_caps = self._session.client_params.capabilities if self._session.client_params else None\n        require_task_augmented_elicitation(client_caps)\n\n        if self._handler is None:\n            raise RuntimeError(\"handler is required for elicit_as_task()\")\n\n        # Update status to input_required\n        await self._store.update_task(self.task_id, status=TASK_STATUS_INPUT_REQUIRED)\n\n        request = self._session._build_elicit_form_request(  # pyright: ignore[reportPrivateUsage]\n            message=message,\n            requested_schema=requested_schema,\n            related_task_id=self.task_id,\n            task=TaskMetadata(ttl=ttl),\n        )\n        request_id: RequestId = request.id\n\n        resolver: Resolver[dict[str, Any]] = Resolver()\n        self._handler._pending_requests[request_id] = resolver  # pyright: ignore[reportPrivateUsage]\n\n        queued = QueuedMessage(\n            type=\"request\",\n            message=request,\n            resolver=resolver,\n            original_request_id=request_id,\n        )\n        await self._queue.enqueue(self.task_id, queued)\n\n        try:\n            # Wait for initial response (CreateTaskResult from client)\n            response_data = await resolver.wait()\n            create_result = CreateTaskResult.model_validate(response_data)\n            client_task_id = create_result.task.task_id\n\n            # Poll the client's task using session.experimental\n            async for _ in self._session.experimental.poll_task(client_task_id):\n                pass\n\n            # Get final result from client\n            result = await self._session.experimental.get_task_result(\n                client_task_id,\n                ElicitResult,\n            )\n\n            await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)\n            return result\n\n        except anyio.get_cancelled_exc_class():  # pragma: no cover\n            await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)\n            raise\n\n    async def create_message_as_task(\n        self,\n        messages: list[SamplingMessage],\n        *,\n        max_tokens: int,\n        ttl: int = 60000,\n        system_prompt: str | None = None,\n        include_context: IncludeContext | None = None,\n        temperature: float | None = None,\n        stop_sequences: list[str] | None = None,\n        metadata: dict[str, Any] | None = None,\n        model_preferences: ModelPreferences | None = None,\n        tools: list[Tool] | None = None,\n        tool_choice: ToolChoice | None = None,\n    ) -> CreateMessageResult:\n        \"\"\"Send a task-augmented sampling request via the queue, then poll client.\n\n        This is for use inside a task-augmented tool call when you want the client\n        to handle the sampling as its own task. The request is queued and delivered\n        when the client calls tasks/result. After the client responds with\n        CreateTaskResult, we poll the client's task until complete.\n\n        Args:\n            messages: The conversation messages for sampling\n            max_tokens: Maximum tokens in the response\n            ttl: Task time-to-live in milliseconds for the client's task\n            system_prompt: Optional system prompt\n            include_context: Context inclusion strategy\n            temperature: Sampling temperature\n            stop_sequences: Stop sequences\n            metadata: Additional metadata\n            model_preferences: Model selection preferences\n            tools: Optional list of tools the LLM can use during sampling\n            tool_choice: Optional control over tool usage behavior\n\n        Returns:\n            The sampling result from the client\n\n        Raises:\n            MCPError: If client doesn't support task-augmented sampling or tools\n            ValueError: If tool_use or tool_result message structure is invalid\n            RuntimeError: If handler is not configured\n        \"\"\"\n        client_caps = self._session.client_params.capabilities if self._session.client_params else None\n        require_task_augmented_sampling(client_caps)\n        validate_sampling_tools(client_caps, tools, tool_choice)\n        validate_tool_use_result_messages(messages)\n\n        if self._handler is None:\n            raise RuntimeError(\"handler is required for create_message_as_task()\")\n\n        # Update status to input_required\n        await self._store.update_task(self.task_id, status=TASK_STATUS_INPUT_REQUIRED)\n\n        # Build request WITH task field for task-augmented sampling\n        request = self._session._build_create_message_request(  # pyright: ignore[reportPrivateUsage]\n            messages=messages,\n            max_tokens=max_tokens,\n            system_prompt=system_prompt,\n            include_context=include_context,\n            temperature=temperature,\n            stop_sequences=stop_sequences,\n            metadata=metadata,\n            model_preferences=model_preferences,\n            tools=tools,\n            tool_choice=tool_choice,\n            related_task_id=self.task_id,\n            task=TaskMetadata(ttl=ttl),\n        )\n        request_id: RequestId = request.id\n\n        resolver: Resolver[dict[str, Any]] = Resolver()\n        self._handler._pending_requests[request_id] = resolver  # pyright: ignore[reportPrivateUsage]\n\n        queued = QueuedMessage(\n            type=\"request\",\n            message=request,\n            resolver=resolver,\n            original_request_id=request_id,\n        )\n        await self._queue.enqueue(self.task_id, queued)\n\n        try:\n            # Wait for initial response (CreateTaskResult from client)\n            response_data = await resolver.wait()\n            create_result = CreateTaskResult.model_validate(response_data)\n            client_task_id = create_result.task.task_id\n\n            # Poll the client's task using session.experimental\n            async for _ in self._session.experimental.poll_task(client_task_id):\n                pass\n\n            # Get final result from client\n            result = await self._session.experimental.get_task_result(\n                client_task_id,\n                CreateMessageResult,\n            )\n\n            await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)\n            return result\n\n        except anyio.get_cancelled_exc_class():  # pragma: no cover\n            await self._store.update_task(self.task_id, status=TASK_STATUS_WORKING)\n            raise\n"
  },
  {
    "path": "src/mcp/server/experimental/task_result_handler.py",
    "content": "\"\"\"TaskResultHandler - Integrated handler for tasks/result endpoint.\n\nThis implements the dequeue-send-wait pattern from the MCP Tasks spec:\n1. Dequeue all pending messages for the task\n2. Send them to the client via transport with relatedRequestId routing\n3. Wait if task is not in terminal state\n4. Return final result when task completes\n\nThis is the core of the task message queue pattern.\n\"\"\"\n\nimport logging\nfrom typing import Any\n\nimport anyio\n\nfrom mcp.server.session import ServerSession\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.experimental.tasks.helpers import RELATED_TASK_METADATA_KEY, is_terminal\nfrom mcp.shared.experimental.tasks.message_queue import TaskMessageQueue\nfrom mcp.shared.experimental.tasks.resolver import Resolver\nfrom mcp.shared.experimental.tasks.store import TaskStore\nfrom mcp.shared.message import ServerMessageMetadata, SessionMessage\nfrom mcp.types import (\n    INVALID_PARAMS,\n    ErrorData,\n    GetTaskPayloadRequest,\n    GetTaskPayloadResult,\n    RelatedTaskMetadata,\n    RequestId,\n)\n\nlogger = logging.getLogger(__name__)\n\n\nclass TaskResultHandler:\n    \"\"\"Handler for tasks/result that implements the message queue pattern.\n\n    This handler:\n    1. Dequeues pending messages (elicitations, notifications) for the task\n    2. Sends them to the client via the response stream\n    3. Waits for responses and resolves them back to callers\n    4. Blocks until task reaches terminal state\n    5. Returns the final result\n\n    Usage:\n        async def handle_task_result(\n            ctx: ServerRequestContext, params: GetTaskPayloadRequestParams\n        ) -> GetTaskPayloadResult:\n            ...\n\n        server.experimental.enable_tasks(\n            on_task_result=handle_task_result,\n        )\n    \"\"\"\n\n    def __init__(\n        self,\n        store: TaskStore,\n        queue: TaskMessageQueue,\n    ):\n        self._store = store\n        self._queue = queue\n        # Map from internal request ID to resolver for routing responses\n        self._pending_requests: dict[RequestId, Resolver[dict[str, Any]]] = {}\n\n    async def send_message(\n        self,\n        session: ServerSession,\n        message: SessionMessage,\n    ) -> None:\n        \"\"\"Send a message via the session.\n\n        This is a helper for delivering queued task messages.\n        \"\"\"\n        await session.send_message(message)\n\n    async def handle(\n        self,\n        request: GetTaskPayloadRequest,\n        session: ServerSession,\n        request_id: RequestId,\n    ) -> GetTaskPayloadResult:\n        \"\"\"Handle a tasks/result request.\n\n        This implements the dequeue-send-wait loop:\n        1. Dequeue all pending messages\n        2. Send each via transport with relatedRequestId = this request's ID\n        3. If task not terminal, wait for status change\n        4. Loop until task is terminal\n        5. Return final result\n\n        Args:\n            request: The GetTaskPayloadRequest\n            session: The server session for sending messages\n            request_id: The request ID for relatedRequestId routing\n\n        Returns:\n            GetTaskPayloadResult with the task's final payload\n        \"\"\"\n        task_id = request.params.task_id\n\n        while True:\n            task = await self._store.get_task(task_id)\n            if task is None:\n                raise MCPError(code=INVALID_PARAMS, message=f\"Task not found: {task_id}\")\n\n            await self._deliver_queued_messages(task_id, session, request_id)\n\n            # If task is terminal, return result\n            if is_terminal(task.status):\n                result = await self._store.get_result(task_id)\n                # GetTaskPayloadResult is a Result with extra=\"allow\"\n                # The stored result contains the actual payload data\n                # Per spec: tasks/result MUST include _meta with related-task metadata\n                related_task = RelatedTaskMetadata(task_id=task_id)\n                related_task_meta: dict[str, Any] = {RELATED_TASK_METADATA_KEY: related_task.model_dump(by_alias=True)}\n                if result is not None:\n                    result_data = result.model_dump(by_alias=True)\n                    existing_meta: dict[str, Any] = result_data.get(\"_meta\") or {}\n                    result_data[\"_meta\"] = {**existing_meta, **related_task_meta}\n                    return GetTaskPayloadResult.model_validate(result_data)\n                return GetTaskPayloadResult.model_validate({\"_meta\": related_task_meta})\n\n            # Wait for task update (status change or new messages)\n            await self._wait_for_task_update(task_id)\n\n    async def _deliver_queued_messages(\n        self,\n        task_id: str,\n        session: ServerSession,\n        request_id: RequestId,\n    ) -> None:\n        \"\"\"Dequeue and send all pending messages for a task.\n\n        Each message is sent via the session's write stream with\n        relatedRequestId set so responses route back to this stream.\n        \"\"\"\n        while True:\n            message = await self._queue.dequeue(task_id)\n            if message is None:\n                break\n\n            # If this is a request (not notification), wait for response\n            if message.type == \"request\" and message.resolver is not None:\n                # Store the resolver so we can route the response back\n                original_id = message.original_request_id\n                if original_id is not None:\n                    self._pending_requests[original_id] = message.resolver\n\n            logger.debug(\"Delivering queued message for task %s: %s\", task_id, message.type)\n\n            # Send the message with relatedRequestId for routing\n            session_message = SessionMessage(\n                message=message.message,\n                metadata=ServerMessageMetadata(related_request_id=request_id),\n            )\n            await self.send_message(session, session_message)\n\n    async def _wait_for_task_update(self, task_id: str) -> None:\n        \"\"\"Wait for task to be updated (status change or new message).\n\n        Races between store update and queue message - first one wins.\n        \"\"\"\n        async with anyio.create_task_group() as tg:\n\n            async def wait_for_store() -> None:\n                try:\n                    await self._store.wait_for_update(task_id)\n                except Exception:\n                    pass\n                finally:\n                    tg.cancel_scope.cancel()\n\n            async def wait_for_queue() -> None:\n                try:\n                    await self._queue.wait_for_message(task_id)\n                except Exception:\n                    pass\n                finally:\n                    tg.cancel_scope.cancel()\n\n            tg.start_soon(wait_for_store)\n            tg.start_soon(wait_for_queue)\n\n    def route_response(self, request_id: RequestId, response: dict[str, Any]) -> bool:\n        \"\"\"Route a response back to the waiting resolver.\n\n        This is called when a response arrives for a queued request.\n\n        Args:\n            request_id: The request ID from the response\n            response: The response data\n\n        Returns:\n            True if response was routed, False if no pending request\n        \"\"\"\n        resolver = self._pending_requests.pop(request_id, None)\n        if resolver is not None and not resolver.done():\n            resolver.set_result(response)\n            return True\n        return False\n\n    def route_error(self, request_id: RequestId, error: ErrorData) -> bool:\n        \"\"\"Route an error back to the waiting resolver.\n\n        Args:\n            request_id: The request ID from the error response\n            error: The error data\n\n        Returns:\n            True if error was routed, False if no pending request\n        \"\"\"\n        resolver = self._pending_requests.pop(request_id, None)\n        if resolver is not None and not resolver.done():\n            resolver.set_exception(MCPError.from_error_data(error))\n            return True\n        return False\n"
  },
  {
    "path": "src/mcp/server/experimental/task_support.py",
    "content": "\"\"\"TaskSupport - Configuration for experimental task support.\n\nThis module provides the TaskSupport class which encapsulates all the\ninfrastructure needed for task-augmented requests: store, queue, and handler.\n\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom dataclasses import dataclass, field\n\nimport anyio\nfrom anyio.abc import TaskGroup\n\nfrom mcp.server.experimental.task_result_handler import TaskResultHandler\nfrom mcp.server.session import ServerSession\nfrom mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore\nfrom mcp.shared.experimental.tasks.message_queue import InMemoryTaskMessageQueue, TaskMessageQueue\nfrom mcp.shared.experimental.tasks.store import TaskStore\n\n\n@dataclass\nclass TaskSupport:\n    \"\"\"Configuration for experimental task support.\n\n    Encapsulates the task store, message queue, result handler, and task group\n    for spawning background work.\n\n    When enabled on a server, this automatically:\n    - Configures response routing for each session\n    - Provides default handlers for task operations\n    - Manages a task group for background task execution\n\n    Example:\n        Simple in-memory setup:\n\n        ```python\n        server.experimental.enable_tasks()\n        ```\n\n        Custom store/queue for distributed systems:\n\n        ```python\n        server.experimental.enable_tasks(\n            store=RedisTaskStore(redis_url),\n            queue=RedisTaskMessageQueue(redis_url),\n        )\n        ```\n    \"\"\"\n\n    store: TaskStore\n    queue: TaskMessageQueue\n    handler: TaskResultHandler = field(init=False)\n    _task_group: TaskGroup | None = field(init=False, default=None)\n\n    def __post_init__(self) -> None:\n        \"\"\"Create the result handler from store and queue.\"\"\"\n        self.handler = TaskResultHandler(self.store, self.queue)\n\n    @property\n    def task_group(self) -> TaskGroup:\n        \"\"\"Get the task group for spawning background work.\n\n        Raises:\n            RuntimeError: If not within a run() context\n        \"\"\"\n        if self._task_group is None:\n            raise RuntimeError(\"TaskSupport not running. Ensure Server.run() is active.\")\n        return self._task_group\n\n    @asynccontextmanager\n    async def run(self) -> AsyncIterator[None]:\n        \"\"\"Run the task support lifecycle.\n\n        This creates a task group for spawning background task work.\n        Called automatically by Server.run().\n\n        Usage:\n            async with task_support.run():\n                # Task group is now available\n                ...\n        \"\"\"\n        async with anyio.create_task_group() as tg:\n            self._task_group = tg\n            try:\n                yield\n            finally:\n                self._task_group = None\n\n    def configure_session(self, session: ServerSession) -> None:\n        \"\"\"Configure a session for task support.\n\n        This registers the result handler as a response router so that\n        responses to queued requests (elicitation, sampling) are routed\n        back to the waiting resolvers.\n\n        Called automatically by Server.run() for each new session.\n\n        Args:\n            session: The session to configure\n        \"\"\"\n        session.add_response_router(self.handler)\n\n    @classmethod\n    def in_memory(cls) -> \"TaskSupport\":\n        \"\"\"Create in-memory task support.\n\n        Suitable for development, testing, and single-process servers.\n        For distributed systems, provide custom store and queue implementations.\n\n        Returns:\n            TaskSupport configured with in-memory store and queue\n        \"\"\"\n        return cls(\n            store=InMemoryTaskStore(),\n            queue=InMemoryTaskMessageQueue(),\n        )\n"
  },
  {
    "path": "src/mcp/server/lowlevel/__init__.py",
    "content": "from .server import NotificationOptions, Server\n\n__all__ = [\"NotificationOptions\", \"Server\"]\n"
  },
  {
    "path": "src/mcp/server/lowlevel/experimental.py",
    "content": "\"\"\"Experimental handlers for the low-level MCP server.\n\nWARNING: These APIs are experimental and may change without notice.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom collections.abc import Awaitable, Callable\nfrom typing import Any, Generic\n\nfrom typing_extensions import TypeVar\n\nfrom mcp.server.context import ServerRequestContext\nfrom mcp.server.experimental.task_support import TaskSupport\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.experimental.tasks.helpers import cancel_task\nfrom mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore\nfrom mcp.shared.experimental.tasks.message_queue import InMemoryTaskMessageQueue, TaskMessageQueue\nfrom mcp.shared.experimental.tasks.store import TaskStore\nfrom mcp.types import (\n    INVALID_PARAMS,\n    CancelTaskRequestParams,\n    CancelTaskResult,\n    GetTaskPayloadRequest,\n    GetTaskPayloadRequestParams,\n    GetTaskPayloadResult,\n    GetTaskRequestParams,\n    GetTaskResult,\n    ListTasksResult,\n    PaginatedRequestParams,\n    ServerCapabilities,\n    ServerTasksCapability,\n    ServerTasksRequestsCapability,\n    TasksCallCapability,\n    TasksCancelCapability,\n    TasksListCapability,\n    TasksToolsCapability,\n)\n\nlogger = logging.getLogger(__name__)\n\nLifespanResultT = TypeVar(\"LifespanResultT\", default=Any)\n\n\nclass ExperimentalHandlers(Generic[LifespanResultT]):\n    \"\"\"Experimental request/notification handlers.\n\n    WARNING: These APIs are experimental and may change without notice.\n    \"\"\"\n\n    def __init__(\n        self,\n        add_request_handler: Callable[\n            [str, Callable[[ServerRequestContext[LifespanResultT], Any], Awaitable[Any]]], None\n        ],\n        has_handler: Callable[[str], bool],\n    ) -> None:\n        self._add_request_handler = add_request_handler\n        self._has_handler = has_handler\n        self._task_support: TaskSupport | None = None\n\n    @property\n    def task_support(self) -> TaskSupport | None:\n        \"\"\"Get the task support configuration, if enabled.\"\"\"\n        return self._task_support\n\n    def update_capabilities(self, capabilities: ServerCapabilities) -> None:\n        # Only add tasks capability if handlers are registered\n        if not any(self._has_handler(method) for method in [\"tasks/get\", \"tasks/list\", \"tasks/cancel\", \"tasks/result\"]):\n            return\n\n        capabilities.tasks = ServerTasksCapability()\n        if self._has_handler(\"tasks/list\"):\n            capabilities.tasks.list = TasksListCapability()\n        if self._has_handler(\"tasks/cancel\"):\n            capabilities.tasks.cancel = TasksCancelCapability()\n\n        capabilities.tasks.requests = ServerTasksRequestsCapability(\n            tools=TasksToolsCapability(call=TasksCallCapability())\n        )  # assuming always supported for now\n\n    def enable_tasks(\n        self,\n        store: TaskStore | None = None,\n        queue: TaskMessageQueue | None = None,\n        *,\n        on_get_task: Callable[[ServerRequestContext[LifespanResultT], GetTaskRequestParams], Awaitable[GetTaskResult]]\n        | None = None,\n        on_task_result: Callable[\n            [ServerRequestContext[LifespanResultT], GetTaskPayloadRequestParams], Awaitable[GetTaskPayloadResult]\n        ]\n        | None = None,\n        on_list_tasks: Callable[\n            [ServerRequestContext[LifespanResultT], PaginatedRequestParams | None], Awaitable[ListTasksResult]\n        ]\n        | None = None,\n        on_cancel_task: Callable[\n            [ServerRequestContext[LifespanResultT], CancelTaskRequestParams], Awaitable[CancelTaskResult]\n        ]\n        | None = None,\n    ) -> TaskSupport:\n        \"\"\"Enable experimental task support.\n\n        This sets up the task infrastructure and registers handlers for\n        tasks/get, tasks/result, tasks/list, and tasks/cancel. Custom handlers\n        can be provided via the on_* kwargs; any not provided will use defaults.\n\n        Args:\n            store: Custom TaskStore implementation (defaults to InMemoryTaskStore)\n            queue: Custom TaskMessageQueue implementation (defaults to InMemoryTaskMessageQueue)\n            on_get_task: Custom handler for tasks/get\n            on_task_result: Custom handler for tasks/result\n            on_list_tasks: Custom handler for tasks/list\n            on_cancel_task: Custom handler for tasks/cancel\n\n        Returns:\n            The TaskSupport configuration object\n\n        Example:\n            Simple in-memory setup:\n\n            ```python\n            server.experimental.enable_tasks()\n            ```\n\n            Custom store/queue for distributed systems:\n\n            ```python\n            server.experimental.enable_tasks(\n                store=RedisTaskStore(redis_url),\n                queue=RedisTaskMessageQueue(redis_url),\n            )\n            ```\n\n        WARNING: This API is experimental and may change without notice.\n        \"\"\"\n        if store is None:\n            store = InMemoryTaskStore()\n        if queue is None:\n            queue = InMemoryTaskMessageQueue()\n\n        self._task_support = TaskSupport(store=store, queue=queue)\n        task_support = self._task_support\n\n        # Register user-provided handlers\n        if on_get_task is not None:\n            self._add_request_handler(\"tasks/get\", on_get_task)\n        if on_task_result is not None:\n            self._add_request_handler(\"tasks/result\", on_task_result)\n        if on_list_tasks is not None:\n            self._add_request_handler(\"tasks/list\", on_list_tasks)\n        if on_cancel_task is not None:\n            self._add_request_handler(\"tasks/cancel\", on_cancel_task)\n\n        # Fill in defaults for any not provided\n        if not self._has_handler(\"tasks/get\"):\n\n            async def _default_get_task(\n                ctx: ServerRequestContext[LifespanResultT], params: GetTaskRequestParams\n            ) -> GetTaskResult:\n                task = await task_support.store.get_task(params.task_id)\n                if task is None:\n                    raise MCPError(code=INVALID_PARAMS, message=f\"Task not found: {params.task_id}\")\n                return GetTaskResult(\n                    task_id=task.task_id,\n                    status=task.status,\n                    status_message=task.status_message,\n                    created_at=task.created_at,\n                    last_updated_at=task.last_updated_at,\n                    ttl=task.ttl,\n                    poll_interval=task.poll_interval,\n                )\n\n            self._add_request_handler(\"tasks/get\", _default_get_task)\n\n        if not self._has_handler(\"tasks/result\"):\n\n            async def _default_get_task_result(\n                ctx: ServerRequestContext[LifespanResultT], params: GetTaskPayloadRequestParams\n            ) -> GetTaskPayloadResult:\n                assert ctx.request_id is not None\n                req = GetTaskPayloadRequest(params=params)\n                result = await task_support.handler.handle(req, ctx.session, ctx.request_id)\n                return result\n\n            self._add_request_handler(\"tasks/result\", _default_get_task_result)\n\n        if not self._has_handler(\"tasks/list\"):\n\n            async def _default_list_tasks(\n                ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None\n            ) -> ListTasksResult:\n                cursor = params.cursor if params else None\n                tasks, next_cursor = await task_support.store.list_tasks(cursor)\n                return ListTasksResult(tasks=tasks, next_cursor=next_cursor)\n\n            self._add_request_handler(\"tasks/list\", _default_list_tasks)\n\n        if not self._has_handler(\"tasks/cancel\"):\n\n            async def _default_cancel_task(\n                ctx: ServerRequestContext[LifespanResultT], params: CancelTaskRequestParams\n            ) -> CancelTaskResult:\n                result = await cancel_task(task_support.store, params.task_id)\n                return result\n\n            self._add_request_handler(\"tasks/cancel\", _default_cancel_task)\n\n        return task_support\n"
  },
  {
    "path": "src/mcp/server/lowlevel/helper_types.py",
    "content": "from dataclasses import dataclass\nfrom typing import Any\n\n\n@dataclass\nclass ReadResourceContents:\n    \"\"\"Contents returned from a read_resource call.\"\"\"\n\n    content: str | bytes\n    mime_type: str | None = None\n    meta: dict[str, Any] | None = None\n"
  },
  {
    "path": "src/mcp/server/lowlevel/server.py",
    "content": "\"\"\"MCP Server Module\n\nThis module provides a framework for creating an MCP (Model Context Protocol) server.\nIt allows you to easily define and handle various types of requests and notifications\nusing constructor-based handler registration.\n\nUsage:\n1. Define handler functions:\n   async def my_list_tools(ctx, params):\n       return types.ListToolsResult(tools=[...])\n\n   async def my_call_tool(ctx, params):\n       return types.CallToolResult(content=[...])\n\n2. Create a Server instance with on_* handlers:\n   server = Server(\n       \"your_server_name\",\n       on_list_tools=my_list_tools,\n       on_call_tool=my_call_tool,\n   )\n\n3. Run the server:\n   async def main():\n       async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):\n           await server.run(\n               read_stream,\n               write_stream,\n               server.create_initialization_options(),\n           )\n\n   asyncio.run(main())\n\nThe Server class dispatches incoming requests and notifications to registered\nhandler callables by method string.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport warnings\nfrom collections.abc import AsyncIterator, Awaitable, Callable\nfrom contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager\nfrom importlib.metadata import version as importlib_version\nfrom typing import Any, Generic\n\nimport anyio\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\nfrom starlette.applications import Starlette\nfrom starlette.middleware import Middleware\nfrom starlette.middleware.authentication import AuthenticationMiddleware\nfrom starlette.routing import Mount, Route\nfrom typing_extensions import TypeVar\n\nfrom mcp import types\nfrom mcp.server.auth.middleware.auth_context import AuthContextMiddleware\nfrom mcp.server.auth.middleware.bearer_auth import BearerAuthBackend, RequireAuthMiddleware\nfrom mcp.server.auth.provider import OAuthAuthorizationServerProvider, TokenVerifier\nfrom mcp.server.auth.routes import build_resource_metadata_url, create_auth_routes, create_protected_resource_routes\nfrom mcp.server.auth.settings import AuthSettings\nfrom mcp.server.context import ServerRequestContext\nfrom mcp.server.experimental.request_context import Experimental\nfrom mcp.server.lowlevel.experimental import ExperimentalHandlers\nfrom mcp.server.models import InitializationOptions\nfrom mcp.server.session import ServerSession\nfrom mcp.server.streamable_http import EventStore\nfrom mcp.server.streamable_http_manager import StreamableHTTPASGIApp, StreamableHTTPSessionManager\nfrom mcp.server.transport_security import TransportSecuritySettings\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.message import ServerMessageMetadata, SessionMessage\nfrom mcp.shared.session import RequestResponder\n\nlogger = logging.getLogger(__name__)\n\nLifespanResultT = TypeVar(\"LifespanResultT\", default=Any)\n\n\nclass NotificationOptions:\n    def __init__(self, prompts_changed: bool = False, resources_changed: bool = False, tools_changed: bool = False):\n        self.prompts_changed = prompts_changed\n        self.resources_changed = resources_changed\n        self.tools_changed = tools_changed\n\n\n@asynccontextmanager\nasync def lifespan(_: Server[LifespanResultT]) -> AsyncIterator[dict[str, Any]]:\n    \"\"\"Default lifespan context manager that does nothing.\n\n    Returns:\n        An empty context object\n    \"\"\"\n    yield {}\n\n\nasync def _ping_handler(ctx: ServerRequestContext[Any], params: types.RequestParams | None) -> types.EmptyResult:\n    return types.EmptyResult()\n\n\nclass Server(Generic[LifespanResultT]):\n    def __init__(\n        self,\n        name: str,\n        *,\n        version: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        instructions: str | None = None,\n        website_url: str | None = None,\n        icons: list[types.Icon] | None = None,\n        lifespan: Callable[\n            [Server[LifespanResultT]],\n            AbstractAsyncContextManager[LifespanResultT],\n        ] = lifespan,\n        # Request handlers\n        on_list_tools: Callable[\n            [ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None],\n            Awaitable[types.ListToolsResult],\n        ]\n        | None = None,\n        on_call_tool: Callable[\n            [ServerRequestContext[LifespanResultT], types.CallToolRequestParams],\n            Awaitable[types.CallToolResult | types.CreateTaskResult],\n        ]\n        | None = None,\n        on_list_resources: Callable[\n            [ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None],\n            Awaitable[types.ListResourcesResult],\n        ]\n        | None = None,\n        on_list_resource_templates: Callable[\n            [ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None],\n            Awaitable[types.ListResourceTemplatesResult],\n        ]\n        | None = None,\n        on_read_resource: Callable[\n            [ServerRequestContext[LifespanResultT], types.ReadResourceRequestParams],\n            Awaitable[types.ReadResourceResult],\n        ]\n        | None = None,\n        on_subscribe_resource: Callable[\n            [ServerRequestContext[LifespanResultT], types.SubscribeRequestParams],\n            Awaitable[types.EmptyResult],\n        ]\n        | None = None,\n        on_unsubscribe_resource: Callable[\n            [ServerRequestContext[LifespanResultT], types.UnsubscribeRequestParams],\n            Awaitable[types.EmptyResult],\n        ]\n        | None = None,\n        on_list_prompts: Callable[\n            [ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None],\n            Awaitable[types.ListPromptsResult],\n        ]\n        | None = None,\n        on_get_prompt: Callable[\n            [ServerRequestContext[LifespanResultT], types.GetPromptRequestParams],\n            Awaitable[types.GetPromptResult],\n        ]\n        | None = None,\n        on_completion: Callable[\n            [ServerRequestContext[LifespanResultT], types.CompleteRequestParams],\n            Awaitable[types.CompleteResult],\n        ]\n        | None = None,\n        on_set_logging_level: Callable[\n            [ServerRequestContext[LifespanResultT], types.SetLevelRequestParams],\n            Awaitable[types.EmptyResult],\n        ]\n        | None = None,\n        on_ping: Callable[\n            [ServerRequestContext[LifespanResultT], types.RequestParams | None],\n            Awaitable[types.EmptyResult],\n        ] = _ping_handler,\n        # Notification handlers\n        on_roots_list_changed: Callable[\n            [ServerRequestContext[LifespanResultT], types.NotificationParams | None],\n            Awaitable[None],\n        ]\n        | None = None,\n        on_progress: Callable[\n            [ServerRequestContext[LifespanResultT], types.ProgressNotificationParams],\n            Awaitable[None],\n        ]\n        | None = None,\n    ):\n        self.name = name\n        self.version = version\n        self.title = title\n        self.description = description\n        self.instructions = instructions\n        self.website_url = website_url\n        self.icons = icons\n        self.lifespan = lifespan\n        self._request_handlers: dict[str, Callable[[ServerRequestContext[LifespanResultT], Any], Awaitable[Any]]] = {}\n        self._notification_handlers: dict[\n            str, Callable[[ServerRequestContext[LifespanResultT], Any], Awaitable[None]]\n        ] = {}\n        self._experimental_handlers: ExperimentalHandlers[LifespanResultT] | None = None\n        self._session_manager: StreamableHTTPSessionManager | None = None\n        logger.debug(\"Initializing server %r\", name)\n\n        # Populate internal handler dicts from on_* kwargs\n        self._request_handlers.update(\n            {\n                method: handler\n                for method, handler in {\n                    \"ping\": on_ping,\n                    \"prompts/list\": on_list_prompts,\n                    \"prompts/get\": on_get_prompt,\n                    \"resources/list\": on_list_resources,\n                    \"resources/templates/list\": on_list_resource_templates,\n                    \"resources/read\": on_read_resource,\n                    \"resources/subscribe\": on_subscribe_resource,\n                    \"resources/unsubscribe\": on_unsubscribe_resource,\n                    \"tools/list\": on_list_tools,\n                    \"tools/call\": on_call_tool,\n                    \"logging/setLevel\": on_set_logging_level,\n                    \"completion/complete\": on_completion,\n                }.items()\n                if handler is not None\n            }\n        )\n\n        self._notification_handlers.update(\n            {\n                method: handler\n                for method, handler in {\n                    \"notifications/roots/list_changed\": on_roots_list_changed,\n                    \"notifications/progress\": on_progress,\n                }.items()\n                if handler is not None\n            }\n        )\n\n    def _add_request_handler(\n        self,\n        method: str,\n        handler: Callable[[ServerRequestContext[LifespanResultT], Any], Awaitable[Any]],\n    ) -> None:\n        \"\"\"Add a request handler, silently replacing any existing handler for the same method.\"\"\"\n        self._request_handlers[method] = handler\n\n    def _has_handler(self, method: str) -> bool:\n        \"\"\"Check if a handler is registered for the given method.\"\"\"\n        return method in self._request_handlers or method in self._notification_handlers\n\n    # TODO: Rethink capabilities API. Currently capabilities are derived from registered\n    # handlers but require NotificationOptions to be passed externally for list_changed\n    # flags, and experimental_capabilities as a separate dict. Consider deriving capabilities\n    # entirely from server state (e.g. constructor params for list_changed) instead of\n    # requiring callers to assemble them at create_initialization_options() time.\n    def create_initialization_options(\n        self,\n        notification_options: NotificationOptions | None = None,\n        experimental_capabilities: dict[str, dict[str, Any]] | None = None,\n    ) -> InitializationOptions:\n        \"\"\"Create initialization options from this server instance.\"\"\"\n\n        def pkg_version(package: str) -> str:\n            try:\n                return importlib_version(package)\n            except Exception:  # pragma: no cover\n                pass\n\n            return \"unknown\"  # pragma: no cover\n\n        return InitializationOptions(\n            server_name=self.name,\n            server_version=self.version if self.version else pkg_version(\"mcp\"),\n            title=self.title,\n            description=self.description,\n            capabilities=self.get_capabilities(\n                notification_options or NotificationOptions(),\n                experimental_capabilities or {},\n            ),\n            instructions=self.instructions,\n            website_url=self.website_url,\n            icons=self.icons,\n        )\n\n    def get_capabilities(\n        self,\n        notification_options: NotificationOptions,\n        experimental_capabilities: dict[str, dict[str, Any]],\n    ) -> types.ServerCapabilities:\n        \"\"\"Convert existing handlers to a ServerCapabilities object.\"\"\"\n        prompts_capability = None\n        resources_capability = None\n        tools_capability = None\n        logging_capability = None\n        completions_capability = None\n\n        # Set prompt capabilities if handler exists\n        if \"prompts/list\" in self._request_handlers:\n            prompts_capability = types.PromptsCapability(list_changed=notification_options.prompts_changed)\n\n        # Set resource capabilities if handler exists\n        if \"resources/list\" in self._request_handlers:\n            resources_capability = types.ResourcesCapability(\n                subscribe=\"resources/subscribe\" in self._request_handlers,\n                list_changed=notification_options.resources_changed,\n            )\n\n        # Set tool capabilities if handler exists\n        if \"tools/list\" in self._request_handlers:\n            tools_capability = types.ToolsCapability(list_changed=notification_options.tools_changed)\n\n        # Set logging capabilities if handler exists\n        if \"logging/setLevel\" in self._request_handlers:\n            logging_capability = types.LoggingCapability()\n\n        # Set completions capabilities if handler exists\n        if \"completion/complete\" in self._request_handlers:\n            completions_capability = types.CompletionsCapability()\n\n        capabilities = types.ServerCapabilities(\n            prompts=prompts_capability,\n            resources=resources_capability,\n            tools=tools_capability,\n            logging=logging_capability,\n            experimental=experimental_capabilities,\n            completions=completions_capability,\n        )\n        if self._experimental_handlers:\n            self._experimental_handlers.update_capabilities(capabilities)\n        return capabilities\n\n    @property\n    def experimental(self) -> ExperimentalHandlers[LifespanResultT]:\n        \"\"\"Experimental APIs for tasks and other features.\n\n        WARNING: These APIs are experimental and may change without notice.\n        \"\"\"\n\n        # We create this inline so we only add these capabilities _if_ they're actually used\n        if self._experimental_handlers is None:\n            self._experimental_handlers = ExperimentalHandlers(\n                add_request_handler=self._add_request_handler,\n                has_handler=self._has_handler,\n            )\n        return self._experimental_handlers\n\n    @property\n    def session_manager(self) -> StreamableHTTPSessionManager:\n        \"\"\"Get the StreamableHTTP session manager.\n\n        Raises:\n            RuntimeError: If called before streamable_http_app() has been called.\n        \"\"\"\n        if self._session_manager is None:  # pragma: no cover\n            raise RuntimeError(\n                \"Session manager can only be accessed after calling streamable_http_app(). \"\n                \"The session manager is created lazily to avoid unnecessary initialization.\"\n            )\n        return self._session_manager  # pragma: no cover\n\n    async def run(\n        self,\n        read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],\n        write_stream: MemoryObjectSendStream[SessionMessage],\n        initialization_options: InitializationOptions,\n        # When False, exceptions are returned as messages to the client.\n        # When True, exceptions are raised, which will cause the server to shut down\n        # but also make tracing exceptions much easier during testing and when using\n        # in-process servers.\n        raise_exceptions: bool = False,\n        # When True, the server is stateless and\n        # clients can perform initialization with any node. The client must still follow\n        # the initialization lifecycle, but can do so with any available node\n        # rather than requiring initialization for each connection.\n        stateless: bool = False,\n    ):\n        async with AsyncExitStack() as stack:\n            lifespan_context = await stack.enter_async_context(self.lifespan(self))\n            session = await stack.enter_async_context(\n                ServerSession(\n                    read_stream,\n                    write_stream,\n                    initialization_options,\n                    stateless=stateless,\n                )\n            )\n\n            # Configure task support for this session if enabled\n            task_support = self._experimental_handlers.task_support if self._experimental_handlers else None\n            if task_support is not None:\n                task_support.configure_session(session)\n                await stack.enter_async_context(task_support.run())\n\n            async with anyio.create_task_group() as tg:\n                async for message in session.incoming_messages:\n                    logger.debug(\"Received message: %s\", message)\n\n                    tg.start_soon(\n                        self._handle_message,\n                        message,\n                        session,\n                        lifespan_context,\n                        raise_exceptions,\n                    )\n\n    async def _handle_message(\n        self,\n        message: RequestResponder[types.ClientRequest, types.ServerResult] | types.ClientNotification | Exception,\n        session: ServerSession,\n        lifespan_context: LifespanResultT,\n        raise_exceptions: bool = False,\n    ):\n        with warnings.catch_warnings(record=True) as w:\n            match message:\n                case RequestResponder() as responder:\n                    with responder:\n                        await self._handle_request(\n                            message, responder.request, session, lifespan_context, raise_exceptions\n                        )\n                case Exception():\n                    logger.error(f\"Received exception from stream: {message}\")\n                    if raise_exceptions:\n                        raise message\n                case _:\n                    await self._handle_notification(message, session, lifespan_context)\n\n            for warning in w:  # pragma: lax no cover\n                logger.info(\"Warning: %s: %s\", warning.category.__name__, warning.message)\n\n    async def _handle_request(\n        self,\n        message: RequestResponder[types.ClientRequest, types.ServerResult],\n        req: types.ClientRequest,\n        session: ServerSession,\n        lifespan_context: LifespanResultT,\n        raise_exceptions: bool,\n    ):\n        logger.info(\"Processing request of type %s\", type(req).__name__)\n\n        if handler := self._request_handlers.get(req.method):\n            logger.debug(\"Dispatching request of type %s\", type(req).__name__)\n\n            try:\n                # Extract request context and close_sse_stream from message metadata\n                request_data = None\n                close_sse_stream_cb = None\n                close_standalone_sse_stream_cb = None\n                if message.message_metadata is not None and isinstance(message.message_metadata, ServerMessageMetadata):\n                    request_data = message.message_metadata.request_context\n                    close_sse_stream_cb = message.message_metadata.close_sse_stream\n                    close_standalone_sse_stream_cb = message.message_metadata.close_standalone_sse_stream\n\n                client_capabilities = session.client_params.capabilities if session.client_params else None\n                task_support = self._experimental_handlers.task_support if self._experimental_handlers else None\n                # Get task metadata from request params if present\n                task_metadata = None\n                if hasattr(req, \"params\") and req.params is not None:\n                    task_metadata = getattr(req.params, \"task\", None)\n                ctx = ServerRequestContext(\n                    request_id=message.request_id,\n                    meta=message.request_meta,\n                    session=session,\n                    lifespan_context=lifespan_context,\n                    experimental=Experimental(\n                        task_metadata=task_metadata,\n                        _client_capabilities=client_capabilities,\n                        _session=session,\n                        _task_support=task_support,\n                    ),\n                    request=request_data,\n                    close_sse_stream=close_sse_stream_cb,\n                    close_standalone_sse_stream=close_standalone_sse_stream_cb,\n                )\n                response = await handler(ctx, req.params)\n            except MCPError as err:\n                response = err.error\n            except anyio.get_cancelled_exc_class():\n                logger.info(\"Request %s cancelled - duplicate response suppressed\", message.request_id)\n                return\n            except Exception as err:\n                if raise_exceptions:  # pragma: no cover\n                    raise err\n                response = types.ErrorData(code=0, message=str(err))\n\n            await message.respond(response)\n        else:  # pragma: no cover\n            await message.respond(types.ErrorData(code=types.METHOD_NOT_FOUND, message=\"Method not found\"))\n\n        logger.debug(\"Response sent\")\n\n    async def _handle_notification(\n        self,\n        notify: types.ClientNotification,\n        session: ServerSession,\n        lifespan_context: LifespanResultT,\n    ) -> None:\n        if handler := self._notification_handlers.get(notify.method):\n            logger.debug(\"Dispatching notification of type %s\", type(notify).__name__)\n\n            try:\n                client_capabilities = session.client_params.capabilities if session.client_params else None\n                task_support = self._experimental_handlers.task_support if self._experimental_handlers else None\n                ctx = ServerRequestContext(\n                    session=session,\n                    lifespan_context=lifespan_context,\n                    experimental=Experimental(\n                        task_metadata=None,\n                        _client_capabilities=client_capabilities,\n                        _session=session,\n                        _task_support=task_support,\n                    ),\n                )\n                await handler(ctx, notify.params)\n            except Exception:  # pragma: no cover\n                logger.exception(\"Uncaught exception in notification handler\")\n\n    def streamable_http_app(\n        self,\n        *,\n        streamable_http_path: str = \"/mcp\",\n        json_response: bool = False,\n        stateless_http: bool = False,\n        event_store: EventStore | None = None,\n        retry_interval: int | None = None,\n        transport_security: TransportSecuritySettings | None = None,\n        host: str = \"127.0.0.1\",\n        auth: AuthSettings | None = None,\n        token_verifier: TokenVerifier | None = None,\n        auth_server_provider: OAuthAuthorizationServerProvider[Any, Any, Any] | None = None,\n        custom_starlette_routes: list[Route] | None = None,\n        debug: bool = False,\n    ) -> Starlette:\n        \"\"\"Return an instance of the StreamableHTTP server app.\"\"\"\n        # Auto-enable DNS rebinding protection for localhost (IPv4 and IPv6)\n        if transport_security is None and host in (\"127.0.0.1\", \"localhost\", \"::1\"):\n            transport_security = TransportSecuritySettings(\n                enable_dns_rebinding_protection=True,\n                allowed_hosts=[\"127.0.0.1:*\", \"localhost:*\", \"[::1]:*\"],\n                allowed_origins=[\"http://127.0.0.1:*\", \"http://localhost:*\", \"http://[::1]:*\"],\n            )\n\n        session_manager = StreamableHTTPSessionManager(\n            app=self,\n            event_store=event_store,\n            retry_interval=retry_interval,\n            json_response=json_response,\n            stateless=stateless_http,\n            security_settings=transport_security,\n        )\n        self._session_manager = session_manager\n\n        # Create the ASGI handler\n        streamable_http_app = StreamableHTTPASGIApp(session_manager)\n\n        # Create routes\n        routes: list[Route | Mount] = []\n        middleware: list[Middleware] = []\n        required_scopes: list[str] = []\n\n        # Set up auth if configured\n        if auth:  # pragma: no cover\n            required_scopes = auth.required_scopes or []\n\n            # Add auth middleware if token verifier is available\n            if token_verifier:\n                middleware = [\n                    Middleware(\n                        AuthenticationMiddleware,\n                        backend=BearerAuthBackend(token_verifier),\n                    ),\n                    Middleware(AuthContextMiddleware),\n                ]\n\n            # Add auth endpoints if auth server provider is configured\n            if auth_server_provider:\n                routes.extend(\n                    create_auth_routes(\n                        provider=auth_server_provider,\n                        issuer_url=auth.issuer_url,\n                        service_documentation_url=auth.service_documentation_url,\n                        client_registration_options=auth.client_registration_options,\n                        revocation_options=auth.revocation_options,\n                    )\n                )\n\n        # Set up routes with or without auth\n        if token_verifier:  # pragma: no cover\n            # Determine resource metadata URL\n            resource_metadata_url = None\n            if auth and auth.resource_server_url:\n                # Build compliant metadata URL for WWW-Authenticate header\n                resource_metadata_url = build_resource_metadata_url(auth.resource_server_url)\n\n            routes.append(\n                Route(\n                    streamable_http_path,\n                    endpoint=RequireAuthMiddleware(streamable_http_app, required_scopes, resource_metadata_url),\n                )\n            )\n        else:\n            # Auth is disabled, no wrapper needed\n            routes.append(\n                Route(\n                    streamable_http_path,\n                    endpoint=streamable_http_app,\n                )\n            )\n\n        # Add protected resource metadata endpoint if configured as RS\n        if auth and auth.resource_server_url:  # pragma: no cover\n            routes.extend(\n                create_protected_resource_routes(\n                    resource_url=auth.resource_server_url,\n                    authorization_servers=[auth.issuer_url],\n                    scopes_supported=auth.required_scopes,\n                )\n            )\n\n        if custom_starlette_routes:  # pragma: no cover\n            routes.extend(custom_starlette_routes)\n\n        return Starlette(\n            debug=debug,\n            routes=routes,\n            middleware=middleware,\n            lifespan=lambda app: session_manager.run(),\n        )\n"
  },
  {
    "path": "src/mcp/server/mcpserver/__init__.py",
    "content": "\"\"\"MCPServer - A more ergonomic interface for MCP servers.\"\"\"\n\nfrom mcp.types import Icon\n\nfrom .context import Context\nfrom .server import MCPServer\nfrom .utilities.types import Audio, Image\n\n__all__ = [\"MCPServer\", \"Context\", \"Image\", \"Audio\", \"Icon\"]\n"
  },
  {
    "path": "src/mcp/server/mcpserver/context.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Iterable\nfrom typing import TYPE_CHECKING, Any, Generic, Literal\n\nfrom pydantic import AnyUrl, BaseModel\n\nfrom mcp.server.context import LifespanContextT, RequestT, ServerRequestContext\nfrom mcp.server.elicitation import (\n    ElicitationResult,\n    ElicitSchemaModelT,\n    UrlElicitationResult,\n    elicit_url,\n    elicit_with_validation,\n)\nfrom mcp.server.lowlevel.helper_types import ReadResourceContents\n\nif TYPE_CHECKING:\n    from mcp.server.mcpserver.server import MCPServer\n\n\nclass Context(BaseModel, Generic[LifespanContextT, RequestT]):\n    \"\"\"Context object providing access to MCP capabilities.\n\n    This provides a cleaner interface to MCP's RequestContext functionality.\n    It gets injected into tool and resource functions that request it via type hints.\n\n    To use context in a tool function, add a parameter with the Context type annotation:\n\n    ```python\n    @server.tool()\n    async def my_tool(x: int, ctx: Context) -> str:\n        # Log messages to the client\n        await ctx.info(f\"Processing {x}\")\n        await ctx.debug(\"Debug info\")\n        await ctx.warning(\"Warning message\")\n        await ctx.error(\"Error message\")\n\n        # Report progress\n        await ctx.report_progress(50, 100)\n\n        # Access resources\n        data = await ctx.read_resource(\"resource://data\")\n\n        # Get request info\n        request_id = ctx.request_id\n        client_id = ctx.client_id\n\n        return str(x)\n    ```\n\n    The context parameter name can be anything as long as it's annotated with Context.\n    The context is optional - tools that don't need it can omit the parameter.\n    \"\"\"\n\n    _request_context: ServerRequestContext[LifespanContextT, RequestT] | None\n    _mcp_server: MCPServer | None\n\n    # TODO(maxisbey): Consider making request_context/mcp_server required, or refactor Context entirely.\n    def __init__(\n        self,\n        *,\n        request_context: ServerRequestContext[LifespanContextT, RequestT] | None = None,\n        mcp_server: MCPServer | None = None,\n        # TODO(Marcelo): We should drop this kwargs parameter.\n        **kwargs: Any,\n    ):\n        super().__init__(**kwargs)\n        self._request_context = request_context\n        self._mcp_server = mcp_server\n\n    @property\n    def mcp_server(self) -> MCPServer:\n        \"\"\"Access to the MCPServer instance.\"\"\"\n        if self._mcp_server is None:  # pragma: no cover\n            raise ValueError(\"Context is not available outside of a request\")\n        return self._mcp_server  # pragma: no cover\n\n    @property\n    def request_context(self) -> ServerRequestContext[LifespanContextT, RequestT]:\n        \"\"\"Access to the underlying request context.\"\"\"\n        if self._request_context is None:  # pragma: no cover\n            raise ValueError(\"Context is not available outside of a request\")\n        return self._request_context\n\n    async def report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None:\n        \"\"\"Report progress for the current operation.\n\n        Args:\n            progress: Current progress value (e.g., 24)\n            total: Optional total value (e.g., 100)\n            message: Optional message (e.g., \"Starting render...\")\n        \"\"\"\n        progress_token = self.request_context.meta.get(\"progress_token\") if self.request_context.meta else None\n\n        if progress_token is None:  # pragma: no cover\n            return\n\n        await self.request_context.session.send_progress_notification(\n            progress_token=progress_token,\n            progress=progress,\n            total=total,\n            message=message,\n            related_request_id=self.request_id,\n        )\n\n    async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContents]:\n        \"\"\"Read a resource by URI.\n\n        Args:\n            uri: Resource URI to read\n\n        Returns:\n            The resource content as either text or bytes\n        \"\"\"\n        assert self._mcp_server is not None, \"Context is not available outside of a request\"\n        return await self._mcp_server.read_resource(uri, self)\n\n    async def elicit(\n        self,\n        message: str,\n        schema: type[ElicitSchemaModelT],\n    ) -> ElicitationResult[ElicitSchemaModelT]:\n        \"\"\"Elicit information from the client/user.\n\n        This method can be used to interactively ask for additional information from the\n        client within a tool's execution. The client might display the message to the\n        user and collect a response according to the provided schema. If the client\n        is an agent, it might decide how to handle the elicitation -- either by asking\n        the user or automatically generating a response.\n\n        Args:\n            message: Message to present to the user\n            schema: A Pydantic model class defining the expected response structure.\n                    According to the specification, only primitive types are allowed.\n\n        Returns:\n            An ElicitationResult containing the action taken and the data if accepted\n\n        Note:\n            Check the result.action to determine if the user accepted, declined, or cancelled.\n            The result.data will only be populated if action is \"accept\" and validation succeeded.\n        \"\"\"\n\n        return await elicit_with_validation(\n            session=self.request_context.session,\n            message=message,\n            schema=schema,\n            related_request_id=self.request_id,\n        )\n\n    async def elicit_url(\n        self,\n        message: str,\n        url: str,\n        elicitation_id: str,\n    ) -> UrlElicitationResult:\n        \"\"\"Request URL mode elicitation from the client.\n\n        This directs the user to an external URL for out-of-band interactions\n        that must not pass through the MCP client. Use this for:\n        - Collecting sensitive credentials (API keys, passwords)\n        - OAuth authorization flows with third-party services\n        - Payment and subscription flows\n        - Any interaction where data should not pass through the LLM context\n\n        The response indicates whether the user consented to navigate to the URL.\n        The actual interaction happens out-of-band. When the elicitation completes,\n        call `ctx.session.send_elicit_complete(elicitation_id)` to notify the client.\n\n        Args:\n            message: Human-readable explanation of why the interaction is needed\n            url: The URL the user should navigate to\n            elicitation_id: Unique identifier for tracking this elicitation\n\n        Returns:\n            UrlElicitationResult indicating accept, decline, or cancel\n        \"\"\"\n        return await elicit_url(\n            session=self.request_context.session,\n            message=message,\n            url=url,\n            elicitation_id=elicitation_id,\n            related_request_id=self.request_id,\n        )\n\n    async def log(\n        self,\n        level: Literal[\"debug\", \"info\", \"warning\", \"error\"],\n        message: str,\n        *,\n        logger_name: str | None = None,\n        extra: dict[str, Any] | None = None,\n    ) -> None:\n        \"\"\"Send a log message to the client.\n\n        Args:\n            level: Log level (debug, info, warning, error)\n            message: Log message\n            logger_name: Optional logger name\n            extra: Optional dictionary with additional structured data to include\n        \"\"\"\n\n        if extra:\n            log_data = {\"message\": message, **extra}\n        else:\n            log_data = message\n\n        await self.request_context.session.send_log_message(\n            level=level,\n            data=log_data,\n            logger=logger_name,\n            related_request_id=self.request_id,\n        )\n\n    @property\n    def client_id(self) -> str | None:\n        \"\"\"Get the client ID if available.\"\"\"\n        return self.request_context.meta.get(\"client_id\") if self.request_context.meta else None  # pragma: no cover\n\n    @property\n    def request_id(self) -> str:\n        \"\"\"Get the unique ID for this request.\"\"\"\n        return str(self.request_context.request_id)\n\n    @property\n    def session(self):\n        \"\"\"Access to the underlying session for advanced usage.\"\"\"\n        return self.request_context.session\n\n    async def close_sse_stream(self) -> None:\n        \"\"\"Close the SSE stream to trigger client reconnection.\n\n        This method closes the HTTP connection for the current request, triggering\n        client reconnection. Events continue to be stored in the event store and will\n        be replayed when the client reconnects with Last-Event-ID.\n\n        Use this to implement polling behavior during long-running operations -\n        the client will reconnect after the retry interval specified in the priming event.\n\n        Note:\n            This is a no-op if not using StreamableHTTP transport with event_store.\n            The callback is only available when event_store is configured.\n        \"\"\"\n        if self._request_context and self._request_context.close_sse_stream:  # pragma: no cover\n            await self._request_context.close_sse_stream()\n\n    async def close_standalone_sse_stream(self) -> None:\n        \"\"\"Close the standalone GET SSE stream to trigger client reconnection.\n\n        This method closes the HTTP connection for the standalone GET stream used\n        for unsolicited server-to-client notifications. The client SHOULD reconnect\n        with Last-Event-ID to resume receiving notifications.\n\n        Note:\n            This is a no-op if not using StreamableHTTP transport with event_store.\n            Currently, client reconnection for standalone GET streams is NOT\n            implemented - this is a known gap.\n        \"\"\"\n        if self._request_context and self._request_context.close_standalone_sse_stream:  # pragma: no cover\n            await self._request_context.close_standalone_sse_stream()\n\n    # Convenience methods for common log levels\n    async def debug(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:\n        \"\"\"Send a debug log message.\"\"\"\n        await self.log(\"debug\", message, logger_name=logger_name, extra=extra)\n\n    async def info(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:\n        \"\"\"Send an info log message.\"\"\"\n        await self.log(\"info\", message, logger_name=logger_name, extra=extra)\n\n    async def warning(\n        self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None\n    ) -> None:\n        \"\"\"Send a warning log message.\"\"\"\n        await self.log(\"warning\", message, logger_name=logger_name, extra=extra)\n\n    async def error(self, message: str, *, logger_name: str | None = None, extra: dict[str, Any] | None = None) -> None:\n        \"\"\"Send an error log message.\"\"\"\n        await self.log(\"error\", message, logger_name=logger_name, extra=extra)\n"
  },
  {
    "path": "src/mcp/server/mcpserver/exceptions.py",
    "content": "\"\"\"Custom exceptions for MCPServer.\"\"\"\n\n\nclass MCPServerError(Exception):\n    \"\"\"Base error for MCPServer.\"\"\"\n\n\nclass ValidationError(MCPServerError):\n    \"\"\"Error in validating parameters or return values.\"\"\"\n\n\nclass ResourceError(MCPServerError):\n    \"\"\"Error in resource operations.\"\"\"\n\n\nclass ToolError(MCPServerError):\n    \"\"\"Error in tool operations.\"\"\"\n\n\nclass InvalidSignature(Exception):\n    \"\"\"Invalid signature for use with MCPServer.\"\"\"\n"
  },
  {
    "path": "src/mcp/server/mcpserver/prompts/__init__.py",
    "content": "from .base import Prompt\nfrom .manager import PromptManager\n\n__all__ = [\"Prompt\", \"PromptManager\"]\n"
  },
  {
    "path": "src/mcp/server/mcpserver/prompts/base.py",
    "content": "\"\"\"Base classes for MCPServer prompts.\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nfrom collections.abc import Awaitable, Callable, Sequence\nfrom typing import TYPE_CHECKING, Any, Literal\n\nimport pydantic_core\nfrom pydantic import BaseModel, Field, TypeAdapter, validate_call\n\nfrom mcp.server.mcpserver.utilities.context_injection import find_context_parameter, inject_context\nfrom mcp.server.mcpserver.utilities.func_metadata import func_metadata\nfrom mcp.types import ContentBlock, Icon, TextContent\n\nif TYPE_CHECKING:\n    from mcp.server.context import LifespanContextT, RequestT\n    from mcp.server.mcpserver.context import Context\n\n\nclass Message(BaseModel):\n    \"\"\"Base class for all prompt messages.\"\"\"\n\n    role: Literal[\"user\", \"assistant\"]\n    content: ContentBlock\n\n    def __init__(self, content: str | ContentBlock, **kwargs: Any):\n        if isinstance(content, str):\n            content = TextContent(type=\"text\", text=content)\n        super().__init__(content=content, **kwargs)\n\n\nclass UserMessage(Message):\n    \"\"\"A message from the user.\"\"\"\n\n    role: Literal[\"user\", \"assistant\"] = \"user\"\n\n    def __init__(self, content: str | ContentBlock, **kwargs: Any):\n        super().__init__(content=content, **kwargs)\n\n\nclass AssistantMessage(Message):\n    \"\"\"A message from the assistant.\"\"\"\n\n    role: Literal[\"user\", \"assistant\"] = \"assistant\"\n\n    def __init__(self, content: str | ContentBlock, **kwargs: Any):\n        super().__init__(content=content, **kwargs)\n\n\nmessage_validator = TypeAdapter[UserMessage | AssistantMessage](UserMessage | AssistantMessage)\n\nSyncPromptResult = str | Message | dict[str, Any] | Sequence[str | Message | dict[str, Any]]\nPromptResult = SyncPromptResult | Awaitable[SyncPromptResult]\n\n\nclass PromptArgument(BaseModel):\n    \"\"\"An argument that can be passed to a prompt.\"\"\"\n\n    name: str = Field(description=\"Name of the argument\")\n    description: str | None = Field(None, description=\"Description of what the argument does\")\n    required: bool = Field(default=False, description=\"Whether the argument is required\")\n\n\nclass Prompt(BaseModel):\n    \"\"\"A prompt template that can be rendered with parameters.\"\"\"\n\n    name: str = Field(description=\"Name of the prompt\")\n    title: str | None = Field(None, description=\"Human-readable title of the prompt\")\n    description: str | None = Field(None, description=\"Description of what the prompt does\")\n    arguments: list[PromptArgument] | None = Field(None, description=\"Arguments that can be passed to the prompt\")\n    fn: Callable[..., PromptResult | Awaitable[PromptResult]] = Field(exclude=True)\n    icons: list[Icon] | None = Field(default=None, description=\"Optional list of icons for this prompt\")\n    context_kwarg: str | None = Field(None, description=\"Name of the kwarg that should receive context\", exclude=True)\n\n    @classmethod\n    def from_function(\n        cls,\n        fn: Callable[..., PromptResult | Awaitable[PromptResult]],\n        name: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[Icon] | None = None,\n        context_kwarg: str | None = None,\n    ) -> Prompt:\n        \"\"\"Create a Prompt from a function.\n\n        The function can return:\n        - A string (converted to a message)\n        - A Message object\n        - A dict (converted to a message)\n        - A sequence of any of the above\n        \"\"\"\n        func_name = name or fn.__name__\n\n        if func_name == \"<lambda>\":  # pragma: no cover\n            raise ValueError(\"You must provide a name for lambda functions\")\n\n        # Find context parameter if it exists\n        if context_kwarg is None:  # pragma: no branch\n            context_kwarg = find_context_parameter(fn)\n\n        # Get schema from func_metadata, excluding context parameter\n        func_arg_metadata = func_metadata(\n            fn,\n            skip_names=[context_kwarg] if context_kwarg is not None else [],\n        )\n        parameters = func_arg_metadata.arg_model.model_json_schema()\n\n        # Convert parameters to PromptArguments\n        arguments: list[PromptArgument] = []\n        if \"properties\" in parameters:  # pragma: no branch\n            for param_name, param in parameters[\"properties\"].items():\n                required = param_name in parameters.get(\"required\", [])\n                arguments.append(\n                    PromptArgument(\n                        name=param_name,\n                        description=param.get(\"description\"),\n                        required=required,\n                    )\n                )\n\n        # ensure the arguments are properly cast\n        fn = validate_call(fn)\n\n        return cls(\n            name=func_name,\n            title=title,\n            description=description or fn.__doc__ or \"\",\n            arguments=arguments,\n            fn=fn,\n            icons=icons,\n            context_kwarg=context_kwarg,\n        )\n\n    async def render(\n        self,\n        arguments: dict[str, Any] | None,\n        context: Context[LifespanContextT, RequestT],\n    ) -> list[Message]:\n        \"\"\"Render the prompt with arguments.\n\n        Raises:\n            ValueError: If required arguments are missing, or if rendering fails.\n        \"\"\"\n        # Validate required arguments\n        if self.arguments:\n            required = {arg.name for arg in self.arguments if arg.required}\n            provided = set(arguments or {})\n            missing = required - provided\n            if missing:\n                raise ValueError(f\"Missing required arguments: {missing}\")\n\n        try:\n            # Add context to arguments if needed\n            call_args = inject_context(self.fn, arguments or {}, context, self.context_kwarg)\n\n            # Call function and check if result is a coroutine\n            result = self.fn(**call_args)\n            if inspect.iscoroutine(result):\n                result = await result\n\n            # Validate messages\n            if not isinstance(result, list | tuple):\n                result = [result]\n\n            # Convert result to messages\n            messages: list[Message] = []\n            for msg in result:  # type: ignore[reportUnknownVariableType]\n                try:\n                    if isinstance(msg, Message):\n                        messages.append(msg)\n                    elif isinstance(msg, dict):\n                        messages.append(message_validator.validate_python(msg))\n                    elif isinstance(msg, str):\n                        content = TextContent(type=\"text\", text=msg)\n                        messages.append(UserMessage(content=content))\n                    else:  # pragma: no cover\n                        content = pydantic_core.to_json(msg, fallback=str, indent=2).decode()\n                        messages.append(Message(role=\"user\", content=content))\n                except Exception:  # pragma: no cover\n                    raise ValueError(f\"Could not convert prompt result to message: {msg}\")\n\n            return messages\n        except Exception as e:  # pragma: no cover\n            raise ValueError(f\"Error rendering prompt {self.name}: {e}\")\n"
  },
  {
    "path": "src/mcp/server/mcpserver/prompts/manager.py",
    "content": "\"\"\"Prompt management functionality.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import TYPE_CHECKING, Any\n\nfrom mcp.server.mcpserver.prompts.base import Message, Prompt\nfrom mcp.server.mcpserver.utilities.logging import get_logger\n\nif TYPE_CHECKING:\n    from mcp.server.context import LifespanContextT, RequestT\n    from mcp.server.mcpserver.context import Context\n\nlogger = get_logger(__name__)\n\n\nclass PromptManager:\n    \"\"\"Manages MCPServer prompts.\"\"\"\n\n    def __init__(self, warn_on_duplicate_prompts: bool = True):\n        self._prompts: dict[str, Prompt] = {}\n        self.warn_on_duplicate_prompts = warn_on_duplicate_prompts\n\n    def get_prompt(self, name: str) -> Prompt | None:\n        \"\"\"Get prompt by name.\"\"\"\n        return self._prompts.get(name)\n\n    def list_prompts(self) -> list[Prompt]:\n        \"\"\"List all registered prompts.\"\"\"\n        return list(self._prompts.values())\n\n    def add_prompt(\n        self,\n        prompt: Prompt,\n    ) -> Prompt:\n        \"\"\"Add a prompt to the manager.\"\"\"\n\n        # Check for duplicates\n        existing = self._prompts.get(prompt.name)\n        if existing:\n            if self.warn_on_duplicate_prompts:\n                logger.warning(f\"Prompt already exists: {prompt.name}\")\n            return existing\n\n        self._prompts[prompt.name] = prompt\n        return prompt\n\n    async def render_prompt(\n        self,\n        name: str,\n        arguments: dict[str, Any] | None,\n        context: Context[LifespanContextT, RequestT],\n    ) -> list[Message]:\n        \"\"\"Render a prompt by name with arguments.\"\"\"\n        prompt = self.get_prompt(name)\n        if not prompt:\n            raise ValueError(f\"Unknown prompt: {name}\")\n\n        return await prompt.render(arguments, context)\n"
  },
  {
    "path": "src/mcp/server/mcpserver/resources/__init__.py",
    "content": "from .base import Resource\nfrom .resource_manager import ResourceManager\nfrom .templates import ResourceTemplate\nfrom .types import (\n    BinaryResource,\n    DirectoryResource,\n    FileResource,\n    FunctionResource,\n    HttpResource,\n    TextResource,\n)\n\n__all__ = [\n    \"Resource\",\n    \"TextResource\",\n    \"BinaryResource\",\n    \"FunctionResource\",\n    \"FileResource\",\n    \"HttpResource\",\n    \"DirectoryResource\",\n    \"ResourceTemplate\",\n    \"ResourceManager\",\n]\n"
  },
  {
    "path": "src/mcp/server/mcpserver/resources/base.py",
    "content": "\"\"\"Base classes and interfaces for MCPServer resources.\"\"\"\n\nimport abc\nfrom typing import Any\n\nfrom pydantic import (\n    BaseModel,\n    ConfigDict,\n    Field,\n    ValidationInfo,\n    field_validator,\n)\n\nfrom mcp.types import Annotations, Icon\n\n\nclass Resource(BaseModel, abc.ABC):\n    \"\"\"Base class for all resources.\"\"\"\n\n    model_config = ConfigDict(validate_default=True)\n\n    uri: str = Field(default=..., description=\"URI of the resource\")\n    name: str | None = Field(description=\"Name of the resource\", default=None)\n    title: str | None = Field(description=\"Human-readable title of the resource\", default=None)\n    description: str | None = Field(description=\"Description of the resource\", default=None)\n    mime_type: str = Field(default=\"text/plain\", description=\"MIME type of the resource content\")\n    icons: list[Icon] | None = Field(default=None, description=\"Optional list of icons for this resource\")\n    annotations: Annotations | None = Field(default=None, description=\"Optional annotations for the resource\")\n    meta: dict[str, Any] | None = Field(default=None, description=\"Optional metadata for this resource\")\n\n    @field_validator(\"name\", mode=\"before\")\n    @classmethod\n    def set_default_name(cls, name: str | None, info: ValidationInfo) -> str:\n        \"\"\"Set default name from URI if not provided.\"\"\"\n        if name:\n            return name\n        if uri := info.data.get(\"uri\"):\n            return str(uri)\n        raise ValueError(\"Either name or uri must be provided\")\n\n    @abc.abstractmethod\n    async def read(self) -> str | bytes:\n        \"\"\"Read the resource content.\"\"\"\n        pass  # pragma: no cover\n"
  },
  {
    "path": "src/mcp/server/mcpserver/resources/resource_manager.py",
    "content": "\"\"\"Resource manager functionality.\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Any\n\nfrom pydantic import AnyUrl\n\nfrom mcp.server.mcpserver.resources.base import Resource\nfrom mcp.server.mcpserver.resources.templates import ResourceTemplate\nfrom mcp.server.mcpserver.utilities.logging import get_logger\nfrom mcp.types import Annotations, Icon\n\nif TYPE_CHECKING:\n    from mcp.server.context import LifespanContextT, RequestT\n    from mcp.server.mcpserver.context import Context\n\nlogger = get_logger(__name__)\n\n\nclass ResourceManager:\n    \"\"\"Manages MCPServer resources.\"\"\"\n\n    def __init__(self, warn_on_duplicate_resources: bool = True):\n        self._resources: dict[str, Resource] = {}\n        self._templates: dict[str, ResourceTemplate] = {}\n        self.warn_on_duplicate_resources = warn_on_duplicate_resources\n\n    def add_resource(self, resource: Resource) -> Resource:\n        \"\"\"Add a resource to the manager.\n\n        Args:\n            resource: A Resource instance to add\n\n        Returns:\n            The added resource. If a resource with the same URI already exists,\n            returns the existing resource.\n        \"\"\"\n        logger.debug(\n            \"Adding resource\",\n            extra={\n                \"uri\": resource.uri,\n                \"type\": type(resource).__name__,\n                \"resource_name\": resource.name,\n            },\n        )\n        existing = self._resources.get(str(resource.uri))\n        if existing:\n            if self.warn_on_duplicate_resources:\n                logger.warning(f\"Resource already exists: {resource.uri}\")\n            return existing\n        self._resources[str(resource.uri)] = resource\n        return resource\n\n    def add_template(\n        self,\n        fn: Callable[..., Any],\n        uri_template: str,\n        name: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        mime_type: str | None = None,\n        icons: list[Icon] | None = None,\n        annotations: Annotations | None = None,\n        meta: dict[str, Any] | None = None,\n    ) -> ResourceTemplate:\n        \"\"\"Add a template from a function.\"\"\"\n        template = ResourceTemplate.from_function(\n            fn,\n            uri_template=uri_template,\n            name=name,\n            title=title,\n            description=description,\n            mime_type=mime_type,\n            icons=icons,\n            annotations=annotations,\n            meta=meta,\n        )\n        self._templates[template.uri_template] = template\n        return template\n\n    async def get_resource(self, uri: AnyUrl | str, context: Context[LifespanContextT, RequestT]) -> Resource:\n        \"\"\"Get resource by URI, checking concrete resources first, then templates.\"\"\"\n        uri_str = str(uri)\n        logger.debug(\"Getting resource\", extra={\"uri\": uri_str})\n\n        # First check concrete resources\n        if resource := self._resources.get(uri_str):\n            return resource\n\n        # Then check templates\n        for template in self._templates.values():\n            if params := template.matches(uri_str):\n                try:\n                    return await template.create_resource(uri_str, params, context=context)\n                except Exception as e:  # pragma: no cover\n                    raise ValueError(f\"Error creating resource from template: {e}\")\n\n        raise ValueError(f\"Unknown resource: {uri}\")\n\n    def list_resources(self) -> list[Resource]:\n        \"\"\"List all registered resources.\"\"\"\n        logger.debug(\"Listing resources\", extra={\"count\": len(self._resources)})\n        return list(self._resources.values())\n\n    def list_templates(self) -> list[ResourceTemplate]:\n        \"\"\"List all registered templates.\"\"\"\n        logger.debug(\"Listing templates\", extra={\"count\": len(self._templates)})\n        return list(self._templates.values())\n"
  },
  {
    "path": "src/mcp/server/mcpserver/resources/templates.py",
    "content": "\"\"\"Resource template functionality.\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nimport re\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Any\nfrom urllib.parse import unquote\n\nfrom pydantic import BaseModel, Field, validate_call\n\nfrom mcp.server.mcpserver.resources.types import FunctionResource, Resource\nfrom mcp.server.mcpserver.utilities.context_injection import find_context_parameter, inject_context\nfrom mcp.server.mcpserver.utilities.func_metadata import func_metadata\nfrom mcp.types import Annotations, Icon\n\nif TYPE_CHECKING:\n    from mcp.server.context import LifespanContextT, RequestT\n    from mcp.server.mcpserver.context import Context\n\n\nclass ResourceTemplate(BaseModel):\n    \"\"\"A template for dynamically creating resources.\"\"\"\n\n    uri_template: str = Field(description=\"URI template with parameters (e.g. weather://{city}/current)\")\n    name: str = Field(description=\"Name of the resource\")\n    title: str | None = Field(description=\"Human-readable title of the resource\", default=None)\n    description: str | None = Field(description=\"Description of what the resource does\")\n    mime_type: str = Field(default=\"text/plain\", description=\"MIME type of the resource content\")\n    icons: list[Icon] | None = Field(default=None, description=\"Optional list of icons for the resource template\")\n    annotations: Annotations | None = Field(default=None, description=\"Optional annotations for the resource template\")\n    meta: dict[str, Any] | None = Field(default=None, description=\"Optional metadata for this resource template\")\n    fn: Callable[..., Any] = Field(exclude=True)\n    parameters: dict[str, Any] = Field(description=\"JSON schema for function parameters\")\n    context_kwarg: str | None = Field(None, description=\"Name of the kwarg that should receive context\")\n\n    @classmethod\n    def from_function(\n        cls,\n        fn: Callable[..., Any],\n        uri_template: str,\n        name: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        mime_type: str | None = None,\n        icons: list[Icon] | None = None,\n        annotations: Annotations | None = None,\n        meta: dict[str, Any] | None = None,\n        context_kwarg: str | None = None,\n    ) -> ResourceTemplate:\n        \"\"\"Create a template from a function.\"\"\"\n        func_name = name or fn.__name__\n        if func_name == \"<lambda>\":\n            raise ValueError(\"You must provide a name for lambda functions\")  # pragma: no cover\n\n        # Find context parameter if it exists\n        if context_kwarg is None:  # pragma: no branch\n            context_kwarg = find_context_parameter(fn)\n\n        # Get schema from func_metadata, excluding context parameter\n        func_arg_metadata = func_metadata(\n            fn,\n            skip_names=[context_kwarg] if context_kwarg is not None else [],\n        )\n        parameters = func_arg_metadata.arg_model.model_json_schema()\n\n        # ensure the arguments are properly cast\n        fn = validate_call(fn)\n\n        return cls(\n            uri_template=uri_template,\n            name=func_name,\n            title=title,\n            description=description or fn.__doc__ or \"\",\n            mime_type=mime_type or \"text/plain\",\n            icons=icons,\n            annotations=annotations,\n            meta=meta,\n            fn=fn,\n            parameters=parameters,\n            context_kwarg=context_kwarg,\n        )\n\n    def matches(self, uri: str) -> dict[str, Any] | None:\n        \"\"\"Check if URI matches template and extract parameters.\n\n        Extracted parameters are URL-decoded to handle percent-encoded characters.\n        \"\"\"\n        # Convert template to regex pattern\n        pattern = self.uri_template.replace(\"{\", \"(?P<\").replace(\"}\", \">[^/]+)\")\n        match = re.match(f\"^{pattern}$\", uri)\n        if match:\n            # URL-decode all extracted parameter values\n            return {key: unquote(value) for key, value in match.groupdict().items()}\n        return None\n\n    async def create_resource(\n        self,\n        uri: str,\n        params: dict[str, Any],\n        context: Context[LifespanContextT, RequestT],\n    ) -> Resource:\n        \"\"\"Create a resource from the template with the given parameters.\n\n        Raises:\n            ValueError: If creating the resource fails.\n        \"\"\"\n        try:\n            # Add context to params if needed\n            params = inject_context(self.fn, params, context, self.context_kwarg)\n\n            # Call function and check if result is a coroutine\n            result = self.fn(**params)\n            if inspect.iscoroutine(result):\n                result = await result\n\n            return FunctionResource(\n                uri=uri,  # type: ignore\n                name=self.name,\n                title=self.title,\n                description=self.description,\n                mime_type=self.mime_type,\n                icons=self.icons,\n                annotations=self.annotations,\n                meta=self.meta,\n                fn=lambda: result,  # Capture result in closure\n            )\n        except Exception as e:\n            raise ValueError(f\"Error creating resource from template: {e}\")\n"
  },
  {
    "path": "src/mcp/server/mcpserver/resources/types.py",
    "content": "\"\"\"Concrete resource implementations.\"\"\"\n\nimport inspect\nimport json\nfrom collections.abc import Callable\nfrom pathlib import Path\nfrom typing import Any\n\nimport anyio\nimport anyio.to_thread\nimport httpx\nimport pydantic\nimport pydantic_core\nfrom pydantic import Field, ValidationInfo, validate_call\n\nfrom mcp.server.mcpserver.resources.base import Resource\nfrom mcp.types import Annotations, Icon\n\n\nclass TextResource(Resource):\n    \"\"\"A resource that reads from a string.\"\"\"\n\n    text: str = Field(description=\"Text content of the resource\")\n\n    async def read(self) -> str:\n        \"\"\"Read the text content.\"\"\"\n        return self.text  # pragma: no cover\n\n\nclass BinaryResource(Resource):\n    \"\"\"A resource that reads from bytes.\"\"\"\n\n    data: bytes = Field(description=\"Binary content of the resource\")\n\n    async def read(self) -> bytes:\n        \"\"\"Read the binary content.\"\"\"\n        return self.data  # pragma: no cover\n\n\nclass FunctionResource(Resource):\n    \"\"\"A resource that defers data loading by wrapping a function.\n\n    The function is only called when the resource is read, allowing for lazy loading\n    of potentially expensive data. This is particularly useful when listing resources,\n    as the function won't be called until the resource is actually accessed.\n\n    The function can return:\n    - str for text content (default)\n    - bytes for binary content\n    - other types will be converted to JSON\n    \"\"\"\n\n    fn: Callable[[], Any] = Field(exclude=True)\n\n    async def read(self) -> str | bytes:\n        \"\"\"Read the resource by calling the wrapped function.\"\"\"\n        try:\n            # Call the function first to see if it returns a coroutine\n            result = self.fn()\n            # If it's a coroutine, await it\n            if inspect.iscoroutine(result):\n                result = await result\n\n            if isinstance(result, Resource):  # pragma: no cover\n                return await result.read()\n            elif isinstance(result, bytes):\n                return result\n            elif isinstance(result, str):\n                return result\n            else:\n                return pydantic_core.to_json(result, fallback=str, indent=2).decode()\n        except Exception as e:\n            raise ValueError(f\"Error reading resource {self.uri}: {e}\")\n\n    @classmethod\n    def from_function(\n        cls,\n        fn: Callable[..., Any],\n        uri: str,\n        name: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        mime_type: str | None = None,\n        icons: list[Icon] | None = None,\n        annotations: Annotations | None = None,\n        meta: dict[str, Any] | None = None,\n    ) -> \"FunctionResource\":\n        \"\"\"Create a FunctionResource from a function.\"\"\"\n        func_name = name or fn.__name__\n        if func_name == \"<lambda>\":  # pragma: no cover\n            raise ValueError(\"You must provide a name for lambda functions\")\n\n        # ensure the arguments are properly cast\n        fn = validate_call(fn)\n\n        return cls(\n            uri=uri,\n            name=func_name,\n            title=title,\n            description=description or fn.__doc__ or \"\",\n            mime_type=mime_type or \"text/plain\",\n            fn=fn,\n            icons=icons,\n            annotations=annotations,\n            meta=meta,\n        )\n\n\nclass FileResource(Resource):\n    \"\"\"A resource that reads from a file.\n\n    Set is_binary=True to read the file as binary data instead of text.\n    \"\"\"\n\n    path: Path = Field(description=\"Path to the file\")\n    is_binary: bool = Field(\n        default=False,\n        description=\"Whether to read the file as binary data\",\n    )\n    mime_type: str = Field(\n        default=\"text/plain\",\n        description=\"MIME type of the resource content\",\n    )\n\n    @pydantic.field_validator(\"path\")\n    @classmethod\n    def validate_absolute_path(cls, path: Path) -> Path:\n        \"\"\"Ensure path is absolute.\"\"\"\n        if not path.is_absolute():\n            raise ValueError(\"Path must be absolute\")\n        return path\n\n    @pydantic.field_validator(\"is_binary\")\n    @classmethod\n    def set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool:\n        \"\"\"Set is_binary based on mime_type if not explicitly set.\"\"\"\n        if is_binary:\n            return True\n        mime_type = info.data.get(\"mime_type\", \"text/plain\")\n        return not mime_type.startswith(\"text/\")\n\n    async def read(self) -> str | bytes:\n        \"\"\"Read the file content.\"\"\"\n        try:\n            if self.is_binary:\n                return await anyio.to_thread.run_sync(self.path.read_bytes)\n            return await anyio.to_thread.run_sync(self.path.read_text)\n        except Exception as e:\n            raise ValueError(f\"Error reading file {self.path}: {e}\")\n\n\nclass HttpResource(Resource):\n    \"\"\"A resource that reads from an HTTP endpoint.\"\"\"\n\n    url: str = Field(description=\"URL to fetch content from\")\n    mime_type: str = Field(default=\"application/json\", description=\"MIME type of the resource content\")\n\n    async def read(self) -> str | bytes:\n        \"\"\"Read the HTTP content.\"\"\"\n        async with httpx.AsyncClient() as client:  # pragma: no cover\n            response = await client.get(self.url)\n            response.raise_for_status()\n            return response.text\n\n\nclass DirectoryResource(Resource):\n    \"\"\"A resource that lists files in a directory.\"\"\"\n\n    path: Path = Field(description=\"Path to the directory\")\n    recursive: bool = Field(default=False, description=\"Whether to list files recursively\")\n    pattern: str | None = Field(default=None, description=\"Optional glob pattern to filter files\")\n    mime_type: str = Field(default=\"application/json\", description=\"MIME type of the resource content\")\n\n    @pydantic.field_validator(\"path\")\n    @classmethod\n    def validate_absolute_path(cls, path: Path) -> Path:  # pragma: no cover\n        \"\"\"Ensure path is absolute.\"\"\"\n        if not path.is_absolute():\n            raise ValueError(\"Path must be absolute\")\n        return path\n\n    def list_files(self) -> list[Path]:  # pragma: no cover\n        \"\"\"List files in the directory.\"\"\"\n        if not self.path.exists():\n            raise FileNotFoundError(f\"Directory not found: {self.path}\")\n        if not self.path.is_dir():\n            raise NotADirectoryError(f\"Not a directory: {self.path}\")\n\n        try:\n            if self.pattern:\n                return list(self.path.glob(self.pattern)) if not self.recursive else list(self.path.rglob(self.pattern))\n            return list(self.path.glob(\"*\")) if not self.recursive else list(self.path.rglob(\"*\"))\n        except Exception as e:\n            raise ValueError(f\"Error listing directory {self.path}: {e}\")\n\n    async def read(self) -> str:  # Always returns JSON string  # pragma: no cover\n        \"\"\"Read the directory listing.\"\"\"\n        try:\n            files = await anyio.to_thread.run_sync(self.list_files)\n            file_list = [str(f.relative_to(self.path)) for f in files if f.is_file()]\n            return json.dumps({\"files\": file_list}, indent=2)\n        except Exception as e:\n            raise ValueError(f\"Error reading directory {self.path}: {e}\")\n"
  },
  {
    "path": "src/mcp/server/mcpserver/server.py",
    "content": "\"\"\"MCPServer - A more ergonomic interface for MCP servers.\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport inspect\nimport json\nimport re\nfrom collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence\nfrom contextlib import AbstractAsyncContextManager, asynccontextmanager\nfrom typing import Any, Generic, Literal, TypeVar, overload\n\nimport anyio\nimport pydantic_core\nfrom pydantic.networks import AnyUrl\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nfrom starlette.applications import Starlette\nfrom starlette.middleware import Middleware\nfrom starlette.middleware.authentication import AuthenticationMiddleware\nfrom starlette.requests import Request\nfrom starlette.responses import Response\nfrom starlette.routing import Mount, Route\nfrom starlette.types import Receive, Scope, Send\n\nfrom mcp.server.auth.middleware.auth_context import AuthContextMiddleware\nfrom mcp.server.auth.middleware.bearer_auth import BearerAuthBackend, RequireAuthMiddleware\nfrom mcp.server.auth.provider import OAuthAuthorizationServerProvider, ProviderTokenVerifier, TokenVerifier\nfrom mcp.server.auth.settings import AuthSettings\nfrom mcp.server.context import ServerRequestContext\nfrom mcp.server.lowlevel.helper_types import ReadResourceContents\nfrom mcp.server.lowlevel.server import LifespanResultT, Server\nfrom mcp.server.lowlevel.server import lifespan as default_lifespan\nfrom mcp.server.mcpserver.context import Context\nfrom mcp.server.mcpserver.exceptions import ResourceError\nfrom mcp.server.mcpserver.prompts import Prompt, PromptManager\nfrom mcp.server.mcpserver.resources import FunctionResource, Resource, ResourceManager\nfrom mcp.server.mcpserver.tools import Tool, ToolManager\nfrom mcp.server.mcpserver.utilities.context_injection import find_context_parameter\nfrom mcp.server.mcpserver.utilities.logging import configure_logging, get_logger\nfrom mcp.server.sse import SseServerTransport\nfrom mcp.server.stdio import stdio_server\nfrom mcp.server.streamable_http import EventStore\nfrom mcp.server.streamable_http_manager import StreamableHTTPSessionManager\nfrom mcp.server.transport_security import TransportSecuritySettings\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.types import (\n    Annotations,\n    BlobResourceContents,\n    CallToolRequestParams,\n    CallToolResult,\n    CompleteRequestParams,\n    CompleteResult,\n    Completion,\n    ContentBlock,\n    GetPromptRequestParams,\n    GetPromptResult,\n    Icon,\n    ListPromptsResult,\n    ListResourcesResult,\n    ListResourceTemplatesResult,\n    ListToolsResult,\n    PaginatedRequestParams,\n    ReadResourceRequestParams,\n    ReadResourceResult,\n    TextContent,\n    TextResourceContents,\n    ToolAnnotations,\n)\nfrom mcp.types import Prompt as MCPPrompt\nfrom mcp.types import PromptArgument as MCPPromptArgument\nfrom mcp.types import Resource as MCPResource\nfrom mcp.types import ResourceTemplate as MCPResourceTemplate\nfrom mcp.types import Tool as MCPTool\n\nlogger = get_logger(__name__)\n\n_CallableT = TypeVar(\"_CallableT\", bound=Callable[..., Any])\n\n\nclass Settings(BaseSettings, Generic[LifespanResultT]):\n    \"\"\"MCPServer settings.\n\n    All settings can be configured via environment variables with the prefix MCP_.\n    For example, MCP_DEBUG=true will set debug=True.\n    \"\"\"\n\n    model_config = SettingsConfigDict(\n        env_prefix=\"MCP_\",\n        env_file=\".env\",\n        env_nested_delimiter=\"__\",\n        nested_model_default_partial_update=True,\n        extra=\"ignore\",\n    )\n\n    # Server settings\n    debug: bool\n    log_level: Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"]\n\n    # resource settings\n    warn_on_duplicate_resources: bool\n\n    # tool settings\n    warn_on_duplicate_tools: bool\n\n    # prompt settings\n    warn_on_duplicate_prompts: bool\n\n    lifespan: Callable[[MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]] | None\n    \"\"\"An async context manager that will be called when the server is started.\"\"\"\n\n    auth: AuthSettings | None\n\n\ndef lifespan_wrapper(\n    app: MCPServer[LifespanResultT],\n    lifespan: Callable[[MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]],\n) -> Callable[[Server[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]]:\n    @asynccontextmanager\n    async def wrap(_: Server[LifespanResultT]) -> AsyncIterator[LifespanResultT]:\n        async with lifespan(app) as context:\n            yield context\n\n    return wrap\n\n\nclass MCPServer(Generic[LifespanResultT]):\n    def __init__(\n        self,\n        name: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        instructions: str | None = None,\n        website_url: str | None = None,\n        icons: list[Icon] | None = None,\n        version: str | None = None,\n        auth_server_provider: OAuthAuthorizationServerProvider[Any, Any, Any] | None = None,\n        token_verifier: TokenVerifier | None = None,\n        *,\n        tools: list[Tool] | None = None,\n        debug: bool = False,\n        log_level: Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"] = \"INFO\",\n        warn_on_duplicate_resources: bool = True,\n        warn_on_duplicate_tools: bool = True,\n        warn_on_duplicate_prompts: bool = True,\n        lifespan: Callable[[MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]] | None = None,\n        auth: AuthSettings | None = None,\n    ):\n        self.settings = Settings(\n            debug=debug,\n            log_level=log_level,\n            warn_on_duplicate_resources=warn_on_duplicate_resources,\n            warn_on_duplicate_tools=warn_on_duplicate_tools,\n            warn_on_duplicate_prompts=warn_on_duplicate_prompts,\n            lifespan=lifespan,\n            auth=auth,\n        )\n\n        self._tool_manager = ToolManager(tools=tools, warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools)\n        self._resource_manager = ResourceManager(warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources)\n        self._prompt_manager = PromptManager(warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts)\n        self._lowlevel_server = Server(\n            name=name or \"mcp-server\",\n            title=title,\n            description=description,\n            instructions=instructions,\n            website_url=website_url,\n            icons=icons,\n            version=version,\n            on_list_tools=self._handle_list_tools,\n            on_call_tool=self._handle_call_tool,\n            on_list_resources=self._handle_list_resources,\n            on_read_resource=self._handle_read_resource,\n            on_list_resource_templates=self._handle_list_resource_templates,\n            on_list_prompts=self._handle_list_prompts,\n            on_get_prompt=self._handle_get_prompt,\n            # TODO(Marcelo): It seems there's a type mismatch between the lifespan type from an MCPServer and Server.\n            # We need to create a Lifespan type that is a generic on the server type, like Starlette does.\n            lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan),  # type: ignore\n        )\n        # Validate auth configuration\n        if self.settings.auth is not None:\n            if auth_server_provider and token_verifier:  # pragma: no cover\n                raise ValueError(\"Cannot specify both auth_server_provider and token_verifier\")\n            if not auth_server_provider and not token_verifier:  # pragma: no cover\n                raise ValueError(\"Must specify either auth_server_provider or token_verifier when auth is enabled\")\n        elif auth_server_provider or token_verifier:  # pragma: no cover\n            raise ValueError(\"Cannot specify auth_server_provider or token_verifier without auth settings\")\n\n        self._auth_server_provider = auth_server_provider\n        self._token_verifier = token_verifier\n\n        # Create token verifier from provider if needed (backwards compatibility)\n        if auth_server_provider and not token_verifier:  # pragma: no cover\n            self._token_verifier = ProviderTokenVerifier(auth_server_provider)\n        self._custom_starlette_routes: list[Route] = []\n\n        # Configure logging\n        configure_logging(self.settings.log_level)\n\n    @property\n    def name(self) -> str:\n        return self._lowlevel_server.name\n\n    @property\n    def title(self) -> str | None:\n        return self._lowlevel_server.title\n\n    @property\n    def description(self) -> str | None:\n        return self._lowlevel_server.description\n\n    @property\n    def instructions(self) -> str | None:\n        return self._lowlevel_server.instructions\n\n    @property\n    def website_url(self) -> str | None:\n        return self._lowlevel_server.website_url\n\n    @property\n    def icons(self) -> list[Icon] | None:\n        return self._lowlevel_server.icons\n\n    @property\n    def version(self) -> str | None:\n        return self._lowlevel_server.version\n\n    @property\n    def session_manager(self) -> StreamableHTTPSessionManager:\n        \"\"\"Get the StreamableHTTP session manager.\n\n        This is exposed to enable advanced use cases like mounting multiple\n        MCPServer instances in a single FastAPI application.\n\n        Raises:\n            RuntimeError: If called before streamable_http_app() has been called.\n        \"\"\"\n        return self._lowlevel_server.session_manager  # pragma: no cover\n\n    @overload\n    def run(self, transport: Literal[\"stdio\"] = ...) -> None: ...\n\n    @overload\n    def run(\n        self,\n        transport: Literal[\"sse\"],\n        *,\n        host: str = ...,\n        port: int = ...,\n        sse_path: str = ...,\n        message_path: str = ...,\n        transport_security: TransportSecuritySettings | None = ...,\n    ) -> None: ...\n\n    @overload\n    def run(\n        self,\n        transport: Literal[\"streamable-http\"],\n        *,\n        host: str = ...,\n        port: int = ...,\n        streamable_http_path: str = ...,\n        json_response: bool = ...,\n        stateless_http: bool = ...,\n        event_store: EventStore | None = ...,\n        retry_interval: int | None = ...,\n        transport_security: TransportSecuritySettings | None = ...,\n    ) -> None: ...\n\n    def run(\n        self,\n        transport: Literal[\"stdio\", \"sse\", \"streamable-http\"] = \"stdio\",\n        **kwargs: Any,\n    ) -> None:\n        \"\"\"Run the MCP server. Note this is a synchronous function.\n\n        Args:\n            transport: Transport protocol to use (\"stdio\", \"sse\", or \"streamable-http\")\n            **kwargs: Transport-specific options (see overloads for details)\n        \"\"\"\n        TRANSPORTS = Literal[\"stdio\", \"sse\", \"streamable-http\"]\n        if transport not in TRANSPORTS.__args__:  # type: ignore  # pragma: no cover\n            raise ValueError(f\"Unknown transport: {transport}\")\n\n        match transport:\n            case \"stdio\":\n                anyio.run(self.run_stdio_async)\n            case \"sse\":  # pragma: no cover\n                anyio.run(lambda: self.run_sse_async(**kwargs))\n            case \"streamable-http\":  # pragma: no cover\n                anyio.run(lambda: self.run_streamable_http_async(**kwargs))\n\n    async def _handle_list_tools(\n        self, ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None\n    ) -> ListToolsResult:\n        return ListToolsResult(tools=await self.list_tools())\n\n    async def _handle_call_tool(\n        self, ctx: ServerRequestContext[LifespanResultT], params: CallToolRequestParams\n    ) -> CallToolResult:\n        context = Context(request_context=ctx, mcp_server=self)\n        try:\n            result = await self.call_tool(params.name, params.arguments or {}, context)\n        except MCPError:\n            raise\n        except Exception as e:\n            return CallToolResult(content=[TextContent(type=\"text\", text=str(e))], is_error=True)\n        if isinstance(result, CallToolResult):\n            return result\n        if isinstance(result, tuple) and len(result) == 2:\n            unstructured_content, structured_content = result\n            return CallToolResult(\n                content=list(unstructured_content),  # type: ignore[arg-type]\n                structured_content=structured_content,  # type: ignore[arg-type]\n            )\n        if isinstance(result, dict):  # pragma: no cover\n            # TODO: this code path is unreachable — convert_result never returns a raw dict.\n            # The call_tool return type (Sequence[ContentBlock] | dict[str, Any]) is wrong\n            # and needs to be cleaned up.\n            return CallToolResult(\n                content=[TextContent(type=\"text\", text=json.dumps(result, indent=2))],\n                structured_content=result,\n            )\n        return CallToolResult(content=list(result))\n\n    async def _handle_list_resources(\n        self, ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None\n    ) -> ListResourcesResult:\n        return ListResourcesResult(resources=await self.list_resources())\n\n    async def _handle_read_resource(\n        self, ctx: ServerRequestContext[LifespanResultT], params: ReadResourceRequestParams\n    ) -> ReadResourceResult:\n        context = Context(request_context=ctx, mcp_server=self)\n        results = await self.read_resource(params.uri, context)\n        contents: list[TextResourceContents | BlobResourceContents] = []\n        for item in results:\n            if isinstance(item.content, bytes):\n                contents.append(\n                    BlobResourceContents(\n                        uri=params.uri,\n                        blob=base64.b64encode(item.content).decode(),\n                        mime_type=item.mime_type or \"application/octet-stream\",\n                        _meta=item.meta,\n                    )\n                )\n            else:\n                contents.append(\n                    TextResourceContents(\n                        uri=params.uri,\n                        text=item.content,\n                        mime_type=item.mime_type or \"text/plain\",\n                        _meta=item.meta,\n                    )\n                )\n        return ReadResourceResult(contents=contents)\n\n    async def _handle_list_resource_templates(\n        self, ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None\n    ) -> ListResourceTemplatesResult:\n        return ListResourceTemplatesResult(resource_templates=await self.list_resource_templates())\n\n    async def _handle_list_prompts(\n        self, ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None\n    ) -> ListPromptsResult:\n        return ListPromptsResult(prompts=await self.list_prompts())\n\n    async def _handle_get_prompt(\n        self, ctx: ServerRequestContext[LifespanResultT], params: GetPromptRequestParams\n    ) -> GetPromptResult:\n        context = Context(request_context=ctx, mcp_server=self)\n        return await self.get_prompt(params.name, params.arguments, context)\n\n    async def list_tools(self) -> list[MCPTool]:\n        \"\"\"List all available tools.\"\"\"\n        tools = self._tool_manager.list_tools()\n        return [\n            MCPTool(\n                name=info.name,\n                title=info.title,\n                description=info.description,\n                input_schema=info.parameters,\n                output_schema=info.output_schema,\n                annotations=info.annotations,\n                icons=info.icons,\n                _meta=info.meta,\n            )\n            for info in tools\n        ]\n\n    async def call_tool(\n        self, name: str, arguments: dict[str, Any], context: Context[LifespanResultT, Any] | None = None\n    ) -> Sequence[ContentBlock] | dict[str, Any]:\n        \"\"\"Call a tool by name with arguments.\"\"\"\n        if context is None:\n            context = Context(mcp_server=self)\n        return await self._tool_manager.call_tool(name, arguments, context, convert_result=True)\n\n    async def list_resources(self) -> list[MCPResource]:\n        \"\"\"List all available resources.\"\"\"\n\n        resources = self._resource_manager.list_resources()\n        return [\n            MCPResource(\n                uri=resource.uri,\n                name=resource.name or \"\",\n                title=resource.title,\n                description=resource.description,\n                mime_type=resource.mime_type,\n                icons=resource.icons,\n                annotations=resource.annotations,\n                _meta=resource.meta,\n            )\n            for resource in resources\n        ]\n\n    async def list_resource_templates(self) -> list[MCPResourceTemplate]:\n        templates = self._resource_manager.list_templates()\n        return [\n            MCPResourceTemplate(\n                uri_template=template.uri_template,\n                name=template.name,\n                title=template.title,\n                description=template.description,\n                mime_type=template.mime_type,\n                icons=template.icons,\n                annotations=template.annotations,\n                _meta=template.meta,\n            )\n            for template in templates\n        ]\n\n    async def read_resource(\n        self, uri: AnyUrl | str, context: Context[LifespanResultT, Any] | None = None\n    ) -> Iterable[ReadResourceContents]:\n        \"\"\"Read a resource by URI.\"\"\"\n        if context is None:\n            context = Context(mcp_server=self)\n        try:\n            resource = await self._resource_manager.get_resource(uri, context)\n        except ValueError:\n            raise ResourceError(f\"Unknown resource: {uri}\")\n\n        try:\n            content = await resource.read()\n            return [ReadResourceContents(content=content, mime_type=resource.mime_type, meta=resource.meta)]\n        except Exception as exc:\n            logger.exception(f\"Error getting resource {uri}\")\n            # If an exception happens when reading the resource, we should not leak the exception to the client.\n            raise ResourceError(f\"Error reading resource {uri}\") from exc\n\n    def add_tool(\n        self,\n        fn: Callable[..., Any],\n        name: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        annotations: ToolAnnotations | None = None,\n        icons: list[Icon] | None = None,\n        meta: dict[str, Any] | None = None,\n        structured_output: bool | None = None,\n    ) -> None:\n        \"\"\"Add a tool to the server.\n\n        The tool function can optionally request a Context object by adding a parameter\n        with the Context type annotation. See the @tool decorator for examples.\n\n        Args:\n            fn: The function to register as a tool\n            name: Optional name for the tool (defaults to function name)\n            title: Optional human-readable title for the tool\n            description: Optional description of what the tool does\n            annotations: Optional ToolAnnotations providing additional tool information\n            icons: Optional list of icons for the tool\n            meta: Optional metadata dictionary for the tool\n            structured_output: Controls whether the tool's output is structured or unstructured\n                - If None, auto-detects based on the function's return type annotation\n                - If True, creates a structured tool (return type annotation permitting)\n                - If False, unconditionally creates an unstructured tool\n        \"\"\"\n        self._tool_manager.add_tool(\n            fn,\n            name=name,\n            title=title,\n            description=description,\n            annotations=annotations,\n            icons=icons,\n            meta=meta,\n            structured_output=structured_output,\n        )\n\n    def remove_tool(self, name: str) -> None:\n        \"\"\"Remove a tool from the server by name.\n\n        Args:\n            name: The name of the tool to remove\n\n        Raises:\n            ToolError: If the tool does not exist\n        \"\"\"\n        self._tool_manager.remove_tool(name)\n\n    def tool(\n        self,\n        name: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        annotations: ToolAnnotations | None = None,\n        icons: list[Icon] | None = None,\n        meta: dict[str, Any] | None = None,\n        structured_output: bool | None = None,\n    ) -> Callable[[_CallableT], _CallableT]:\n        \"\"\"Decorator to register a tool.\n\n        Tools can optionally request a Context object by adding a parameter with the\n        Context type annotation. The context provides access to MCP capabilities like\n        logging, progress reporting, and resource access.\n\n        Args:\n            name: Optional name for the tool (defaults to function name)\n            title: Optional human-readable title for the tool\n            description: Optional description of what the tool does\n            annotations: Optional ToolAnnotations providing additional tool information\n            icons: Optional list of icons for the tool\n            meta: Optional metadata dictionary for the tool\n            structured_output: Controls whether the tool's output is structured or unstructured\n                - If None, auto-detects based on the function's return type annotation\n                - If True, creates a structured tool (return type annotation permitting)\n                - If False, unconditionally creates an unstructured tool\n\n        Example:\n            ```python\n            @server.tool()\n            def my_tool(x: int) -> str:\n                return str(x)\n            ```\n\n            ```python\n            @server.tool()\n            async def tool_with_context(x: int, ctx: Context) -> str:\n                await ctx.info(f\"Processing {x}\")\n                return str(x)\n            ```\n\n            ```python\n            @server.tool()\n            async def async_tool(x: int, context: Context) -> str:\n                await context.report_progress(50, 100)\n                return str(x)\n            ```\n        \"\"\"\n        # Check if user passed function directly instead of calling decorator\n        if callable(name):\n            raise TypeError(\n                \"The @tool decorator was used incorrectly. Did you forget to call it? Use @tool() instead of @tool\"\n            )\n\n        def decorator(fn: _CallableT) -> _CallableT:\n            self.add_tool(\n                fn,\n                name=name,\n                title=title,\n                description=description,\n                annotations=annotations,\n                icons=icons,\n                meta=meta,\n                structured_output=structured_output,\n            )\n            return fn\n\n        return decorator\n\n    def completion(self):\n        \"\"\"Decorator to register a completion handler.\n\n        The completion handler receives:\n        - ref: PromptReference or ResourceTemplateReference\n        - argument: CompletionArgument with name and partial value\n        - context: Optional CompletionContext with previously resolved arguments\n\n        Example:\n            ```python\n            @mcp.completion()\n            async def handle_completion(ref, argument, context):\n                if isinstance(ref, ResourceTemplateReference):\n                    # Return completions based on ref, argument, and context\n                    return Completion(values=[\"option1\", \"option2\"])\n                return None\n            ```\n        \"\"\"\n\n        def decorator(func: _CallableT) -> _CallableT:\n            async def handler(\n                ctx: ServerRequestContext[LifespanResultT], params: CompleteRequestParams\n            ) -> CompleteResult:\n                result = await func(params.ref, params.argument, params.context)\n                return CompleteResult(\n                    completion=result if result is not None else Completion(values=[], total=None, has_more=None),\n                )\n\n            # TODO(maxisbey): remove private access — completion needs post-construction\n            #   handler registration, find a better pattern for this\n            self._lowlevel_server._add_request_handler(  # pyright: ignore[reportPrivateUsage]\n                \"completion/complete\", handler\n            )\n            return func\n\n        return decorator\n\n    def add_resource(self, resource: Resource) -> None:\n        \"\"\"Add a resource to the server.\n\n        Args:\n            resource: A Resource instance to add\n        \"\"\"\n        self._resource_manager.add_resource(resource)\n\n    def resource(\n        self,\n        uri: str,\n        *,\n        name: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        mime_type: str | None = None,\n        icons: list[Icon] | None = None,\n        annotations: Annotations | None = None,\n        meta: dict[str, Any] | None = None,\n    ) -> Callable[[_CallableT], _CallableT]:\n        \"\"\"Decorator to register a function as a resource.\n\n        The function will be called when the resource is read to generate its content.\n        The function can return:\n        - str for text content\n        - bytes for binary content\n        - other types will be converted to JSON\n\n        If the URI contains parameters (e.g. \"resource://{param}\") or the function\n        has parameters, it will be registered as a template resource.\n\n        Args:\n            uri: URI for the resource (e.g. \"resource://my-resource\" or \"resource://{param}\")\n            name: Optional name for the resource\n            title: Optional human-readable title for the resource\n            description: Optional description of the resource\n            mime_type: Optional MIME type for the resource\n            icons: Optional list of icons for the resource\n            annotations: Optional annotations for the resource\n            meta: Optional metadata dictionary for the resource\n\n        Example:\n            ```python\n            @server.resource(\"resource://my-resource\")\n            def get_data() -> str:\n                return \"Hello, world!\"\n\n            @server.resource(\"resource://my-resource\")\n            async def get_data() -> str:\n                data = await fetch_data()\n                return f\"Hello, world! {data}\"\n\n            @server.resource(\"resource://{city}/weather\")\n            def get_weather(city: str) -> str:\n                return f\"Weather for {city}\"\n\n            @server.resource(\"resource://{city}/weather\")\n            async def get_weather(city: str) -> str:\n                data = await fetch_weather(city)\n                return f\"Weather for {city}: {data}\"\n            ```\n        \"\"\"\n        # Check if user passed function directly instead of calling decorator\n        if callable(uri):\n            raise TypeError(\n                \"The @resource decorator was used incorrectly. \"\n                \"Did you forget to call it? Use @resource('uri') instead of @resource\"\n            )\n\n        def decorator(fn: _CallableT) -> _CallableT:\n            # Check if this should be a template\n            sig = inspect.signature(fn)\n            has_uri_params = \"{\" in uri and \"}\" in uri\n            has_func_params = bool(sig.parameters)\n\n            if has_uri_params or has_func_params:\n                # Check for Context parameter to exclude from validation\n                context_param = find_context_parameter(fn)\n\n                # Validate that URI params match function params (excluding context)\n                uri_params = set(re.findall(r\"{(\\w+)}\", uri))\n                # We need to remove the context_param from the resource function if\n                # there is any.\n                func_params = {p for p in sig.parameters.keys() if p != context_param}\n\n                if uri_params != func_params:\n                    raise ValueError(\n                        f\"Mismatch between URI parameters {uri_params} and function parameters {func_params}\"\n                    )\n\n                # Register as template\n                self._resource_manager.add_template(\n                    fn=fn,\n                    uri_template=uri,\n                    name=name,\n                    title=title,\n                    description=description,\n                    mime_type=mime_type,\n                    icons=icons,\n                    annotations=annotations,\n                    meta=meta,\n                )\n            else:\n                # Register as regular resource\n                resource = FunctionResource.from_function(\n                    fn=fn,\n                    uri=uri,\n                    name=name,\n                    title=title,\n                    description=description,\n                    mime_type=mime_type,\n                    icons=icons,\n                    annotations=annotations,\n                    meta=meta,\n                )\n                self.add_resource(resource)\n            return fn\n\n        return decorator\n\n    def add_prompt(self, prompt: Prompt) -> None:\n        \"\"\"Add a prompt to the server.\n\n        Args:\n            prompt: A Prompt instance to add\n        \"\"\"\n        self._prompt_manager.add_prompt(prompt)\n\n    def prompt(\n        self,\n        name: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        icons: list[Icon] | None = None,\n    ) -> Callable[[_CallableT], _CallableT]:\n        \"\"\"Decorator to register a prompt.\n\n        Args:\n            name: Optional name for the prompt (defaults to function name)\n            title: Optional human-readable title for the prompt\n            description: Optional description of what the prompt does\n            icons: Optional list of icons for the prompt\n\n        Example:\n            ```python\n            @server.prompt()\n            def analyze_table(table_name: str) -> list[Message]:\n                schema = read_table_schema(table_name)\n                return [\n                    {\n                        \"role\": \"user\",\n                        \"content\": f\"Analyze this schema:\\n{schema}\"\n                    }\n                ]\n\n            @server.prompt()\n            async def analyze_file(path: str) -> list[Message]:\n                content = await read_file(path)\n                return [\n                    {\n                        \"role\": \"user\",\n                        \"content\": {\n                            \"type\": \"resource\",\n                            \"resource\": {\n                                \"uri\": f\"file://{path}\",\n                                \"text\": content\n                            }\n                        }\n                    }\n                ]\n            ```\n        \"\"\"\n        # Check if user passed function directly instead of calling decorator\n        if callable(name):\n            raise TypeError(\n                \"The @prompt decorator was used incorrectly. \"\n                \"Did you forget to call it? Use @prompt() instead of @prompt\"\n            )\n\n        def decorator(func: _CallableT) -> _CallableT:\n            prompt = Prompt.from_function(func, name=name, title=title, description=description, icons=icons)\n            self.add_prompt(prompt)\n            return func\n\n        return decorator\n\n    def custom_route(\n        self,\n        path: str,\n        methods: list[str],\n        name: str | None = None,\n        include_in_schema: bool = True,\n    ):\n        \"\"\"Decorator to register a custom HTTP route on the MCP server.\n\n        Allows adding arbitrary HTTP endpoints outside the standard MCP protocol,\n        which can be useful for OAuth callbacks, health checks, or admin APIs.\n        The handler function must be an async function that accepts a Starlette\n        Request and returns a Response.\n\n        Routes using this decorator will not require authorization. It is intended\n        for uses that are either a part of authorization flows or intended to be\n        public such as health check endpoints.\n\n        Args:\n            path: URL path for the route (e.g., \"/oauth/callback\")\n            methods: List of HTTP methods to support (e.g., [\"GET\", \"POST\"])\n            name: Optional name for the route (to reference this route with\n                  Starlette's reverse URL lookup feature)\n            include_in_schema: Whether to include in OpenAPI schema, defaults to True\n\n        Example:\n            ```python\n            @server.custom_route(\"/health\", methods=[\"GET\"])\n            async def health_check(request: Request) -> Response:\n                return JSONResponse({\"status\": \"ok\"})\n            ```\n        \"\"\"\n\n        def decorator(  # pragma: no cover\n            func: Callable[[Request], Awaitable[Response]],\n        ) -> Callable[[Request], Awaitable[Response]]:\n            self._custom_starlette_routes.append(\n                Route(path, endpoint=func, methods=methods, name=name, include_in_schema=include_in_schema)\n            )\n            return func\n\n        return decorator  # pragma: no cover\n\n    async def run_stdio_async(self) -> None:\n        \"\"\"Run the server using stdio transport.\"\"\"\n        async with stdio_server() as (read_stream, write_stream):\n            await self._lowlevel_server.run(\n                read_stream,\n                write_stream,\n                self._lowlevel_server.create_initialization_options(),\n            )\n\n    async def run_sse_async(  # pragma: no cover\n        self,\n        *,\n        host: str = \"127.0.0.1\",\n        port: int = 8000,\n        sse_path: str = \"/sse\",\n        message_path: str = \"/messages/\",\n        transport_security: TransportSecuritySettings | None = None,\n    ) -> None:\n        \"\"\"Run the server using SSE transport.\"\"\"\n        import uvicorn\n\n        starlette_app = self.sse_app(\n            sse_path=sse_path,\n            message_path=message_path,\n            transport_security=transport_security,\n            host=host,\n        )\n\n        config = uvicorn.Config(\n            starlette_app,\n            host=host,\n            port=port,\n            log_level=self.settings.log_level.lower(),\n        )\n        server = uvicorn.Server(config)\n        await server.serve()\n\n    async def run_streamable_http_async(  # pragma: no cover\n        self,\n        *,\n        host: str = \"127.0.0.1\",\n        port: int = 8000,\n        streamable_http_path: str = \"/mcp\",\n        json_response: bool = False,\n        stateless_http: bool = False,\n        event_store: EventStore | None = None,\n        retry_interval: int | None = None,\n        transport_security: TransportSecuritySettings | None = None,\n    ) -> None:\n        \"\"\"Run the server using StreamableHTTP transport.\"\"\"\n        import uvicorn\n\n        starlette_app = self.streamable_http_app(\n            streamable_http_path=streamable_http_path,\n            json_response=json_response,\n            stateless_http=stateless_http,\n            event_store=event_store,\n            retry_interval=retry_interval,\n            transport_security=transport_security,\n            host=host,\n        )\n\n        config = uvicorn.Config(\n            starlette_app,\n            host=host,\n            port=port,\n            log_level=self.settings.log_level.lower(),\n        )\n        server = uvicorn.Server(config)\n        await server.serve()\n\n    def sse_app(\n        self,\n        *,\n        sse_path: str = \"/sse\",\n        message_path: str = \"/messages/\",\n        transport_security: TransportSecuritySettings | None = None,\n        host: str = \"127.0.0.1\",\n    ) -> Starlette:\n        \"\"\"Return an instance of the SSE server app.\"\"\"\n        # Auto-enable DNS rebinding protection for localhost (IPv4 and IPv6)\n        if transport_security is None and host in (\"127.0.0.1\", \"localhost\", \"::1\"):\n            transport_security = TransportSecuritySettings(\n                enable_dns_rebinding_protection=True,\n                allowed_hosts=[\"127.0.0.1:*\", \"localhost:*\", \"[::1]:*\"],\n                allowed_origins=[\"http://127.0.0.1:*\", \"http://localhost:*\", \"http://[::1]:*\"],\n            )\n\n        sse = SseServerTransport(message_path, security_settings=transport_security)\n\n        async def handle_sse(scope: Scope, receive: Receive, send: Send):  # pragma: no cover\n            # Add client ID from auth context into request context if available\n\n            async with sse.connect_sse(scope, receive, send) as streams:\n                await self._lowlevel_server.run(\n                    streams[0], streams[1], self._lowlevel_server.create_initialization_options()\n                )\n            return Response()\n\n        # Create routes\n        routes: list[Route | Mount] = []\n        middleware: list[Middleware] = []\n        required_scopes: list[str] = []\n\n        # Set up auth if configured\n        if self.settings.auth:  # pragma: no cover\n            required_scopes = self.settings.auth.required_scopes or []\n\n            # Add auth middleware if token verifier is available\n            if self._token_verifier:\n                middleware = [\n                    # extract auth info from request (but do not require it)\n                    Middleware(\n                        AuthenticationMiddleware,\n                        backend=BearerAuthBackend(self._token_verifier),\n                    ),\n                    # Add the auth context middleware to store\n                    # authenticated user in a contextvar\n                    Middleware(AuthContextMiddleware),\n                ]\n\n            # Add auth endpoints if auth server provider is configured\n            if self._auth_server_provider:\n                from mcp.server.auth.routes import create_auth_routes\n\n                routes.extend(\n                    create_auth_routes(\n                        provider=self._auth_server_provider,\n                        issuer_url=self.settings.auth.issuer_url,\n                        service_documentation_url=self.settings.auth.service_documentation_url,\n                        client_registration_options=self.settings.auth.client_registration_options,\n                        revocation_options=self.settings.auth.revocation_options,\n                    )\n                )\n\n        # When auth is configured, require authentication\n        if self._token_verifier:  # pragma: no cover\n            # Determine resource metadata URL\n            resource_metadata_url = None\n            if self.settings.auth and self.settings.auth.resource_server_url:\n                from mcp.server.auth.routes import build_resource_metadata_url\n\n                # Build compliant metadata URL for WWW-Authenticate header\n                resource_metadata_url = build_resource_metadata_url(self.settings.auth.resource_server_url)\n\n            # Auth is enabled, wrap the endpoints with RequireAuthMiddleware\n            routes.append(\n                Route(\n                    sse_path,\n                    endpoint=RequireAuthMiddleware(handle_sse, required_scopes, resource_metadata_url),\n                    methods=[\"GET\"],\n                )\n            )\n            routes.append(\n                Mount(\n                    message_path,\n                    app=RequireAuthMiddleware(sse.handle_post_message, required_scopes, resource_metadata_url),\n                )\n            )\n        else:\n            # Auth is disabled, no need for RequireAuthMiddleware\n            # Since handle_sse is an ASGI app, we need to create a compatible endpoint\n            async def sse_endpoint(request: Request) -> Response:  # pragma: no cover\n                # Convert the Starlette request to ASGI parameters\n                return await handle_sse(request.scope, request.receive, request._send)  # type: ignore[reportPrivateUsage]\n\n            routes.append(\n                Route(\n                    sse_path,\n                    endpoint=sse_endpoint,\n                    methods=[\"GET\"],\n                )\n            )\n            routes.append(\n                Mount(\n                    message_path,\n                    app=sse.handle_post_message,\n                )\n            )\n        # Add protected resource metadata endpoint if configured as RS\n        if self.settings.auth and self.settings.auth.resource_server_url:  # pragma: no cover\n            from mcp.server.auth.routes import create_protected_resource_routes\n\n            routes.extend(\n                create_protected_resource_routes(\n                    resource_url=self.settings.auth.resource_server_url,\n                    authorization_servers=[self.settings.auth.issuer_url],\n                    scopes_supported=self.settings.auth.required_scopes,\n                )\n            )\n\n        # mount these routes last, so they have the lowest route matching precedence\n        routes.extend(self._custom_starlette_routes)\n\n        # Create Starlette app with routes and middleware\n        return Starlette(debug=self.settings.debug, routes=routes, middleware=middleware)\n\n    def streamable_http_app(\n        self,\n        *,\n        streamable_http_path: str = \"/mcp\",\n        json_response: bool = False,\n        stateless_http: bool = False,\n        event_store: EventStore | None = None,\n        retry_interval: int | None = None,\n        transport_security: TransportSecuritySettings | None = None,\n        host: str = \"127.0.0.1\",\n    ) -> Starlette:\n        \"\"\"Return an instance of the StreamableHTTP server app.\"\"\"\n        return self._lowlevel_server.streamable_http_app(\n            streamable_http_path=streamable_http_path,\n            json_response=json_response,\n            stateless_http=stateless_http,\n            event_store=event_store,\n            retry_interval=retry_interval,\n            transport_security=transport_security,\n            host=host,\n            auth=self.settings.auth,\n            token_verifier=self._token_verifier,\n            auth_server_provider=self._auth_server_provider,\n            custom_starlette_routes=self._custom_starlette_routes,\n            debug=self.settings.debug,\n        )\n\n    async def list_prompts(self) -> list[MCPPrompt]:\n        \"\"\"List all available prompts.\"\"\"\n        prompts = self._prompt_manager.list_prompts()\n        return [\n            MCPPrompt(\n                name=prompt.name,\n                title=prompt.title,\n                description=prompt.description,\n                arguments=[\n                    MCPPromptArgument(\n                        name=arg.name,\n                        description=arg.description,\n                        required=arg.required,\n                    )\n                    for arg in (prompt.arguments or [])\n                ],\n                icons=prompt.icons,\n            )\n            for prompt in prompts\n        ]\n\n    async def get_prompt(\n        self, name: str, arguments: dict[str, Any] | None = None, context: Context[LifespanResultT, Any] | None = None\n    ) -> GetPromptResult:\n        \"\"\"Get a prompt by name with arguments.\"\"\"\n        if context is None:\n            context = Context(mcp_server=self)\n        try:\n            prompt = self._prompt_manager.get_prompt(name)\n            if not prompt:\n                raise ValueError(f\"Unknown prompt: {name}\")\n\n            messages = await prompt.render(arguments, context)\n\n            return GetPromptResult(\n                description=prompt.description,\n                messages=pydantic_core.to_jsonable_python(messages),\n            )\n        except Exception as e:\n            logger.exception(f\"Error getting prompt {name}\")\n            raise ValueError(str(e))\n"
  },
  {
    "path": "src/mcp/server/mcpserver/tools/__init__.py",
    "content": "from .base import Tool\nfrom .tool_manager import ToolManager\n\n__all__ = [\"Tool\", \"ToolManager\"]\n"
  },
  {
    "path": "src/mcp/server/mcpserver/tools/base.py",
    "content": "from __future__ import annotations\n\nimport functools\nimport inspect\nfrom collections.abc import Callable\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING, Any\n\nfrom pydantic import BaseModel, Field\n\nfrom mcp.server.mcpserver.exceptions import ToolError\nfrom mcp.server.mcpserver.utilities.context_injection import find_context_parameter\nfrom mcp.server.mcpserver.utilities.func_metadata import FuncMetadata, func_metadata\nfrom mcp.shared.exceptions import UrlElicitationRequiredError\nfrom mcp.shared.tool_name_validation import validate_and_warn_tool_name\nfrom mcp.types import Icon, ToolAnnotations\n\nif TYPE_CHECKING:\n    from mcp.server.context import LifespanContextT, RequestT\n    from mcp.server.mcpserver.context import Context\n\n\nclass Tool(BaseModel):\n    \"\"\"Internal tool registration info.\"\"\"\n\n    fn: Callable[..., Any] = Field(exclude=True)\n    name: str = Field(description=\"Name of the tool\")\n    title: str | None = Field(None, description=\"Human-readable title of the tool\")\n    description: str = Field(description=\"Description of what the tool does\")\n    parameters: dict[str, Any] = Field(description=\"JSON schema for tool parameters\")\n    fn_metadata: FuncMetadata = Field(\n        description=\"Metadata about the function including a pydantic model for tool arguments\"\n    )\n    is_async: bool = Field(description=\"Whether the tool is async\")\n    context_kwarg: str | None = Field(None, description=\"Name of the kwarg that should receive context\")\n    annotations: ToolAnnotations | None = Field(None, description=\"Optional annotations for the tool\")\n    icons: list[Icon] | None = Field(default=None, description=\"Optional list of icons for this tool\")\n    meta: dict[str, Any] | None = Field(default=None, description=\"Optional metadata for this tool\")\n\n    @cached_property\n    def output_schema(self) -> dict[str, Any] | None:\n        return self.fn_metadata.output_schema\n\n    @classmethod\n    def from_function(\n        cls,\n        fn: Callable[..., Any],\n        name: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        context_kwarg: str | None = None,\n        annotations: ToolAnnotations | None = None,\n        icons: list[Icon] | None = None,\n        meta: dict[str, Any] | None = None,\n        structured_output: bool | None = None,\n    ) -> Tool:\n        \"\"\"Create a Tool from a function.\"\"\"\n        func_name = name or fn.__name__\n\n        validate_and_warn_tool_name(func_name)\n\n        if func_name == \"<lambda>\":\n            raise ValueError(\"You must provide a name for lambda functions\")\n\n        func_doc = description or fn.__doc__ or \"\"\n        is_async = _is_async_callable(fn)\n\n        if context_kwarg is None:  # pragma: no branch\n            context_kwarg = find_context_parameter(fn)\n\n        func_arg_metadata = func_metadata(\n            fn,\n            skip_names=[context_kwarg] if context_kwarg is not None else [],\n            structured_output=structured_output,\n        )\n        parameters = func_arg_metadata.arg_model.model_json_schema(by_alias=True)\n\n        return cls(\n            fn=fn,\n            name=func_name,\n            title=title,\n            description=func_doc,\n            parameters=parameters,\n            fn_metadata=func_arg_metadata,\n            is_async=is_async,\n            context_kwarg=context_kwarg,\n            annotations=annotations,\n            icons=icons,\n            meta=meta,\n        )\n\n    async def run(\n        self,\n        arguments: dict[str, Any],\n        context: Context[LifespanContextT, RequestT],\n        convert_result: bool = False,\n    ) -> Any:\n        \"\"\"Run the tool with arguments.\n\n        Raises:\n            ToolError: If the tool function raises during execution.\n        \"\"\"\n        try:\n            result = await self.fn_metadata.call_fn_with_arg_validation(\n                self.fn,\n                self.is_async,\n                arguments,\n                {self.context_kwarg: context} if self.context_kwarg is not None else None,\n            )\n\n            if convert_result:\n                result = self.fn_metadata.convert_result(result)\n\n            return result\n        except UrlElicitationRequiredError:\n            # Re-raise UrlElicitationRequiredError so it can be properly handled\n            # as an MCP error response with code -32042\n            raise\n        except Exception as e:\n            raise ToolError(f\"Error executing tool {self.name}: {e}\") from e\n\n\ndef _is_async_callable(obj: Any) -> bool:\n    while isinstance(obj, functools.partial):  # pragma: lax no cover\n        obj = obj.func\n\n    return inspect.iscoroutinefunction(obj) or (\n        callable(obj) and inspect.iscoroutinefunction(getattr(obj, \"__call__\", None))\n    )\n"
  },
  {
    "path": "src/mcp/server/mcpserver/tools/tool_manager.py",
    "content": "from __future__ import annotations\n\nfrom collections.abc import Callable\nfrom typing import TYPE_CHECKING, Any\n\nfrom mcp.server.mcpserver.exceptions import ToolError\nfrom mcp.server.mcpserver.tools.base import Tool\nfrom mcp.server.mcpserver.utilities.logging import get_logger\nfrom mcp.types import Icon, ToolAnnotations\n\nif TYPE_CHECKING:\n    from mcp.server.context import LifespanContextT, RequestT\n    from mcp.server.mcpserver.context import Context\n\nlogger = get_logger(__name__)\n\n\nclass ToolManager:\n    \"\"\"Manages MCPServer tools.\"\"\"\n\n    def __init__(\n        self,\n        warn_on_duplicate_tools: bool = True,\n        *,\n        tools: list[Tool] | None = None,\n    ):\n        self._tools: dict[str, Tool] = {}\n        if tools is not None:\n            for tool in tools:\n                if warn_on_duplicate_tools and tool.name in self._tools:\n                    logger.warning(f\"Tool already exists: {tool.name}\")\n                self._tools[tool.name] = tool\n\n        self.warn_on_duplicate_tools = warn_on_duplicate_tools\n\n    def get_tool(self, name: str) -> Tool | None:\n        \"\"\"Get tool by name.\"\"\"\n        return self._tools.get(name)\n\n    def list_tools(self) -> list[Tool]:\n        \"\"\"List all registered tools.\"\"\"\n        return list(self._tools.values())\n\n    def add_tool(\n        self,\n        fn: Callable[..., Any],\n        name: str | None = None,\n        title: str | None = None,\n        description: str | None = None,\n        annotations: ToolAnnotations | None = None,\n        icons: list[Icon] | None = None,\n        meta: dict[str, Any] | None = None,\n        structured_output: bool | None = None,\n    ) -> Tool:\n        \"\"\"Add a tool to the server.\"\"\"\n        tool = Tool.from_function(\n            fn,\n            name=name,\n            title=title,\n            description=description,\n            annotations=annotations,\n            icons=icons,\n            meta=meta,\n            structured_output=structured_output,\n        )\n        existing = self._tools.get(tool.name)\n        if existing:\n            if self.warn_on_duplicate_tools:\n                logger.warning(f\"Tool already exists: {tool.name}\")\n            return existing\n        self._tools[tool.name] = tool\n        return tool\n\n    def remove_tool(self, name: str) -> None:\n        \"\"\"Remove a tool by name.\"\"\"\n        if name not in self._tools:\n            raise ToolError(f\"Unknown tool: {name}\")\n        del self._tools[name]\n\n    async def call_tool(\n        self,\n        name: str,\n        arguments: dict[str, Any],\n        context: Context[LifespanContextT, RequestT],\n        convert_result: bool = False,\n    ) -> Any:\n        \"\"\"Call a tool by name with arguments.\"\"\"\n        tool = self.get_tool(name)\n        if not tool:\n            raise ToolError(f\"Unknown tool: {name}\")\n\n        return await tool.run(arguments, context, convert_result=convert_result)\n"
  },
  {
    "path": "src/mcp/server/mcpserver/utilities/__init__.py",
    "content": "\"\"\"MCPServer utility modules.\"\"\"\n"
  },
  {
    "path": "src/mcp/server/mcpserver/utilities/context_injection.py",
    "content": "\"\"\"Context injection utilities for MCPServer.\"\"\"\n\nfrom __future__ import annotations\n\nimport inspect\nimport typing\nfrom collections.abc import Callable\nfrom typing import Any\n\nfrom mcp.server.mcpserver.context import Context\n\n\ndef find_context_parameter(fn: Callable[..., Any]) -> str | None:\n    \"\"\"Find the parameter that should receive the Context object.\n\n    Searches through the function's signature to find a parameter\n    with a Context type annotation.\n\n    Args:\n        fn: The function to inspect\n\n    Returns:\n        The name of the context parameter, or None if not found\n    \"\"\"\n    # Get type hints to properly resolve string annotations\n    try:\n        hints = typing.get_type_hints(fn)\n    except Exception:  # pragma: lax no cover\n        # If we can't resolve type hints, we can't find the context parameter\n        return None\n\n    # Check each parameter's type hint\n    for param_name, annotation in hints.items():\n        # Handle direct Context type\n        if inspect.isclass(annotation) and issubclass(annotation, Context):\n            return param_name\n\n        # Handle generic types like Optional[Context]\n        origin = typing.get_origin(annotation)\n        if origin is not None:\n            args = typing.get_args(annotation)\n            for arg in args:\n                if inspect.isclass(arg) and issubclass(arg, Context):\n                    return param_name\n\n    return None\n\n\ndef inject_context(\n    fn: Callable[..., Any],\n    kwargs: dict[str, Any],\n    context: Any | None,\n    context_kwarg: str | None,\n) -> dict[str, Any]:\n    \"\"\"Inject context into function kwargs if needed.\n\n    Args:\n        fn: The function that will be called\n        kwargs: The current keyword arguments\n        context: The context object to inject (if any)\n        context_kwarg: The name of the parameter to inject into\n\n    Returns:\n        Updated kwargs with context injected if applicable\n    \"\"\"\n    if context_kwarg is not None and context is not None:\n        return {**kwargs, context_kwarg: context}\n    return kwargs\n"
  },
  {
    "path": "src/mcp/server/mcpserver/utilities/func_metadata.py",
    "content": "import functools\nimport inspect\nimport json\nfrom collections.abc import Awaitable, Callable, Sequence\nfrom itertools import chain\nfrom types import GenericAlias\nfrom typing import Annotated, Any, cast, get_args, get_origin, get_type_hints\n\nimport anyio\nimport anyio.to_thread\nimport pydantic_core\nfrom pydantic import BaseModel, ConfigDict, Field, WithJsonSchema, create_model\nfrom pydantic.fields import FieldInfo\nfrom pydantic.json_schema import GenerateJsonSchema, JsonSchemaWarningKind\nfrom typing_extensions import is_typeddict\nfrom typing_inspection.introspection import (\n    UNKNOWN,\n    AnnotationSource,\n    ForbiddenQualifier,\n    inspect_annotation,\n    is_union_origin,\n)\n\nfrom mcp.server.mcpserver.exceptions import InvalidSignature\nfrom mcp.server.mcpserver.utilities.logging import get_logger\nfrom mcp.server.mcpserver.utilities.types import Audio, Image\nfrom mcp.types import CallToolResult, ContentBlock, TextContent\n\nlogger = get_logger(__name__)\n\n\nclass StrictJsonSchema(GenerateJsonSchema):\n    \"\"\"A JSON schema generator that raises exceptions instead of emitting warnings.\n\n    This is used to detect non-serializable types during schema generation.\n    \"\"\"\n\n    def emit_warning(self, kind: JsonSchemaWarningKind, detail: str) -> None:\n        # Raise an exception instead of emitting a warning\n        raise ValueError(f\"JSON schema warning: {kind} - {detail}\")\n\n\nclass ArgModelBase(BaseModel):\n    \"\"\"A model representing the arguments to a function.\"\"\"\n\n    def model_dump_one_level(self) -> dict[str, Any]:\n        \"\"\"Return a dict of the model's fields, one level deep.\n\n        That is, sub-models etc are not dumped - they are kept as Pydantic models.\n        \"\"\"\n        kwargs: dict[str, Any] = {}\n        for field_name, field_info in self.__class__.model_fields.items():\n            value = getattr(self, field_name)\n            # Use the alias if it exists, otherwise use the field name\n            output_name = field_info.alias if field_info.alias else field_name\n            kwargs[output_name] = value\n        return kwargs\n\n    model_config = ConfigDict(arbitrary_types_allowed=True)\n\n\nclass FuncMetadata(BaseModel):\n    arg_model: Annotated[type[ArgModelBase], WithJsonSchema(None)]\n    output_schema: dict[str, Any] | None = None\n    output_model: Annotated[type[BaseModel], WithJsonSchema(None)] | None = None\n    wrap_output: bool = False\n\n    async def call_fn_with_arg_validation(\n        self,\n        fn: Callable[..., Any | Awaitable[Any]],\n        fn_is_async: bool,\n        arguments_to_validate: dict[str, Any],\n        arguments_to_pass_directly: dict[str, Any] | None,\n    ) -> Any:\n        \"\"\"Call the given function with arguments validated and injected.\n\n        Arguments are first attempted to be parsed from JSON, then validated against\n        the argument model, before being passed to the function.\n        \"\"\"\n        arguments_pre_parsed = self.pre_parse_json(arguments_to_validate)\n        arguments_parsed_model = self.arg_model.model_validate(arguments_pre_parsed)\n        arguments_parsed_dict = arguments_parsed_model.model_dump_one_level()\n\n        arguments_parsed_dict |= arguments_to_pass_directly or {}\n\n        if fn_is_async:\n            return await fn(**arguments_parsed_dict)\n        else:\n            return await anyio.to_thread.run_sync(functools.partial(fn, **arguments_parsed_dict))\n\n    def convert_result(self, result: Any) -> Any:\n        \"\"\"Convert a function call result to the format for the lowlevel tool call handler.\n\n        - If output_model is None, return the unstructured content directly.\n        - If output_model is not None, convert the result to structured output format\n            (dict[str, Any]) and return both unstructured and structured content.\n\n        Note: we return unstructured content here **even though the lowlevel server\n        tool call handler provides generic backwards compatibility serialization of\n        structured content**. This is for MCPServer backwards compatibility: we need to\n        retain MCPServer's ad hoc conversion logic for constructing unstructured output\n        from function return values, whereas the lowlevel server simply serializes\n        the structured output.\n        \"\"\"\n        if isinstance(result, CallToolResult):\n            if self.output_schema is not None:\n                assert self.output_model is not None, \"Output model must be set if output schema is defined\"\n                self.output_model.model_validate(result.structured_content)\n            return result\n\n        unstructured_content = _convert_to_content(result)\n\n        if self.output_schema is None:\n            return unstructured_content\n        else:\n            if self.wrap_output:\n                result = {\"result\": result}\n\n            assert self.output_model is not None, \"Output model must be set if output schema is defined\"\n            validated = self.output_model.model_validate(result)\n            structured_content = validated.model_dump(mode=\"json\", by_alias=True)\n\n            return (unstructured_content, structured_content)\n\n    def pre_parse_json(self, data: dict[str, Any]) -> dict[str, Any]:\n        \"\"\"Pre-parse data from JSON.\n\n        Return a dict with the same keys as input but with values parsed from JSON\n        if appropriate.\n\n        This is to handle cases like `[\"a\", \"b\", \"c\"]` being passed in as JSON inside\n        a string rather than an actual list. Claude Desktop is prone to this - in fact\n        it seems incapable of NOT doing this. For sub-models, it tends to pass\n        dicts (JSON objects) as JSON strings, which can be pre-parsed here.\n        \"\"\"\n        new_data = data.copy()  # Shallow copy\n\n        # Build a mapping from input keys (including aliases) to field info\n        key_to_field_info: dict[str, FieldInfo] = {}\n        for field_name, field_info in self.arg_model.model_fields.items():\n            # Map both the field name and its alias (if any) to the field info\n            key_to_field_info[field_name] = field_info\n            if field_info.alias:\n                key_to_field_info[field_info.alias] = field_info\n\n        for data_key, data_value in data.items():\n            if data_key not in key_to_field_info:  # pragma: no cover\n                continue\n\n            field_info = key_to_field_info[data_key]\n            if isinstance(data_value, str) and field_info.annotation is not str:\n                try:\n                    pre_parsed = json.loads(data_value)\n                except json.JSONDecodeError:\n                    continue  # Not JSON - skip\n                if isinstance(pre_parsed, str | int | float):\n                    # This is likely that the raw value is e.g. `\"hello\"` which we\n                    # Should really be parsed as '\"hello\"' in Python - but if we parse\n                    # it as JSON it'll turn into just 'hello'. So we skip it.\n                    continue\n                new_data[data_key] = pre_parsed\n        assert new_data.keys() == data.keys()\n        return new_data\n\n    model_config = ConfigDict(\n        arbitrary_types_allowed=True,\n    )\n\n\ndef func_metadata(\n    func: Callable[..., Any],\n    skip_names: Sequence[str] = (),\n    structured_output: bool | None = None,\n) -> FuncMetadata:\n    \"\"\"Given a function, return metadata including a Pydantic model representing its signature.\n\n    The use case for this is\n    ```\n    meta = func_metadata(func)\n    validated_args = meta.arg_model.model_validate(some_raw_data_dict)\n    return func(**validated_args.model_dump_one_level())\n    ```\n\n    **critically** it also provides a pre-parse helper to attempt to parse things from\n    JSON.\n\n    Args:\n        func: The function to convert to a Pydantic model\n        skip_names: A list of parameter names to skip. These will not be included in\n            the model.\n        structured_output: Controls whether the tool's output is structured or unstructured\n            - If None, auto-detects based on the function's return type annotation\n            - If True, creates a structured tool (return type annotation permitting)\n            - If False, unconditionally creates an unstructured tool\n\n            If structured, creates a Pydantic model for the function's result based on its annotation.\n            Supports various return types:\n            - BaseModel subclasses (used directly)\n            - Primitive types (str, int, float, bool, bytes, None) - wrapped in a\n                model with a 'result' field\n            - TypedDict - converted to a Pydantic model with same fields\n            - Dataclasses and other annotated classes - converted to Pydantic models\n            - Generic types (list, dict, Union, etc.) - wrapped in a model with a 'result' field\n\n    Returns:\n        A FuncMetadata object containing:\n        - arg_model: A Pydantic model representing the function's arguments\n        - output_model: A Pydantic model for the return type if the output is structured\n        - wrap_output: Whether the function result needs to be wrapped in `{\"result\": ...}` for structured output.\n    \"\"\"\n    try:\n        sig = inspect.signature(func, eval_str=True)\n    except NameError as e:  # pragma: no cover\n        # This raise could perhaps be skipped, and we (MCPServer) just call\n        # model_rebuild right before using it 🤷\n        raise InvalidSignature(f\"Unable to evaluate type annotations for callable {func.__name__!r}\") from e\n    params = sig.parameters\n    dynamic_pydantic_model_params: dict[str, Any] = {}\n    for param in params.values():\n        if param.name.startswith(\"_\"):  # pragma: no cover\n            raise InvalidSignature(f\"Parameter {param.name} of {func.__name__} cannot start with '_'\")\n        if param.name in skip_names:\n            continue\n\n        annotation = param.annotation if param.annotation is not inspect.Parameter.empty else Any\n        field_name = param.name\n        field_kwargs: dict[str, Any] = {}\n        field_metadata: list[Any] = []\n\n        if param.annotation is inspect.Parameter.empty:\n            field_metadata.append(WithJsonSchema({\"title\": param.name, \"type\": \"string\"}))\n        # Check if the parameter name conflicts with BaseModel attributes\n        # This is necessary because Pydantic warns about shadowing parent attributes\n        if hasattr(BaseModel, field_name) and callable(getattr(BaseModel, field_name)):\n            # Use an alias to avoid the shadowing warning\n            field_kwargs[\"alias\"] = field_name\n            # Use a prefixed field name\n            field_name = f\"field_{field_name}\"\n\n        if param.default is not inspect.Parameter.empty:\n            dynamic_pydantic_model_params[field_name] = (\n                Annotated[(annotation, *field_metadata, Field(**field_kwargs))],\n                param.default,\n            )\n        else:\n            dynamic_pydantic_model_params[field_name] = Annotated[(annotation, *field_metadata, Field(**field_kwargs))]\n\n    arguments_model = create_model(\n        f\"{func.__name__}Arguments\",\n        __base__=ArgModelBase,\n        **dynamic_pydantic_model_params,\n    )\n\n    if structured_output is False:\n        return FuncMetadata(arg_model=arguments_model)\n\n    # set up structured output support based on return type annotation\n\n    if sig.return_annotation is inspect.Parameter.empty and structured_output is True:\n        raise InvalidSignature(f\"Function {func.__name__}: return annotation required for structured output\")\n\n    try:\n        inspected_return_ann = inspect_annotation(sig.return_annotation, annotation_source=AnnotationSource.FUNCTION)\n    except ForbiddenQualifier as e:\n        raise InvalidSignature(f\"Function {func.__name__}: return annotation contains an invalid type qualifier\") from e\n\n    return_type_expr = inspected_return_ann.type\n\n    # `AnnotationSource.FUNCTION` allows no type qualifier to be used, so `return_type_expr` is guaranteed to *not* be\n    # unknown (i.e. a bare `Final`).\n    assert return_type_expr is not UNKNOWN\n\n    if is_union_origin(get_origin(return_type_expr)):\n        args = get_args(return_type_expr)\n        # Check if CallToolResult appears in the union (excluding None for Optional check)\n        if any(isinstance(arg, type) and issubclass(arg, CallToolResult) for arg in args if arg is not type(None)):\n            raise InvalidSignature(\n                f\"Function {func.__name__}: CallToolResult cannot be used in Union or Optional types. \"\n                \"To return empty results, use: CallToolResult(content=[])\"\n            )\n\n    original_annotation: Any\n    # if the typehint is CallToolResult, the user either intends to return without validation\n    # or they provided validation as Annotated metadata\n    if isinstance(return_type_expr, type) and issubclass(return_type_expr, CallToolResult):\n        if inspected_return_ann.metadata:\n            return_type_expr = inspected_return_ann.metadata[0]\n            if len(inspected_return_ann.metadata) >= 2:\n                # Reconstruct the original annotation, by preserving the remaining metadata,\n                # i.e. from `Annotated[CallToolResult, ReturnType, Gt(1)]` to\n                # `Annotated[ReturnType, Gt(1)]`:\n                original_annotation = Annotated[\n                    (return_type_expr, *inspected_return_ann.metadata[1:])\n                ]  # pragma: no cover\n            else:\n                # We only had `Annotated[CallToolResult, ReturnType]`, treat the original annotation\n                # as being `ReturnType`:\n                original_annotation = return_type_expr\n        else:\n            return FuncMetadata(arg_model=arguments_model)\n    else:\n        original_annotation = sig.return_annotation\n\n    output_model, output_schema, wrap_output = _try_create_model_and_schema(\n        original_annotation, return_type_expr, func.__name__\n    )\n\n    if output_model is None and structured_output is True:\n        # Model creation failed or produced warnings - no structured output\n        raise InvalidSignature(\n            f\"Function {func.__name__}: return type {return_type_expr} is not serializable for structured output\"\n        )\n\n    return FuncMetadata(\n        arg_model=arguments_model,\n        output_schema=output_schema,\n        output_model=output_model,\n        wrap_output=wrap_output,\n    )\n\n\ndef _try_create_model_and_schema(\n    original_annotation: Any,\n    type_expr: Any,\n    func_name: str,\n) -> tuple[type[BaseModel] | None, dict[str, Any] | None, bool]:\n    \"\"\"Try to create a model and schema for the given annotation without warnings.\n\n    Args:\n        original_annotation: The original return annotation (may be wrapped in `Annotated`).\n        type_expr: The underlying type expression derived from the return annotation\n            (`Annotated` and type qualifiers were stripped).\n        func_name: The name of the function.\n\n    Returns:\n        tuple of (model or None, schema or None, wrap_output)\n        Model and schema are None if warnings occur or creation fails.\n        wrap_output is True if the result needs to be wrapped in {\"result\": ...}\n    \"\"\"\n    model = None\n    wrap_output = False\n\n    # First handle special case: None\n    if type_expr is None:\n        model = _create_wrapped_model(func_name, original_annotation)\n        wrap_output = True\n\n    # Handle GenericAlias types (list[str], dict[str, int], Union[str, int], etc.)\n    elif isinstance(type_expr, GenericAlias):\n        origin = get_origin(type_expr)\n\n        # Special case: dict with string keys can use RootModel\n        if origin is dict:\n            args = get_args(type_expr)\n            if len(args) == 2 and args[0] is str:\n                # TODO: should we use the original annotation? We are losing any potential `Annotated`\n                # metadata for Pydantic here:\n                model = _create_dict_model(func_name, type_expr)\n            else:\n                # dict with non-str keys needs wrapping\n                model = _create_wrapped_model(func_name, original_annotation)\n                wrap_output = True\n        else:\n            # All other generic types need wrapping (list, tuple, Union, Optional, etc.)\n            model = _create_wrapped_model(func_name, original_annotation)\n            wrap_output = True\n\n    # Handle regular type objects\n    elif isinstance(type_expr, type):\n        type_annotation = cast(type[Any], type_expr)\n\n        # Case 1: BaseModel subclasses (can be used directly)\n        if issubclass(type_annotation, BaseModel):\n            model = type_annotation\n\n        # Case 2: TypedDicts:\n        elif is_typeddict(type_annotation):\n            model = _create_model_from_typeddict(type_annotation)\n\n        # Case 3: Primitive types that need wrapping\n        elif type_annotation in (str, int, float, bool, bytes, type(None)):\n            model = _create_wrapped_model(func_name, original_annotation)\n            wrap_output = True\n\n        # Case 4: Other class types (dataclasses, regular classes with annotations)\n        else:\n            type_hints = get_type_hints(type_annotation)\n            if type_hints:\n                # Classes with type hints can be converted to Pydantic models\n                model = _create_model_from_class(type_annotation, type_hints)\n            # Classes without type hints are not serializable - model remains None\n\n    # Handle any other types not covered above\n    else:\n        # This includes typing constructs that aren't GenericAlias in Python 3.10\n        # (e.g., Union, Optional in some Python versions)\n        model = _create_wrapped_model(func_name, original_annotation)\n        wrap_output = True\n\n    if model:\n        # If we successfully created a model, try to get its schema\n        # Use StrictJsonSchema to raise exceptions instead of warnings\n        try:\n            schema = model.model_json_schema(schema_generator=StrictJsonSchema)\n        except (TypeError, ValueError, pydantic_core.SchemaError, pydantic_core.ValidationError) as e:\n            # These are expected errors when a type can't be converted to a Pydantic schema\n            # TypeError: When Pydantic can't handle the type\n            # ValueError: When there are issues with the type definition (including our custom warnings)\n            # SchemaError: When Pydantic can't build a schema\n            # ValidationError: When validation fails\n            logger.info(f\"Cannot create schema for type {type_expr} in {func_name}: {type(e).__name__}: {e}\")\n            return None, None, False\n\n        return model, schema, wrap_output\n\n    return None, None, False\n\n\n_no_default = object()\n\n\ndef _create_model_from_class(cls: type[Any], type_hints: dict[str, Any]) -> type[BaseModel]:\n    \"\"\"Create a Pydantic model from an ordinary class.\n\n    The created model will:\n    - Have the same name as the class\n    - Have fields with the same names and types as the class's fields\n    - Include all fields whose type does not include None in the set of required fields\n\n    Precondition: cls must have type hints (i.e., `type_hints` is non-empty)\n    \"\"\"\n    model_fields: dict[str, Any] = {}\n    for field_name, field_type in type_hints.items():\n        if field_name.startswith(\"_\"):  # pragma: no cover\n            continue\n\n        default = getattr(cls, field_name, _no_default)\n        if default is _no_default:\n            model_fields[field_name] = field_type\n        else:\n            model_fields[field_name] = (field_type, default)\n\n    return create_model(cls.__name__, __config__=ConfigDict(from_attributes=True), **model_fields)\n\n\ndef _create_model_from_typeddict(td_type: type[Any]) -> type[BaseModel]:\n    \"\"\"Create a Pydantic model from a TypedDict.\n\n    The created model will have the same name and fields as the TypedDict.\n    \"\"\"\n    type_hints = get_type_hints(td_type)\n    required_keys = getattr(td_type, \"__required_keys__\", set(type_hints.keys()))\n\n    model_fields: dict[str, Any] = {}\n    for field_name, field_type in type_hints.items():\n        if field_name not in required_keys:\n            # For optional TypedDict fields, set default=None\n            # This makes them not required in the Pydantic model\n            # The model should use exclude_unset=True when dumping to get TypedDict semantics\n            model_fields[field_name] = (field_type, None)\n        else:\n            model_fields[field_name] = field_type\n\n    return create_model(td_type.__name__, **model_fields)\n\n\ndef _create_wrapped_model(func_name: str, annotation: Any) -> type[BaseModel]:\n    \"\"\"Create a model that wraps a type in a 'result' field.\n\n    This is used for primitive types, generic types like list/dict, etc.\n    \"\"\"\n    model_name = f\"{func_name}Output\"\n\n    return create_model(model_name, result=annotation)\n\n\ndef _create_dict_model(func_name: str, dict_annotation: Any) -> type[BaseModel]:\n    \"\"\"Create a RootModel for dict[str, T] types.\"\"\"\n    # TODO(Marcelo): We should not rely on RootModel for this.\n    from pydantic import RootModel  # noqa: TID251\n\n    class DictModel(RootModel[dict_annotation]):\n        pass\n\n    # Give it a meaningful name\n    DictModel.__name__ = f\"{func_name}DictOutput\"\n    DictModel.__qualname__ = f\"{func_name}DictOutput\"\n\n    return DictModel\n\n\ndef _convert_to_content(result: Any) -> Sequence[ContentBlock]:\n    \"\"\"Convert a result to a sequence of content objects.\n\n    Note: This conversion logic comes from previous versions of MCPServer and is being\n    retained for purposes of backwards compatibility. It produces different unstructured\n    output than the lowlevel server tool call handler, which just serializes structured\n    content verbatim.\n    \"\"\"\n    if result is None:  # pragma: no cover\n        return []\n\n    if isinstance(result, ContentBlock):\n        return [result]\n\n    if isinstance(result, Image):\n        return [result.to_image_content()]\n\n    if isinstance(result, Audio):\n        return [result.to_audio_content()]\n\n    if isinstance(result, list | tuple):\n        return list(\n            chain.from_iterable(\n                _convert_to_content(item)\n                for item in result  # type: ignore\n            )\n        )\n\n    if not isinstance(result, str):\n        result = pydantic_core.to_json(result, fallback=str, indent=2).decode()\n\n    return [TextContent(type=\"text\", text=result)]\n"
  },
  {
    "path": "src/mcp/server/mcpserver/utilities/logging.py",
    "content": "\"\"\"Logging utilities for MCPServer.\"\"\"\n\nimport logging\nfrom typing import Literal\n\n\ndef get_logger(name: str) -> logging.Logger:\n    \"\"\"Get a logger nested under MCP namespace.\n\n    Args:\n        name: The name of the logger.\n\n    Returns:\n        A configured logger instance.\n    \"\"\"\n    return logging.getLogger(name)\n\n\ndef configure_logging(\n    level: Literal[\"DEBUG\", \"INFO\", \"WARNING\", \"ERROR\", \"CRITICAL\"] = \"INFO\",\n) -> None:\n    \"\"\"Configure logging for MCP.\n\n    Args:\n        level: The log level to use.\n    \"\"\"\n    handlers: list[logging.Handler] = []\n    try:\n        from rich.console import Console\n        from rich.logging import RichHandler\n\n        handlers.append(RichHandler(console=Console(stderr=True), rich_tracebacks=True))\n    except ImportError:  # pragma: no cover\n        pass\n\n    if not handlers:  # pragma: no cover\n        handlers.append(logging.StreamHandler())\n\n    logging.basicConfig(level=level, format=\"%(message)s\", handlers=handlers)\n"
  },
  {
    "path": "src/mcp/server/mcpserver/utilities/types.py",
    "content": "\"\"\"Common types used across MCPServer.\"\"\"\n\nimport base64\nfrom pathlib import Path\n\nfrom mcp.types import AudioContent, ImageContent\n\n\nclass Image:\n    \"\"\"Helper class for returning images from tools.\"\"\"\n\n    def __init__(\n        self,\n        path: str | Path | None = None,\n        data: bytes | None = None,\n        format: str | None = None,\n    ):\n        if path is None and data is None:  # pragma: no cover\n            raise ValueError(\"Either path or data must be provided\")\n        if path is not None and data is not None:  # pragma: no cover\n            raise ValueError(\"Only one of path or data can be provided\")\n\n        self.path = Path(path) if path else None\n        self.data = data\n        self._format = format\n        self._mime_type = self._get_mime_type()\n\n    def _get_mime_type(self) -> str:\n        \"\"\"Get MIME type from format or guess from file extension.\"\"\"\n        if self._format:  # pragma: no cover\n            return f\"image/{self._format.lower()}\"\n\n        if self.path:\n            suffix = self.path.suffix.lower()\n            return {\n                \".png\": \"image/png\",\n                \".jpg\": \"image/jpeg\",\n                \".jpeg\": \"image/jpeg\",\n                \".gif\": \"image/gif\",\n                \".webp\": \"image/webp\",\n            }.get(suffix, \"application/octet-stream\")\n        return \"image/png\"  # pragma: no cover  # default for raw binary data\n\n    def to_image_content(self) -> ImageContent:\n        \"\"\"Convert to MCP ImageContent.\"\"\"\n        if self.path:\n            with open(self.path, \"rb\") as f:\n                data = base64.b64encode(f.read()).decode()\n        elif self.data is not None:  # pragma: no cover\n            data = base64.b64encode(self.data).decode()\n        else:  # pragma: no cover\n            raise ValueError(\"No image data available\")\n\n        return ImageContent(type=\"image\", data=data, mime_type=self._mime_type)\n\n\nclass Audio:\n    \"\"\"Helper class for returning audio from tools.\"\"\"\n\n    def __init__(\n        self,\n        path: str | Path | None = None,\n        data: bytes | None = None,\n        format: str | None = None,\n    ):\n        if not bool(path) ^ bool(data):  # pragma: no cover\n            raise ValueError(\"Either path or data can be provided\")\n\n        self.path = Path(path) if path else None\n        self.data = data\n        self._format = format\n        self._mime_type = self._get_mime_type()\n\n    def _get_mime_type(self) -> str:\n        \"\"\"Get MIME type from format or guess from file extension.\"\"\"\n        if self._format:  # pragma: no cover\n            return f\"audio/{self._format.lower()}\"\n\n        if self.path:\n            suffix = self.path.suffix.lower()\n            return {\n                \".wav\": \"audio/wav\",\n                \".mp3\": \"audio/mpeg\",\n                \".ogg\": \"audio/ogg\",\n                \".flac\": \"audio/flac\",\n                \".aac\": \"audio/aac\",\n                \".m4a\": \"audio/mp4\",\n            }.get(suffix, \"application/octet-stream\")\n        return \"audio/wav\"  # pragma: no cover  # default for raw binary data\n\n    def to_audio_content(self) -> AudioContent:\n        \"\"\"Convert to MCP AudioContent.\"\"\"\n        if self.path:\n            with open(self.path, \"rb\") as f:\n                data = base64.b64encode(f.read()).decode()\n        elif self.data is not None:  # pragma: no cover\n            data = base64.b64encode(self.data).decode()\n        else:  # pragma: no cover\n            raise ValueError(\"No audio data available\")\n\n        return AudioContent(type=\"audio\", data=data, mime_type=self._mime_type)\n"
  },
  {
    "path": "src/mcp/server/models.py",
    "content": "\"\"\"This module provides simplified types to use with the server for managing prompts\nand tools.\n\"\"\"\n\nfrom pydantic import BaseModel\n\nfrom mcp.types import Icon, ServerCapabilities\n\n\nclass InitializationOptions(BaseModel):\n    server_name: str\n    server_version: str\n    title: str | None = None\n    description: str | None = None\n    capabilities: ServerCapabilities\n    instructions: str | None = None\n    website_url: str | None = None\n    icons: list[Icon] | None = None\n"
  },
  {
    "path": "src/mcp/server/session.py",
    "content": "\"\"\"ServerSession Module\n\nThis module provides the ServerSession class, which manages communication between the\nserver and client in the MCP (Model Context Protocol) framework. It is most commonly\nused in MCP servers to interact with the client.\n\nCommon usage pattern:\n```\n    async def handle_call_tool(ctx: RequestContext, params: CallToolRequestParams) -> CallToolResult:\n        # Check client capabilities before proceeding\n        if ctx.session.check_client_capability(\n            types.ClientCapabilities(experimental={\"advanced_tools\": dict()})\n        ):\n            result = await perform_advanced_tool_operation(params.arguments)\n        else:\n            result = await perform_basic_tool_operation(params.arguments)\n        return result\n\n    async def handle_list_prompts(ctx: RequestContext, params) -> ListPromptsResult:\n        if ctx.session.client_params:\n            return ListPromptsResult(prompts=generate_custom_prompts(ctx.session.client_params))\n        return ListPromptsResult(prompts=default_prompts)\n\n    server = Server(name, on_call_tool=handle_call_tool, on_list_prompts=handle_list_prompts)\n```\n\nThe ServerSession class is typically used internally by the Server class and should not\nbe instantiated directly by users of the MCP framework.\n\"\"\"\n\nfrom enum import Enum\nfrom typing import Any, TypeVar, overload\n\nimport anyio\nimport anyio.lowlevel\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\nfrom pydantic import AnyUrl, TypeAdapter\n\nfrom mcp import types\nfrom mcp.server.experimental.session_features import ExperimentalServerSessionFeatures\nfrom mcp.server.models import InitializationOptions\nfrom mcp.server.validation import validate_sampling_tools, validate_tool_use_result_messages\nfrom mcp.shared.exceptions import StatelessModeNotSupported\nfrom mcp.shared.experimental.tasks.capabilities import check_tasks_capability\nfrom mcp.shared.experimental.tasks.helpers import RELATED_TASK_METADATA_KEY\nfrom mcp.shared.message import ServerMessageMetadata, SessionMessage\nfrom mcp.shared.session import (\n    BaseSession,\n    RequestResponder,\n)\nfrom mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS\n\n\nclass InitializationState(Enum):\n    NotInitialized = 1\n    Initializing = 2\n    Initialized = 3\n\n\nServerSessionT = TypeVar(\"ServerSessionT\", bound=\"ServerSession\")\n\nServerRequestResponder = (\n    RequestResponder[types.ClientRequest, types.ServerResult] | types.ClientNotification | Exception\n)\n\n\nclass ServerSession(\n    BaseSession[\n        types.ServerRequest,\n        types.ServerNotification,\n        types.ServerResult,\n        types.ClientRequest,\n        types.ClientNotification,\n    ]\n):\n    _initialized: InitializationState = InitializationState.NotInitialized\n    _client_params: types.InitializeRequestParams | None = None\n    _experimental_features: ExperimentalServerSessionFeatures | None = None\n\n    def __init__(\n        self,\n        read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],\n        write_stream: MemoryObjectSendStream[SessionMessage],\n        init_options: InitializationOptions,\n        stateless: bool = False,\n    ) -> None:\n        super().__init__(read_stream, write_stream)\n        self._stateless = stateless\n        self._initialization_state = (\n            InitializationState.Initialized if stateless else InitializationState.NotInitialized\n        )\n\n        self._init_options = init_options\n        self._incoming_message_stream_writer, self._incoming_message_stream_reader = anyio.create_memory_object_stream[\n            ServerRequestResponder\n        ](0)\n        self._exit_stack.push_async_callback(lambda: self._incoming_message_stream_reader.aclose())\n\n    @property\n    def _receive_request_adapter(self) -> TypeAdapter[types.ClientRequest]:\n        return types.client_request_adapter\n\n    @property\n    def _receive_notification_adapter(self) -> TypeAdapter[types.ClientNotification]:\n        return types.client_notification_adapter\n\n    @property\n    def client_params(self) -> types.InitializeRequestParams | None:\n        return self._client_params\n\n    @property\n    def experimental(self) -> ExperimentalServerSessionFeatures:\n        \"\"\"Experimental APIs for server→client task operations.\n\n        WARNING: These APIs are experimental and may change without notice.\n        \"\"\"\n        if self._experimental_features is None:\n            self._experimental_features = ExperimentalServerSessionFeatures(self)\n        return self._experimental_features\n\n    def check_client_capability(self, capability: types.ClientCapabilities) -> bool:\n        \"\"\"Check if the client supports a specific capability.\"\"\"\n        if self._client_params is None:  # pragma: lax no cover\n            return False\n\n        client_caps = self._client_params.capabilities\n\n        if capability.roots is not None:  # pragma: lax no cover\n            if client_caps.roots is None:\n                return False\n            if capability.roots.list_changed and not client_caps.roots.list_changed:\n                return False\n\n        if capability.sampling is not None:  # pragma: lax no cover\n            if client_caps.sampling is None:\n                return False\n            if capability.sampling.context is not None and client_caps.sampling.context is None:\n                return False\n            if capability.sampling.tools is not None and client_caps.sampling.tools is None:\n                return False\n\n        if capability.elicitation is not None and client_caps.elicitation is None:  # pragma: lax no cover\n            return False\n\n        if capability.experimental is not None:  # pragma: lax no cover\n            if client_caps.experimental is None:\n                return False\n            for exp_key, exp_value in capability.experimental.items():\n                if exp_key not in client_caps.experimental or client_caps.experimental[exp_key] != exp_value:\n                    return False\n\n        if capability.tasks is not None:  # pragma: lax no cover\n            if client_caps.tasks is None:\n                return False\n            if not check_tasks_capability(capability.tasks, client_caps.tasks):\n                return False\n\n        return True\n\n    async def _receive_loop(self) -> None:\n        async with self._incoming_message_stream_writer:\n            await super()._receive_loop()\n\n    async def _received_request(self, responder: RequestResponder[types.ClientRequest, types.ServerResult]):\n        match responder.request:\n            case types.InitializeRequest(params=params):\n                requested_version = params.protocol_version\n                self._initialization_state = InitializationState.Initializing\n                self._client_params = params\n                with responder:\n                    await responder.respond(\n                        types.InitializeResult(\n                            protocol_version=requested_version\n                            if requested_version in SUPPORTED_PROTOCOL_VERSIONS\n                            else types.LATEST_PROTOCOL_VERSION,\n                            capabilities=self._init_options.capabilities,\n                            server_info=types.Implementation(\n                                name=self._init_options.server_name,\n                                title=self._init_options.title,\n                                description=self._init_options.description,\n                                version=self._init_options.server_version,\n                                website_url=self._init_options.website_url,\n                                icons=self._init_options.icons,\n                            ),\n                            instructions=self._init_options.instructions,\n                        )\n                    )\n                self._initialization_state = InitializationState.Initialized\n            case types.PingRequest():\n                # Ping requests are allowed at any time\n                pass\n            case _:\n                if self._initialization_state != InitializationState.Initialized:\n                    raise RuntimeError(\"Received request before initialization was complete\")\n\n    async def _received_notification(self, notification: types.ClientNotification) -> None:\n        # Need this to avoid ASYNC910\n        await anyio.lowlevel.checkpoint()\n        match notification:\n            case types.InitializedNotification():\n                self._initialization_state = InitializationState.Initialized\n            case _:\n                if self._initialization_state != InitializationState.Initialized:  # pragma: no cover\n                    raise RuntimeError(\"Received notification before initialization was complete\")\n\n    async def send_log_message(\n        self,\n        level: types.LoggingLevel,\n        data: Any,\n        logger: str | None = None,\n        related_request_id: types.RequestId | None = None,\n    ) -> None:\n        \"\"\"Send a log message notification.\"\"\"\n        await self.send_notification(\n            types.LoggingMessageNotification(\n                params=types.LoggingMessageNotificationParams(\n                    level=level,\n                    data=data,\n                    logger=logger,\n                ),\n            ),\n            related_request_id,\n        )\n\n    async def send_resource_updated(self, uri: str | AnyUrl) -> None:  # pragma: no cover\n        \"\"\"Send a resource updated notification.\"\"\"\n        await self.send_notification(\n            types.ResourceUpdatedNotification(\n                params=types.ResourceUpdatedNotificationParams(uri=str(uri)),\n            )\n        )\n\n    @overload\n    async def create_message(\n        self,\n        messages: list[types.SamplingMessage],\n        *,\n        max_tokens: int,\n        system_prompt: str | None = None,\n        include_context: types.IncludeContext | None = None,\n        temperature: float | None = None,\n        stop_sequences: list[str] | None = None,\n        metadata: dict[str, Any] | None = None,\n        model_preferences: types.ModelPreferences | None = None,\n        tools: None = None,\n        tool_choice: types.ToolChoice | None = None,\n        related_request_id: types.RequestId | None = None,\n    ) -> types.CreateMessageResult:\n        \"\"\"Overload: Without tools, returns single content.\"\"\"\n        ...\n\n    @overload\n    async def create_message(\n        self,\n        messages: list[types.SamplingMessage],\n        *,\n        max_tokens: int,\n        system_prompt: str | None = None,\n        include_context: types.IncludeContext | None = None,\n        temperature: float | None = None,\n        stop_sequences: list[str] | None = None,\n        metadata: dict[str, Any] | None = None,\n        model_preferences: types.ModelPreferences | None = None,\n        tools: list[types.Tool],\n        tool_choice: types.ToolChoice | None = None,\n        related_request_id: types.RequestId | None = None,\n    ) -> types.CreateMessageResultWithTools:\n        \"\"\"Overload: With tools, returns array-capable content.\"\"\"\n        ...\n\n    async def create_message(\n        self,\n        messages: list[types.SamplingMessage],\n        *,\n        max_tokens: int,\n        system_prompt: str | None = None,\n        include_context: types.IncludeContext | None = None,\n        temperature: float | None = None,\n        stop_sequences: list[str] | None = None,\n        metadata: dict[str, Any] | None = None,\n        model_preferences: types.ModelPreferences | None = None,\n        tools: list[types.Tool] | None = None,\n        tool_choice: types.ToolChoice | None = None,\n        related_request_id: types.RequestId | None = None,\n    ) -> types.CreateMessageResult | types.CreateMessageResultWithTools:\n        \"\"\"Send a sampling/create_message request.\n\n        Args:\n            messages: The conversation messages to send.\n            max_tokens: Maximum number of tokens to generate.\n            system_prompt: Optional system prompt.\n            include_context: Optional context inclusion setting.\n                Should only be set to \"thisServer\" or \"allServers\"\n                if the client has sampling.context capability.\n            temperature: Optional sampling temperature.\n            stop_sequences: Optional stop sequences.\n            metadata: Optional metadata to pass through to the LLM provider.\n            model_preferences: Optional model selection preferences.\n            tools: Optional list of tools the LLM can use during sampling.\n                Requires client to have sampling.tools capability.\n            tool_choice: Optional control over tool usage behavior.\n                Requires client to have sampling.tools capability.\n            related_request_id: Optional ID of a related request.\n\n        Returns:\n            The sampling result from the client.\n\n        Raises:\n            MCPError: If tools are provided but client doesn't support them.\n            ValueError: If tool_use or tool_result message structure is invalid.\n            StatelessModeNotSupported: If called in stateless HTTP mode.\n        \"\"\"\n        if self._stateless:\n            raise StatelessModeNotSupported(method=\"sampling\")\n        client_caps = self._client_params.capabilities if self._client_params else None\n        validate_sampling_tools(client_caps, tools, tool_choice)\n        validate_tool_use_result_messages(messages)\n\n        request = types.CreateMessageRequest(\n            params=types.CreateMessageRequestParams(\n                messages=messages,\n                system_prompt=system_prompt,\n                include_context=include_context,\n                temperature=temperature,\n                max_tokens=max_tokens,\n                stop_sequences=stop_sequences,\n                metadata=metadata,\n                model_preferences=model_preferences,\n                tools=tools,\n                tool_choice=tool_choice,\n            ),\n        )\n        metadata_obj = ServerMessageMetadata(related_request_id=related_request_id)\n\n        # Use different result types based on whether tools are provided\n        if tools is not None:\n            return await self.send_request(\n                request=request,\n                result_type=types.CreateMessageResultWithTools,\n                metadata=metadata_obj,\n            )\n        return await self.send_request(\n            request=request,\n            result_type=types.CreateMessageResult,\n            metadata=metadata_obj,\n        )\n\n    async def list_roots(self) -> types.ListRootsResult:\n        \"\"\"Send a roots/list request.\"\"\"\n        if self._stateless:\n            raise StatelessModeNotSupported(method=\"list_roots\")\n        return await self.send_request(\n            types.ListRootsRequest(),\n            types.ListRootsResult,\n        )\n\n    async def elicit(\n        self,\n        message: str,\n        requested_schema: types.ElicitRequestedSchema,\n        related_request_id: types.RequestId | None = None,\n    ) -> types.ElicitResult:\n        \"\"\"Send a form mode elicitation/create request.\n\n        Args:\n            message: The message to present to the user.\n            requested_schema: Schema defining the expected response structure.\n            related_request_id: Optional ID of the request that triggered this elicitation.\n\n        Returns:\n            The client's response.\n\n        Note:\n            This method is deprecated in favor of elicit_form(). It remains for\n            backward compatibility but new code should use elicit_form().\n        \"\"\"\n        return await self.elicit_form(message, requested_schema, related_request_id)\n\n    async def elicit_form(\n        self,\n        message: str,\n        requested_schema: types.ElicitRequestedSchema,\n        related_request_id: types.RequestId | None = None,\n    ) -> types.ElicitResult:\n        \"\"\"Send a form mode elicitation/create request.\n\n        Args:\n            message: The message to present to the user.\n            requested_schema: Schema defining the expected response structure.\n            related_request_id: Optional ID of the request that triggered this elicitation.\n\n        Returns:\n            The client's response with form data.\n\n        Raises:\n            StatelessModeNotSupported: If called in stateless HTTP mode.\n        \"\"\"\n        if self._stateless:\n            raise StatelessModeNotSupported(method=\"elicitation\")\n        return await self.send_request(\n            types.ElicitRequest(\n                params=types.ElicitRequestFormParams(\n                    message=message,\n                    requested_schema=requested_schema,\n                ),\n            ),\n            types.ElicitResult,\n            metadata=ServerMessageMetadata(related_request_id=related_request_id),\n        )\n\n    async def elicit_url(\n        self,\n        message: str,\n        url: str,\n        elicitation_id: str,\n        related_request_id: types.RequestId | None = None,\n    ) -> types.ElicitResult:\n        \"\"\"Send a URL mode elicitation/create request.\n\n        This directs the user to an external URL for out-of-band interactions\n        like OAuth flows, credential collection, or payment processing.\n\n        Args:\n            message: Human-readable explanation of why the interaction is needed.\n            url: The URL the user should navigate to.\n            elicitation_id: Unique identifier for tracking this elicitation.\n            related_request_id: Optional ID of the request that triggered this elicitation.\n\n        Returns:\n            The client's response indicating acceptance, decline, or cancellation.\n\n        Raises:\n            StatelessModeNotSupported: If called in stateless HTTP mode.\n        \"\"\"\n        if self._stateless:\n            raise StatelessModeNotSupported(method=\"elicitation\")\n        return await self.send_request(\n            types.ElicitRequest(\n                params=types.ElicitRequestURLParams(\n                    message=message,\n                    url=url,\n                    elicitation_id=elicitation_id,\n                ),\n            ),\n            types.ElicitResult,\n            metadata=ServerMessageMetadata(related_request_id=related_request_id),\n        )\n\n    async def send_ping(self) -> types.EmptyResult:  # pragma: no cover\n        \"\"\"Send a ping request.\"\"\"\n        return await self.send_request(\n            types.PingRequest(),\n            types.EmptyResult,\n        )\n\n    async def send_progress_notification(\n        self,\n        progress_token: str | int,\n        progress: float,\n        total: float | None = None,\n        message: str | None = None,\n        related_request_id: str | None = None,\n    ) -> None:\n        \"\"\"Send a progress notification.\"\"\"\n        await self.send_notification(\n            types.ProgressNotification(\n                params=types.ProgressNotificationParams(\n                    progress_token=progress_token,\n                    progress=progress,\n                    total=total,\n                    message=message,\n                ),\n            ),\n            related_request_id,\n        )\n\n    async def send_resource_list_changed(self) -> None:\n        \"\"\"Send a resource list changed notification.\"\"\"\n        await self.send_notification(types.ResourceListChangedNotification())\n\n    async def send_tool_list_changed(self) -> None:  # pragma: no cover\n        \"\"\"Send a tool list changed notification.\"\"\"\n        await self.send_notification(types.ToolListChangedNotification())\n\n    async def send_prompt_list_changed(self) -> None:  # pragma: no cover\n        \"\"\"Send a prompt list changed notification.\"\"\"\n        await self.send_notification(types.PromptListChangedNotification())\n\n    async def send_elicit_complete(\n        self,\n        elicitation_id: str,\n        related_request_id: types.RequestId | None = None,\n    ) -> None:\n        \"\"\"Send an elicitation completion notification.\n\n        This should be sent when a URL mode elicitation has been completed\n        out-of-band to inform the client that it may retry any requests\n        that were waiting for this elicitation.\n\n        Args:\n            elicitation_id: The unique identifier of the completed elicitation\n            related_request_id: Optional ID of the request that triggered this notification\n        \"\"\"\n        await self.send_notification(\n            types.ElicitCompleteNotification(\n                params=types.ElicitCompleteNotificationParams(elicitation_id=elicitation_id)\n            ),\n            related_request_id,\n        )\n\n    def _build_elicit_form_request(\n        self,\n        message: str,\n        requested_schema: types.ElicitRequestedSchema,\n        related_task_id: str | None = None,\n        task: types.TaskMetadata | None = None,\n    ) -> types.JSONRPCRequest:\n        \"\"\"Build a form mode elicitation request without sending it.\n\n        Args:\n            message: The message to present to the user\n            requested_schema: Schema defining the expected response structure\n            related_task_id: If provided, adds io.modelcontextprotocol/related-task metadata\n            task: If provided, makes this a task-augmented request\n\n        Returns:\n            A JSONRPCRequest ready to be sent or queued\n        \"\"\"\n        params = types.ElicitRequestFormParams(\n            message=message,\n            requested_schema=requested_schema,\n            task=task,\n        )\n        params_data = params.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n\n        # Add related-task metadata if associated with a parent task\n        if related_task_id is not None:\n            # Defensive: model_dump() never includes _meta, but guard against future changes\n            if \"_meta\" not in params_data:  # pragma: no branch\n                params_data[\"_meta\"] = {}\n            params_data[\"_meta\"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata(\n                task_id=related_task_id\n            ).model_dump(by_alias=True)\n\n        request_id = f\"task-{related_task_id}-{id(params)}\" if related_task_id else self._request_id\n        if related_task_id is None:\n            self._request_id += 1\n\n        return types.JSONRPCRequest(\n            jsonrpc=\"2.0\",\n            id=request_id,\n            method=\"elicitation/create\",\n            params=params_data,\n        )\n\n    def _build_elicit_url_request(\n        self,\n        message: str,\n        url: str,\n        elicitation_id: str,\n        related_task_id: str | None = None,\n    ) -> types.JSONRPCRequest:\n        \"\"\"Build a URL mode elicitation request without sending it.\n\n        Args:\n            message: Human-readable explanation of why the interaction is needed\n            url: The URL the user should navigate to\n            elicitation_id: Unique identifier for tracking this elicitation\n            related_task_id: If provided, adds io.modelcontextprotocol/related-task metadata\n\n        Returns:\n            A JSONRPCRequest ready to be sent or queued\n        \"\"\"\n        params = types.ElicitRequestURLParams(\n            message=message,\n            url=url,\n            elicitation_id=elicitation_id,\n        )\n        params_data = params.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n\n        # Add related-task metadata if associated with a parent task\n        if related_task_id is not None:\n            # Defensive: model_dump() never includes _meta, but guard against future changes\n            if \"_meta\" not in params_data:  # pragma: no branch\n                params_data[\"_meta\"] = {}\n            params_data[\"_meta\"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata(\n                task_id=related_task_id\n            ).model_dump(by_alias=True)\n\n        request_id = f\"task-{related_task_id}-{id(params)}\" if related_task_id else self._request_id\n        if related_task_id is None:\n            self._request_id += 1\n\n        return types.JSONRPCRequest(\n            jsonrpc=\"2.0\",\n            id=request_id,\n            method=\"elicitation/create\",\n            params=params_data,\n        )\n\n    def _build_create_message_request(\n        self,\n        messages: list[types.SamplingMessage],\n        *,\n        max_tokens: int,\n        system_prompt: str | None = None,\n        include_context: types.IncludeContext | None = None,\n        temperature: float | None = None,\n        stop_sequences: list[str] | None = None,\n        metadata: dict[str, Any] | None = None,\n        model_preferences: types.ModelPreferences | None = None,\n        tools: list[types.Tool] | None = None,\n        tool_choice: types.ToolChoice | None = None,\n        related_task_id: str | None = None,\n        task: types.TaskMetadata | None = None,\n    ) -> types.JSONRPCRequest:\n        \"\"\"Build a sampling/createMessage request without sending it.\n\n        Args:\n            messages: The conversation messages to send\n            max_tokens: Maximum number of tokens to generate\n            system_prompt: Optional system prompt\n            include_context: Optional context inclusion setting\n            temperature: Optional sampling temperature\n            stop_sequences: Optional stop sequences\n            metadata: Optional metadata to pass through to the LLM provider\n            model_preferences: Optional model selection preferences\n            tools: Optional list of tools the LLM can use during sampling\n            tool_choice: Optional control over tool usage behavior\n            related_task_id: If provided, adds io.modelcontextprotocol/related-task metadata\n            task: If provided, makes this a task-augmented request\n\n        Returns:\n            A JSONRPCRequest ready to be sent or queued\n        \"\"\"\n        params = types.CreateMessageRequestParams(\n            messages=messages,\n            system_prompt=system_prompt,\n            include_context=include_context,\n            temperature=temperature,\n            max_tokens=max_tokens,\n            stop_sequences=stop_sequences,\n            metadata=metadata,\n            model_preferences=model_preferences,\n            tools=tools,\n            tool_choice=tool_choice,\n            task=task,\n        )\n        params_data = params.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n\n        # Add related-task metadata if associated with a parent task\n        if related_task_id is not None:\n            # Defensive: model_dump() never includes _meta, but guard against future changes\n            if \"_meta\" not in params_data:  # pragma: no branch\n                params_data[\"_meta\"] = {}\n            params_data[\"_meta\"][RELATED_TASK_METADATA_KEY] = types.RelatedTaskMetadata(\n                task_id=related_task_id\n            ).model_dump(by_alias=True)\n\n        request_id = f\"task-{related_task_id}-{id(params)}\" if related_task_id else self._request_id\n        if related_task_id is None:\n            self._request_id += 1\n\n        return types.JSONRPCRequest(\n            jsonrpc=\"2.0\",\n            id=request_id,\n            method=\"sampling/createMessage\",\n            params=params_data,\n        )\n\n    async def send_message(self, message: SessionMessage) -> None:\n        \"\"\"Send a raw session message.\n\n        This is primarily used by TaskResultHandler to deliver queued messages\n        (elicitation/sampling requests) to the client during task execution.\n\n        WARNING: This is a low-level experimental method that may change without\n        notice. Prefer using higher-level methods like send_notification() or\n        send_request() for normal operations.\n\n        Args:\n            message: The session message to send\n        \"\"\"\n        await self._write_stream.send(message)\n\n    async def _handle_incoming(self, req: ServerRequestResponder) -> None:\n        await self._incoming_message_stream_writer.send(req)\n\n    @property\n    def incoming_messages(self) -> MemoryObjectReceiveStream[ServerRequestResponder]:\n        return self._incoming_message_stream_reader\n"
  },
  {
    "path": "src/mcp/server/sse.py",
    "content": "\"\"\"SSE Server Transport Module\n\nThis module implements a Server-Sent Events (SSE) transport layer for MCP servers.\n\nExample:\n    ```python\n    # Create an SSE transport at an endpoint\n    sse = SseServerTransport(\"/messages/\")\n\n    # Create Starlette routes for SSE and message handling\n    routes = [\n        Route(\"/sse\", endpoint=handle_sse, methods=[\"GET\"]),\n        Mount(\"/messages/\", app=sse.handle_post_message),\n    ]\n\n    # Define handler functions\n    async def handle_sse(request):\n        async with sse.connect_sse(\n            request.scope, request.receive, request._send\n        ) as streams:\n            await app.run(\n                streams[0], streams[1], app.create_initialization_options()\n            )\n        # Return empty response to avoid NoneType error\n        return Response()\n\n    # Create and run Starlette app\n    starlette_app = Starlette(routes=routes)\n    uvicorn.run(starlette_app, host=\"127.0.0.1\", port=port)\n    ```\n\nNote: The handle_sse function must return a Response to avoid a\n\"TypeError: 'NoneType' object is not callable\" error when client disconnects. The example above returns\nan empty Response() after the SSE connection ends to fix this.\n\nSee SseServerTransport class documentation for more details.\n\"\"\"\n\nimport logging\nfrom contextlib import asynccontextmanager\nfrom typing import Any\nfrom urllib.parse import quote\nfrom uuid import UUID, uuid4\n\nimport anyio\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\nfrom pydantic import ValidationError\nfrom sse_starlette import EventSourceResponse\nfrom starlette.requests import Request\nfrom starlette.responses import Response\nfrom starlette.types import Receive, Scope, Send\n\nfrom mcp import types\nfrom mcp.server.transport_security import (\n    TransportSecurityMiddleware,\n    TransportSecuritySettings,\n)\nfrom mcp.shared.message import ServerMessageMetadata, SessionMessage\n\nlogger = logging.getLogger(__name__)\n\n\nclass SseServerTransport:\n    \"\"\"SSE server transport for MCP. This class provides two ASGI applications,\n    suitable for use with a framework like Starlette and a server like Hypercorn:\n\n        1. connect_sse() is an ASGI application which receives incoming GET requests,\n           and sets up a new SSE stream to send server messages to the client.\n        2. handle_post_message() is an ASGI application which receives incoming POST\n           requests, which should contain client messages that link to a\n           previously-established SSE session.\n    \"\"\"\n\n    _endpoint: str\n    _read_stream_writers: dict[UUID, MemoryObjectSendStream[SessionMessage | Exception]]\n    _security: TransportSecurityMiddleware\n\n    def __init__(self, endpoint: str, security_settings: TransportSecuritySettings | None = None) -> None:\n        \"\"\"Creates a new SSE server transport, which will direct the client to POST\n        messages to the relative path given.\n\n        Args:\n            endpoint: A relative path where messages should be posted\n                    (e.g., \"/messages/\").\n            security_settings: Optional security settings for DNS rebinding protection.\n\n        Note:\n            We use relative paths instead of full URLs for several reasons:\n            1. Security: Prevents cross-origin requests by ensuring clients only connect\n               to the same origin they established the SSE connection with\n            2. Flexibility: The server can be mounted at any path without needing to\n               know its full URL\n            3. Portability: The same endpoint configuration works across different\n               environments (development, staging, production)\n\n        Raises:\n            ValueError: If the endpoint is a full URL instead of a relative path\n        \"\"\"\n\n        super().__init__()\n\n        # Validate that endpoint is a relative path and not a full URL\n        if \"://\" in endpoint or endpoint.startswith(\"//\") or \"?\" in endpoint or \"#\" in endpoint:\n            raise ValueError(\n                f\"Given endpoint: {endpoint} is not a relative path (e.g., '/messages/'), \"\n                \"expecting a relative path (e.g., '/messages/').\"\n            )\n\n        # Ensure endpoint starts with a forward slash\n        if not endpoint.startswith(\"/\"):\n            endpoint = \"/\" + endpoint\n\n        self._endpoint = endpoint\n        self._read_stream_writers = {}\n        self._security = TransportSecurityMiddleware(security_settings)\n        logger.debug(f\"SseServerTransport initialized with endpoint: {endpoint}\")\n\n    @asynccontextmanager\n    async def connect_sse(self, scope: Scope, receive: Receive, send: Send):  # pragma: no cover\n        if scope[\"type\"] != \"http\":\n            logger.error(\"connect_sse received non-HTTP request\")\n            raise ValueError(\"connect_sse can only handle HTTP requests\")\n\n        # Validate request headers for DNS rebinding protection\n        request = Request(scope, receive)\n        error_response = await self._security.validate_request(request, is_post=False)\n        if error_response:\n            await error_response(scope, receive, send)\n            raise ValueError(\"Request validation failed\")\n\n        logger.debug(\"Setting up SSE connection\")\n        read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]\n        read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]\n\n        write_stream: MemoryObjectSendStream[SessionMessage]\n        write_stream_reader: MemoryObjectReceiveStream[SessionMessage]\n\n        read_stream_writer, read_stream = anyio.create_memory_object_stream(0)\n        write_stream, write_stream_reader = anyio.create_memory_object_stream(0)\n\n        session_id = uuid4()\n        self._read_stream_writers[session_id] = read_stream_writer\n        logger.debug(f\"Created new session with ID: {session_id}\")\n\n        # Determine the full path for the message endpoint to be sent to the client.\n        # scope['root_path'] is the prefix where the current Starlette app\n        # instance is mounted.\n        # e.g., \"\" if top-level, or \"/api_prefix\" if mounted under \"/api_prefix\".\n        root_path = scope.get(\"root_path\", \"\")\n\n        # self._endpoint is the path *within* this app, e.g., \"/messages\".\n        # Concatenating them gives the full absolute path from the server root.\n        # e.g., \"\" + \"/messages\" -> \"/messages\"\n        # e.g., \"/api_prefix\" + \"/messages\" -> \"/api_prefix/messages\"\n        full_message_path_for_client = root_path.rstrip(\"/\") + self._endpoint\n\n        # This is the URI (path + query) the client will use to POST messages.\n        client_post_uri_data = f\"{quote(full_message_path_for_client)}?session_id={session_id.hex}\"\n\n        sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[dict[str, Any]](0)\n\n        async def sse_writer():\n            logger.debug(\"Starting SSE writer\")\n            async with sse_stream_writer, write_stream_reader:\n                await sse_stream_writer.send({\"event\": \"endpoint\", \"data\": client_post_uri_data})\n                logger.debug(f\"Sent endpoint event: {client_post_uri_data}\")\n\n                async for session_message in write_stream_reader:\n                    logger.debug(f\"Sending message via SSE: {session_message}\")\n                    await sse_stream_writer.send(\n                        {\n                            \"event\": \"message\",\n                            \"data\": session_message.message.model_dump_json(by_alias=True, exclude_unset=True),\n                        }\n                    )\n\n        async with anyio.create_task_group() as tg:\n\n            async def response_wrapper(scope: Scope, receive: Receive, send: Send):\n                \"\"\"The EventSourceResponse returning signals a client close / disconnect.\n                In this case we close our side of the streams to signal the client that\n                the connection has been closed.\n                \"\"\"\n                await EventSourceResponse(content=sse_stream_reader, data_sender_callable=sse_writer)(\n                    scope, receive, send\n                )\n                await read_stream_writer.aclose()\n                await write_stream_reader.aclose()\n                self._read_stream_writers.pop(session_id, None)\n                logging.debug(f\"Client session disconnected {session_id}\")\n\n            logger.debug(\"Starting SSE response task\")\n            tg.start_soon(response_wrapper, scope, receive, send)\n\n            logger.debug(\"Yielding read and write streams\")\n            yield (read_stream, write_stream)\n\n    async def handle_post_message(self, scope: Scope, receive: Receive, send: Send) -> None:  # pragma: no cover\n        logger.debug(\"Handling POST message\")\n        request = Request(scope, receive)\n\n        # Validate request headers for DNS rebinding protection\n        error_response = await self._security.validate_request(request, is_post=True)\n        if error_response:\n            return await error_response(scope, receive, send)\n\n        session_id_param = request.query_params.get(\"session_id\")\n        if session_id_param is None:\n            logger.warning(\"Received request without session_id\")\n            response = Response(\"session_id is required\", status_code=400)\n            return await response(scope, receive, send)\n\n        try:\n            session_id = UUID(hex=session_id_param)\n            logger.debug(f\"Parsed session ID: {session_id}\")\n        except ValueError:\n            logger.warning(f\"Received invalid session ID: {session_id_param}\")\n            response = Response(\"Invalid session ID\", status_code=400)\n            return await response(scope, receive, send)\n\n        writer = self._read_stream_writers.get(session_id)\n        if not writer:\n            logger.warning(f\"Could not find session for ID: {session_id}\")\n            response = Response(\"Could not find session\", status_code=404)\n            return await response(scope, receive, send)\n\n        body = await request.body()\n        logger.debug(f\"Received JSON: {body}\")\n\n        try:\n            message = types.jsonrpc_message_adapter.validate_json(body, by_name=False)\n            logger.debug(f\"Validated client message: {message}\")\n        except ValidationError as err:\n            logger.exception(\"Failed to parse message\")\n            response = Response(\"Could not parse message\", status_code=400)\n            await response(scope, receive, send)\n            await writer.send(err)\n            return\n\n        # Pass the ASGI scope for framework-agnostic access to request data\n        metadata = ServerMessageMetadata(request_context=request)\n        session_message = SessionMessage(message, metadata=metadata)\n        logger.debug(f\"Sending session message to writer: {session_message}\")\n        response = Response(\"Accepted\", status_code=202)\n        await response(scope, receive, send)\n        await writer.send(session_message)\n"
  },
  {
    "path": "src/mcp/server/stdio.py",
    "content": "\"\"\"Stdio Server Transport Module\n\nThis module provides functionality for creating an stdio-based transport layer\nthat can be used to communicate with an MCP client through standard input/output\nstreams.\n\nExample:\n    ```python\n    async def run_server():\n        async with stdio_server() as (read_stream, write_stream):\n            # read_stream contains incoming JSONRPCMessages from stdin\n            # write_stream allows sending JSONRPCMessages to stdout\n            server = await create_my_server()\n            await server.run(read_stream, write_stream, init_options)\n\n    anyio.run(run_server)\n    ```\n\"\"\"\n\nimport sys\nfrom contextlib import asynccontextmanager\nfrom io import TextIOWrapper\n\nimport anyio\nimport anyio.lowlevel\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\n\nfrom mcp import types\nfrom mcp.shared.message import SessionMessage\n\n\n@asynccontextmanager\nasync def stdio_server(stdin: anyio.AsyncFile[str] | None = None, stdout: anyio.AsyncFile[str] | None = None):\n    \"\"\"Server transport for stdio: this communicates with an MCP client by reading\n    from the current process' stdin and writing to stdout.\n    \"\"\"\n    # Purposely not using context managers for these, as we don't want to close\n    # standard process handles. Encoding of stdin/stdout as text streams on\n    # python is platform-dependent (Windows is particularly problematic), so we\n    # re-wrap the underlying binary stream to ensure UTF-8.\n    if not stdin:\n        stdin = anyio.wrap_file(TextIOWrapper(sys.stdin.buffer, encoding=\"utf-8\", errors=\"replace\"))\n    if not stdout:\n        stdout = anyio.wrap_file(TextIOWrapper(sys.stdout.buffer, encoding=\"utf-8\"))\n\n    read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]\n    read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]\n\n    write_stream: MemoryObjectSendStream[SessionMessage]\n    write_stream_reader: MemoryObjectReceiveStream[SessionMessage]\n\n    read_stream_writer, read_stream = anyio.create_memory_object_stream(0)\n    write_stream, write_stream_reader = anyio.create_memory_object_stream(0)\n\n    async def stdin_reader():\n        try:\n            async with read_stream_writer:\n                async for line in stdin:\n                    try:\n                        message = types.jsonrpc_message_adapter.validate_json(line, by_name=False)\n                    except Exception as exc:\n                        await read_stream_writer.send(exc)\n                        continue\n\n                    session_message = SessionMessage(message)\n                    await read_stream_writer.send(session_message)\n        except anyio.ClosedResourceError:  # pragma: no cover\n            await anyio.lowlevel.checkpoint()\n\n    async def stdout_writer():\n        try:\n            async with write_stream_reader:\n                async for session_message in write_stream_reader:\n                    json = session_message.message.model_dump_json(by_alias=True, exclude_unset=True)\n                    await stdout.write(json + \"\\n\")\n                    await stdout.flush()\n        except anyio.ClosedResourceError:  # pragma: no cover\n            await anyio.lowlevel.checkpoint()\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(stdin_reader)\n        tg.start_soon(stdout_writer)\n        yield read_stream, write_stream\n"
  },
  {
    "path": "src/mcp/server/streamable_http.py",
    "content": "\"\"\"StreamableHTTP Server Transport Module\n\nThis module implements an HTTP transport layer with Streamable HTTP.\n\nThe transport handles bidirectional communication using HTTP requests and\nresponses, with streaming support for long-running operations.\n\"\"\"\n\nimport logging\nimport re\nfrom abc import ABC, abstractmethod\nfrom collections.abc import AsyncGenerator, Awaitable, Callable\nfrom contextlib import asynccontextmanager\nfrom dataclasses import dataclass\nfrom http import HTTPStatus\nfrom typing import Any\n\nimport anyio\nimport pydantic_core\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\nfrom pydantic import ValidationError\nfrom sse_starlette import EventSourceResponse\nfrom starlette.requests import Request\nfrom starlette.responses import Response\nfrom starlette.types import Receive, Scope, Send\n\nfrom mcp.server.transport_security import TransportSecurityMiddleware, TransportSecuritySettings\nfrom mcp.shared.message import ServerMessageMetadata, SessionMessage\nfrom mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS\nfrom mcp.types import (\n    DEFAULT_NEGOTIATED_VERSION,\n    INTERNAL_ERROR,\n    INVALID_PARAMS,\n    INVALID_REQUEST,\n    PARSE_ERROR,\n    ErrorData,\n    JSONRPCError,\n    JSONRPCMessage,\n    JSONRPCRequest,\n    JSONRPCResponse,\n    RequestId,\n    jsonrpc_message_adapter,\n)\n\nlogger = logging.getLogger(__name__)\n\n\n# Header names\nMCP_SESSION_ID_HEADER = \"mcp-session-id\"\nMCP_PROTOCOL_VERSION_HEADER = \"mcp-protocol-version\"\nLAST_EVENT_ID_HEADER = \"last-event-id\"\n\n# Content types\nCONTENT_TYPE_JSON = \"application/json\"\nCONTENT_TYPE_SSE = \"text/event-stream\"\n\n# Special key for the standalone GET stream\nGET_STREAM_KEY = \"_GET_stream\"\n\n# Session ID validation pattern (visible ASCII characters ranging from 0x21 to 0x7E)\n# Pattern ensures entire string contains only valid characters by using ^ and $ anchors\nSESSION_ID_PATTERN = re.compile(r\"^[\\x21-\\x7E]+$\")\n\n# Type aliases\nStreamId = str\nEventId = str\n\n\n@dataclass\nclass EventMessage:\n    \"\"\"A JSONRPCMessage with an optional event ID for stream resumability.\"\"\"\n\n    message: JSONRPCMessage\n    event_id: str | None = None\n\n\nEventCallback = Callable[[EventMessage], Awaitable[None]]\n\n\nclass EventStore(ABC):\n    \"\"\"Interface for resumability support via event storage.\"\"\"\n\n    @abstractmethod\n    async def store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId:\n        \"\"\"Stores an event for later retrieval.\n\n        Args:\n            stream_id: ID of the stream the event belongs to\n            message: The JSON-RPC message to store, or None for priming events\n\n        Returns:\n            The generated event ID for the stored event.\n        \"\"\"\n        pass  # pragma: no cover\n\n    @abstractmethod\n    async def replay_events_after(\n        self,\n        last_event_id: EventId,\n        send_callback: EventCallback,\n    ) -> StreamId | None:\n        \"\"\"Replays events that occurred after the specified event ID.\n\n        Args:\n            last_event_id: The ID of the last event the client received\n            send_callback: A callback function to send events to the client\n\n        Returns:\n            The stream ID of the replayed events, or None if no events were found.\n        \"\"\"\n        pass  # pragma: no cover\n\n\nclass StreamableHTTPServerTransport:\n    \"\"\"HTTP server transport with event streaming support for MCP.\n\n    Handles JSON-RPC messages in HTTP POST requests with SSE streaming.\n    Supports optional JSON responses and session management.\n    \"\"\"\n\n    # Server notification streams for POST requests as well as standalone SSE stream\n    _read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception] | None = None\n    _read_stream: MemoryObjectReceiveStream[SessionMessage | Exception] | None = None\n    _write_stream: MemoryObjectSendStream[SessionMessage] | None = None\n    _write_stream_reader: MemoryObjectReceiveStream[SessionMessage] | None = None\n    _security: TransportSecurityMiddleware\n\n    def __init__(\n        self,\n        mcp_session_id: str | None,\n        is_json_response_enabled: bool = False,\n        event_store: EventStore | None = None,\n        security_settings: TransportSecuritySettings | None = None,\n        retry_interval: int | None = None,\n    ) -> None:\n        \"\"\"Initialize a new StreamableHTTP server transport.\n\n        Args:\n            mcp_session_id: Optional session identifier for this connection.\n                            Must contain only visible ASCII characters (0x21-0x7E).\n            is_json_response_enabled: If True, return JSON responses for requests\n                                    instead of SSE streams. Default is False.\n            event_store: Event store for resumability support. If provided,\n                        resumability will be enabled, allowing clients to\n                        reconnect and resume messages.\n            security_settings: Optional security settings for DNS rebinding protection.\n            retry_interval: Retry interval in milliseconds to suggest to clients in SSE\n                           retry field. When set, the server will send a retry field in\n                           SSE priming events to control client reconnection timing for\n                           polling behavior. Only used when event_store is provided.\n\n        Raises:\n            ValueError: If the session ID contains invalid characters.\n        \"\"\"\n        if mcp_session_id is not None and not SESSION_ID_PATTERN.fullmatch(mcp_session_id):\n            raise ValueError(\"Session ID must only contain visible ASCII characters (0x21-0x7E)\")\n\n        self.mcp_session_id = mcp_session_id\n        self.is_json_response_enabled = is_json_response_enabled\n        self._event_store = event_store\n        self._security = TransportSecurityMiddleware(security_settings)\n        self._retry_interval = retry_interval\n        self._request_streams: dict[\n            RequestId,\n            tuple[\n                MemoryObjectSendStream[EventMessage],\n                MemoryObjectReceiveStream[EventMessage],\n            ],\n        ] = {}\n        self._sse_stream_writers: dict[RequestId, MemoryObjectSendStream[dict[str, str]]] = {}\n        self._terminated = False\n        # Idle timeout cancel scope; managed by the session manager.\n        self.idle_scope: anyio.CancelScope | None = None\n\n    @property\n    def is_terminated(self) -> bool:\n        \"\"\"Check if this transport has been explicitly terminated.\"\"\"\n        return self._terminated\n\n    def close_sse_stream(self, request_id: RequestId) -> None:  # pragma: no cover\n        \"\"\"Close SSE connection for a specific request without terminating the stream.\n\n        This method closes the HTTP connection for the specified request, triggering\n        client reconnection. Events continue to be stored in the event store and will\n        be replayed when the client reconnects with Last-Event-ID.\n\n        Use this to implement polling behavior during long-running operations -\n        the client will reconnect after the retry interval specified in the priming event.\n\n        Args:\n            request_id: The request ID whose SSE stream should be closed.\n\n        Note:\n            This is a no-op if there is no active stream for the request ID.\n            Requires event_store to be configured for events to be stored during\n            the disconnect.\n        \"\"\"\n        writer = self._sse_stream_writers.pop(request_id, None)\n        if writer:\n            writer.close()\n\n        # Also close and remove request streams\n        if request_id in self._request_streams:\n            send_stream, receive_stream = self._request_streams.pop(request_id)\n            send_stream.close()\n            receive_stream.close()\n\n    def close_standalone_sse_stream(self) -> None:  # pragma: no cover\n        \"\"\"Close the standalone GET SSE stream, triggering client reconnection.\n\n        This method closes the HTTP connection for the standalone GET stream used\n        for unsolicited server-to-client notifications. The client SHOULD reconnect\n        with Last-Event-ID to resume receiving notifications.\n\n        Use this to implement polling behavior for the notification stream -\n        the client will reconnect after the retry interval specified in the priming event.\n\n        Note:\n            This is a no-op if there is no active standalone SSE stream.\n            Requires event_store to be configured for events to be stored during\n            the disconnect.\n            Currently, client reconnection for standalone GET streams is NOT\n            implemented - this is a known gap (see test_standalone_get_stream_reconnection).\n        \"\"\"\n        self.close_sse_stream(GET_STREAM_KEY)\n\n    def _create_session_message(\n        self,\n        message: JSONRPCMessage,\n        request: Request,\n        request_id: RequestId,\n        protocol_version: str,\n    ) -> SessionMessage:\n        \"\"\"Create a session message with metadata including close_sse_stream callback.\n\n        The close_sse_stream callbacks are only provided when the client supports\n        resumability (protocol version >= 2025-11-25). Old clients can't resume if\n        the stream is closed early because they didn't receive a priming event.\n        \"\"\"\n        # Only provide close callbacks when client supports resumability\n        if self._event_store and protocol_version >= \"2025-11-25\":\n\n            async def close_stream_callback() -> None:  # pragma: no cover\n                self.close_sse_stream(request_id)\n\n            async def close_standalone_stream_callback() -> None:  # pragma: no cover\n                self.close_standalone_sse_stream()\n\n            metadata = ServerMessageMetadata(\n                request_context=request,\n                close_sse_stream=close_stream_callback,\n                close_standalone_sse_stream=close_standalone_stream_callback,\n            )\n        else:\n            metadata = ServerMessageMetadata(request_context=request)\n\n        return SessionMessage(message, metadata=metadata)\n\n    async def _maybe_send_priming_event(\n        self,\n        request_id: RequestId,\n        sse_stream_writer: MemoryObjectSendStream[dict[str, Any]],\n        protocol_version: str,\n    ) -> None:\n        \"\"\"Send priming event for SSE resumability if event_store is configured.\n\n        Only sends priming events to clients with protocol version >= 2025-11-25,\n        which includes the fix for handling empty SSE data. Older clients would\n        crash trying to parse empty data as JSON.\n        \"\"\"\n        if not self._event_store:\n            return\n        # Priming events have empty data which older clients cannot handle.\n        if protocol_version < \"2025-11-25\":\n            return\n        priming_event_id = await self._event_store.store_event(\n            str(request_id),  # Convert RequestId to StreamId (str)\n            None,  # Priming event has no payload\n        )\n        priming_event: dict[str, str | int] = {\"id\": priming_event_id, \"data\": \"\"}\n        if self._retry_interval is not None:\n            priming_event[\"retry\"] = self._retry_interval\n        await sse_stream_writer.send(priming_event)\n\n    def _create_error_response(\n        self,\n        error_message: str,\n        status_code: HTTPStatus,\n        error_code: int = INVALID_REQUEST,\n        headers: dict[str, str] | None = None,\n    ) -> Response:\n        \"\"\"Create an error response with a simple string message.\"\"\"\n        response_headers = {\"Content-Type\": CONTENT_TYPE_JSON}\n        if headers:  # pragma: no cover\n            response_headers.update(headers)\n\n        if self.mcp_session_id:\n            response_headers[MCP_SESSION_ID_HEADER] = self.mcp_session_id\n\n        # Return a properly formatted JSON error response\n        error_response = JSONRPCError(\n            jsonrpc=\"2.0\",\n            id=None,\n            error=ErrorData(code=error_code, message=error_message),\n        )\n\n        return Response(\n            error_response.model_dump_json(by_alias=True, exclude_unset=True),\n            status_code=status_code,\n            headers=response_headers,\n        )\n\n    def _create_json_response(\n        self,\n        response_message: JSONRPCMessage | None,\n        status_code: HTTPStatus = HTTPStatus.OK,\n        headers: dict[str, str] | None = None,\n    ) -> Response:\n        \"\"\"Create a JSON response from a JSONRPCMessage.\"\"\"\n        response_headers = {\"Content-Type\": CONTENT_TYPE_JSON}\n        if headers:  # pragma: lax no cover\n            response_headers.update(headers)\n\n        if self.mcp_session_id:  # pragma: lax no cover\n            response_headers[MCP_SESSION_ID_HEADER] = self.mcp_session_id\n\n        return Response(\n            response_message.model_dump_json(by_alias=True, exclude_unset=True) if response_message else None,\n            status_code=status_code,\n            headers=response_headers,\n        )\n\n    def _get_session_id(self, request: Request) -> str | None:\n        \"\"\"Extract the session ID from request headers.\"\"\"\n        return request.headers.get(MCP_SESSION_ID_HEADER)\n\n    def _create_event_data(self, event_message: EventMessage) -> dict[str, str]:\n        \"\"\"Create event data dictionary from an EventMessage.\"\"\"\n        event_data = {\n            \"event\": \"message\",\n            \"data\": event_message.message.model_dump_json(by_alias=True, exclude_unset=True),\n        }\n\n        # If an event ID was provided, include it\n        if event_message.event_id:  # pragma: no cover\n            event_data[\"id\"] = event_message.event_id\n\n        return event_data\n\n    async def _clean_up_memory_streams(self, request_id: RequestId) -> None:\n        \"\"\"Clean up memory streams for a given request ID.\"\"\"\n        if request_id in self._request_streams:  # pragma: no branch\n            try:\n                # Close the request stream\n                await self._request_streams[request_id][0].aclose()\n                await self._request_streams[request_id][1].aclose()\n            except Exception:  # pragma: no cover\n                # During cleanup, we catch all exceptions since streams might be in various states\n                logger.debug(\"Error closing memory streams - may already be closed\")\n            finally:\n                # Remove the request stream from the mapping\n                self._request_streams.pop(request_id, None)\n\n    async def handle_request(self, scope: Scope, receive: Receive, send: Send) -> None:\n        \"\"\"Application entry point that handles all HTTP requests.\"\"\"\n        request = Request(scope, receive)\n\n        # Validate request headers for DNS rebinding protection\n        is_post = request.method == \"POST\"\n        error_response = await self._security.validate_request(request, is_post=is_post)\n        if error_response:\n            await error_response(scope, receive, send)\n            return\n\n        if self._terminated:  # pragma: no cover\n            # If the session has been terminated, return 404 Not Found\n            response = self._create_error_response(\n                \"Not Found: Session has been terminated\",\n                HTTPStatus.NOT_FOUND,\n            )\n            await response(scope, receive, send)\n            return\n\n        if request.method == \"POST\":\n            await self._handle_post_request(scope, request, receive, send)\n        elif request.method == \"GET\":\n            await self._handle_get_request(request, send)\n        elif request.method == \"DELETE\":\n            await self._handle_delete_request(request, send)\n        else:  # pragma: no cover\n            await self._handle_unsupported_request(request, send)\n\n    def _check_accept_headers(self, request: Request) -> tuple[bool, bool]:\n        \"\"\"Check if the request accepts the required media types.\n\n        Supports wildcard media types per RFC 7231, section 5.3.2:\n        - */* matches any media type\n        - application/* matches any application/ subtype\n        - text/* matches any text/ subtype\n        \"\"\"\n        accept_header = request.headers.get(\"accept\", \"\")\n        accept_types = [media_type.strip().split(\";\")[0].strip().lower() for media_type in accept_header.split(\",\")]\n\n        has_wildcard = \"*/*\" in accept_types\n        has_json = has_wildcard or any(t in (CONTENT_TYPE_JSON, \"application/*\") for t in accept_types)\n        has_sse = has_wildcard or any(t in (CONTENT_TYPE_SSE, \"text/*\") for t in accept_types)\n\n        return has_json, has_sse\n\n    def _check_content_type(self, request: Request) -> bool:\n        \"\"\"Check if the request has the correct Content-Type.\"\"\"\n        content_type = request.headers.get(\"content-type\", \"\")\n        content_type_parts = [part.strip() for part in content_type.split(\";\")[0].split(\",\")]\n\n        return any(part == CONTENT_TYPE_JSON for part in content_type_parts)\n\n    async def _validate_accept_header(self, request: Request, scope: Scope, send: Send) -> bool:\n        \"\"\"Validate Accept header based on response mode. Returns True if valid.\"\"\"\n        has_json, has_sse = self._check_accept_headers(request)\n        if self.is_json_response_enabled:\n            # For JSON-only responses, only require application/json\n            if not has_json:  # pragma: lax no cover\n                response = self._create_error_response(\n                    \"Not Acceptable: Client must accept application/json\",\n                    HTTPStatus.NOT_ACCEPTABLE,\n                )\n                await response(scope, request.receive, send)\n                return False\n        # For SSE responses, require both content types\n        elif not (has_json and has_sse):\n            response = self._create_error_response(\n                \"Not Acceptable: Client must accept both application/json and text/event-stream\",\n                HTTPStatus.NOT_ACCEPTABLE,\n            )\n            await response(scope, request.receive, send)\n            return False\n        return True\n\n    async def _handle_post_request(self, scope: Scope, request: Request, receive: Receive, send: Send) -> None:\n        \"\"\"Handle POST requests containing JSON-RPC messages.\"\"\"\n        writer = self._read_stream_writer\n        if writer is None:  # pragma: no cover\n            raise ValueError(\"No read stream writer available. Ensure connect() is called first.\")\n        try:\n            # Validate Accept header\n            if not await self._validate_accept_header(request, scope, send):\n                return\n\n            # Validate Content-Type\n            if not self._check_content_type(request):  # pragma: no cover\n                response = self._create_error_response(\n                    \"Unsupported Media Type: Content-Type must be application/json\",\n                    HTTPStatus.UNSUPPORTED_MEDIA_TYPE,\n                )\n                await response(scope, receive, send)\n                return\n\n            # Parse the body - only read it once\n            body = await request.body()\n\n            try:\n                raw_message = pydantic_core.from_json(body)\n            except ValueError as e:\n                response = self._create_error_response(f\"Parse error: {str(e)}\", HTTPStatus.BAD_REQUEST, PARSE_ERROR)\n                await response(scope, receive, send)\n                return\n\n            try:\n                message = jsonrpc_message_adapter.validate_python(raw_message, by_name=False)\n            except ValidationError as e:  # pragma: no cover\n                response = self._create_error_response(\n                    f\"Validation error: {str(e)}\",\n                    HTTPStatus.BAD_REQUEST,\n                    INVALID_PARAMS,\n                )\n                await response(scope, receive, send)\n                return\n\n            # Check if this is an initialization request\n            is_initialization_request = isinstance(message, JSONRPCRequest) and message.method == \"initialize\"\n\n            if is_initialization_request:\n                # Check if the server already has an established session\n                if self.mcp_session_id:\n                    # Check if request has a session ID\n                    request_session_id = self._get_session_id(request)\n\n                    # If request has a session ID but doesn't match, return 404\n                    if request_session_id and request_session_id != self.mcp_session_id:  # pragma: no cover\n                        response = self._create_error_response(\n                            \"Not Found: Invalid or expired session ID\",\n                            HTTPStatus.NOT_FOUND,\n                        )\n                        await response(scope, receive, send)\n                        return\n            elif not await self._validate_request_headers(request, send):  # pragma: no cover\n                return\n\n            # For notifications and responses only, return 202 Accepted\n            if not isinstance(message, JSONRPCRequest):\n                # Create response object and send it\n                response = self._create_json_response(\n                    None,\n                    HTTPStatus.ACCEPTED,\n                )\n                await response(scope, receive, send)\n\n                # Process the message after sending the response\n                metadata = ServerMessageMetadata(request_context=request)\n                session_message = SessionMessage(message, metadata=metadata)\n                await writer.send(session_message)\n\n                return\n\n            # Extract protocol version for priming event decision.\n            # For initialize requests, get from request params.\n            # For other requests, get from header (already validated).\n            protocol_version = (\n                str(message.params.get(\"protocolVersion\", DEFAULT_NEGOTIATED_VERSION))\n                if is_initialization_request and message.params\n                else request.headers.get(MCP_PROTOCOL_VERSION_HEADER, DEFAULT_NEGOTIATED_VERSION)\n            )\n\n            # Extract the request ID outside the try block for proper scope\n            request_id = str(message.id)\n            # Register this stream for the request ID\n            self._request_streams[request_id] = anyio.create_memory_object_stream[EventMessage](0)\n            request_stream_reader = self._request_streams[request_id][1]\n\n            if self.is_json_response_enabled:\n                # Process the message\n                metadata = ServerMessageMetadata(request_context=request)\n                session_message = SessionMessage(message, metadata=metadata)\n                await writer.send(session_message)\n                try:\n                    # Process messages from the request-specific stream\n                    # We need to collect all messages until we get a response\n                    response_message = None\n\n                    # Use similar approach to SSE writer for consistency\n                    async for event_message in request_stream_reader:  # pragma: no branch\n                        # If it's a response, this is what we're waiting for\n                        if isinstance(event_message.message, JSONRPCResponse | JSONRPCError):\n                            response_message = event_message.message\n                            break\n                        # For notifications and requests, keep waiting\n                        else:  # pragma: no cover\n                            logger.debug(f\"received: {event_message.message.method}\")\n\n                    # At this point we should have a response\n                    if response_message:\n                        # Create JSON response\n                        response = self._create_json_response(response_message)\n                        await response(scope, receive, send)\n                    else:  # pragma: no cover\n                        # This shouldn't happen in normal operation\n                        logger.error(\"No response message received before stream closed\")\n                        response = self._create_error_response(\n                            \"Error processing request: No response received\",\n                            HTTPStatus.INTERNAL_SERVER_ERROR,\n                        )\n                        await response(scope, receive, send)\n                except Exception:  # pragma: no cover\n                    logger.exception(\"Error processing JSON response\")\n                    response = self._create_error_response(\n                        \"Error processing request\",\n                        HTTPStatus.INTERNAL_SERVER_ERROR,\n                        INTERNAL_ERROR,\n                    )\n                    await response(scope, receive, send)\n                finally:\n                    await self._clean_up_memory_streams(request_id)\n            else:\n                # Create SSE stream\n                sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[dict[str, str]](0)\n\n                # Store writer reference so close_sse_stream() can close it\n                self._sse_stream_writers[request_id] = sse_stream_writer\n\n                async def sse_writer():  # pragma: lax no cover\n                    # Get the request ID from the incoming request message\n                    try:\n                        async with sse_stream_writer, request_stream_reader:\n                            # Send priming event for SSE resumability\n                            await self._maybe_send_priming_event(request_id, sse_stream_writer, protocol_version)\n\n                            # Process messages from the request-specific stream\n                            async for event_message in request_stream_reader:\n                                # Build the event data\n                                event_data = self._create_event_data(event_message)\n                                await sse_stream_writer.send(event_data)\n\n                                # If response, remove from pending streams and close\n                                if isinstance(event_message.message, JSONRPCResponse | JSONRPCError):\n                                    break\n                    except anyio.ClosedResourceError:\n                        # Expected when close_sse_stream() is called\n                        logger.debug(\"SSE stream closed by close_sse_stream()\")\n                    except Exception:\n                        logger.exception(\"Error in SSE writer\")\n                    finally:\n                        logger.debug(\"Closing SSE writer\")\n                        self._sse_stream_writers.pop(request_id, None)\n                        await self._clean_up_memory_streams(request_id)\n\n                # Create and start EventSourceResponse\n                # SSE stream mode (original behavior)\n                # Set up headers\n                headers = {\n                    \"Cache-Control\": \"no-cache, no-transform\",\n                    \"Connection\": \"keep-alive\",\n                    \"Content-Type\": CONTENT_TYPE_SSE,\n                    **({MCP_SESSION_ID_HEADER: self.mcp_session_id} if self.mcp_session_id else {}),\n                }\n                response = EventSourceResponse(\n                    content=sse_stream_reader,\n                    data_sender_callable=sse_writer,\n                    headers=headers,\n                )\n\n                # Start the SSE response (this will send headers immediately)\n                try:\n                    # First send the response to establish the SSE connection\n                    async with anyio.create_task_group() as tg:\n                        tg.start_soon(response, scope, receive, send)\n                        # Then send the message to be processed by the server\n                        session_message = self._create_session_message(message, request, request_id, protocol_version)\n                        await writer.send(session_message)\n                except Exception:  # pragma: no cover\n                    logger.exception(\"SSE response error\")\n                    await sse_stream_writer.aclose()\n                    await self._clean_up_memory_streams(request_id)\n                finally:\n                    await sse_stream_reader.aclose()\n\n        except Exception as err:  # pragma: no cover\n            logger.exception(\"Error handling POST request\")\n            response = self._create_error_response(\n                f\"Error handling POST request: {err}\",\n                HTTPStatus.INTERNAL_SERVER_ERROR,\n                INTERNAL_ERROR,\n            )\n            await response(scope, receive, send)\n            if writer:\n                await writer.send(Exception(err))\n            return\n\n    async def _handle_get_request(self, request: Request, send: Send) -> None:\n        \"\"\"Handle GET request to establish SSE.\n\n        This allows the server to communicate to the client without the client\n        first sending data via HTTP POST. The server can send JSON-RPC requests\n        and notifications on this stream.\n        \"\"\"\n        writer = self._read_stream_writer\n        if writer is None:  # pragma: no cover\n            raise ValueError(\"No read stream writer available. Ensure connect() is called first.\")\n\n        # Validate Accept header - must include text/event-stream\n        _, has_sse = self._check_accept_headers(request)\n\n        if not has_sse:  # pragma: no cover\n            response = self._create_error_response(\n                \"Not Acceptable: Client must accept text/event-stream\",\n                HTTPStatus.NOT_ACCEPTABLE,\n            )\n            await response(request.scope, request.receive, send)\n            return\n\n        if not await self._validate_request_headers(request, send):  # pragma: no cover\n            return\n\n        # Handle resumability: check for Last-Event-ID header\n        if last_event_id := request.headers.get(LAST_EVENT_ID_HEADER):  # pragma: no cover\n            await self._replay_events(last_event_id, request, send)\n            return\n\n        headers = {\n            \"Cache-Control\": \"no-cache, no-transform\",\n            \"Connection\": \"keep-alive\",\n            \"Content-Type\": CONTENT_TYPE_SSE,\n        }\n\n        if self.mcp_session_id:\n            headers[MCP_SESSION_ID_HEADER] = self.mcp_session_id\n\n        # Check if we already have an active GET stream\n        if GET_STREAM_KEY in self._request_streams:  # pragma: no cover\n            response = self._create_error_response(\n                \"Conflict: Only one SSE stream is allowed per session\",\n                HTTPStatus.CONFLICT,\n            )\n            await response(request.scope, request.receive, send)\n            return\n\n        # Create SSE stream\n        sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[dict[str, str]](0)\n\n        async def standalone_sse_writer():\n            try:\n                # Create a standalone message stream for server-initiated messages\n\n                self._request_streams[GET_STREAM_KEY] = anyio.create_memory_object_stream[EventMessage](0)\n                standalone_stream_reader = self._request_streams[GET_STREAM_KEY][1]\n\n                async with sse_stream_writer, standalone_stream_reader:\n                    # Process messages from the standalone stream\n                    async for event_message in standalone_stream_reader:  # pragma: lax no cover\n                        # For the standalone stream, we handle:\n                        # - JSONRPCNotification (server sends notifications to client)\n                        # - JSONRPCRequest (server sends requests to client)\n                        # We should NOT receive JSONRPCResponse\n\n                        # Send the message via SSE\n                        event_data = self._create_event_data(event_message)\n                        await sse_stream_writer.send(event_data)\n            except Exception:  # pragma: no cover\n                logger.exception(\"Error in standalone SSE writer\")\n            finally:\n                logger.debug(\"Closing standalone SSE writer\")\n                await self._clean_up_memory_streams(GET_STREAM_KEY)\n\n        # Create and start EventSourceResponse\n        response = EventSourceResponse(\n            content=sse_stream_reader,\n            data_sender_callable=standalone_sse_writer,\n            headers=headers,\n        )\n\n        try:\n            # This will send headers immediately and establish the SSE connection\n            await response(request.scope, request.receive, send)\n        except Exception:  # pragma: lax no cover\n            logger.exception(\"Error in standalone SSE response\")\n            await self._clean_up_memory_streams(GET_STREAM_KEY)\n        finally:\n            await sse_stream_writer.aclose()\n            await sse_stream_reader.aclose()\n\n    async def _handle_delete_request(self, request: Request, send: Send) -> None:\n        \"\"\"Handle DELETE requests for explicit session termination.\"\"\"\n        # Validate session ID\n        if not self.mcp_session_id:  # pragma: no cover\n            # If no session ID set, return Method Not Allowed\n            response = self._create_error_response(\n                \"Method Not Allowed: Session termination not supported\",\n                HTTPStatus.METHOD_NOT_ALLOWED,\n            )\n            await response(request.scope, request.receive, send)\n            return\n\n        if not await self._validate_request_headers(request, send):  # pragma: no cover\n            return\n\n        await self.terminate()\n\n        response = self._create_json_response(\n            None,\n            HTTPStatus.OK,\n        )\n        await response(request.scope, request.receive, send)\n\n    async def terminate(self) -> None:\n        \"\"\"Terminate the current session, closing all streams.\n\n        Once terminated, all requests with this session ID will receive 404 Not Found.\n        \"\"\"\n\n        self._terminated = True\n        logger.info(f\"Terminating session: {self.mcp_session_id}\")\n\n        # We need a copy of the keys to avoid modification during iteration\n        request_stream_keys = list(self._request_streams.keys())\n\n        # Close all request streams asynchronously\n        for key in request_stream_keys:  # pragma: lax no cover\n            await self._clean_up_memory_streams(key)\n\n        # Clear the request streams dictionary immediately\n        self._request_streams.clear()\n        try:\n            if self._read_stream_writer is not None:  # pragma: no branch\n                await self._read_stream_writer.aclose()\n            if self._read_stream is not None:  # pragma: no branch\n                await self._read_stream.aclose()\n            if self._write_stream_reader is not None:  # pragma: no branch\n                await self._write_stream_reader.aclose()\n            if self._write_stream is not None:  # pragma: no branch\n                await self._write_stream.aclose()\n        except Exception as e:  # pragma: no cover\n            # During cleanup, we catch all exceptions since streams might be in various states\n            logger.debug(f\"Error closing streams: {e}\")\n\n    async def _handle_unsupported_request(self, request: Request, send: Send) -> None:  # pragma: no cover\n        \"\"\"Handle unsupported HTTP methods.\"\"\"\n        headers = {\n            \"Content-Type\": CONTENT_TYPE_JSON,\n            \"Allow\": \"GET, POST, DELETE\",\n        }\n        if self.mcp_session_id:\n            headers[MCP_SESSION_ID_HEADER] = self.mcp_session_id\n\n        response = self._create_error_response(\n            \"Method Not Allowed\",\n            HTTPStatus.METHOD_NOT_ALLOWED,\n            headers=headers,\n        )\n        await response(request.scope, request.receive, send)\n\n    async def _validate_request_headers(self, request: Request, send: Send) -> bool:  # pragma: lax no cover\n        if not await self._validate_session(request, send):\n            return False\n        if not await self._validate_protocol_version(request, send):\n            return False\n        return True\n\n    async def _validate_session(self, request: Request, send: Send) -> bool:\n        \"\"\"Validate the session ID in the request.\"\"\"\n        if not self.mcp_session_id:  # pragma: no cover\n            # If we're not using session IDs, return True\n            return True\n\n        # Get the session ID from the request headers\n        request_session_id = self._get_session_id(request)\n\n        # If no session ID provided but required, return error\n        if not request_session_id:  # pragma: no cover\n            response = self._create_error_response(\n                \"Bad Request: Missing session ID\",\n                HTTPStatus.BAD_REQUEST,\n            )\n            await response(request.scope, request.receive, send)\n            return False\n\n        # If session ID doesn't match, return error\n        if request_session_id != self.mcp_session_id:  # pragma: no cover\n            response = self._create_error_response(\n                \"Not Found: Invalid or expired session ID\",\n                HTTPStatus.NOT_FOUND,\n            )\n            await response(request.scope, request.receive, send)\n            return False\n\n        return True\n\n    async def _validate_protocol_version(self, request: Request, send: Send) -> bool:\n        \"\"\"Validate the protocol version header in the request.\"\"\"\n        # Get the protocol version from the request headers\n        protocol_version = request.headers.get(MCP_PROTOCOL_VERSION_HEADER)\n\n        # If no protocol version provided, assume default version\n        if protocol_version is None:  # pragma: no cover\n            protocol_version = DEFAULT_NEGOTIATED_VERSION\n\n        # Check if the protocol version is supported\n        if protocol_version not in SUPPORTED_PROTOCOL_VERSIONS:  # pragma: no cover\n            supported_versions = \", \".join(SUPPORTED_PROTOCOL_VERSIONS)\n            response = self._create_error_response(\n                f\"Bad Request: Unsupported protocol version: {protocol_version}. \"\n                + f\"Supported versions: {supported_versions}\",\n                HTTPStatus.BAD_REQUEST,\n            )\n            await response(request.scope, request.receive, send)\n            return False\n\n        return True\n\n    async def _replay_events(self, last_event_id: str, request: Request, send: Send) -> None:  # pragma: no cover\n        \"\"\"Replays events that would have been sent after the specified event ID.\n\n        Only used when resumability is enabled.\n        \"\"\"\n        event_store = self._event_store\n        if not event_store:\n            return\n\n        try:\n            headers = {\n                \"Cache-Control\": \"no-cache, no-transform\",\n                \"Connection\": \"keep-alive\",\n                \"Content-Type\": CONTENT_TYPE_SSE,\n            }\n\n            if self.mcp_session_id:\n                headers[MCP_SESSION_ID_HEADER] = self.mcp_session_id\n\n            # Get protocol version from header (already validated in _validate_protocol_version)\n            replay_protocol_version = request.headers.get(MCP_PROTOCOL_VERSION_HEADER, DEFAULT_NEGOTIATED_VERSION)\n\n            # Create SSE stream for replay\n            sse_stream_writer, sse_stream_reader = anyio.create_memory_object_stream[dict[str, str]](0)\n\n            async def replay_sender():\n                try:\n                    async with sse_stream_writer:\n                        # Define an async callback for sending events\n                        async def send_event(event_message: EventMessage) -> None:\n                            event_data = self._create_event_data(event_message)\n                            await sse_stream_writer.send(event_data)\n\n                        # Replay past events and get the stream ID\n                        stream_id = await event_store.replay_events_after(last_event_id, send_event)\n\n                        # If stream ID not in mapping, create it\n                        if stream_id and stream_id not in self._request_streams:\n                            # Register SSE writer so close_sse_stream() can close it\n                            self._sse_stream_writers[stream_id] = sse_stream_writer\n\n                            # Send priming event for this new connection\n                            await self._maybe_send_priming_event(stream_id, sse_stream_writer, replay_protocol_version)\n\n                            # Create new request streams for this connection\n                            self._request_streams[stream_id] = anyio.create_memory_object_stream[EventMessage](0)\n                            msg_reader = self._request_streams[stream_id][1]\n\n                            # Forward messages to SSE\n                            async with msg_reader:\n                                async for event_message in msg_reader:\n                                    event_data = self._create_event_data(event_message)\n\n                                    await sse_stream_writer.send(event_data)\n                except anyio.ClosedResourceError:\n                    # Expected when close_sse_stream() is called\n                    logger.debug(\"Replay SSE stream closed by close_sse_stream()\")\n                except Exception:\n                    logger.exception(\"Error in replay sender\")\n\n            # Create and start EventSourceResponse\n            response = EventSourceResponse(\n                content=sse_stream_reader,\n                data_sender_callable=replay_sender,\n                headers=headers,\n            )\n\n            try:\n                await response(request.scope, request.receive, send)\n            except Exception:\n                logger.exception(\"Error in replay response\")\n            finally:\n                await sse_stream_writer.aclose()\n                await sse_stream_reader.aclose()\n\n        except Exception:\n            logger.exception(\"Error replaying events\")\n            response = self._create_error_response(\n                \"Error replaying events\",\n                HTTPStatus.INTERNAL_SERVER_ERROR,\n                INTERNAL_ERROR,\n            )\n            await response(request.scope, request.receive, send)\n\n    @asynccontextmanager\n    async def connect(\n        self,\n    ) -> AsyncGenerator[\n        tuple[\n            MemoryObjectReceiveStream[SessionMessage | Exception],\n            MemoryObjectSendStream[SessionMessage],\n        ],\n        None,\n    ]:\n        \"\"\"Context manager that provides read and write streams for a connection.\n\n        Yields:\n            Tuple of (read_stream, write_stream) for bidirectional communication\n        \"\"\"\n\n        # Create the memory streams for this connection\n\n        read_stream_writer, read_stream = anyio.create_memory_object_stream[SessionMessage | Exception](0)\n        write_stream, write_stream_reader = anyio.create_memory_object_stream[SessionMessage](0)\n\n        # Store the streams\n        self._read_stream_writer = read_stream_writer\n        self._read_stream = read_stream\n        self._write_stream_reader = write_stream_reader\n        self._write_stream = write_stream\n\n        # Start a task group for message routing\n        async with anyio.create_task_group() as tg:\n            # Create a message router that distributes messages to request streams\n            async def message_router():\n                try:\n                    async for session_message in write_stream_reader:  # pragma: no branch\n                        # Determine which request stream(s) should receive this message\n                        message = session_message.message\n                        target_request_id = None\n                        # Check if this is a response with a known request id.\n                        # Null-id errors (e.g., parse errors) fall through to\n                        # the GET stream since they can't be correlated.\n                        if isinstance(message, JSONRPCResponse | JSONRPCError) and message.id is not None:\n                            target_request_id = str(message.id)\n                        # Extract related_request_id from meta if it exists\n                        elif (  # pragma: no cover\n                            session_message.metadata is not None\n                            and isinstance(\n                                session_message.metadata,\n                                ServerMessageMetadata,\n                            )\n                            and session_message.metadata.related_request_id is not None\n                        ):\n                            target_request_id = str(session_message.metadata.related_request_id)\n\n                        request_stream_id = target_request_id if target_request_id is not None else GET_STREAM_KEY\n\n                        # Store the event if we have an event store,\n                        # regardless of whether a client is connected\n                        # messages will be replayed on the re-connect\n                        event_id = None\n                        if self._event_store:  # pragma: lax no cover\n                            event_id = await self._event_store.store_event(request_stream_id, message)\n                            logger.debug(f\"Stored {event_id} from {request_stream_id}\")\n\n                        if request_stream_id in self._request_streams:\n                            try:\n                                # Send both the message and the event ID\n                                await self._request_streams[request_stream_id][0].send(EventMessage(message, event_id))\n                            except (anyio.BrokenResourceError, anyio.ClosedResourceError):  # pragma: no cover\n                                # Stream might be closed, remove from registry\n                                self._request_streams.pop(request_stream_id, None)\n                        else:  # pragma: no cover\n                            logger.debug(\n                                f\"\"\"Request stream {request_stream_id} not found\n                                for message. Still processing message as the client\n                                might reconnect and replay.\"\"\"\n                            )\n                except anyio.ClosedResourceError:\n                    if self._terminated:\n                        logger.debug(\"Read stream closed by client\")\n                    else:\n                        logger.exception(\"Unexpected closure of read stream in message router\")\n                except Exception:  # pragma: lax no cover\n                    logger.exception(\"Error in message router\")\n\n            # Start the message router\n            tg.start_soon(message_router)\n\n            try:\n                # Yield the streams for the caller to use\n                yield read_stream, write_stream\n            finally:\n                for stream_id in list(self._request_streams.keys()):  # pragma: lax no cover\n                    await self._clean_up_memory_streams(stream_id)\n                self._request_streams.clear()\n\n                # Clean up the read and write streams\n                try:\n                    await read_stream_writer.aclose()\n                    await read_stream.aclose()\n                    await write_stream_reader.aclose()\n                    await write_stream.aclose()\n                except Exception as e:  # pragma: no cover\n                    # During cleanup, we catch all exceptions since streams might be in various states\n                    logger.debug(f\"Error closing streams: {e}\")\n"
  },
  {
    "path": "src/mcp/server/streamable_http_manager.py",
    "content": "\"\"\"StreamableHTTP Session Manager for MCP servers.\"\"\"\n\nfrom __future__ import annotations\n\nimport contextlib\nimport logging\nfrom collections.abc import AsyncIterator\nfrom http import HTTPStatus\nfrom typing import TYPE_CHECKING, Any\nfrom uuid import uuid4\n\nimport anyio\nfrom anyio.abc import TaskStatus\nfrom starlette.requests import Request\nfrom starlette.responses import Response\nfrom starlette.types import Receive, Scope, Send\n\nfrom mcp.server.streamable_http import (\n    MCP_SESSION_ID_HEADER,\n    EventStore,\n    StreamableHTTPServerTransport,\n)\nfrom mcp.server.transport_security import TransportSecuritySettings\nfrom mcp.types import INVALID_REQUEST, ErrorData, JSONRPCError\n\nif TYPE_CHECKING:\n    from mcp.server.lowlevel.server import Server\n\nlogger = logging.getLogger(__name__)\n\n\nclass StreamableHTTPSessionManager:\n    \"\"\"Manages StreamableHTTP sessions with optional resumability via event store.\n\n    This class abstracts away the complexity of session management, event storage,\n    and request handling for StreamableHTTP transports. It handles:\n\n    1. Session tracking for clients\n    2. Resumability via an optional event store\n    3. Connection management and lifecycle\n    4. Request handling and transport setup\n    5. Idle session cleanup via optional timeout\n\n    Important: Only one StreamableHTTPSessionManager instance should be created\n    per application. The instance cannot be reused after its run() context has\n    completed. If you need to restart the manager, create a new instance.\n\n    Args:\n        app: The MCP server instance\n        event_store: Optional event store for resumability support. If provided, enables resumable connections\n            where clients can reconnect and receive missed events. If None, sessions are still tracked but not\n            resumable.\n        json_response: Whether to use JSON responses instead of SSE streams\n        stateless: If True, creates a completely fresh transport for each request with no session tracking or\n            state persistence between requests.\n        security_settings: Optional transport security settings.\n        retry_interval: Retry interval in milliseconds to suggest to clients in SSE retry field. Used for SSE\n            polling behavior.\n        session_idle_timeout: Optional idle timeout in seconds for stateful sessions. If set, sessions that\n            receive no HTTP requests for this duration will be automatically terminated and removed. When\n            retry_interval is also configured, ensure the idle timeout comfortably exceeds the retry interval to\n            avoid reaping sessions during normal SSE polling gaps. Default is None (no timeout). A value of 1800\n            (30 minutes) is recommended for most deployments.\n    \"\"\"\n\n    def __init__(\n        self,\n        app: Server[Any],\n        event_store: EventStore | None = None,\n        json_response: bool = False,\n        stateless: bool = False,\n        security_settings: TransportSecuritySettings | None = None,\n        retry_interval: int | None = None,\n        session_idle_timeout: float | None = None,\n    ):\n        if session_idle_timeout is not None and session_idle_timeout <= 0:\n            raise ValueError(\"session_idle_timeout must be a positive number of seconds\")\n        if stateless and session_idle_timeout is not None:\n            raise RuntimeError(\"session_idle_timeout is not supported in stateless mode\")\n\n        self.app = app\n        self.event_store = event_store\n        self.json_response = json_response\n        self.stateless = stateless\n        self.security_settings = security_settings\n        self.retry_interval = retry_interval\n        self.session_idle_timeout = session_idle_timeout\n\n        # Session tracking (only used if not stateless)\n        self._session_creation_lock = anyio.Lock()\n        self._server_instances: dict[str, StreamableHTTPServerTransport] = {}\n\n        # The task group will be set during lifespan\n        self._task_group = None\n        # Thread-safe tracking of run() calls\n        self._run_lock = anyio.Lock()\n        self._has_started = False\n\n    @contextlib.asynccontextmanager\n    async def run(self) -> AsyncIterator[None]:\n        \"\"\"Run the session manager with proper lifecycle management.\n\n        This creates and manages the task group for all session operations.\n\n        Important: This method can only be called once per instance. The same\n        StreamableHTTPSessionManager instance cannot be reused after this\n        context manager exits. Create a new instance if you need to restart.\n\n        Use this in the lifespan context manager of your Starlette app:\n\n        @contextlib.asynccontextmanager\n        async def lifespan(app: Starlette) -> AsyncIterator[None]:\n            async with session_manager.run():\n                yield\n        \"\"\"\n        # Thread-safe check to ensure run() is only called once\n        async with self._run_lock:\n            if self._has_started:\n                raise RuntimeError(\n                    \"StreamableHTTPSessionManager .run() can only be called \"\n                    \"once per instance. Create a new instance if you need to run again.\"\n                )\n            self._has_started = True\n\n        async with anyio.create_task_group() as tg:\n            # Store the task group for later use\n            self._task_group = tg\n            logger.info(\"StreamableHTTP session manager started\")\n            try:\n                yield  # Let the application run\n            finally:\n                logger.info(\"StreamableHTTP session manager shutting down\")\n                # Cancel task group to stop all spawned tasks\n                tg.cancel_scope.cancel()\n                self._task_group = None\n                # Clear any remaining server instances\n                self._server_instances.clear()\n\n    async def handle_request(self, scope: Scope, receive: Receive, send: Send) -> None:\n        \"\"\"Process ASGI request with proper session handling and transport setup.\n\n        Dispatches to the appropriate handler based on stateless mode.\n        \"\"\"\n        if self._task_group is None:\n            raise RuntimeError(\"Task group is not initialized. Make sure to use run().\")\n\n        # Dispatch to the appropriate handler\n        if self.stateless:\n            await self._handle_stateless_request(scope, receive, send)\n        else:\n            await self._handle_stateful_request(scope, receive, send)\n\n    async def _handle_stateless_request(self, scope: Scope, receive: Receive, send: Send) -> None:\n        \"\"\"Process request in stateless mode - creating a new transport for each request.\"\"\"\n        logger.debug(\"Stateless mode: Creating new transport for this request\")\n        # No session ID needed in stateless mode\n        http_transport = StreamableHTTPServerTransport(\n            mcp_session_id=None,  # No session tracking in stateless mode\n            is_json_response_enabled=self.json_response,\n            event_store=None,  # No event store in stateless mode\n            security_settings=self.security_settings,\n        )\n\n        # Start server in a new task\n        async def run_stateless_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED):\n            async with http_transport.connect() as streams:\n                read_stream, write_stream = streams\n                task_status.started()\n                try:\n                    await self.app.run(\n                        read_stream,\n                        write_stream,\n                        self.app.create_initialization_options(),\n                        stateless=True,\n                    )\n                except Exception:  # pragma: no cover\n                    logger.exception(\"Stateless session crashed\")\n\n        # Assert task group is not None for type checking\n        assert self._task_group is not None\n        # Start the server task\n        await self._task_group.start(run_stateless_server)\n\n        # Handle the HTTP request and return the response\n        await http_transport.handle_request(scope, receive, send)\n\n        # Terminate the transport after the request is handled\n        await http_transport.terminate()\n\n    async def _handle_stateful_request(self, scope: Scope, receive: Receive, send: Send) -> None:\n        \"\"\"Process request in stateful mode - maintaining session state between requests.\"\"\"\n        request = Request(scope, receive)\n        request_mcp_session_id = request.headers.get(MCP_SESSION_ID_HEADER)\n\n        # Existing session case\n        if request_mcp_session_id is not None and request_mcp_session_id in self._server_instances:\n            transport = self._server_instances[request_mcp_session_id]\n            logger.debug(\"Session already exists, handling request directly\")\n            # Push back idle deadline on activity\n            if transport.idle_scope is not None and self.session_idle_timeout is not None:\n                transport.idle_scope.deadline = anyio.current_time() + self.session_idle_timeout  # pragma: no cover\n            await transport.handle_request(scope, receive, send)\n            return\n\n        if request_mcp_session_id is None:\n            # New session case\n            logger.debug(\"Creating new transport\")\n            async with self._session_creation_lock:\n                new_session_id = uuid4().hex\n                http_transport = StreamableHTTPServerTransport(\n                    mcp_session_id=new_session_id,\n                    is_json_response_enabled=self.json_response,\n                    event_store=self.event_store,  # May be None (no resumability)\n                    security_settings=self.security_settings,\n                    retry_interval=self.retry_interval,\n                )\n\n                assert http_transport.mcp_session_id is not None\n                self._server_instances[http_transport.mcp_session_id] = http_transport\n                logger.info(f\"Created new transport with session ID: {new_session_id}\")\n\n                # Define the server runner\n                async def run_server(*, task_status: TaskStatus[None] = anyio.TASK_STATUS_IGNORED) -> None:\n                    async with http_transport.connect() as streams:\n                        read_stream, write_stream = streams\n                        task_status.started()\n                        try:\n                            # Use a cancel scope for idle timeout — when the\n                            # deadline passes the scope cancels app.run() and\n                            # execution continues after the ``with`` block.\n                            # Incoming requests push the deadline forward.\n                            idle_scope = anyio.CancelScope()\n                            if self.session_idle_timeout is not None:\n                                idle_scope.deadline = anyio.current_time() + self.session_idle_timeout\n                                http_transport.idle_scope = idle_scope\n\n                            with idle_scope:\n                                await self.app.run(\n                                    read_stream,\n                                    write_stream,\n                                    self.app.create_initialization_options(),\n                                    stateless=False,\n                                )\n\n                            if idle_scope.cancelled_caught:\n                                assert http_transport.mcp_session_id is not None\n                                logger.info(f\"Session {http_transport.mcp_session_id} idle timeout\")\n                                self._server_instances.pop(http_transport.mcp_session_id, None)\n                                await http_transport.terminate()\n                        except Exception:\n                            logger.exception(f\"Session {http_transport.mcp_session_id} crashed\")\n                        finally:\n                            if (  # pragma: no branch\n                                http_transport.mcp_session_id\n                                and http_transport.mcp_session_id in self._server_instances\n                                and not http_transport.is_terminated\n                            ):\n                                logger.info(\n                                    \"Cleaning up crashed session \"\n                                    f\"{http_transport.mcp_session_id} from active instances.\"\n                                )\n                                del self._server_instances[http_transport.mcp_session_id]\n\n                # Assert task group is not None for type checking\n                assert self._task_group is not None\n                # Start the server task\n                await self._task_group.start(run_server)\n\n                # Handle the HTTP request and return the response\n                await http_transport.handle_request(scope, receive, send)\n        else:\n            # Unknown or expired session ID - return 404 per MCP spec\n            # TODO: Align error code once spec clarifies\n            # See: https://github.com/modelcontextprotocol/python-sdk/issues/1821\n            logger.info(f\"Rejected request with unknown or expired session ID: {request_mcp_session_id[:64]}\")\n            error_response = JSONRPCError(\n                jsonrpc=\"2.0\",\n                id=None,\n                error=ErrorData(code=INVALID_REQUEST, message=\"Session not found\"),\n            )\n            response = Response(\n                content=error_response.model_dump_json(by_alias=True, exclude_unset=True),\n                status_code=HTTPStatus.NOT_FOUND,\n                media_type=\"application/json\",\n            )\n            await response(scope, receive, send)\n\n\nclass StreamableHTTPASGIApp:\n    \"\"\"ASGI application for Streamable HTTP server transport.\"\"\"\n\n    def __init__(self, session_manager: StreamableHTTPSessionManager):\n        self.session_manager = session_manager\n\n    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:\n        await self.session_manager.handle_request(scope, receive, send)\n"
  },
  {
    "path": "src/mcp/server/transport_security.py",
    "content": "\"\"\"DNS rebinding protection for MCP server transports.\"\"\"\n\nimport logging\n\nfrom pydantic import BaseModel, Field\nfrom starlette.requests import Request\nfrom starlette.responses import Response\n\nlogger = logging.getLogger(__name__)\n\n\n# TODO(Marcelo): We should flatten these settings. To be fair, I don't think we should even have this middleware.\nclass TransportSecuritySettings(BaseModel):\n    \"\"\"Settings for MCP transport security features.\n\n    These settings help protect against DNS rebinding attacks by validating incoming request headers.\n    \"\"\"\n\n    enable_dns_rebinding_protection: bool = True\n    \"\"\"Enable DNS rebinding protection (recommended for production).\"\"\"\n\n    allowed_hosts: list[str] = Field(default_factory=list)\n    \"\"\"List of allowed Host header values.\n\n    Only applies when `enable_dns_rebinding_protection` is `True`.\n    \"\"\"\n\n    allowed_origins: list[str] = Field(default_factory=list)\n    \"\"\"List of allowed Origin header values.\n\n    Only applies when `enable_dns_rebinding_protection` is `True`.\n    \"\"\"\n\n\n# TODO(Marcelo): This should be a proper ASGI middleware. I'm sad to see this.\nclass TransportSecurityMiddleware:\n    \"\"\"Middleware to enforce DNS rebinding protection for MCP transport endpoints.\"\"\"\n\n    def __init__(self, settings: TransportSecuritySettings | None = None):\n        # If not specified, disable DNS rebinding protection by default for backwards compatibility\n        self.settings = settings or TransportSecuritySettings(enable_dns_rebinding_protection=False)\n\n    def _validate_host(self, host: str | None) -> bool:  # pragma: no cover\n        \"\"\"Validate the Host header against allowed values.\"\"\"\n        if not host:\n            logger.warning(\"Missing Host header in request\")\n            return False\n\n        # Check exact match first\n        if host in self.settings.allowed_hosts:\n            return True\n\n        # Check wildcard port patterns\n        for allowed in self.settings.allowed_hosts:\n            if allowed.endswith(\":*\"):\n                # Extract base host from pattern\n                base_host = allowed[:-2]\n                # Check if the actual host starts with base host and has a port\n                if host.startswith(base_host + \":\"):\n                    return True\n\n        logger.warning(f\"Invalid Host header: {host}\")\n        return False\n\n    def _validate_origin(self, origin: str | None) -> bool:  # pragma: no cover\n        \"\"\"Validate the Origin header against allowed values.\"\"\"\n        # Origin can be absent for same-origin requests\n        if not origin:\n            return True\n\n        # Check exact match first\n        if origin in self.settings.allowed_origins:\n            return True\n\n        # Check wildcard port patterns\n        for allowed in self.settings.allowed_origins:\n            if allowed.endswith(\":*\"):\n                # Extract base origin from pattern\n                base_origin = allowed[:-2]\n                # Check if the actual origin starts with base origin and has a port\n                if origin.startswith(base_origin + \":\"):\n                    return True\n\n        logger.warning(f\"Invalid Origin header: {origin}\")\n        return False\n\n    def _validate_content_type(self, content_type: str | None) -> bool:\n        \"\"\"Validate the Content-Type header for POST requests.\"\"\"\n        return content_type is not None and content_type.lower().startswith(\"application/json\")\n\n    async def validate_request(self, request: Request, is_post: bool = False) -> Response | None:\n        \"\"\"Validate request headers for DNS rebinding protection.\n\n        Returns None if validation passes, or an error Response if validation fails.\n        \"\"\"\n        # Always validate Content-Type for POST requests\n        if is_post:  # pragma: no branch\n            content_type = request.headers.get(\"content-type\")\n            if not self._validate_content_type(content_type):\n                return Response(\"Invalid Content-Type header\", status_code=400)\n\n        # Skip remaining validation if DNS rebinding protection is disabled\n        if not self.settings.enable_dns_rebinding_protection:\n            return None\n\n        # Validate Host header  # pragma: no cover\n        host = request.headers.get(\"host\")  # pragma: no cover\n        if not self._validate_host(host):  # pragma: no cover\n            return Response(\"Invalid Host header\", status_code=421)  # pragma: no cover\n\n        # Validate Origin header  # pragma: no cover\n        origin = request.headers.get(\"origin\")  # pragma: no cover\n        if not self._validate_origin(origin):  # pragma: no cover\n            return Response(\"Invalid Origin header\", status_code=403)  # pragma: no cover\n\n        return None  # pragma: no cover\n"
  },
  {
    "path": "src/mcp/server/validation.py",
    "content": "\"\"\"Shared validation functions for server requests.\n\nThis module provides validation logic for sampling and elicitation requests\nthat is shared across normal and task-augmented code paths.\n\"\"\"\n\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.types import INVALID_PARAMS, ClientCapabilities, SamplingMessage, Tool, ToolChoice\n\n\ndef check_sampling_tools_capability(client_caps: ClientCapabilities | None) -> bool:\n    \"\"\"Check if the client supports sampling tools capability.\n\n    Args:\n        client_caps: The client's declared capabilities\n\n    Returns:\n        True if client supports sampling.tools, False otherwise\n    \"\"\"\n    if client_caps is None:\n        return False\n    if client_caps.sampling is None:\n        return False\n    if client_caps.sampling.tools is None:\n        return False\n    return True\n\n\ndef validate_sampling_tools(\n    client_caps: ClientCapabilities | None,\n    tools: list[Tool] | None,\n    tool_choice: ToolChoice | None,\n) -> None:\n    \"\"\"Validate that the client supports sampling tools if tools are being used.\n\n    Args:\n        client_caps: The client's declared capabilities\n        tools: The tools list, if provided\n        tool_choice: The tool choice setting, if provided\n\n    Raises:\n        MCPError: If tools/tool_choice are provided but client doesn't support them\n    \"\"\"\n    if tools is not None or tool_choice is not None:\n        if not check_sampling_tools_capability(client_caps):\n            raise MCPError(code=INVALID_PARAMS, message=\"Client does not support sampling tools capability\")\n\n\ndef validate_tool_use_result_messages(messages: list[SamplingMessage]) -> None:\n    \"\"\"Validate tool_use/tool_result message structure per SEP-1577.\n\n    This validation ensures:\n    1. Messages with tool_result content contain ONLY tool_result content\n    2. tool_result messages are preceded by a message with tool_use\n    3. tool_result IDs match the tool_use IDs from the previous message\n\n    See: https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1577\n\n    Args:\n        messages: The list of sampling messages to validate\n\n    Raises:\n        ValueError: If the message structure is invalid\n    \"\"\"\n    if not messages:\n        return\n\n    last_content = messages[-1].content_as_list\n    has_tool_results = any(c.type == \"tool_result\" for c in last_content)\n\n    previous_content = messages[-2].content_as_list if len(messages) >= 2 else None\n    has_previous_tool_use = previous_content and any(c.type == \"tool_use\" for c in previous_content)\n\n    if has_tool_results:\n        # Per spec: \"SamplingMessage with tool result content blocks\n        # MUST NOT contain other content types.\"\n        if any(c.type != \"tool_result\" for c in last_content):\n            raise ValueError(\"The last message must contain only tool_result content if any is present\")\n        if previous_content is None:\n            raise ValueError(\"tool_result requires a previous message containing tool_use\")\n        if not has_previous_tool_use:\n            raise ValueError(\"tool_result blocks do not match any tool_use in the previous message\")\n\n    if has_previous_tool_use and previous_content:\n        tool_use_ids = {c.id for c in previous_content if c.type == \"tool_use\"}\n        tool_result_ids = {c.tool_use_id for c in last_content if c.type == \"tool_result\"}\n        if tool_use_ids != tool_result_ids:\n            raise ValueError(\"ids of tool_result blocks and tool_use blocks from previous message do not match\")\n"
  },
  {
    "path": "src/mcp/server/websocket.py",
    "content": "from contextlib import asynccontextmanager\n\nimport anyio\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\nfrom pydantic_core import ValidationError\nfrom starlette.types import Receive, Scope, Send\nfrom starlette.websockets import WebSocket\n\nfrom mcp import types\nfrom mcp.shared.message import SessionMessage\n\n\n@asynccontextmanager\nasync def websocket_server(scope: Scope, receive: Receive, send: Send):\n    \"\"\"WebSocket server transport for MCP. This is an ASGI application, suitable for use\n    with a framework like Starlette and a server like Hypercorn.\n    \"\"\"\n\n    websocket = WebSocket(scope, receive, send)\n    await websocket.accept(subprotocol=\"mcp\")\n\n    read_stream: MemoryObjectReceiveStream[SessionMessage | Exception]\n    read_stream_writer: MemoryObjectSendStream[SessionMessage | Exception]\n\n    write_stream: MemoryObjectSendStream[SessionMessage]\n    write_stream_reader: MemoryObjectReceiveStream[SessionMessage]\n\n    read_stream_writer, read_stream = anyio.create_memory_object_stream(0)\n    write_stream, write_stream_reader = anyio.create_memory_object_stream(0)\n\n    async def ws_reader():\n        try:\n            async with read_stream_writer:\n                async for msg in websocket.iter_text():\n                    try:\n                        client_message = types.jsonrpc_message_adapter.validate_json(msg, by_name=False)\n                    except ValidationError as exc:  # pragma: no cover\n                        await read_stream_writer.send(exc)\n                        continue\n\n                    session_message = SessionMessage(client_message)\n                    await read_stream_writer.send(session_message)\n        except anyio.ClosedResourceError:  # pragma: no cover\n            await websocket.close()\n\n    async def ws_writer():\n        try:\n            async with write_stream_reader:\n                async for session_message in write_stream_reader:\n                    obj = session_message.message.model_dump_json(by_alias=True, exclude_unset=True)\n                    await websocket.send_text(obj)\n        except anyio.ClosedResourceError:  # pragma: no cover\n            await websocket.close()\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(ws_reader)\n        tg.start_soon(ws_writer)\n        yield (read_stream, write_stream)\n"
  },
  {
    "path": "src/mcp/shared/__init__.py",
    "content": ""
  },
  {
    "path": "src/mcp/shared/_context.py",
    "content": "\"\"\"Request context for MCP handlers.\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import Any, Generic\n\nfrom typing_extensions import TypeVar\n\nfrom mcp.shared.session import BaseSession\nfrom mcp.types import RequestId, RequestParamsMeta\n\nSessionT = TypeVar(\"SessionT\", bound=BaseSession[Any, Any, Any, Any, Any])\n\n\n@dataclass(kw_only=True)\nclass RequestContext(Generic[SessionT]):\n    \"\"\"Common context for handling incoming requests.\n\n    For request handlers, request_id is always populated.\n    For notification handlers, request_id is None.\n    \"\"\"\n\n    session: SessionT\n    request_id: RequestId | None = None\n    meta: RequestParamsMeta | None = None\n"
  },
  {
    "path": "src/mcp/shared/_httpx_utils.py",
    "content": "\"\"\"Utilities for creating standardized httpx AsyncClient instances.\"\"\"\n\nfrom typing import Any, Protocol\n\nimport httpx\n\n__all__ = [\"create_mcp_http_client\", \"MCP_DEFAULT_TIMEOUT\", \"MCP_DEFAULT_SSE_READ_TIMEOUT\"]\n\n# Default MCP timeout configuration\nMCP_DEFAULT_TIMEOUT = 30.0  # General operations (seconds)\nMCP_DEFAULT_SSE_READ_TIMEOUT = 300.0  # SSE streams - 5 minutes (seconds)\n\n\nclass McpHttpClientFactory(Protocol):  # pragma: no branch\n    def __call__(  # pragma: no branch\n        self,\n        headers: dict[str, str] | None = None,\n        timeout: httpx.Timeout | None = None,\n        auth: httpx.Auth | None = None,\n    ) -> httpx.AsyncClient: ...\n\n\ndef create_mcp_http_client(\n    headers: dict[str, str] | None = None,\n    timeout: httpx.Timeout | None = None,\n    auth: httpx.Auth | None = None,\n) -> httpx.AsyncClient:\n    \"\"\"Create a standardized httpx AsyncClient with MCP defaults.\n\n    This function provides common defaults used throughout the MCP codebase:\n    - follow_redirects=True (always enabled)\n    - Default timeout of 30 seconds if not specified\n\n    Args:\n        headers: Optional headers to include with all requests.\n        timeout: Request timeout as httpx.Timeout object.\n            Defaults to 30 seconds if not specified.\n        auth: Optional authentication handler.\n\n    Returns:\n        Configured httpx.AsyncClient instance with MCP defaults.\n\n    Note:\n        The returned AsyncClient must be used as a context manager to ensure\n        proper cleanup of connections.\n\n    Example:\n        Basic usage with MCP defaults:\n\n        ```python\n        async with create_mcp_http_client() as client:\n            response = await client.get(\"https://api.example.com\")\n        ```\n\n        With custom headers:\n\n        ```python\n        headers = {\"Authorization\": \"Bearer token\"}\n        async with create_mcp_http_client(headers) as client:\n            response = await client.get(\"/endpoint\")\n        ```\n\n        With both custom headers and timeout:\n\n        ```python\n        timeout = httpx.Timeout(60.0, read=300.0)\n        async with create_mcp_http_client(headers, timeout) as client:\n            response = await client.get(\"/long-request\")\n        ```\n\n        With authentication:\n\n        ```python\n        from httpx import BasicAuth\n        auth = BasicAuth(username=\"user\", password=\"pass\")\n        async with create_mcp_http_client(headers, timeout, auth) as client:\n            response = await client.get(\"/protected-endpoint\")\n        ```\n    \"\"\"\n    # Set MCP defaults\n    kwargs: dict[str, Any] = {\"follow_redirects\": True}\n\n    # Handle timeout\n    if timeout is None:\n        kwargs[\"timeout\"] = httpx.Timeout(MCP_DEFAULT_TIMEOUT, read=MCP_DEFAULT_SSE_READ_TIMEOUT)\n    else:\n        kwargs[\"timeout\"] = timeout\n\n    # Handle headers\n    if headers is not None:\n        kwargs[\"headers\"] = headers\n\n    # Handle authentication\n    if auth is not None:  # pragma: no cover\n        kwargs[\"auth\"] = auth\n\n    return httpx.AsyncClient(**kwargs)\n"
  },
  {
    "path": "src/mcp/shared/auth.py",
    "content": "from typing import Any, Literal\n\nfrom pydantic import AnyHttpUrl, AnyUrl, BaseModel, Field, field_validator\n\n\nclass OAuthToken(BaseModel):\n    \"\"\"See https://datatracker.ietf.org/doc/html/rfc6749#section-5.1\"\"\"\n\n    access_token: str\n    token_type: Literal[\"Bearer\"] = \"Bearer\"\n    expires_in: int | None = None\n    scope: str | None = None\n    refresh_token: str | None = None\n\n    @field_validator(\"token_type\", mode=\"before\")\n    @classmethod\n    def normalize_token_type(cls, v: str | None) -> str | None:\n        if isinstance(v, str):\n            # Bearer is title-cased in the spec, so we normalize it\n            # https://datatracker.ietf.org/doc/html/rfc6750#section-4\n            return v.title()\n        return v  # pragma: no cover\n\n\nclass InvalidScopeError(Exception):\n    def __init__(self, message: str):\n        self.message = message\n\n\nclass InvalidRedirectUriError(Exception):\n    def __init__(self, message: str):\n        self.message = message\n\n\nclass OAuthClientMetadata(BaseModel):\n    \"\"\"RFC 7591 OAuth 2.0 Dynamic Client Registration Metadata.\n    See https://datatracker.ietf.org/doc/html/rfc7591#section-2\n    \"\"\"\n\n    redirect_uris: list[AnyUrl] | None = Field(..., min_length=1)\n    # supported auth methods for the token endpoint\n    token_endpoint_auth_method: (\n        Literal[\"none\", \"client_secret_post\", \"client_secret_basic\", \"private_key_jwt\"] | None\n    ) = None\n    # supported grant_types of this implementation\n    grant_types: list[\n        Literal[\"authorization_code\", \"refresh_token\", \"urn:ietf:params:oauth:grant-type:jwt-bearer\"] | str\n    ] = [\n        \"authorization_code\",\n        \"refresh_token\",\n    ]\n    # The MCP spec requires the \"code\" response type, but OAuth\n    # servers may also return additional types they support\n    response_types: list[str] = [\"code\"]\n    scope: str | None = None\n\n    # these fields are currently unused, but we support & store them for potential\n    # future use\n    client_name: str | None = None\n    client_uri: AnyHttpUrl | None = None\n    logo_uri: AnyHttpUrl | None = None\n    contacts: list[str] | None = None\n    tos_uri: AnyHttpUrl | None = None\n    policy_uri: AnyHttpUrl | None = None\n    jwks_uri: AnyHttpUrl | None = None\n    jwks: Any | None = None\n    software_id: str | None = None\n    software_version: str | None = None\n\n    def validate_scope(self, requested_scope: str | None) -> list[str] | None:\n        if requested_scope is None:\n            return None\n        requested_scopes = requested_scope.split(\" \")\n        allowed_scopes = [] if self.scope is None else self.scope.split(\" \")\n        for scope in requested_scopes:\n            if scope not in allowed_scopes:  # pragma: no branch\n                raise InvalidScopeError(f\"Client was not registered with scope {scope}\")\n        return requested_scopes  # pragma: no cover\n\n    def validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl:\n        if redirect_uri is not None:\n            # Validate redirect_uri against client's registered redirect URIs\n            if self.redirect_uris is None or redirect_uri not in self.redirect_uris:\n                raise InvalidRedirectUriError(f\"Redirect URI '{redirect_uri}' not registered for client\")\n            return redirect_uri\n        elif self.redirect_uris is not None and len(self.redirect_uris) == 1:\n            return self.redirect_uris[0]\n        else:\n            raise InvalidRedirectUriError(\"redirect_uri must be specified when client has multiple registered URIs\")\n\n\nclass OAuthClientInformationFull(OAuthClientMetadata):\n    \"\"\"RFC 7591 OAuth 2.0 Dynamic Client Registration full response\n    (client information plus metadata).\n    \"\"\"\n\n    client_id: str | None = None\n    client_secret: str | None = None\n    client_id_issued_at: int | None = None\n    client_secret_expires_at: int | None = None\n\n\nclass OAuthMetadata(BaseModel):\n    \"\"\"RFC 8414 OAuth 2.0 Authorization Server Metadata.\n    See https://datatracker.ietf.org/doc/html/rfc8414#section-2\n    \"\"\"\n\n    issuer: AnyHttpUrl\n    authorization_endpoint: AnyHttpUrl\n    token_endpoint: AnyHttpUrl\n    registration_endpoint: AnyHttpUrl | None = None\n    scopes_supported: list[str] | None = None\n    response_types_supported: list[str] = [\"code\"]\n    response_modes_supported: list[str] | None = None\n    grant_types_supported: list[str] | None = None\n    token_endpoint_auth_methods_supported: list[str] | None = None\n    token_endpoint_auth_signing_alg_values_supported: list[str] | None = None\n    service_documentation: AnyHttpUrl | None = None\n    ui_locales_supported: list[str] | None = None\n    op_policy_uri: AnyHttpUrl | None = None\n    op_tos_uri: AnyHttpUrl | None = None\n    revocation_endpoint: AnyHttpUrl | None = None\n    revocation_endpoint_auth_methods_supported: list[str] | None = None\n    revocation_endpoint_auth_signing_alg_values_supported: list[str] | None = None\n    introspection_endpoint: AnyHttpUrl | None = None\n    introspection_endpoint_auth_methods_supported: list[str] | None = None\n    introspection_endpoint_auth_signing_alg_values_supported: list[str] | None = None\n    code_challenge_methods_supported: list[str] | None = None\n    client_id_metadata_document_supported: bool | None = None\n\n\nclass ProtectedResourceMetadata(BaseModel):\n    \"\"\"RFC 9728 OAuth 2.0 Protected Resource Metadata.\n    See https://datatracker.ietf.org/doc/html/rfc9728#section-2\n    \"\"\"\n\n    resource: AnyHttpUrl\n    authorization_servers: list[AnyHttpUrl] = Field(..., min_length=1)\n    jwks_uri: AnyHttpUrl | None = None\n    scopes_supported: list[str] | None = None\n    bearer_methods_supported: list[str] | None = Field(default=[\"header\"])  # MCP only supports header method\n    resource_signing_alg_values_supported: list[str] | None = None\n    resource_name: str | None = None\n    resource_documentation: AnyHttpUrl | None = None\n    resource_policy_uri: AnyHttpUrl | None = None\n    resource_tos_uri: AnyHttpUrl | None = None\n    # tls_client_certificate_bound_access_tokens default is False, but omitted here for clarity\n    tls_client_certificate_bound_access_tokens: bool | None = None\n    authorization_details_types_supported: list[str] | None = None\n    dpop_signing_alg_values_supported: list[str] | None = None\n    # dpop_bound_access_tokens_required default is False, but omitted here for clarity\n    dpop_bound_access_tokens_required: bool | None = None\n"
  },
  {
    "path": "src/mcp/shared/auth_utils.py",
    "content": "\"\"\"Utilities for OAuth 2.0 Resource Indicators (RFC 8707) and PKCE (RFC 7636).\"\"\"\n\nimport time\nfrom urllib.parse import urlparse, urlsplit, urlunsplit\n\nfrom pydantic import AnyUrl, HttpUrl\n\n\ndef resource_url_from_server_url(url: str | HttpUrl | AnyUrl) -> str:\n    \"\"\"Convert server URL to canonical resource URL per RFC 8707.\n\n    RFC 8707 section 2 states that resource URIs \"MUST NOT include a fragment component\".\n    Returns absolute URI with lowercase scheme/host for canonical form.\n\n    Args:\n        url: Server URL to convert\n\n    Returns:\n        Canonical resource URL string\n    \"\"\"\n    # Convert to string if needed\n    url_str = str(url)\n\n    # Parse the URL and remove fragment, create canonical form\n    parsed = urlsplit(url_str)\n    canonical = urlunsplit(parsed._replace(scheme=parsed.scheme.lower(), netloc=parsed.netloc.lower(), fragment=\"\"))\n\n    return canonical\n\n\ndef check_resource_allowed(requested_resource: str, configured_resource: str) -> bool:\n    \"\"\"Check if a requested resource URL matches a configured resource URL.\n\n    A requested resource matches if it has the same scheme, domain, port,\n    and its path starts with the configured resource's path. This allows\n    hierarchical matching where a token for a parent resource can be used\n    for child resources.\n\n    Args:\n        requested_resource: The resource URL being requested\n        configured_resource: The resource URL that has been configured\n\n    Returns:\n        True if the requested resource matches the configured resource\n    \"\"\"\n    # Parse both URLs\n    requested = urlparse(requested_resource)\n    configured = urlparse(configured_resource)\n\n    # Compare scheme, host, and port (origin)\n    if requested.scheme.lower() != configured.scheme.lower() or requested.netloc.lower() != configured.netloc.lower():\n        return False\n\n    # Normalize trailing slashes before comparison so that\n    # \"/foo\" and \"/foo/\" are treated as equivalent.\n    requested_path = requested.path\n    configured_path = configured.path\n    if not requested_path.endswith(\"/\"):\n        requested_path += \"/\"\n    if not configured_path.endswith(\"/\"):\n        configured_path += \"/\"\n\n    # Check hierarchical match: requested must start with configured path.\n    # The trailing-slash normalization ensures \"/api123/\" won't match \"/api/\".\n    return requested_path.startswith(configured_path)\n\n\ndef calculate_token_expiry(expires_in: int | str | None) -> float | None:\n    \"\"\"Calculate token expiry timestamp from expires_in seconds.\n\n    Args:\n        expires_in: Seconds until token expiration (may be string from some servers)\n\n    Returns:\n        Unix timestamp when token expires, or None if no expiry specified\n    \"\"\"\n    if expires_in is None:\n        return None  # pragma: no cover\n    # Defensive: handle servers that return expires_in as string\n    return time.time() + int(expires_in)\n"
  },
  {
    "path": "src/mcp/shared/exceptions.py",
    "content": "from __future__ import annotations\n\nfrom typing import Any, cast\n\nfrom mcp.types import URL_ELICITATION_REQUIRED, ElicitRequestURLParams, ErrorData, JSONRPCError\n\n\nclass MCPError(Exception):\n    \"\"\"Exception type raised when an error arrives over an MCP connection.\"\"\"\n\n    error: ErrorData\n\n    def __init__(self, code: int, message: str, data: Any = None):\n        super().__init__(code, message, data)\n        if data is not None:\n            self.error = ErrorData(code=code, message=message, data=data)\n        else:\n            self.error = ErrorData(code=code, message=message)\n\n    @property\n    def code(self) -> int:\n        return self.error.code\n\n    @property\n    def message(self) -> str:\n        return self.error.message\n\n    @property\n    def data(self) -> Any:\n        return self.error.data  # pragma: no cover\n\n    @classmethod\n    def from_jsonrpc_error(cls, error: JSONRPCError) -> MCPError:\n        return cls.from_error_data(error.error)\n\n    @classmethod\n    def from_error_data(cls, error: ErrorData) -> MCPError:\n        return cls(code=error.code, message=error.message, data=error.data)\n\n    def __str__(self) -> str:\n        return self.message\n\n\nclass StatelessModeNotSupported(RuntimeError):\n    \"\"\"Raised when attempting to use a method that is not supported in stateless mode.\n\n    Server-to-client requests (sampling, elicitation, list_roots) are not\n    supported in stateless HTTP mode because there is no persistent connection\n    for bidirectional communication.\n    \"\"\"\n\n    def __init__(self, method: str):\n        super().__init__(\n            f\"Cannot use {method} in stateless HTTP mode. \"\n            \"Stateless mode does not support server-to-client requests. \"\n            \"Use stateful mode (stateless_http=False) to enable this feature.\"\n        )\n        self.method = method\n\n\nclass UrlElicitationRequiredError(MCPError):\n    \"\"\"Specialized error for when a tool requires URL mode elicitation(s) before proceeding.\n\n    Servers can raise this error from tool handlers to indicate that the client\n    must complete one or more URL elicitations before the request can be processed.\n\n    Example:\n        ```python\n        raise UrlElicitationRequiredError([\n            ElicitRequestURLParams(\n                message=\"Authorization required for your files\",\n                url=\"https://example.com/oauth/authorize\",\n                elicitation_id=\"auth-001\"\n            )\n        ])\n        ```\n    \"\"\"\n\n    def __init__(self, elicitations: list[ElicitRequestURLParams], message: str | None = None):\n        \"\"\"Initialize UrlElicitationRequiredError.\"\"\"\n        if message is None:\n            message = f\"URL elicitation{'s' if len(elicitations) > 1 else ''} required\"\n\n        self._elicitations = elicitations\n\n        super().__init__(\n            code=URL_ELICITATION_REQUIRED,\n            message=message,\n            data={\"elicitations\": [e.model_dump(by_alias=True, exclude_none=True) for e in elicitations]},\n        )\n\n    @property\n    def elicitations(self) -> list[ElicitRequestURLParams]:\n        \"\"\"The list of URL elicitations required before the request can proceed.\"\"\"\n        return self._elicitations\n\n    @classmethod\n    def from_error(cls, error: ErrorData) -> UrlElicitationRequiredError:\n        \"\"\"Reconstruct from an ErrorData received over the wire.\"\"\"\n        if error.code != URL_ELICITATION_REQUIRED:\n            raise ValueError(f\"Expected error code {URL_ELICITATION_REQUIRED}, got {error.code}\")\n\n        data = cast(dict[str, Any], error.data or {})\n        raw_elicitations = cast(list[dict[str, Any]], data.get(\"elicitations\", []))\n        elicitations = [ElicitRequestURLParams.model_validate(e) for e in raw_elicitations]\n        return cls(elicitations, error.message)\n"
  },
  {
    "path": "src/mcp/shared/experimental/__init__.py",
    "content": "\"\"\"Pure experimental MCP features (no server dependencies).\n\nWARNING: These APIs are experimental and may change without notice.\n\nFor server-integrated experimental features, use mcp.server.experimental.\n\"\"\"\n"
  },
  {
    "path": "src/mcp/shared/experimental/tasks/__init__.py",
    "content": "\"\"\"Pure task state management for MCP.\n\nWARNING: These APIs are experimental and may change without notice.\n\nImport directly from submodules:\n- mcp.shared.experimental.tasks.store.TaskStore\n- mcp.shared.experimental.tasks.context.TaskContext\n- mcp.shared.experimental.tasks.in_memory_task_store.InMemoryTaskStore\n- mcp.shared.experimental.tasks.message_queue.TaskMessageQueue\n- mcp.shared.experimental.tasks.helpers.is_terminal\n\"\"\"\n"
  },
  {
    "path": "src/mcp/shared/experimental/tasks/capabilities.py",
    "content": "\"\"\"Tasks capability checking utilities.\n\nThis module provides functions for checking and requiring task-related\ncapabilities. All tasks capability logic is centralized here to keep\nthe main session code clean.\n\nWARNING: These APIs are experimental and may change without notice.\n\"\"\"\n\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.types import INVALID_REQUEST, ClientCapabilities, ClientTasksCapability\n\n\ndef check_tasks_capability(\n    required: ClientTasksCapability,\n    client: ClientTasksCapability,\n) -> bool:\n    \"\"\"Check if client's tasks capability matches the required capability.\n\n    Args:\n        required: The capability being checked for\n        client: The client's declared capabilities\n\n    Returns:\n        True if client has the required capability, False otherwise\n    \"\"\"\n    if required.requests is None:\n        return True\n    if client.requests is None:\n        return False\n\n    # Check elicitation.create\n    if required.requests.elicitation is not None:\n        if client.requests.elicitation is None:\n            return False\n        if required.requests.elicitation.create is not None:\n            if client.requests.elicitation.create is None:\n                return False\n\n    # Check sampling.createMessage\n    if required.requests.sampling is not None:\n        if client.requests.sampling is None:\n            return False\n        if required.requests.sampling.create_message is not None:\n            if client.requests.sampling.create_message is None:\n                return False\n\n    return True\n\n\ndef has_task_augmented_elicitation(caps: ClientCapabilities) -> bool:\n    \"\"\"Check if capabilities include task-augmented elicitation support.\"\"\"\n    if caps.tasks is None:\n        return False\n    if caps.tasks.requests is None:\n        return False\n    if caps.tasks.requests.elicitation is None:\n        return False\n    return caps.tasks.requests.elicitation.create is not None\n\n\ndef has_task_augmented_sampling(caps: ClientCapabilities) -> bool:\n    \"\"\"Check if capabilities include task-augmented sampling support.\"\"\"\n    if caps.tasks is None:\n        return False\n    if caps.tasks.requests is None:\n        return False\n    if caps.tasks.requests.sampling is None:\n        return False\n    return caps.tasks.requests.sampling.create_message is not None\n\n\ndef require_task_augmented_elicitation(client_caps: ClientCapabilities | None) -> None:\n    \"\"\"Raise MCPError if client doesn't support task-augmented elicitation.\n\n    Args:\n        client_caps: The client's declared capabilities, or None if not initialized\n\n    Raises:\n        MCPError: If client doesn't support task-augmented elicitation\n    \"\"\"\n    if client_caps is None or not has_task_augmented_elicitation(client_caps):\n        raise MCPError(code=INVALID_REQUEST, message=\"Client does not support task-augmented elicitation\")\n\n\ndef require_task_augmented_sampling(client_caps: ClientCapabilities | None) -> None:\n    \"\"\"Raise MCPError if client doesn't support task-augmented sampling.\n\n    Args:\n        client_caps: The client's declared capabilities, or None if not initialized\n\n    Raises:\n        MCPError: If client doesn't support task-augmented sampling\n    \"\"\"\n    if client_caps is None or not has_task_augmented_sampling(client_caps):\n        raise MCPError(code=INVALID_REQUEST, message=\"Client does not support task-augmented sampling\")\n"
  },
  {
    "path": "src/mcp/shared/experimental/tasks/context.py",
    "content": "\"\"\"TaskContext - Pure task state management.\n\nThis module provides TaskContext, which manages task state without any\nserver/session dependencies. It can be used standalone for distributed\nworkers or wrapped by ServerTaskContext for full server integration.\n\"\"\"\n\nfrom mcp.shared.experimental.tasks.store import TaskStore\nfrom mcp.types import TASK_STATUS_COMPLETED, TASK_STATUS_FAILED, Result, Task\n\n\nclass TaskContext:\n    \"\"\"Pure task state management - no session dependencies.\n\n    This class handles:\n    - Task state (status, result)\n    - Cancellation tracking\n    - Store interactions\n\n    For server-integrated features (elicit, create_message, notifications),\n    use ServerTaskContext from mcp.server.experimental.\n\n    Example (distributed worker):\n        async def worker_job(task_id: str):\n            store = RedisTaskStore(redis_url)\n            task = await store.get_task(task_id)\n            ctx = TaskContext(task=task, store=store)\n\n            await ctx.update_status(\"Working...\")\n            result = await do_work()\n            await ctx.complete(result)\n    \"\"\"\n\n    def __init__(self, task: Task, store: TaskStore):\n        self._task = task\n        self._store = store\n        self._cancelled = False\n\n    @property\n    def task_id(self) -> str:\n        \"\"\"The task identifier.\"\"\"\n        return self._task.task_id\n\n    @property\n    def task(self) -> Task:\n        \"\"\"The current task state.\"\"\"\n        return self._task\n\n    @property\n    def is_cancelled(self) -> bool:\n        \"\"\"Whether cancellation has been requested.\"\"\"\n        return self._cancelled\n\n    def request_cancellation(self) -> None:\n        \"\"\"Request cancellation of this task.\n\n        This sets is_cancelled=True. Task work should check this\n        periodically and exit gracefully if set.\n        \"\"\"\n        self._cancelled = True\n\n    async def update_status(self, message: str) -> None:\n        \"\"\"Update the task's status message.\n\n        Args:\n            message: The new status message\n        \"\"\"\n        self._task = await self._store.update_task(\n            self.task_id,\n            status_message=message,\n        )\n\n    async def complete(self, result: Result) -> None:\n        \"\"\"Mark the task as completed with the given result.\n\n        Args:\n            result: The task result\n        \"\"\"\n        await self._store.store_result(self.task_id, result)\n        self._task = await self._store.update_task(\n            self.task_id,\n            status=TASK_STATUS_COMPLETED,\n        )\n\n    async def fail(self, error: str) -> None:\n        \"\"\"Mark the task as failed with an error message.\n\n        Args:\n            error: The error message\n        \"\"\"\n        self._task = await self._store.update_task(\n            self.task_id,\n            status=TASK_STATUS_FAILED,\n            status_message=error,\n        )\n"
  },
  {
    "path": "src/mcp/shared/experimental/tasks/helpers.py",
    "content": "\"\"\"Helper functions for pure task management.\n\nThese helpers work with pure TaskContext and don't require server dependencies.\nFor server-integrated task helpers, use mcp.server.experimental.\n\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom datetime import datetime, timezone\nfrom uuid import uuid4\n\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.experimental.tasks.context import TaskContext\nfrom mcp.shared.experimental.tasks.store import TaskStore\nfrom mcp.types import (\n    INVALID_PARAMS,\n    TASK_STATUS_CANCELLED,\n    TASK_STATUS_COMPLETED,\n    TASK_STATUS_FAILED,\n    TASK_STATUS_WORKING,\n    CancelTaskResult,\n    Task,\n    TaskMetadata,\n    TaskStatus,\n)\n\n# Metadata key for model-immediate-response (per MCP spec)\n# Servers MAY include this in CreateTaskResult._meta to provide an immediate\n# response string while the task executes in the background.\nMODEL_IMMEDIATE_RESPONSE_KEY = \"io.modelcontextprotocol/model-immediate-response\"\n\n# Metadata key for associating requests with a task (per MCP spec)\nRELATED_TASK_METADATA_KEY = \"io.modelcontextprotocol/related-task\"\n\n\ndef is_terminal(status: TaskStatus) -> bool:\n    \"\"\"Check if a task status represents a terminal state.\n\n    Terminal states are those where the task has finished and will not change.\n\n    Args:\n        status: The task status to check\n\n    Returns:\n        True if the status is terminal (completed, failed, or cancelled)\n    \"\"\"\n    return status in (TASK_STATUS_COMPLETED, TASK_STATUS_FAILED, TASK_STATUS_CANCELLED)\n\n\nasync def cancel_task(\n    store: TaskStore,\n    task_id: str,\n) -> CancelTaskResult:\n    \"\"\"Cancel a task with spec-compliant validation.\n\n    Per spec: \"Receivers MUST reject cancellation of terminal status tasks\n    with -32602 (Invalid params)\"\n\n    This helper validates that the task exists and is not in a terminal state\n    before setting it to \"cancelled\".\n\n    Args:\n        store: The task store\n        task_id: The task identifier to cancel\n\n    Returns:\n        CancelTaskResult with the cancelled task state\n\n    Raises:\n        MCPError: With INVALID_PARAMS (-32602) if:\n            - Task does not exist\n            - Task is already in a terminal state (completed, failed, cancelled)\n\n    Example:\n        ```python\n        async def handle_cancel(ctx, params: CancelTaskRequestParams) -> CancelTaskResult:\n            return await cancel_task(store, params.task_id)\n        ```\n    \"\"\"\n    task = await store.get_task(task_id)\n    if task is None:\n        raise MCPError(code=INVALID_PARAMS, message=f\"Task not found: {task_id}\")\n\n    if is_terminal(task.status):\n        raise MCPError(code=INVALID_PARAMS, message=f\"Cannot cancel task in terminal state '{task.status}'\")\n\n    # Update task to cancelled status\n    cancelled_task = await store.update_task(task_id, status=TASK_STATUS_CANCELLED)\n    return CancelTaskResult(**cancelled_task.model_dump())\n\n\ndef generate_task_id() -> str:\n    \"\"\"Generate a unique task ID.\"\"\"\n    return str(uuid4())\n\n\ndef create_task_state(\n    metadata: TaskMetadata,\n    task_id: str | None = None,\n) -> Task:\n    \"\"\"Create a Task object with initial state.\n\n    This is a helper for TaskStore implementations.\n\n    Args:\n        metadata: Task metadata\n        task_id: Optional task ID (generated if not provided)\n\n    Returns:\n        A new Task in \"working\" status\n    \"\"\"\n    now = datetime.now(timezone.utc)\n    return Task(\n        task_id=task_id or generate_task_id(),\n        status=TASK_STATUS_WORKING,\n        created_at=now,\n        last_updated_at=now,\n        ttl=metadata.ttl,\n        poll_interval=500,  # Default 500ms poll interval\n    )\n\n\n@asynccontextmanager\nasync def task_execution(\n    task_id: str,\n    store: TaskStore,\n) -> AsyncIterator[TaskContext]:\n    \"\"\"Context manager for safe task execution (pure, no server dependencies).\n\n    Loads a task from the store and provides a TaskContext for the work.\n    If an unhandled exception occurs, the task is automatically marked as failed\n    and the exception is suppressed (since the failure is captured in task state).\n\n    This is useful for distributed workers that don't have a server session.\n\n    Args:\n        task_id: The task identifier to execute\n        store: The task store (must be accessible by the worker)\n\n    Yields:\n        TaskContext for updating status and completing/failing the task\n\n    Raises:\n        ValueError: If the task is not found in the store\n\n    Example (distributed worker):\n        async def worker_process(task_id: str):\n            store = RedisTaskStore(redis_url)\n            async with task_execution(task_id, store) as ctx:\n                await ctx.update_status(\"Working...\")\n                result = await do_work()\n                await ctx.complete(result)\n    \"\"\"\n    task = await store.get_task(task_id)\n    if task is None:\n        raise ValueError(f\"Task {task_id} not found\")\n\n    ctx = TaskContext(task, store)\n    try:\n        yield ctx\n    except Exception as e:\n        # Auto-fail the task if an exception occurs and task isn't already terminal\n        # Exception is suppressed since failure is captured in task state\n        if not is_terminal(ctx.task.status):\n            await ctx.fail(str(e))\n        # Don't re-raise - the failure is recorded in task state\n"
  },
  {
    "path": "src/mcp/shared/experimental/tasks/in_memory_task_store.py",
    "content": "\"\"\"In-memory implementation of TaskStore for demonstration purposes.\n\nThis implementation stores all tasks in memory and provides automatic cleanup\nbased on the TTL duration specified in the task metadata using lazy expiration.\n\nNote: This is not suitable for production use as all data is lost on restart.\nFor production, consider implementing TaskStore with a database or distributed cache.\n\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timedelta, timezone\n\nimport anyio\n\nfrom mcp.shared.experimental.tasks.helpers import create_task_state, is_terminal\nfrom mcp.shared.experimental.tasks.store import TaskStore\nfrom mcp.types import Result, Task, TaskMetadata, TaskStatus\n\n\n@dataclass\nclass StoredTask:\n    \"\"\"Internal storage representation of a task.\"\"\"\n\n    task: Task\n    result: Result | None = None\n    # Time when this task should be removed (None = never)\n    expires_at: datetime | None = field(default=None)\n\n\nclass InMemoryTaskStore(TaskStore):\n    \"\"\"A simple in-memory implementation of TaskStore.\n\n    Features:\n    - Automatic TTL-based cleanup (lazy expiration)\n    - Thread-safe for single-process async use\n    - Pagination support for list_tasks\n\n    Limitations:\n    - All data lost on restart\n    - Not suitable for distributed systems\n    - No persistence\n\n    For production, implement TaskStore with Redis, PostgreSQL, etc.\n    \"\"\"\n\n    def __init__(self, page_size: int = 10) -> None:\n        self._tasks: dict[str, StoredTask] = {}\n        self._page_size = page_size\n        self._update_events: dict[str, anyio.Event] = {}\n\n    def _calculate_expiry(self, ttl_ms: int | None) -> datetime | None:\n        \"\"\"Calculate expiry time from TTL in milliseconds.\"\"\"\n        if ttl_ms is None:\n            return None\n        return datetime.now(timezone.utc) + timedelta(milliseconds=ttl_ms)\n\n    def _is_expired(self, stored: StoredTask) -> bool:\n        \"\"\"Check if a task has expired.\"\"\"\n        if stored.expires_at is None:\n            return False\n        return datetime.now(timezone.utc) >= stored.expires_at\n\n    def _cleanup_expired(self) -> None:\n        \"\"\"Remove all expired tasks. Called lazily during access operations.\"\"\"\n        expired_ids = [task_id for task_id, stored in self._tasks.items() if self._is_expired(stored)]\n        for task_id in expired_ids:\n            del self._tasks[task_id]\n\n    async def create_task(\n        self,\n        metadata: TaskMetadata,\n        task_id: str | None = None,\n    ) -> Task:\n        \"\"\"Create a new task with the given metadata.\"\"\"\n        # Cleanup expired tasks on access\n        self._cleanup_expired()\n\n        task = create_task_state(metadata, task_id)\n\n        if task.task_id in self._tasks:\n            raise ValueError(f\"Task with ID {task.task_id} already exists\")\n\n        stored = StoredTask(\n            task=task,\n            expires_at=self._calculate_expiry(metadata.ttl),\n        )\n        self._tasks[task.task_id] = stored\n\n        # Return a copy to prevent external modification\n        return Task(**task.model_dump())\n\n    async def get_task(self, task_id: str) -> Task | None:\n        \"\"\"Get a task by ID.\"\"\"\n        # Cleanup expired tasks on access\n        self._cleanup_expired()\n\n        stored = self._tasks.get(task_id)\n        if stored is None:\n            return None\n\n        # Return a copy to prevent external modification\n        return Task(**stored.task.model_dump())\n\n    async def update_task(\n        self,\n        task_id: str,\n        status: TaskStatus | None = None,\n        status_message: str | None = None,\n    ) -> Task:\n        \"\"\"Update a task's status and/or message.\"\"\"\n        stored = self._tasks.get(task_id)\n        if stored is None:\n            raise ValueError(f\"Task with ID {task_id} not found\")\n\n        # Per spec: Terminal states MUST NOT transition to any other status\n        if status is not None and status != stored.task.status and is_terminal(stored.task.status):\n            raise ValueError(f\"Cannot transition from terminal status '{stored.task.status}'\")\n\n        status_changed = False\n        if status is not None and stored.task.status != status:\n            stored.task.status = status\n            status_changed = True\n\n        if status_message is not None:\n            stored.task.status_message = status_message\n\n        # Update last_updated_at on any change\n        stored.task.last_updated_at = datetime.now(timezone.utc)\n\n        # If task is now terminal and has TTL, reset expiry timer\n        if status is not None and is_terminal(status) and stored.task.ttl is not None:\n            stored.expires_at = self._calculate_expiry(stored.task.ttl)\n\n        # Notify waiters if status changed\n        if status_changed:\n            await self.notify_update(task_id)\n\n        return Task(**stored.task.model_dump())\n\n    async def store_result(self, task_id: str, result: Result) -> None:\n        \"\"\"Store the result for a task.\"\"\"\n        stored = self._tasks.get(task_id)\n        if stored is None:\n            raise ValueError(f\"Task with ID {task_id} not found\")\n\n        stored.result = result\n\n    async def get_result(self, task_id: str) -> Result | None:\n        \"\"\"Get the stored result for a task.\"\"\"\n        stored = self._tasks.get(task_id)\n        if stored is None:\n            return None\n\n        return stored.result\n\n    async def list_tasks(\n        self,\n        cursor: str | None = None,\n    ) -> tuple[list[Task], str | None]:\n        \"\"\"List tasks with pagination.\"\"\"\n        # Cleanup expired tasks on access\n        self._cleanup_expired()\n\n        all_task_ids = list(self._tasks.keys())\n\n        start_index = 0\n        if cursor is not None:\n            try:\n                cursor_index = all_task_ids.index(cursor)\n                start_index = cursor_index + 1\n            except ValueError:\n                raise ValueError(f\"Invalid cursor: {cursor}\")\n\n        page_task_ids = all_task_ids[start_index : start_index + self._page_size]\n        tasks = [Task(**self._tasks[tid].task.model_dump()) for tid in page_task_ids]\n\n        # Determine next cursor\n        next_cursor = None\n        if start_index + self._page_size < len(all_task_ids) and page_task_ids:\n            next_cursor = page_task_ids[-1]\n\n        return tasks, next_cursor\n\n    async def delete_task(self, task_id: str) -> bool:\n        \"\"\"Delete a task.\"\"\"\n        if task_id not in self._tasks:\n            return False\n\n        del self._tasks[task_id]\n        return True\n\n    async def wait_for_update(self, task_id: str) -> None:\n        \"\"\"Wait until the task status changes.\"\"\"\n        if task_id not in self._tasks:\n            raise ValueError(f\"Task with ID {task_id} not found\")\n\n        # Create a fresh event for waiting (anyio.Event can't be cleared)\n        self._update_events[task_id] = anyio.Event()\n        event = self._update_events[task_id]\n        await event.wait()\n\n    async def notify_update(self, task_id: str) -> None:\n        \"\"\"Signal that a task has been updated.\"\"\"\n        if task_id in self._update_events:\n            self._update_events[task_id].set()\n\n    # --- Testing/debugging helpers ---\n\n    def cleanup(self) -> None:\n        \"\"\"Cleanup all tasks (useful for testing or graceful shutdown).\"\"\"\n        self._tasks.clear()\n        self._update_events.clear()\n\n    def get_all_tasks(self) -> list[Task]:\n        \"\"\"Get all tasks (useful for debugging). Returns copies to prevent modification.\"\"\"\n        self._cleanup_expired()\n        return [Task(**stored.task.model_dump()) for stored in self._tasks.values()]\n"
  },
  {
    "path": "src/mcp/shared/experimental/tasks/message_queue.py",
    "content": "\"\"\"TaskMessageQueue - FIFO queue for task-related messages.\n\nThis implements the core message queue pattern from the MCP Tasks spec.\nWhen a handler needs to send a request (like elicitation) during a task-augmented\nrequest, the message is enqueued instead of sent directly. Messages are delivered\nto the client only through the `tasks/result` endpoint.\n\nThis pattern enables:\n1. Decoupling request handling from message delivery\n2. Proper bidirectional communication via the tasks/result stream\n3. Automatic status management (working <-> input_required)\n\"\"\"\n\nfrom abc import ABC, abstractmethod\nfrom collections import deque\nfrom dataclasses import dataclass, field\nfrom datetime import datetime, timezone\nfrom typing import Any, Literal\n\nimport anyio\n\nfrom mcp.shared.experimental.tasks.resolver import Resolver\nfrom mcp.types import JSONRPCNotification, JSONRPCRequest, RequestId\n\n\n@dataclass\nclass QueuedMessage:\n    \"\"\"A message queued for delivery via tasks/result.\n\n    Messages are stored with their type and a resolver for requests\n    that expect responses.\n    \"\"\"\n\n    type: Literal[\"request\", \"notification\"]\n    \"\"\"Whether this is a request (expects response) or notification (one-way).\"\"\"\n\n    message: JSONRPCRequest | JSONRPCNotification\n    \"\"\"The JSON-RPC message to send.\"\"\"\n\n    timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))\n    \"\"\"When the message was enqueued.\"\"\"\n\n    resolver: Resolver[dict[str, Any]] | None = None\n    \"\"\"Resolver to set when response arrives (only for requests).\"\"\"\n\n    original_request_id: RequestId | None = None\n    \"\"\"The original request ID used internally, for routing responses back.\"\"\"\n\n\nclass TaskMessageQueue(ABC):\n    \"\"\"Abstract interface for task message queuing.\n\n    This is a FIFO queue that stores messages to be delivered via `tasks/result`.\n    When a task-augmented handler calls elicit() or sends a notification, the\n    message is enqueued here instead of being sent directly to the client.\n\n    The `tasks/result` handler then dequeues and sends these messages through\n    the transport, with `relatedRequestId` set to the tasks/result request ID\n    so responses are routed correctly.\n\n    Implementations can use in-memory storage, Redis, etc.\n    \"\"\"\n\n    @abstractmethod\n    async def enqueue(self, task_id: str, message: QueuedMessage) -> None:\n        \"\"\"Add a message to the queue for a task.\n\n        Args:\n            task_id: The task identifier\n            message: The message to enqueue\n        \"\"\"\n\n    @abstractmethod\n    async def dequeue(self, task_id: str) -> QueuedMessage | None:\n        \"\"\"Remove and return the next message from the queue.\n\n        Args:\n            task_id: The task identifier\n\n        Returns:\n            The next message, or None if queue is empty\n        \"\"\"\n\n    @abstractmethod\n    async def peek(self, task_id: str) -> QueuedMessage | None:\n        \"\"\"Return the next message without removing it.\n\n        Args:\n            task_id: The task identifier\n\n        Returns:\n            The next message, or None if queue is empty\n        \"\"\"\n\n    @abstractmethod\n    async def is_empty(self, task_id: str) -> bool:\n        \"\"\"Check if the queue is empty for a task.\n\n        Args:\n            task_id: The task identifier\n\n        Returns:\n            True if no messages are queued\n        \"\"\"\n\n    @abstractmethod\n    async def clear(self, task_id: str) -> list[QueuedMessage]:\n        \"\"\"Remove and return all messages from the queue.\n\n        This is useful for cleanup when a task is cancelled or completed.\n\n        Args:\n            task_id: The task identifier\n\n        Returns:\n            All queued messages (may be empty)\n        \"\"\"\n\n    @abstractmethod\n    async def wait_for_message(self, task_id: str) -> None:\n        \"\"\"Wait until a message is available in the queue.\n\n        This blocks until either:\n        1. A message is enqueued for this task\n        2. The wait is cancelled\n\n        Args:\n            task_id: The task identifier\n        \"\"\"\n\n    @abstractmethod\n    async def notify_message_available(self, task_id: str) -> None:\n        \"\"\"Signal that a message is available for a task.\n\n        This wakes up any coroutines waiting in wait_for_message().\n\n        Args:\n            task_id: The task identifier\n        \"\"\"\n\n\nclass InMemoryTaskMessageQueue(TaskMessageQueue):\n    \"\"\"In-memory implementation of TaskMessageQueue.\n\n    This is suitable for single-process servers. For distributed systems,\n    implement TaskMessageQueue with Redis, RabbitMQ, etc.\n\n    Features:\n    - FIFO ordering per task\n    - Async wait for message availability\n    - Thread-safe for single-process async use\n    \"\"\"\n\n    def __init__(self) -> None:\n        self._queues: dict[str, deque[QueuedMessage]] = {}\n        self._events: dict[str, anyio.Event] = {}\n\n    def _get_queue(self, task_id: str) -> deque[QueuedMessage]:\n        \"\"\"Get or create the queue for a task.\"\"\"\n        if task_id not in self._queues:\n            self._queues[task_id] = deque()\n        return self._queues[task_id]\n\n    async def enqueue(self, task_id: str, message: QueuedMessage) -> None:\n        \"\"\"Add a message to the queue.\"\"\"\n        queue = self._get_queue(task_id)\n        queue.append(message)\n        # Signal that a message is available\n        await self.notify_message_available(task_id)\n\n    async def dequeue(self, task_id: str) -> QueuedMessage | None:\n        \"\"\"Remove and return the next message.\"\"\"\n        queue = self._get_queue(task_id)\n        if not queue:\n            return None\n        return queue.popleft()\n\n    async def peek(self, task_id: str) -> QueuedMessage | None:\n        \"\"\"Return the next message without removing it.\"\"\"\n        queue = self._get_queue(task_id)\n        if not queue:\n            return None\n        return queue[0]\n\n    async def is_empty(self, task_id: str) -> bool:\n        \"\"\"Check if the queue is empty.\"\"\"\n        queue = self._get_queue(task_id)\n        return len(queue) == 0\n\n    async def clear(self, task_id: str) -> list[QueuedMessage]:\n        \"\"\"Remove and return all messages.\"\"\"\n        queue = self._get_queue(task_id)\n        messages = list(queue)\n        queue.clear()\n        return messages\n\n    async def wait_for_message(self, task_id: str) -> None:\n        \"\"\"Wait until a message is available.\"\"\"\n        # Check if there are already messages\n        if not await self.is_empty(task_id):\n            return\n\n        # Create a fresh event for waiting (anyio.Event can't be cleared)\n        self._events[task_id] = anyio.Event()\n        event = self._events[task_id]\n\n        # Double-check after creating event (avoid race condition)\n        if not await self.is_empty(task_id):\n            return\n\n        # Wait for a new message\n        await event.wait()\n\n    async def notify_message_available(self, task_id: str) -> None:\n        \"\"\"Signal that a message is available.\"\"\"\n        if task_id in self._events:\n            self._events[task_id].set()\n\n    def cleanup(self, task_id: str | None = None) -> None:\n        \"\"\"Clean up queues and events.\n\n        Args:\n            task_id: If provided, clean up only this task. Otherwise clean up all.\n        \"\"\"\n        if task_id is not None:\n            self._queues.pop(task_id, None)\n            self._events.pop(task_id, None)\n        else:\n            self._queues.clear()\n            self._events.clear()\n"
  },
  {
    "path": "src/mcp/shared/experimental/tasks/polling.py",
    "content": "\"\"\"Shared polling utilities for task operations.\n\nThis module provides generic polling logic that works for both client→server\nand server→client task polling.\n\nWARNING: These APIs are experimental and may change without notice.\n\"\"\"\n\nfrom collections.abc import AsyncIterator, Awaitable, Callable\n\nimport anyio\n\nfrom mcp.shared.experimental.tasks.helpers import is_terminal\nfrom mcp.types import GetTaskResult\n\n\nasync def poll_until_terminal(\n    get_task: Callable[[str], Awaitable[GetTaskResult]],\n    task_id: str,\n    default_interval_ms: int = 500,\n) -> AsyncIterator[GetTaskResult]:\n    \"\"\"Poll a task until it reaches terminal status.\n\n    This is a generic utility that works for both client→server and server→client\n    polling. The caller provides the get_task function appropriate for their direction.\n\n    Args:\n        get_task: Async function that takes task_id and returns GetTaskResult\n        task_id: The task to poll\n        default_interval_ms: Fallback poll interval if server doesn't specify\n\n    Yields:\n        GetTaskResult for each poll\n    \"\"\"\n    while True:\n        status = await get_task(task_id)\n        yield status\n\n        if is_terminal(status.status):\n            break\n\n        interval_ms = status.poll_interval if status.poll_interval is not None else default_interval_ms\n        await anyio.sleep(interval_ms / 1000)\n"
  },
  {
    "path": "src/mcp/shared/experimental/tasks/resolver.py",
    "content": "\"\"\"Resolver - An anyio-compatible future-like object for async result passing.\n\nThis provides a simple way to pass a result (or exception) from one coroutine\nto another without depending on asyncio.Future.\n\"\"\"\n\nfrom typing import Generic, TypeVar, cast\n\nimport anyio\n\nT = TypeVar(\"T\")\n\n\nclass Resolver(Generic[T]):\n    \"\"\"A simple resolver for passing results between coroutines.\n\n    Unlike asyncio.Future, this works with any anyio-compatible async backend.\n\n    Usage:\n        resolver: Resolver[str] = Resolver()\n\n        # In one coroutine:\n        resolver.set_result(\"hello\")\n\n        # In another coroutine:\n        result = await resolver.wait()  # returns \"hello\"\n    \"\"\"\n\n    def __init__(self) -> None:\n        self._event = anyio.Event()\n        self._value: T | None = None\n        self._exception: BaseException | None = None\n\n    def set_result(self, value: T) -> None:\n        \"\"\"Set the result value and wake up waiters.\"\"\"\n        if self._event.is_set():\n            raise RuntimeError(\"Resolver already completed\")\n        self._value = value\n        self._event.set()\n\n    def set_exception(self, exc: BaseException) -> None:\n        \"\"\"Set an exception and wake up waiters.\"\"\"\n        if self._event.is_set():\n            raise RuntimeError(\"Resolver already completed\")\n        self._exception = exc\n        self._event.set()\n\n    async def wait(self) -> T:\n        \"\"\"Wait for the result and return it, or raise the exception.\"\"\"\n        await self._event.wait()\n        if self._exception is not None:\n            raise self._exception\n        # If we reach here, set_result() was called, so _value is set\n        return cast(T, self._value)\n\n    def done(self) -> bool:\n        \"\"\"Return True if the resolver has been completed.\"\"\"\n        return self._event.is_set()\n"
  },
  {
    "path": "src/mcp/shared/experimental/tasks/store.py",
    "content": "\"\"\"TaskStore - Abstract interface for task state storage.\"\"\"\n\nfrom abc import ABC, abstractmethod\n\nfrom mcp.types import Result, Task, TaskMetadata, TaskStatus\n\n\nclass TaskStore(ABC):\n    \"\"\"Abstract interface for task state storage.\n\n    This is a pure storage interface - it doesn't manage execution.\n    Implementations can use in-memory storage, databases, Redis, etc.\n\n    All methods are async to support various backends.\n    \"\"\"\n\n    @abstractmethod\n    async def create_task(\n        self,\n        metadata: TaskMetadata,\n        task_id: str | None = None,\n    ) -> Task:\n        \"\"\"Create a new task.\n\n        Args:\n            metadata: Task metadata (ttl, etc.)\n            task_id: Optional task ID. If None, implementation should generate one.\n\n        Returns:\n            The created Task with status=\"working\"\n\n        Raises:\n            ValueError: If task_id already exists\n        \"\"\"\n\n    @abstractmethod\n    async def get_task(self, task_id: str) -> Task | None:\n        \"\"\"Get a task by ID.\n\n        Args:\n            task_id: The task identifier\n\n        Returns:\n            The Task, or None if not found\n        \"\"\"\n\n    @abstractmethod\n    async def update_task(\n        self,\n        task_id: str,\n        status: TaskStatus | None = None,\n        status_message: str | None = None,\n    ) -> Task:\n        \"\"\"Update a task's status and/or message.\n\n        Args:\n            task_id: The task identifier\n            status: New status (if changing)\n            status_message: New status message (if changing)\n\n        Returns:\n            The updated Task\n\n        Raises:\n            ValueError: If task not found\n            ValueError: If attempting to transition from a terminal status\n                (completed, failed, cancelled). Per spec, terminal states\n                MUST NOT transition to any other status.\n        \"\"\"\n\n    @abstractmethod\n    async def store_result(self, task_id: str, result: Result) -> None:\n        \"\"\"Store the result for a task.\n\n        Args:\n            task_id: The task identifier\n            result: The result to store\n\n        Raises:\n            ValueError: If task not found\n        \"\"\"\n\n    @abstractmethod\n    async def get_result(self, task_id: str) -> Result | None:\n        \"\"\"Get the stored result for a task.\n\n        Args:\n            task_id: The task identifier\n\n        Returns:\n            The stored Result, or None if not available\n        \"\"\"\n\n    @abstractmethod\n    async def list_tasks(\n        self,\n        cursor: str | None = None,\n    ) -> tuple[list[Task], str | None]:\n        \"\"\"List tasks with pagination.\n\n        Args:\n            cursor: Optional cursor for pagination\n\n        Returns:\n            Tuple of (tasks, next_cursor). next_cursor is None if no more pages.\n        \"\"\"\n\n    @abstractmethod\n    async def delete_task(self, task_id: str) -> bool:\n        \"\"\"Delete a task.\n\n        Args:\n            task_id: The task identifier\n\n        Returns:\n            True if deleted, False if not found\n        \"\"\"\n\n    @abstractmethod\n    async def wait_for_update(self, task_id: str) -> None:\n        \"\"\"Wait until the task status changes.\n\n        This blocks until either:\n        1. The task status changes\n        2. The wait is cancelled\n\n        Used by tasks/result to wait for task completion or status changes.\n\n        Args:\n            task_id: The task identifier\n\n        Raises:\n            ValueError: If task not found\n        \"\"\"\n\n    @abstractmethod\n    async def notify_update(self, task_id: str) -> None:\n        \"\"\"Signal that a task has been updated.\n\n        This wakes up any coroutines waiting in wait_for_update().\n\n        Args:\n            task_id: The task identifier\n        \"\"\"\n"
  },
  {
    "path": "src/mcp/shared/memory.py",
    "content": "\"\"\"In-memory transports\"\"\"\n\nfrom __future__ import annotations\n\nfrom collections.abc import AsyncGenerator\nfrom contextlib import asynccontextmanager\n\nimport anyio\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\n\nfrom mcp.shared.message import SessionMessage\n\nMessageStream = tuple[MemoryObjectReceiveStream[SessionMessage | Exception], MemoryObjectSendStream[SessionMessage]]\n\n\n@asynccontextmanager\nasync def create_client_server_memory_streams() -> AsyncGenerator[tuple[MessageStream, MessageStream], None]:\n    \"\"\"Creates a pair of bidirectional memory streams for client-server communication.\n\n    Yields:\n        A tuple of (client_streams, server_streams) where each is a tuple of\n        (read_stream, write_stream)\n    \"\"\"\n    # Create streams for both directions\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1)\n\n    client_streams = (server_to_client_receive, client_to_server_send)\n    server_streams = (client_to_server_receive, server_to_client_send)\n\n    async with server_to_client_receive, client_to_server_send, client_to_server_receive, server_to_client_send:\n        yield client_streams, server_streams\n"
  },
  {
    "path": "src/mcp/shared/message.py",
    "content": "\"\"\"Message wrapper with metadata support.\n\nThis module defines a wrapper type that combines JSONRPCMessage with metadata\nto support transport-specific features like resumability.\n\"\"\"\n\nfrom collections.abc import Awaitable, Callable\nfrom dataclasses import dataclass\nfrom typing import Any\n\nfrom mcp.types import JSONRPCMessage, RequestId\n\nResumptionToken = str\n\nResumptionTokenUpdateCallback = Callable[[ResumptionToken], Awaitable[None]]\n\n# Callback type for closing SSE streams without terminating\nCloseSSEStreamCallback = Callable[[], Awaitable[None]]\n\n\n@dataclass\nclass ClientMessageMetadata:\n    \"\"\"Metadata specific to client messages.\"\"\"\n\n    resumption_token: ResumptionToken | None = None\n    on_resumption_token_update: Callable[[ResumptionToken], Awaitable[None]] | None = None\n\n\n@dataclass\nclass ServerMessageMetadata:\n    \"\"\"Metadata specific to server messages.\"\"\"\n\n    related_request_id: RequestId | None = None\n    # Transport-specific request context (e.g. starlette Request for HTTP\n    # transports, None for stdio). Typed as Any because the server layer is\n    # transport-agnostic.\n    request_context: Any = None\n    # Callback to close SSE stream for the current request without terminating\n    close_sse_stream: CloseSSEStreamCallback | None = None\n    # Callback to close the standalone GET SSE stream (for unsolicited notifications)\n    close_standalone_sse_stream: CloseSSEStreamCallback | None = None\n\n\nMessageMetadata = ClientMessageMetadata | ServerMessageMetadata | None\n\n\n@dataclass\nclass SessionMessage:\n    \"\"\"A message with specific metadata for transport-specific features.\"\"\"\n\n    message: JSONRPCMessage\n    metadata: MessageMetadata = None\n"
  },
  {
    "path": "src/mcp/shared/metadata_utils.py",
    "content": "\"\"\"Utility functions for working with metadata in MCP types.\n\nThese utilities are primarily intended for client-side usage to properly display\nhuman-readable names in user interfaces in a spec-compliant way.\n\"\"\"\n\nfrom mcp.types import Implementation, Prompt, Resource, ResourceTemplate, Tool\n\n\ndef get_display_name(obj: Tool | Resource | Prompt | ResourceTemplate | Implementation) -> str:\n    \"\"\"Get the display name for an MCP object with proper precedence.\n\n    This is a client-side utility function designed to help MCP clients display\n    human-readable names in their user interfaces. When servers provide a 'title'\n    field, it should be preferred over the programmatic 'name' field for display.\n\n    For tools: title > annotations.title > name\n    For other objects: title > name\n\n    Example:\n        ```python\n        # In a client displaying available tools\n        tools = await session.list_tools()\n        for tool in tools.tools:\n            display_name = get_display_name(tool)\n            print(f\"Available tool: {display_name}\")\n        ```\n\n    Args:\n        obj: An MCP object with name and optional title fields\n\n    Returns:\n        The display name to use for UI presentation\n    \"\"\"\n    if isinstance(obj, Tool):\n        # Tools have special precedence: title > annotations.title > name\n        if hasattr(obj, \"title\") and obj.title is not None:\n            return obj.title\n        if obj.annotations and hasattr(obj.annotations, \"title\") and obj.annotations.title is not None:\n            return obj.annotations.title\n        return obj.name\n    else:\n        # All other objects: title > name\n        if hasattr(obj, \"title\") and obj.title is not None:\n            return obj.title\n        return obj.name\n"
  },
  {
    "path": "src/mcp/shared/response_router.py",
    "content": "\"\"\"ResponseRouter - Protocol for pluggable response routing.\n\nThis module defines a protocol for routing JSON-RPC responses to alternative\nhandlers before falling back to the default response stream mechanism.\n\nThe primary use case is task-augmented requests: when a TaskSession enqueues\na request (like elicitation), the response needs to be routed back to the\nwaiting resolver instead of the normal response stream.\n\nDesign:\n- Protocol-based for testability and flexibility\n- Returns bool to indicate if response was handled\n- Supports both success responses and errors\n\"\"\"\n\nfrom typing import Any, Protocol\n\nfrom mcp.types import ErrorData, RequestId\n\n\nclass ResponseRouter(Protocol):\n    \"\"\"Protocol for routing responses to alternative handlers.\n\n    Implementations check if they have a pending request for the given ID\n    and deliver the response/error to the appropriate handler.\n\n    Example:\n        ```python\n        class TaskResultHandler(ResponseRouter):\n            def route_response(self, request_id, response):\n                resolver = self._pending_requests.pop(request_id, None)\n                if resolver:\n                    resolver.set_result(response)\n                    return True\n                return False\n        ```\n    \"\"\"\n\n    def route_response(self, request_id: RequestId, response: dict[str, Any]) -> bool:\n        \"\"\"Try to route a response to a pending request handler.\n\n        Args:\n            request_id: The JSON-RPC request ID from the response\n            response: The response result data\n\n        Returns:\n            True if the response was handled, False otherwise\n        \"\"\"\n        ...  # pragma: no cover\n\n    def route_error(self, request_id: RequestId, error: ErrorData) -> bool:\n        \"\"\"Try to route an error to a pending request handler.\n\n        Args:\n            request_id: The JSON-RPC request ID from the error response\n            error: The error data\n\n        Returns:\n            True if the error was handled, False otherwise\n        \"\"\"\n        ...  # pragma: no cover\n"
  },
  {
    "path": "src/mcp/shared/session.py",
    "content": "from __future__ import annotations\n\nimport logging\nfrom collections.abc import Callable\nfrom contextlib import AsyncExitStack\nfrom types import TracebackType\nfrom typing import Any, Generic, Protocol, TypeVar\n\nimport anyio\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\nfrom pydantic import BaseModel, TypeAdapter\nfrom typing_extensions import Self\n\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.message import MessageMetadata, ServerMessageMetadata, SessionMessage\nfrom mcp.shared.response_router import ResponseRouter\nfrom mcp.types import (\n    CONNECTION_CLOSED,\n    INVALID_PARAMS,\n    REQUEST_TIMEOUT,\n    CancelledNotification,\n    ClientNotification,\n    ClientRequest,\n    ClientResult,\n    ErrorData,\n    JSONRPCError,\n    JSONRPCNotification,\n    JSONRPCRequest,\n    JSONRPCResponse,\n    ProgressNotification,\n    ProgressToken,\n    RequestParamsMeta,\n    ServerNotification,\n    ServerRequest,\n    ServerResult,\n)\n\nSendRequestT = TypeVar(\"SendRequestT\", ClientRequest, ServerRequest)\nSendResultT = TypeVar(\"SendResultT\", ClientResult, ServerResult)\nSendNotificationT = TypeVar(\"SendNotificationT\", ClientNotification, ServerNotification)\nReceiveRequestT = TypeVar(\"ReceiveRequestT\", ClientRequest, ServerRequest)\nReceiveResultT = TypeVar(\"ReceiveResultT\", bound=BaseModel)\nReceiveNotificationT = TypeVar(\"ReceiveNotificationT\", ClientNotification, ServerNotification)\n\nRequestId = str | int\n\n\nclass ProgressFnT(Protocol):\n    \"\"\"Protocol for progress notification callbacks.\"\"\"\n\n    async def __call__(\n        self, progress: float, total: float | None, message: str | None\n    ) -> None: ...  # pragma: no branch\n\n\nclass RequestResponder(Generic[ReceiveRequestT, SendResultT]):\n    \"\"\"Handles responding to MCP requests and manages request lifecycle.\n\n    This class MUST be used as a context manager to ensure proper cleanup and\n    cancellation handling:\n\n    Example:\n        ```python\n        with request_responder as resp:\n            await resp.respond(result)\n        ```\n\n    The context manager ensures:\n    1. Proper cancellation scope setup and cleanup\n    2. Request completion tracking\n    3. Cleanup of in-flight requests\n    \"\"\"\n\n    def __init__(\n        self,\n        request_id: RequestId,\n        request_meta: RequestParamsMeta | None,\n        request: ReceiveRequestT,\n        session: BaseSession[SendRequestT, SendNotificationT, SendResultT, ReceiveRequestT, ReceiveNotificationT],\n        on_complete: Callable[[RequestResponder[ReceiveRequestT, SendResultT]], Any],\n        message_metadata: MessageMetadata = None,\n    ) -> None:\n        self.request_id = request_id\n        self.request_meta = request_meta\n        self.request = request\n        self.message_metadata = message_metadata\n        self._session = session\n        self._completed = False\n        self._cancel_scope = anyio.CancelScope()\n        self._on_complete = on_complete\n        self._entered = False  # Track if we're in a context manager\n\n    def __enter__(self) -> RequestResponder[ReceiveRequestT, SendResultT]:\n        \"\"\"Enter the context manager, enabling request cancellation tracking.\"\"\"\n        self._entered = True\n        self._cancel_scope = anyio.CancelScope()\n        self._cancel_scope.__enter__()\n        return self\n\n    def __exit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_val: BaseException | None,\n        exc_tb: TracebackType | None,\n    ) -> None:\n        \"\"\"Exit the context manager, performing cleanup and notifying completion.\"\"\"\n        try:\n            if self._completed:  # pragma: no branch\n                self._on_complete(self)\n        finally:\n            self._entered = False\n            if not self._cancel_scope:  # pragma: no cover\n                raise RuntimeError(\"No active cancel scope\")\n            self._cancel_scope.__exit__(exc_type, exc_val, exc_tb)\n\n    async def respond(self, response: SendResultT | ErrorData) -> None:\n        \"\"\"Send a response for this request.\n\n        Must be called within a context manager block.\n\n        Raises:\n            RuntimeError: If not used within a context manager\n            AssertionError: If request was already responded to\n        \"\"\"\n        if not self._entered:  # pragma: no cover\n            raise RuntimeError(\"RequestResponder must be used as a context manager\")\n        assert not self._completed, \"Request already responded to\"\n\n        if not self.cancelled:  # pragma: no branch\n            self._completed = True\n\n            await self._session._send_response(  # type: ignore[reportPrivateUsage]\n                request_id=self.request_id, response=response\n            )\n\n    async def cancel(self) -> None:\n        \"\"\"Cancel this request and mark it as completed.\"\"\"\n        if not self._entered:  # pragma: no cover\n            raise RuntimeError(\"RequestResponder must be used as a context manager\")\n        if not self._cancel_scope:  # pragma: no cover\n            raise RuntimeError(\"No active cancel scope\")\n\n        self._cancel_scope.cancel()\n        self._completed = True  # Mark as completed so it's removed from in_flight\n        # Send an error response to indicate cancellation\n        await self._session._send_response(  # type: ignore[reportPrivateUsage]\n            request_id=self.request_id,\n            response=ErrorData(code=0, message=\"Request cancelled\"),\n        )\n\n    @property\n    def in_flight(self) -> bool:  # pragma: no cover\n        return not self._completed and not self.cancelled\n\n    @property\n    def cancelled(self) -> bool:\n        return self._cancel_scope.cancel_called\n\n\nclass BaseSession(\n    Generic[\n        SendRequestT,\n        SendNotificationT,\n        SendResultT,\n        ReceiveRequestT,\n        ReceiveNotificationT,\n    ],\n):\n    \"\"\"Implements an MCP \"session\" on top of read/write streams, including features\n    like request/response linking, notifications, and progress.\n\n    This class is an async context manager that automatically starts processing\n    messages when entered.\n    \"\"\"\n\n    _response_streams: dict[RequestId, MemoryObjectSendStream[JSONRPCResponse | JSONRPCError]]\n    _request_id: int\n    _in_flight: dict[RequestId, RequestResponder[ReceiveRequestT, SendResultT]]\n    _progress_callbacks: dict[RequestId, ProgressFnT]\n    _response_routers: list[ResponseRouter]\n\n    def __init__(\n        self,\n        read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],\n        write_stream: MemoryObjectSendStream[SessionMessage],\n        # If none, reading will never time out\n        read_timeout_seconds: float | None = None,\n    ) -> None:\n        self._read_stream = read_stream\n        self._write_stream = write_stream\n        self._response_streams = {}\n        self._request_id = 0\n        self._session_read_timeout_seconds = read_timeout_seconds\n        self._in_flight = {}\n        self._progress_callbacks = {}\n        self._response_routers = []\n        self._exit_stack = AsyncExitStack()\n\n    def add_response_router(self, router: ResponseRouter) -> None:\n        \"\"\"Register a response router to handle responses for non-standard requests.\n\n        Response routers are checked in order before falling back to the default\n        response stream mechanism. This is used by TaskResultHandler to route\n        responses for queued task requests back to their resolvers.\n\n        !!! warning\n            This is an experimental API that may change without notice.\n\n        Args:\n            router: A ResponseRouter implementation\n        \"\"\"\n        self._response_routers.append(router)\n\n    async def __aenter__(self) -> Self:\n        self._task_group = anyio.create_task_group()\n        await self._task_group.__aenter__()\n        self._task_group.start_soon(self._receive_loop)\n        return self\n\n    async def __aexit__(\n        self,\n        exc_type: type[BaseException] | None,\n        exc_val: BaseException | None,\n        exc_tb: TracebackType | None,\n    ) -> bool | None:\n        await self._exit_stack.aclose()\n        # Using BaseSession as a context manager should not block on exit (this\n        # would be very surprising behavior), so make sure to cancel the tasks\n        # in the task group.\n        self._task_group.cancel_scope.cancel()\n        return await self._task_group.__aexit__(exc_type, exc_val, exc_tb)\n\n    async def send_request(\n        self,\n        request: SendRequestT,\n        result_type: type[ReceiveResultT],\n        request_read_timeout_seconds: float | None = None,\n        metadata: MessageMetadata = None,\n        progress_callback: ProgressFnT | None = None,\n    ) -> ReceiveResultT:\n        \"\"\"Sends a request and waits for a response.\n\n        Raises an MCPError if the response contains an error. If a request read timeout is provided, it will take\n        precedence over the session read timeout.\n\n        Do not use this method to emit notifications! Use send_notification() instead.\n        \"\"\"\n        request_id = self._request_id\n        self._request_id = request_id + 1\n\n        response_stream, response_stream_reader = anyio.create_memory_object_stream[JSONRPCResponse | JSONRPCError](1)\n        self._response_streams[request_id] = response_stream\n\n        # Set up progress token if progress callback is provided\n        request_data = request.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n        if progress_callback is not None:\n            # Use request_id as progress token\n            if \"params\" not in request_data:  # pragma: lax no cover\n                request_data[\"params\"] = {}\n            if \"_meta\" not in request_data[\"params\"]:  # pragma: lax no cover\n                request_data[\"params\"][\"_meta\"] = {}\n            request_data[\"params\"][\"_meta\"][\"progressToken\"] = request_id\n            # Store the callback for this request\n            self._progress_callbacks[request_id] = progress_callback\n\n        try:\n            jsonrpc_request = JSONRPCRequest(jsonrpc=\"2.0\", id=request_id, **request_data)\n            await self._write_stream.send(SessionMessage(message=jsonrpc_request, metadata=metadata))\n\n            # request read timeout takes precedence over session read timeout\n            timeout = request_read_timeout_seconds or self._session_read_timeout_seconds\n\n            try:\n                with anyio.fail_after(timeout):\n                    response_or_error = await response_stream_reader.receive()\n            except TimeoutError:\n                class_name = request.__class__.__name__\n                message = f\"Timed out while waiting for response to {class_name}. Waited {timeout} seconds.\"\n                raise MCPError(code=REQUEST_TIMEOUT, message=message)\n\n            if isinstance(response_or_error, JSONRPCError):\n                raise MCPError.from_jsonrpc_error(response_or_error)\n            else:\n                return result_type.model_validate(response_or_error.result, by_name=False)\n\n        finally:\n            self._response_streams.pop(request_id, None)\n            self._progress_callbacks.pop(request_id, None)\n            await response_stream.aclose()\n            await response_stream_reader.aclose()\n\n    async def send_notification(\n        self,\n        notification: SendNotificationT,\n        related_request_id: RequestId | None = None,\n    ) -> None:\n        \"\"\"Emits a notification, which is a one-way message that does not expect a response.\"\"\"\n        # Some transport implementations may need to set the related_request_id\n        # to attribute to the notifications to the request that triggered them.\n        jsonrpc_notification = JSONRPCNotification(\n            jsonrpc=\"2.0\",\n            **notification.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n        )\n        session_message = SessionMessage(\n            message=jsonrpc_notification,\n            metadata=ServerMessageMetadata(related_request_id=related_request_id) if related_request_id else None,\n        )\n        await self._write_stream.send(session_message)\n\n    async def _send_response(self, request_id: RequestId, response: SendResultT | ErrorData) -> None:\n        if isinstance(response, ErrorData):\n            jsonrpc_error = JSONRPCError(jsonrpc=\"2.0\", id=request_id, error=response)\n            session_message = SessionMessage(message=jsonrpc_error)\n            await self._write_stream.send(session_message)\n        else:\n            jsonrpc_response = JSONRPCResponse(\n                jsonrpc=\"2.0\",\n                id=request_id,\n                result=response.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n            )\n            session_message = SessionMessage(message=jsonrpc_response)\n            await self._write_stream.send(session_message)\n\n    @property\n    def _receive_request_adapter(self) -> TypeAdapter[ReceiveRequestT]:\n        \"\"\"Each subclass must provide its own request adapter.\"\"\"\n        raise NotImplementedError\n\n    @property\n    def _receive_notification_adapter(self) -> TypeAdapter[ReceiveNotificationT]:\n        raise NotImplementedError\n\n    async def _receive_loop(self) -> None:\n        async with self._read_stream, self._write_stream:\n            try:\n                async for message in self._read_stream:\n                    if isinstance(message, Exception):\n                        await self._handle_incoming(message)\n                    elif isinstance(message.message, JSONRPCRequest):\n                        try:\n                            validated_request = self._receive_request_adapter.validate_python(\n                                message.message.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                                by_name=False,\n                            )\n                            responder = RequestResponder(\n                                request_id=message.message.id,\n                                request_meta=validated_request.params.meta if validated_request.params else None,\n                                request=validated_request,\n                                session=self,\n                                on_complete=lambda r: self._in_flight.pop(r.request_id, None),\n                                message_metadata=message.metadata,\n                            )\n                            self._in_flight[responder.request_id] = responder\n                            await self._received_request(responder)\n\n                            if not responder._completed:  # type: ignore[reportPrivateUsage]\n                                await self._handle_incoming(responder)\n                        except Exception:\n                            # For request validation errors, send a proper JSON-RPC error\n                            # response instead of crashing the server\n                            logging.warning(\"Failed to validate request\", exc_info=True)\n                            logging.debug(f\"Message that failed validation: {message.message}\")\n                            error_response = JSONRPCError(\n                                jsonrpc=\"2.0\",\n                                id=message.message.id,\n                                error=ErrorData(code=INVALID_PARAMS, message=\"Invalid request parameters\", data=\"\"),\n                            )\n                            session_message = SessionMessage(message=error_response)\n                            await self._write_stream.send(session_message)\n\n                    elif isinstance(message.message, JSONRPCNotification):\n                        try:\n                            notification = self._receive_notification_adapter.validate_python(\n                                message.message.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                                by_name=False,\n                            )\n                            # Handle cancellation notifications\n                            if isinstance(notification, CancelledNotification):\n                                cancelled_id = notification.params.request_id\n                                if cancelled_id in self._in_flight:  # pragma: no branch\n                                    await self._in_flight[cancelled_id].cancel()\n                            else:\n                                # Handle progress notifications callback\n                                if isinstance(notification, ProgressNotification):\n                                    progress_token = notification.params.progress_token\n                                    # If there is a progress callback for this token,\n                                    # call it with the progress information\n                                    if progress_token in self._progress_callbacks:\n                                        callback = self._progress_callbacks[progress_token]\n                                        try:\n                                            await callback(\n                                                notification.params.progress,\n                                                notification.params.total,\n                                                notification.params.message,\n                                            )\n                                        except Exception:\n                                            logging.exception(\"Progress callback raised an exception\")\n                                await self._received_notification(notification)\n                                await self._handle_incoming(notification)\n                        except Exception:\n                            # For other validation errors, log and continue\n                            logging.warning(  # pragma: no cover\n                                f\"Failed to validate notification:. Message was: {message.message}\",\n                                exc_info=True,\n                            )\n                    else:  # Response or error\n                        await self._handle_response(message)\n\n            except anyio.ClosedResourceError:\n                # This is expected when the client disconnects abruptly.\n                # Without this handler, the exception would propagate up and\n                # crash the server's task group.\n                logging.debug(\"Read stream closed by client\")\n            except Exception as e:\n                # Other exceptions are not expected and should be logged. We purposefully\n                # catch all exceptions here to avoid crashing the server.\n                logging.exception(f\"Unhandled exception in receive loop: {e}\")  # pragma: no cover\n            finally:\n                # after the read stream is closed, we need to send errors\n                # to any pending requests\n                for id, stream in self._response_streams.items():\n                    error = ErrorData(code=CONNECTION_CLOSED, message=\"Connection closed\")\n                    try:\n                        await stream.send(JSONRPCError(jsonrpc=\"2.0\", id=id, error=error))\n                        await stream.aclose()\n                    except Exception:  # pragma: no cover\n                        # Stream might already be closed\n                        pass\n                self._response_streams.clear()\n\n    def _normalize_request_id(self, response_id: RequestId) -> RequestId:\n        \"\"\"Normalize a response ID to match how request IDs are stored.\n\n        Since the client always sends integer IDs, we normalize string IDs\n        to integers when possible. This matches the TypeScript SDK approach:\n        https://github.com/modelcontextprotocol/typescript-sdk/blob/a606fb17909ea454e83aab14c73f14ea45c04448/src/shared/protocol.ts#L861\n\n        Args:\n            response_id: The response ID from the incoming message.\n\n        Returns:\n            The normalized ID (int if possible, otherwise original value).\n        \"\"\"\n        if isinstance(response_id, str):\n            try:\n                return int(response_id)\n            except ValueError:\n                logging.warning(f\"Response ID {response_id!r} cannot be normalized to match pending requests\")\n        return response_id\n\n    async def _handle_response(self, message: SessionMessage) -> None:\n        \"\"\"Handle an incoming response or error message.\n\n        Checks response routers first (e.g., for task-related responses),\n        then falls back to the normal response stream mechanism.\n        \"\"\"\n        # This check is always true at runtime: the caller (_receive_loop) only invokes\n        # this method in the else branch after checking for JSONRPCRequest and\n        # JSONRPCNotification. However, the type checker can't infer this from the\n        # method signature, so we need this guard for type narrowing.\n        if not isinstance(message.message, JSONRPCResponse | JSONRPCError):\n            return  # pragma: no cover\n\n        if message.message.id is None:\n            # Narrows to JSONRPCError since JSONRPCResponse.id is always RequestId\n            error = message.message.error\n            logging.warning(f\"Received error with null ID: {error.message}\")\n            await self._handle_incoming(MCPError(error.code, error.message, error.data))\n            return\n        # Normalize response ID to handle type mismatches (e.g., \"0\" vs 0)\n        response_id = self._normalize_request_id(message.message.id)\n\n        # First, check response routers (e.g., TaskResultHandler)\n        if isinstance(message.message, JSONRPCError):\n            # Route error to routers\n            for router in self._response_routers:\n                if router.route_error(response_id, message.message.error):\n                    return  # Handled\n        else:\n            # Route success response to routers\n            response_data: dict[str, Any] = message.message.result or {}\n            for router in self._response_routers:\n                if router.route_response(response_id, response_data):\n                    return  # Handled\n\n        # Fall back to normal response streams\n        stream = self._response_streams.pop(response_id, None)\n        if stream:\n            await stream.send(message.message)\n        else:\n            await self._handle_incoming(RuntimeError(f\"Received response with an unknown request ID: {message}\"))\n\n    async def _received_request(self, responder: RequestResponder[ReceiveRequestT, SendResultT]) -> None:\n        \"\"\"Can be overridden by subclasses to handle a request without needing to\n        listen on the message stream.\n\n        If the request is responded to within this method, it will not be\n        forwarded on to the message stream.\n        \"\"\"\n\n    async def _received_notification(self, notification: ReceiveNotificationT) -> None:\n        \"\"\"Can be overridden by subclasses to handle a notification without needing\n        to listen on the message stream.\n        \"\"\"\n\n    async def send_progress_notification(\n        self,\n        progress_token: ProgressToken,\n        progress: float,\n        total: float | None = None,\n        message: str | None = None,\n    ) -> None:\n        \"\"\"Sends a progress notification for a request that is currently being processed.\"\"\"\n\n    async def _handle_incoming(\n        self, req: RequestResponder[ReceiveRequestT, SendResultT] | ReceiveNotificationT | Exception\n    ) -> None:\n        \"\"\"A generic handler for incoming messages. Overridden by subclasses.\"\"\"\n"
  },
  {
    "path": "src/mcp/shared/tool_name_validation.py",
    "content": "\"\"\"Tool name validation utilities according to SEP-986.\n\nTool names SHOULD be between 1 and 128 characters in length (inclusive).\nTool names are case-sensitive.\nAllowed characters: uppercase and lowercase ASCII letters (A-Z, a-z),\ndigits (0-9), underscore (_), dash (-), and dot (.).\nTool names SHOULD NOT contain spaces, commas, or other special characters.\n\nSee: https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport re\nfrom dataclasses import dataclass, field\n\nlogger = logging.getLogger(__name__)\n\n# Regular expression for valid tool names according to SEP-986 specification\nTOOL_NAME_REGEX = re.compile(r\"^[A-Za-z0-9._-]{1,128}$\")\n\n# SEP reference URL for warning messages\nSEP_986_URL = \"https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names\"\n\n\n@dataclass\nclass ToolNameValidationResult:\n    \"\"\"Result of tool name validation.\n\n    Attributes:\n        is_valid: Whether the tool name conforms to SEP-986 requirements.\n        warnings: List of warning messages for non-conforming aspects.\n    \"\"\"\n\n    is_valid: bool\n    warnings: list[str] = field(default_factory=lambda: [])\n\n\ndef validate_tool_name(name: str) -> ToolNameValidationResult:\n    \"\"\"Validate a tool name according to the SEP-986 specification.\n\n    Args:\n        name: The tool name to validate.\n\n    Returns:\n        ToolNameValidationResult containing validation status and any warnings.\n    \"\"\"\n    warnings: list[str] = []\n\n    # Check for empty name\n    if not name:\n        return ToolNameValidationResult(\n            is_valid=False,\n            warnings=[\"Tool name cannot be empty\"],\n        )\n\n    # Check length\n    if len(name) > 128:\n        return ToolNameValidationResult(\n            is_valid=False,\n            warnings=[f\"Tool name exceeds maximum length of 128 characters (current: {len(name)})\"],\n        )\n\n    # Check for problematic patterns (warnings, not validation failures)\n    if \" \" in name:\n        warnings.append(\"Tool name contains spaces, which may cause parsing issues\")\n\n    if \",\" in name:\n        warnings.append(\"Tool name contains commas, which may cause parsing issues\")\n\n    # Check for potentially confusing leading/trailing characters\n    if name.startswith(\"-\") or name.endswith(\"-\"):\n        warnings.append(\"Tool name starts or ends with a dash, which may cause parsing issues in some contexts\")\n\n    if name.startswith(\".\") or name.endswith(\".\"):\n        warnings.append(\"Tool name starts or ends with a dot, which may cause parsing issues in some contexts\")\n\n    # Check for invalid characters\n    if not TOOL_NAME_REGEX.match(name):\n        # Find all invalid characters (unique, preserving order)\n        invalid_chars: list[str] = []\n        seen: set[str] = set()\n        for char in name:\n            if not re.match(r\"[A-Za-z0-9._-]\", char) and char not in seen:\n                invalid_chars.append(char)\n                seen.add(char)\n\n        warnings.append(f\"Tool name contains invalid characters: {', '.join(repr(c) for c in invalid_chars)}\")\n        warnings.append(\"Allowed characters are: A-Z, a-z, 0-9, underscore (_), dash (-), and dot (.)\")\n\n        return ToolNameValidationResult(is_valid=False, warnings=warnings)\n\n    return ToolNameValidationResult(is_valid=True, warnings=warnings)\n\n\ndef issue_tool_name_warning(name: str, warnings: list[str]) -> None:\n    \"\"\"Log warnings for non-conforming tool names.\n\n    Args:\n        name: The tool name that triggered the warnings.\n        warnings: List of warning messages to log.\n    \"\"\"\n    if not warnings:\n        return\n\n    logger.warning(f'Tool name validation warning for \"{name}\":')\n    for warning in warnings:\n        logger.warning(f\"  - {warning}\")\n    logger.warning(\"Tool registration will proceed, but this may cause compatibility issues.\")\n    logger.warning(\"Consider updating the tool name to conform to the MCP tool naming standard.\")\n    logger.warning(f\"See SEP-986 ({SEP_986_URL}) for more details.\")\n\n\ndef validate_and_warn_tool_name(name: str) -> bool:\n    \"\"\"Validate a tool name and issue warnings for non-conforming names.\n\n    This is the primary entry point for tool name validation. It validates\n    the name and logs any warnings via the logging module.\n\n    Args:\n        name: The tool name to validate.\n\n    Returns:\n        True if the name is valid, False otherwise.\n    \"\"\"\n    result = validate_tool_name(name)\n    issue_tool_name_warning(name, result.warnings)\n    return result.is_valid\n"
  },
  {
    "path": "src/mcp/shared/version.py",
    "content": "from mcp.types import LATEST_PROTOCOL_VERSION\n\nSUPPORTED_PROTOCOL_VERSIONS: list[str] = [\"2024-11-05\", \"2025-03-26\", \"2025-06-18\", LATEST_PROTOCOL_VERSION]\n"
  },
  {
    "path": "src/mcp/types/__init__.py",
    "content": "\"\"\"This module defines the types for the MCP protocol.\n\nCheck the latest schema at:\nhttps://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-11-25/schema.json\n\"\"\"\n\n# Re-export everything from _types for backward compatibility\nfrom mcp.types._types import (\n    DEFAULT_NEGOTIATED_VERSION,\n    LATEST_PROTOCOL_VERSION,\n    TASK_FORBIDDEN,\n    TASK_OPTIONAL,\n    TASK_REQUIRED,\n    TASK_STATUS_CANCELLED,\n    TASK_STATUS_COMPLETED,\n    TASK_STATUS_FAILED,\n    TASK_STATUS_INPUT_REQUIRED,\n    TASK_STATUS_WORKING,\n    Annotations,\n    AudioContent,\n    BaseMetadata,\n    BlobResourceContents,\n    CallToolRequest,\n    CallToolRequestParams,\n    CallToolResult,\n    CancelledNotification,\n    CancelledNotificationParams,\n    CancelTaskRequest,\n    CancelTaskRequestParams,\n    CancelTaskResult,\n    ClientCapabilities,\n    ClientNotification,\n    ClientRequest,\n    ClientResult,\n    ClientTasksCapability,\n    ClientTasksRequestsCapability,\n    CompleteRequest,\n    CompleteRequestParams,\n    CompleteResult,\n    Completion,\n    CompletionArgument,\n    CompletionContext,\n    CompletionsCapability,\n    ContentBlock,\n    CreateMessageRequest,\n    CreateMessageRequestParams,\n    CreateMessageResult,\n    CreateMessageResultWithTools,\n    CreateTaskResult,\n    ElicitationCapability,\n    ElicitationRequiredErrorData,\n    ElicitCompleteNotification,\n    ElicitCompleteNotificationParams,\n    ElicitRequest,\n    ElicitRequestedSchema,\n    ElicitRequestFormParams,\n    ElicitRequestParams,\n    ElicitRequestURLParams,\n    ElicitResult,\n    EmbeddedResource,\n    EmptyResult,\n    FormElicitationCapability,\n    GetPromptRequest,\n    GetPromptRequestParams,\n    GetPromptResult,\n    GetTaskPayloadRequest,\n    GetTaskPayloadRequestParams,\n    GetTaskPayloadResult,\n    GetTaskRequest,\n    GetTaskRequestParams,\n    GetTaskResult,\n    Icon,\n    IconTheme,\n    ImageContent,\n    Implementation,\n    IncludeContext,\n    InitializedNotification,\n    InitializeRequest,\n    InitializeRequestParams,\n    InitializeResult,\n    ListPromptsRequest,\n    ListPromptsResult,\n    ListResourcesRequest,\n    ListResourcesResult,\n    ListResourceTemplatesRequest,\n    ListResourceTemplatesResult,\n    ListRootsRequest,\n    ListRootsResult,\n    ListTasksRequest,\n    ListTasksResult,\n    ListToolsRequest,\n    ListToolsResult,\n    LoggingCapability,\n    LoggingLevel,\n    LoggingMessageNotification,\n    LoggingMessageNotificationParams,\n    ModelHint,\n    ModelPreferences,\n    Notification,\n    NotificationParams,\n    PaginatedRequest,\n    PaginatedRequestParams,\n    PaginatedResult,\n    PingRequest,\n    ProgressNotification,\n    ProgressNotificationParams,\n    ProgressToken,\n    Prompt,\n    PromptArgument,\n    PromptListChangedNotification,\n    PromptMessage,\n    PromptReference,\n    PromptsCapability,\n    ReadResourceRequest,\n    ReadResourceRequestParams,\n    ReadResourceResult,\n    RelatedTaskMetadata,\n    Request,\n    RequestParams,\n    RequestParamsMeta,\n    Resource,\n    ResourceContents,\n    ResourceLink,\n    ResourceListChangedNotification,\n    ResourcesCapability,\n    ResourceTemplate,\n    ResourceTemplateReference,\n    ResourceUpdatedNotification,\n    ResourceUpdatedNotificationParams,\n    Result,\n    Role,\n    Root,\n    RootsCapability,\n    RootsListChangedNotification,\n    SamplingCapability,\n    SamplingContent,\n    SamplingContextCapability,\n    SamplingMessage,\n    SamplingMessageContentBlock,\n    SamplingToolsCapability,\n    ServerCapabilities,\n    ServerNotification,\n    ServerRequest,\n    ServerResult,\n    ServerTasksCapability,\n    ServerTasksRequestsCapability,\n    SetLevelRequest,\n    SetLevelRequestParams,\n    StopReason,\n    SubscribeRequest,\n    SubscribeRequestParams,\n    Task,\n    TaskExecutionMode,\n    TaskMetadata,\n    TasksCallCapability,\n    TasksCancelCapability,\n    TasksCreateElicitationCapability,\n    TasksCreateMessageCapability,\n    TasksElicitationCapability,\n    TasksListCapability,\n    TasksSamplingCapability,\n    TaskStatus,\n    TaskStatusNotification,\n    TaskStatusNotificationParams,\n    TasksToolsCapability,\n    TextContent,\n    TextResourceContents,\n    Tool,\n    ToolAnnotations,\n    ToolChoice,\n    ToolExecution,\n    ToolListChangedNotification,\n    ToolResultContent,\n    ToolsCapability,\n    ToolUseContent,\n    UnsubscribeRequest,\n    UnsubscribeRequestParams,\n    UrlElicitationCapability,\n    client_notification_adapter,\n    client_request_adapter,\n    client_result_adapter,\n    server_notification_adapter,\n    server_request_adapter,\n    server_result_adapter,\n)\n\n# Re-export JSONRPC types\nfrom mcp.types.jsonrpc import (\n    CONNECTION_CLOSED,\n    INTERNAL_ERROR,\n    INVALID_PARAMS,\n    INVALID_REQUEST,\n    METHOD_NOT_FOUND,\n    PARSE_ERROR,\n    REQUEST_TIMEOUT,\n    URL_ELICITATION_REQUIRED,\n    ErrorData,\n    JSONRPCError,\n    JSONRPCMessage,\n    JSONRPCNotification,\n    JSONRPCRequest,\n    JSONRPCResponse,\n    RequestId,\n    jsonrpc_message_adapter,\n)\n\n__all__ = [\n    # Protocol version constants\n    \"LATEST_PROTOCOL_VERSION\",\n    \"DEFAULT_NEGOTIATED_VERSION\",\n    # Task execution mode constants\n    \"TASK_FORBIDDEN\",\n    \"TASK_OPTIONAL\",\n    \"TASK_REQUIRED\",\n    # Task status constants\n    \"TASK_STATUS_CANCELLED\",\n    \"TASK_STATUS_COMPLETED\",\n    \"TASK_STATUS_FAILED\",\n    \"TASK_STATUS_INPUT_REQUIRED\",\n    \"TASK_STATUS_WORKING\",\n    # Type aliases and variables\n    \"ContentBlock\",\n    \"ElicitRequestedSchema\",\n    \"ElicitRequestParams\",\n    \"IncludeContext\",\n    \"LoggingLevel\",\n    \"ProgressToken\",\n    \"Role\",\n    \"SamplingContent\",\n    \"SamplingMessageContentBlock\",\n    \"StopReason\",\n    \"TaskExecutionMode\",\n    \"TaskStatus\",\n    # Base classes\n    \"BaseMetadata\",\n    \"Request\",\n    \"Notification\",\n    \"Result\",\n    \"RequestParams\",\n    \"RequestParamsMeta\",\n    \"NotificationParams\",\n    \"PaginatedRequest\",\n    \"PaginatedRequestParams\",\n    \"PaginatedResult\",\n    \"EmptyResult\",\n    # Capabilities\n    \"ClientCapabilities\",\n    \"ClientTasksCapability\",\n    \"ClientTasksRequestsCapability\",\n    \"CompletionsCapability\",\n    \"ElicitationCapability\",\n    \"FormElicitationCapability\",\n    \"LoggingCapability\",\n    \"PromptsCapability\",\n    \"ResourcesCapability\",\n    \"RootsCapability\",\n    \"SamplingCapability\",\n    \"SamplingContextCapability\",\n    \"SamplingToolsCapability\",\n    \"ServerCapabilities\",\n    \"ServerTasksCapability\",\n    \"ServerTasksRequestsCapability\",\n    \"TasksCancelCapability\",\n    \"TasksCallCapability\",\n    \"TasksCreateElicitationCapability\",\n    \"TasksCreateMessageCapability\",\n    \"TasksElicitationCapability\",\n    \"TasksListCapability\",\n    \"TasksSamplingCapability\",\n    \"TasksToolsCapability\",\n    \"ToolsCapability\",\n    \"UrlElicitationCapability\",\n    # Content types\n    \"Annotations\",\n    \"AudioContent\",\n    \"BlobResourceContents\",\n    \"EmbeddedResource\",\n    \"Icon\",\n    \"IconTheme\",\n    \"ImageContent\",\n    \"ResourceContents\",\n    \"ResourceLink\",\n    \"TextContent\",\n    \"TextResourceContents\",\n    \"ToolResultContent\",\n    \"ToolUseContent\",\n    # Entity types\n    \"Completion\",\n    \"CompletionArgument\",\n    \"CompletionContext\",\n    \"Implementation\",\n    \"ModelHint\",\n    \"ModelPreferences\",\n    \"Prompt\",\n    \"PromptArgument\",\n    \"PromptMessage\",\n    \"PromptReference\",\n    \"Resource\",\n    \"ResourceTemplate\",\n    \"ResourceTemplateReference\",\n    \"Root\",\n    \"SamplingMessage\",\n    \"Task\",\n    \"TaskMetadata\",\n    \"RelatedTaskMetadata\",\n    \"Tool\",\n    \"ToolAnnotations\",\n    \"ToolChoice\",\n    \"ToolExecution\",\n    # Requests\n    \"CallToolRequest\",\n    \"CallToolRequestParams\",\n    \"CancelTaskRequest\",\n    \"CancelTaskRequestParams\",\n    \"CompleteRequest\",\n    \"CompleteRequestParams\",\n    \"CreateMessageRequest\",\n    \"CreateMessageRequestParams\",\n    \"ElicitRequest\",\n    \"ElicitRequestFormParams\",\n    \"ElicitRequestURLParams\",\n    \"GetPromptRequest\",\n    \"GetPromptRequestParams\",\n    \"GetTaskPayloadRequest\",\n    \"GetTaskPayloadRequestParams\",\n    \"GetTaskRequest\",\n    \"GetTaskRequestParams\",\n    \"InitializeRequest\",\n    \"InitializeRequestParams\",\n    \"ListPromptsRequest\",\n    \"ListResourcesRequest\",\n    \"ListResourceTemplatesRequest\",\n    \"ListRootsRequest\",\n    \"ListTasksRequest\",\n    \"ListToolsRequest\",\n    \"PingRequest\",\n    \"ReadResourceRequest\",\n    \"ReadResourceRequestParams\",\n    \"SetLevelRequest\",\n    \"SetLevelRequestParams\",\n    \"SubscribeRequest\",\n    \"SubscribeRequestParams\",\n    \"UnsubscribeRequest\",\n    \"UnsubscribeRequestParams\",\n    # Results\n    \"CallToolResult\",\n    \"CancelTaskResult\",\n    \"CompleteResult\",\n    \"CreateMessageResult\",\n    \"CreateMessageResultWithTools\",\n    \"CreateTaskResult\",\n    \"ElicitResult\",\n    \"ElicitationRequiredErrorData\",\n    \"GetPromptResult\",\n    \"GetTaskPayloadResult\",\n    \"GetTaskResult\",\n    \"InitializeResult\",\n    \"ListPromptsResult\",\n    \"ListResourcesResult\",\n    \"ListResourceTemplatesResult\",\n    \"ListRootsResult\",\n    \"ListTasksResult\",\n    \"ListToolsResult\",\n    \"ReadResourceResult\",\n    # Notifications\n    \"CancelledNotification\",\n    \"CancelledNotificationParams\",\n    \"ElicitCompleteNotification\",\n    \"ElicitCompleteNotificationParams\",\n    \"InitializedNotification\",\n    \"LoggingMessageNotification\",\n    \"LoggingMessageNotificationParams\",\n    \"ProgressNotification\",\n    \"ProgressNotificationParams\",\n    \"PromptListChangedNotification\",\n    \"ResourceListChangedNotification\",\n    \"ResourceUpdatedNotification\",\n    \"ResourceUpdatedNotificationParams\",\n    \"RootsListChangedNotification\",\n    \"TaskStatusNotification\",\n    \"TaskStatusNotificationParams\",\n    \"ToolListChangedNotification\",\n    # Union types for request/response routing\n    \"ClientNotification\",\n    \"ClientRequest\",\n    \"ClientResult\",\n    \"ServerNotification\",\n    \"ServerRequest\",\n    \"ServerResult\",\n    # Type adapters\n    \"client_notification_adapter\",\n    \"client_request_adapter\",\n    \"client_result_adapter\",\n    \"server_notification_adapter\",\n    \"server_request_adapter\",\n    \"server_result_adapter\",\n    # JSON-RPC types\n    \"CONNECTION_CLOSED\",\n    \"INTERNAL_ERROR\",\n    \"INVALID_PARAMS\",\n    \"INVALID_REQUEST\",\n    \"METHOD_NOT_FOUND\",\n    \"PARSE_ERROR\",\n    \"REQUEST_TIMEOUT\",\n    \"URL_ELICITATION_REQUIRED\",\n    \"ErrorData\",\n    \"JSONRPCError\",\n    \"JSONRPCMessage\",\n    \"JSONRPCNotification\",\n    \"JSONRPCRequest\",\n    \"JSONRPCResponse\",\n    \"RequestId\",\n    \"jsonrpc_message_adapter\",\n]\n"
  },
  {
    "path": "src/mcp/types/_types.py",
    "content": "from __future__ import annotations\n\nfrom datetime import datetime\nfrom typing import Annotated, Any, Final, Generic, Literal, TypeAlias, TypeVar\n\nfrom pydantic import BaseModel, ConfigDict, Field, FileUrl, TypeAdapter\nfrom pydantic.alias_generators import to_camel\nfrom typing_extensions import NotRequired, TypedDict\n\nfrom mcp.types.jsonrpc import RequestId\n\nLATEST_PROTOCOL_VERSION = \"2025-11-25\"\n\"\"\"The latest version of the Model Context Protocol.\n\nYou can find the latest specification at https://modelcontextprotocol.io/specification/latest.\n\"\"\"\n\nDEFAULT_NEGOTIATED_VERSION = \"2025-03-26\"\n\"\"\"The default negotiated version of the Model Context Protocol when no version is specified.\n\nWe need this to satisfy the MCP specification, which requires the server to assume a specific version if none is\nprovided by the client.\n\nSee the \"Protocol Version Header\" at\nhttps://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-header.\n\"\"\"\n\nProgressToken = str | int\nRole = Literal[\"user\", \"assistant\"]\n\nIconTheme = Literal[\"light\", \"dark\"]\n\nTaskExecutionMode = Literal[\"forbidden\", \"optional\", \"required\"]\nTASK_FORBIDDEN: Final[Literal[\"forbidden\"]] = \"forbidden\"\nTASK_OPTIONAL: Final[Literal[\"optional\"]] = \"optional\"\nTASK_REQUIRED: Final[Literal[\"required\"]] = \"required\"\n\n\nclass MCPModel(BaseModel):\n    \"\"\"Base class for all MCP protocol types.\"\"\"\n\n    model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)\n\n\nMeta: TypeAlias = dict[str, Any]\n\n\nclass RequestParamsMeta(TypedDict, extra_items=Any):\n    progress_token: NotRequired[ProgressToken]\n    \"\"\"\n    If specified, the caller requests out-of-band progress notifications for\n    this request (as represented by notifications/progress). The value of this\n    parameter is an opaque token that will be attached to any subsequent\n    notifications. The receiver is not obligated to provide these notifications.\n    \"\"\"\n\n\nclass TaskMetadata(MCPModel):\n    \"\"\"Metadata for augmenting a request with task execution.\n\n    Include this in the `task` field of the request parameters.\n    \"\"\"\n\n    ttl: Annotated[int, Field(strict=True)] | None = None\n    \"\"\"Requested duration in milliseconds to retain task from creation.\"\"\"\n\n\nclass RequestParams(MCPModel):\n    task: TaskMetadata | None = None\n    \"\"\"\n    If specified, the caller is requesting task-augmented execution for this request.\n    The request will return a CreateTaskResult immediately, and the actual result can be\n    retrieved later via tasks/result.\n\n    Task augmentation is subject to capability negotiation - receivers MUST declare support\n    for task augmentation of specific request types in their capabilities.\n    \"\"\"\n\n    meta: RequestParamsMeta | None = Field(alias=\"_meta\", default=None)\n\n\nclass PaginatedRequestParams(RequestParams):\n    cursor: str | None = None\n    \"\"\"An opaque token representing the current pagination position.\n\n    If provided, the server should return results starting after this cursor.\n    \"\"\"\n\n\nclass NotificationParams(MCPModel):\n    meta: Meta | None = Field(alias=\"_meta\", default=None)\n    \"\"\"\n    See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)\n    for notes on _meta usage.\n    \"\"\"\n\n\nRequestParamsT = TypeVar(\"RequestParamsT\", bound=RequestParams | dict[str, Any] | None)\nNotificationParamsT = TypeVar(\"NotificationParamsT\", bound=NotificationParams | dict[str, Any] | None)\nMethodT = TypeVar(\"MethodT\", bound=str)\n\n\nclass Request(MCPModel, Generic[RequestParamsT, MethodT]):\n    \"\"\"Base class for JSON-RPC requests.\"\"\"\n\n    method: MethodT\n    params: RequestParamsT\n\n\nclass PaginatedRequest(Request[PaginatedRequestParams | None, MethodT], Generic[MethodT]):\n    \"\"\"Base class for paginated requests, matching the schema's PaginatedRequest interface.\"\"\"\n\n    params: PaginatedRequestParams | None = None\n\n\nclass Notification(MCPModel, Generic[NotificationParamsT, MethodT]):\n    \"\"\"Base class for JSON-RPC notifications.\"\"\"\n\n    method: MethodT\n    params: NotificationParamsT\n\n\nclass Result(MCPModel):\n    \"\"\"Base class for JSON-RPC results.\"\"\"\n\n    meta: Meta | None = Field(alias=\"_meta\", default=None)\n    \"\"\"\n    See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)\n    for notes on _meta usage.\n    \"\"\"\n\n\nclass PaginatedResult(Result):\n    next_cursor: str | None = None\n    \"\"\"\n    An opaque token representing the pagination position after the last returned result.\n    If present, there may be more results available.\n    \"\"\"\n\n\nclass EmptyResult(Result):\n    \"\"\"A response that indicates success but carries no data.\"\"\"\n\n\nclass BaseMetadata(MCPModel):\n    \"\"\"Base class for entities with name and optional title fields.\"\"\"\n\n    name: str\n    \"\"\"The programmatic name of the entity.\"\"\"\n\n    title: str | None = None\n    \"\"\"\n    Intended for UI and end-user contexts — optimized to be human-readable and easily understood,\n    even by those unfamiliar with domain-specific terminology.\n\n    If not provided, the name should be used for display (except for Tool,\n    where `annotations.title` should be given precedence over using `name`,\n    if present).\n    \"\"\"\n\n\nclass Icon(MCPModel):\n    \"\"\"An icon for display in user interfaces.\"\"\"\n\n    src: str\n    \"\"\"URL or data URI for the icon.\"\"\"\n\n    mime_type: str | None = None\n    \"\"\"Optional MIME type for the icon.\"\"\"\n\n    sizes: list[str] | None = None\n    \"\"\"Optional list of strings specifying icon dimensions (e.g., [\"48x48\", \"96x96\"]).\"\"\"\n\n    theme: IconTheme | None = None\n    \"\"\"Optional theme specifier.\n\n    `\"light\"` indicates the icon is designed for a light background, `\"dark\"` indicates the icon\n    is designed for a dark background.\n\n    See https://modelcontextprotocol.io/specification/2025-11-25/schema#icon for more details.\n    \"\"\"\n\n\nclass Implementation(BaseMetadata):\n    \"\"\"Describes the name and version of an MCP implementation.\"\"\"\n\n    version: str\n\n    title: str | None = None\n    \"\"\"An optional human-readable title for this implementation.\"\"\"\n\n    description: str | None = None\n    \"\"\"An optional human-readable description of what this implementation does.\"\"\"\n\n    website_url: str | None = None\n    \"\"\"An optional URL of the website for this implementation.\"\"\"\n\n    icons: list[Icon] | None = None\n    \"\"\"An optional list of icons for this implementation.\"\"\"\n\n\nclass RootsCapability(MCPModel):\n    \"\"\"Capability for root operations.\"\"\"\n\n    list_changed: bool | None = None\n    \"\"\"Whether the client supports notifications for changes to the roots list.\"\"\"\n\n\nclass SamplingContextCapability(MCPModel):\n    \"\"\"Capability for context inclusion during sampling.\n\n    Indicates support for non-'none' values in the includeContext parameter.\n    SOFT-DEPRECATED: New implementations should use tools parameter instead.\n    \"\"\"\n\n\nclass SamplingToolsCapability(MCPModel):\n    \"\"\"Capability indicating support for tool calling during sampling.\n\n    When present in ClientCapabilities.sampling, indicates that the client\n    supports the tools and toolChoice parameters in sampling requests.\n    \"\"\"\n\n\nclass FormElicitationCapability(MCPModel):\n    \"\"\"Capability for form mode elicitation.\"\"\"\n\n\nclass UrlElicitationCapability(MCPModel):\n    \"\"\"Capability for URL mode elicitation.\"\"\"\n\n\nclass ElicitationCapability(MCPModel):\n    \"\"\"Capability for elicitation operations.\n\n    Clients must support at least one mode (form or url).\n    \"\"\"\n\n    form: FormElicitationCapability | None = None\n    \"\"\"Present if the client supports form mode elicitation.\"\"\"\n\n    url: UrlElicitationCapability | None = None\n    \"\"\"Present if the client supports URL mode elicitation.\"\"\"\n\n\nclass SamplingCapability(MCPModel):\n    \"\"\"Sampling capability structure, allowing fine-grained capability advertisement.\"\"\"\n\n    context: SamplingContextCapability | None = None\n    \"\"\"\n    Present if the client supports non-'none' values for includeContext parameter.\n    SOFT-DEPRECATED: New implementations should use tools parameter instead.\n    \"\"\"\n    tools: SamplingToolsCapability | None = None\n    \"\"\"\n    Present if the client supports tools and toolChoice parameters in sampling requests.\n    Presence indicates full tool calling support during sampling.\n    \"\"\"\n\n\nclass TasksListCapability(MCPModel):\n    \"\"\"Capability for tasks listing operations.\"\"\"\n\n\nclass TasksCancelCapability(MCPModel):\n    \"\"\"Capability for tasks cancel operations.\"\"\"\n\n\nclass TasksCreateMessageCapability(MCPModel):\n    \"\"\"Capability for tasks create messages.\"\"\"\n\n\nclass TasksSamplingCapability(MCPModel):\n    \"\"\"Capability for tasks sampling operations.\"\"\"\n\n    create_message: TasksCreateMessageCapability | None = None\n\n\nclass TasksCreateElicitationCapability(MCPModel):\n    \"\"\"Capability for tasks create elicitation operations.\"\"\"\n\n\nclass TasksElicitationCapability(MCPModel):\n    \"\"\"Capability for tasks elicitation operations.\"\"\"\n\n    create: TasksCreateElicitationCapability | None = None\n\n\nclass ClientTasksRequestsCapability(MCPModel):\n    \"\"\"Capability for tasks requests operations.\"\"\"\n\n    sampling: TasksSamplingCapability | None = None\n\n    elicitation: TasksElicitationCapability | None = None\n\n\nclass ClientTasksCapability(MCPModel):\n    \"\"\"Capability for client tasks operations.\"\"\"\n\n    list: TasksListCapability | None = None\n    \"\"\"Whether this client supports tasks/list.\"\"\"\n\n    cancel: TasksCancelCapability | None = None\n    \"\"\"Whether this client supports tasks/cancel.\"\"\"\n\n    requests: ClientTasksRequestsCapability | None = None\n    \"\"\"Specifies which request types can be augmented with tasks.\"\"\"\n\n\nclass ClientCapabilities(MCPModel):\n    \"\"\"Capabilities a client may support.\"\"\"\n\n    experimental: dict[str, dict[str, Any]] | None = None\n    \"\"\"Experimental, non-standard capabilities that the client supports.\"\"\"\n    sampling: SamplingCapability | None = None\n    \"\"\"\n    Present if the client supports sampling from an LLM.\n    Can contain fine-grained capabilities like context and tools support.\n    \"\"\"\n    elicitation: ElicitationCapability | None = None\n    \"\"\"Present if the client supports elicitation from the user.\"\"\"\n    roots: RootsCapability | None = None\n    \"\"\"Present if the client supports listing roots.\"\"\"\n    tasks: ClientTasksCapability | None = None\n    \"\"\"Present if the client supports task-augmented requests.\"\"\"\n\n\nclass PromptsCapability(MCPModel):\n    \"\"\"Capability for prompts operations.\"\"\"\n\n    list_changed: bool | None = None\n    \"\"\"Whether this server supports notifications for changes to the prompt list.\"\"\"\n\n\nclass ResourcesCapability(MCPModel):\n    \"\"\"Capability for resources operations.\"\"\"\n\n    subscribe: bool | None = None\n    \"\"\"Whether this server supports subscribing to resource updates.\"\"\"\n    list_changed: bool | None = None\n    \"\"\"Whether this server supports notifications for changes to the resource list.\"\"\"\n\n\nclass ToolsCapability(MCPModel):\n    \"\"\"Capability for tools operations.\"\"\"\n\n    list_changed: bool | None = None\n    \"\"\"Whether this server supports notifications for changes to the tool list.\"\"\"\n\n\nclass LoggingCapability(MCPModel):\n    \"\"\"Capability for logging operations.\"\"\"\n\n\nclass CompletionsCapability(MCPModel):\n    \"\"\"Capability for completions operations.\"\"\"\n\n\nclass TasksCallCapability(MCPModel):\n    \"\"\"Capability for tasks call operations.\"\"\"\n\n\nclass TasksToolsCapability(MCPModel):\n    \"\"\"Capability for tasks tools operations.\"\"\"\n\n    call: TasksCallCapability | None = None\n\n\nclass ServerTasksRequestsCapability(MCPModel):\n    \"\"\"Capability for tasks requests operations.\"\"\"\n\n    tools: TasksToolsCapability | None = None\n\n\nclass ServerTasksCapability(MCPModel):\n    \"\"\"Capability for server tasks operations.\"\"\"\n\n    list: TasksListCapability | None = None\n    cancel: TasksCancelCapability | None = None\n    requests: ServerTasksRequestsCapability | None = None\n\n\nclass ServerCapabilities(MCPModel):\n    \"\"\"Capabilities that a server may support.\"\"\"\n\n    experimental: dict[str, dict[str, Any]] | None = None\n    \"\"\"Experimental, non-standard capabilities that the server supports.\"\"\"\n\n    logging: LoggingCapability | None = None\n    \"\"\"Present if the server supports sending log messages to the client.\"\"\"\n\n    prompts: PromptsCapability | None = None\n    \"\"\"Present if the server offers any prompt templates.\"\"\"\n\n    resources: ResourcesCapability | None = None\n    \"\"\"Present if the server offers any resources to read.\"\"\"\n\n    tools: ToolsCapability | None = None\n    \"\"\"Present if the server offers any tools to call.\"\"\"\n\n    completions: CompletionsCapability | None = None\n    \"\"\"Present if the server offers autocompletion suggestions for prompts and resources.\"\"\"\n\n    tasks: ServerTasksCapability | None = None\n    \"\"\"Present if the server supports task-augmented requests.\"\"\"\n\n\nTaskStatus = Literal[\"working\", \"input_required\", \"completed\", \"failed\", \"cancelled\"]\n\n# Task status constants\nTASK_STATUS_WORKING: Final[Literal[\"working\"]] = \"working\"\nTASK_STATUS_INPUT_REQUIRED: Final[Literal[\"input_required\"]] = \"input_required\"\nTASK_STATUS_COMPLETED: Final[Literal[\"completed\"]] = \"completed\"\nTASK_STATUS_FAILED: Final[Literal[\"failed\"]] = \"failed\"\nTASK_STATUS_CANCELLED: Final[Literal[\"cancelled\"]] = \"cancelled\"\n\n\nclass RelatedTaskMetadata(MCPModel):\n    \"\"\"Metadata for associating messages with a task.\n\n    Include this in the `_meta` field under the key `io.modelcontextprotocol/related-task`.\n    \"\"\"\n\n    task_id: str\n    \"\"\"The task identifier this message is associated with.\"\"\"\n\n\nclass Task(MCPModel):\n    \"\"\"Data associated with a task.\"\"\"\n\n    task_id: str\n    \"\"\"The task identifier.\"\"\"\n\n    status: TaskStatus\n    \"\"\"Current task state.\"\"\"\n\n    status_message: str | None = None\n    \"\"\"Optional human-readable message describing the current task state.\n\n    This can provide context for any status, including:\n    - Reasons for \"cancelled\" status\n    - Summaries for \"completed\" status\n    - Diagnostic information for \"failed\" status (e.g., error details, what went wrong)\n    \"\"\"\n\n    created_at: datetime  # Pydantic will enforce ISO 8601 and re-serialize as a string later\n    \"\"\"ISO 8601 timestamp when the task was created.\"\"\"\n\n    last_updated_at: datetime\n    \"\"\"ISO 8601 timestamp when the task was last updated.\"\"\"\n\n    ttl: Annotated[int, Field(strict=True)] | None\n    \"\"\"Actual retention duration from creation in milliseconds, null for unlimited.\"\"\"\n\n    poll_interval: Annotated[int, Field(strict=True)] | None = None\n    \"\"\"Suggested polling interval in milliseconds.\"\"\"\n\n\nclass CreateTaskResult(Result):\n    \"\"\"A response to a task-augmented request.\"\"\"\n\n    task: Task\n\n\nclass GetTaskRequestParams(RequestParams):\n    task_id: str\n    \"\"\"The task identifier to query.\"\"\"\n\n\nclass GetTaskRequest(Request[GetTaskRequestParams, Literal[\"tasks/get\"]]):\n    \"\"\"A request to retrieve the state of a task.\"\"\"\n\n    method: Literal[\"tasks/get\"] = \"tasks/get\"\n\n    params: GetTaskRequestParams\n\n\nclass GetTaskResult(Result, Task):\n    \"\"\"The response to a tasks/get request.\"\"\"\n\n\nclass GetTaskPayloadRequestParams(RequestParams):\n    task_id: str\n    \"\"\"The task identifier to retrieve results for.\"\"\"\n\n\nclass GetTaskPayloadRequest(Request[GetTaskPayloadRequestParams, Literal[\"tasks/result\"]]):\n    \"\"\"A request to retrieve the result of a completed task.\"\"\"\n\n    method: Literal[\"tasks/result\"] = \"tasks/result\"\n    params: GetTaskPayloadRequestParams\n\n\nclass GetTaskPayloadResult(Result):\n    \"\"\"The response to a tasks/result request.\n\n    The structure matches the result type of the original request.\n    For example, a tools/call task would return the CallToolResult structure.\n    \"\"\"\n\n    model_config = ConfigDict(extra=\"allow\", alias_generator=to_camel, populate_by_name=True)\n\n\nclass CancelTaskRequestParams(RequestParams):\n    task_id: str\n    \"\"\"The task identifier to cancel.\"\"\"\n\n\nclass CancelTaskRequest(Request[CancelTaskRequestParams, Literal[\"tasks/cancel\"]]):\n    \"\"\"A request to cancel a task.\"\"\"\n\n    method: Literal[\"tasks/cancel\"] = \"tasks/cancel\"\n    params: CancelTaskRequestParams\n\n\nclass CancelTaskResult(Result, Task):\n    \"\"\"The response to a tasks/cancel request.\"\"\"\n\n\nclass ListTasksRequest(PaginatedRequest[Literal[\"tasks/list\"]]):\n    \"\"\"A request to retrieve a list of tasks.\"\"\"\n\n    method: Literal[\"tasks/list\"] = \"tasks/list\"\n\n\nclass ListTasksResult(PaginatedResult):\n    \"\"\"The response to a tasks/list request.\"\"\"\n\n    tasks: list[Task]\n\n\nclass TaskStatusNotificationParams(NotificationParams, Task):\n    \"\"\"Parameters for a `notifications/tasks/status` notification.\"\"\"\n\n\nclass TaskStatusNotification(Notification[TaskStatusNotificationParams, Literal[\"notifications/tasks/status\"]]):\n    \"\"\"An optional notification from the receiver to the requestor, informing them that a task's status has changed.\n    Receivers are not required to send these notifications.\n    \"\"\"\n\n    method: Literal[\"notifications/tasks/status\"] = \"notifications/tasks/status\"\n    params: TaskStatusNotificationParams\n\n\nclass InitializeRequestParams(RequestParams):\n    \"\"\"Parameters for the initialize request.\"\"\"\n\n    protocol_version: str | int\n    \"\"\"The latest version of the Model Context Protocol that the client supports.\"\"\"\n    capabilities: ClientCapabilities\n    client_info: Implementation\n\n\nclass InitializeRequest(Request[InitializeRequestParams, Literal[\"initialize\"]]):\n    \"\"\"This request is sent from the client to the server when it first connects, asking it\n    to begin initialization.\n    \"\"\"\n\n    method: Literal[\"initialize\"] = \"initialize\"\n    params: InitializeRequestParams\n\n\nclass InitializeResult(Result):\n    \"\"\"After receiving an initialize request from the client, the server sends this.\"\"\"\n\n    protocol_version: str | int\n    \"\"\"The version of the Model Context Protocol that the server wants to use.\"\"\"\n    capabilities: ServerCapabilities\n    server_info: Implementation\n    instructions: str | None = None\n    \"\"\"Instructions describing how to use the server and its features.\"\"\"\n\n\nclass InitializedNotification(Notification[NotificationParams | None, Literal[\"notifications/initialized\"]]):\n    \"\"\"This notification is sent from the client to the server after initialization has\n    finished.\n    \"\"\"\n\n    method: Literal[\"notifications/initialized\"] = \"notifications/initialized\"\n    params: NotificationParams | None = None\n\n\nclass PingRequest(Request[RequestParams | None, Literal[\"ping\"]]):\n    \"\"\"A ping, issued by either the server or the client, to check that the other party is\n    still alive.\n    \"\"\"\n\n    method: Literal[\"ping\"] = \"ping\"\n    params: RequestParams | None = None\n\n\nclass ProgressNotificationParams(NotificationParams):\n    \"\"\"Parameters for progress notifications.\"\"\"\n\n    progress_token: ProgressToken\n    \"\"\"\n    The progress token which was given in the initial request, used to associate this\n    notification with the request that is proceeding.\n    \"\"\"\n    progress: float\n    \"\"\"\n    The progress thus far. This should increase every time progress is made, even if the\n    total is unknown.\n    \"\"\"\n    total: float | None = None\n    \"\"\"Total number of items to process (or total progress required), if known.\"\"\"\n    message: str | None = None\n    \"\"\"Message related to progress.\n\n    This should provide relevant human-readable progress information.\n    \"\"\"\n\n\nclass ProgressNotification(Notification[ProgressNotificationParams, Literal[\"notifications/progress\"]]):\n    \"\"\"An out-of-band notification used to inform the receiver of a progress update for a long-running request.\"\"\"\n\n    method: Literal[\"notifications/progress\"] = \"notifications/progress\"\n    params: ProgressNotificationParams\n\n\nclass ListResourcesRequest(PaginatedRequest[Literal[\"resources/list\"]]):\n    \"\"\"Sent from the client to request a list of resources the server has.\"\"\"\n\n    method: Literal[\"resources/list\"] = \"resources/list\"\n\n\nclass Annotations(MCPModel):\n    audience: list[Role] | None = None\n    priority: Annotated[float, Field(ge=0.0, le=1.0)] | None = None\n\n\nclass Resource(BaseMetadata):\n    \"\"\"A known resource that the server is capable of reading.\"\"\"\n\n    uri: str\n    \"\"\"The URI of this resource.\"\"\"\n\n    description: str | None = None\n    \"\"\"A description of what this resource represents.\"\"\"\n\n    mime_type: str | None = None\n    \"\"\"The MIME type of this resource, if known.\"\"\"\n\n    size: int | None = None\n    \"\"\"The size of the raw resource content, in bytes (i.e., before base64 encoding or any tokenization), if known.\n\n    This can be used by Hosts to display file sizes and estimate context window usage.\n    \"\"\"\n\n    icons: list[Icon] | None = None\n    \"\"\"An optional list of icons for this resource.\"\"\"\n\n    annotations: Annotations | None = None\n\n    meta: Meta | None = Field(alias=\"_meta\", default=None)\n    \"\"\"\n    See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)\n    for notes on _meta usage.\n    \"\"\"\n\n\nclass ResourceTemplate(BaseMetadata):\n    \"\"\"A template description for resources available on the server.\"\"\"\n\n    uri_template: str\n    \"\"\"A URI template (according to RFC 6570) that can be used to construct resource URIs.\"\"\"\n\n    description: str | None = None\n    \"\"\"A human-readable description of what this template is for.\"\"\"\n\n    mime_type: str | None = None\n    \"\"\"The MIME type for all resources that match this template.\n\n    This should only be included if all resources matching this template have the same type.\n    \"\"\"\n\n    icons: list[Icon] | None = None\n    \"\"\"An optional list of icons for this resource template.\"\"\"\n\n    annotations: Annotations | None = None\n\n    meta: Meta | None = Field(alias=\"_meta\", default=None)\n    \"\"\"\n    See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)\n    for notes on _meta usage.\n    \"\"\"\n\n\nclass ListResourcesResult(PaginatedResult):\n    \"\"\"The server's response to a resources/list request from the client.\"\"\"\n\n    resources: list[Resource]\n\n\nclass ListResourceTemplatesRequest(PaginatedRequest[Literal[\"resources/templates/list\"]]):\n    \"\"\"Sent from the client to request a list of resource templates the server has.\"\"\"\n\n    method: Literal[\"resources/templates/list\"] = \"resources/templates/list\"\n\n\nclass ListResourceTemplatesResult(PaginatedResult):\n    \"\"\"The server's response to a resources/templates/list request from the client.\"\"\"\n\n    resource_templates: list[ResourceTemplate]\n\n\nclass ReadResourceRequestParams(RequestParams):\n    \"\"\"Parameters for reading a resource.\"\"\"\n\n    uri: str\n    \"\"\"\n    The URI of the resource to read. The URI can use any protocol; it is up to the\n    server how to interpret it.\n    \"\"\"\n\n\nclass ReadResourceRequest(Request[ReadResourceRequestParams, Literal[\"resources/read\"]]):\n    \"\"\"Sent from the client to the server, to read a specific resource URI.\"\"\"\n\n    method: Literal[\"resources/read\"] = \"resources/read\"\n    params: ReadResourceRequestParams\n\n\nclass ResourceContents(MCPModel):\n    \"\"\"The contents of a specific resource or sub-resource.\"\"\"\n\n    uri: str\n    \"\"\"The URI of this resource.\"\"\"\n    mime_type: str | None = None\n    \"\"\"The MIME type of this resource, if known.\"\"\"\n    meta: Meta | None = Field(alias=\"_meta\", default=None)\n    \"\"\"\n    See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)\n    for notes on _meta usage.\n    \"\"\"\n\n\nclass TextResourceContents(ResourceContents):\n    \"\"\"Text contents of a resource.\"\"\"\n\n    text: str\n    \"\"\"\n    The text of the item. This must only be set if the item can actually be represented\n    as text (not binary data).\n    \"\"\"\n\n\nclass BlobResourceContents(ResourceContents):\n    \"\"\"Binary contents of a resource.\"\"\"\n\n    blob: str\n    \"\"\"A base64-encoded string representing the binary data of the item.\"\"\"\n\n\nclass ReadResourceResult(Result):\n    \"\"\"The server's response to a resources/read request from the client.\"\"\"\n\n    contents: list[TextResourceContents | BlobResourceContents]\n\n\nclass ResourceListChangedNotification(\n    Notification[NotificationParams | None, Literal[\"notifications/resources/list_changed\"]]\n):\n    \"\"\"An optional notification from the server to the client, informing it that the list\n    of resources it can read from has changed.\n    \"\"\"\n\n    method: Literal[\"notifications/resources/list_changed\"] = \"notifications/resources/list_changed\"\n    params: NotificationParams | None = None\n\n\nclass SubscribeRequestParams(RequestParams):\n    \"\"\"Parameters for subscribing to a resource.\"\"\"\n\n    uri: str\n    \"\"\"\n    The URI of the resource to subscribe to. The URI can use any protocol; it is up to\n    the server how to interpret it.\n    \"\"\"\n\n\nclass SubscribeRequest(Request[SubscribeRequestParams, Literal[\"resources/subscribe\"]]):\n    \"\"\"Sent from the client to request resources/updated notifications from the server\n    whenever a particular resource changes.\n    \"\"\"\n\n    method: Literal[\"resources/subscribe\"] = \"resources/subscribe\"\n    params: SubscribeRequestParams\n\n\nclass UnsubscribeRequestParams(RequestParams):\n    \"\"\"Parameters for unsubscribing from a resource.\"\"\"\n\n    uri: str\n    \"\"\"The URI of the resource to unsubscribe from.\"\"\"\n\n\nclass UnsubscribeRequest(Request[UnsubscribeRequestParams, Literal[\"resources/unsubscribe\"]]):\n    \"\"\"Sent from the client to request cancellation of resources/updated notifications from\n    the server.\n    \"\"\"\n\n    method: Literal[\"resources/unsubscribe\"] = \"resources/unsubscribe\"\n    params: UnsubscribeRequestParams\n\n\nclass ResourceUpdatedNotificationParams(NotificationParams):\n    \"\"\"Parameters for resource update notifications.\"\"\"\n\n    uri: str\n    \"\"\"\n    The URI of the resource that has been updated. This might be a sub-resource of the\n    one that the client actually subscribed to.\n    \"\"\"\n\n\nclass ResourceUpdatedNotification(\n    Notification[ResourceUpdatedNotificationParams, Literal[\"notifications/resources/updated\"]]\n):\n    \"\"\"A notification from the server to the client, informing it that a resource has\n    changed and may need to be read again.\n    \"\"\"\n\n    method: Literal[\"notifications/resources/updated\"] = \"notifications/resources/updated\"\n    params: ResourceUpdatedNotificationParams\n\n\nclass ListPromptsRequest(PaginatedRequest[Literal[\"prompts/list\"]]):\n    \"\"\"Sent from the client to request a list of prompts and prompt templates.\"\"\"\n\n    method: Literal[\"prompts/list\"] = \"prompts/list\"\n\n\nclass PromptArgument(MCPModel):\n    \"\"\"An argument for a prompt template.\"\"\"\n\n    name: str\n    \"\"\"The name of the argument.\"\"\"\n    description: str | None = None\n    \"\"\"A human-readable description of the argument.\"\"\"\n    required: bool | None = None\n    \"\"\"Whether this argument must be provided.\"\"\"\n\n\nclass Prompt(BaseMetadata):\n    \"\"\"A prompt or prompt template that the server offers.\"\"\"\n\n    description: str | None = None\n    \"\"\"An optional description of what this prompt provides.\"\"\"\n    arguments: list[PromptArgument] | None = None\n    \"\"\"A list of arguments to use for templating the prompt.\"\"\"\n    icons: list[Icon] | None = None\n    \"\"\"An optional list of icons for this prompt.\"\"\"\n    meta: Meta | None = Field(alias=\"_meta\", default=None)\n    \"\"\"\n    See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)\n    for notes on _meta usage.\n    \"\"\"\n\n\nclass ListPromptsResult(PaginatedResult):\n    \"\"\"The server's response to a prompts/list request from the client.\"\"\"\n\n    prompts: list[Prompt]\n\n\nclass GetPromptRequestParams(RequestParams):\n    \"\"\"Parameters for getting a prompt.\"\"\"\n\n    name: str\n    \"\"\"The name of the prompt or prompt template.\"\"\"\n    arguments: dict[str, str] | None = None\n    \"\"\"Arguments to use for templating the prompt.\"\"\"\n\n\nclass GetPromptRequest(Request[GetPromptRequestParams, Literal[\"prompts/get\"]]):\n    \"\"\"Used by the client to get a prompt provided by the server.\"\"\"\n\n    method: Literal[\"prompts/get\"] = \"prompts/get\"\n    params: GetPromptRequestParams\n\n\nclass TextContent(MCPModel):\n    \"\"\"Text content for a message.\"\"\"\n\n    type: Literal[\"text\"] = \"text\"\n    text: str\n    \"\"\"The text content of the message.\"\"\"\n    annotations: Annotations | None = None\n    meta: Meta | None = Field(alias=\"_meta\", default=None)\n    \"\"\"\n    See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)\n    for notes on _meta usage.\n    \"\"\"\n\n\nclass ImageContent(MCPModel):\n    \"\"\"Image content for a message.\"\"\"\n\n    type: Literal[\"image\"] = \"image\"\n    data: str\n    \"\"\"The base64-encoded image data.\"\"\"\n    mime_type: str\n    \"\"\"\n    The MIME type of the image. Different providers may support different\n    image types.\n    \"\"\"\n    annotations: Annotations | None = None\n    meta: Meta | None = Field(alias=\"_meta\", default=None)\n    \"\"\"\n    See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)\n    for notes on _meta usage.\n    \"\"\"\n\n\nclass AudioContent(MCPModel):\n    \"\"\"Audio content for a message.\"\"\"\n\n    type: Literal[\"audio\"] = \"audio\"\n    data: str\n    \"\"\"The base64-encoded audio data.\"\"\"\n    mime_type: str\n    \"\"\"\n    The MIME type of the audio. Different providers may support different\n    audio types.\n    \"\"\"\n    annotations: Annotations | None = None\n    meta: Meta | None = Field(alias=\"_meta\", default=None)\n    \"\"\"\n    See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)\n    for notes on _meta usage.\n    \"\"\"\n\n\nclass ToolUseContent(MCPModel):\n    \"\"\"Content representing an assistant's request to invoke a tool.\n\n    This content type appears in assistant messages when the LLM wants to call a tool\n    during sampling. The server should execute the tool and return a ToolResultContent\n    in the next user message.\n    \"\"\"\n\n    type: Literal[\"tool_use\"] = \"tool_use\"\n    \"\"\"Discriminator for tool use content.\"\"\"\n\n    name: str\n    \"\"\"The name of the tool to invoke. Must match a tool name from the request's tools array.\"\"\"\n\n    id: str\n    \"\"\"Unique identifier for this tool call, used to correlate with ToolResultContent.\"\"\"\n\n    input: dict[str, Any]\n    \"\"\"Arguments to pass to the tool. Must conform to the tool's inputSchema.\"\"\"\n\n    meta: Meta | None = Field(alias=\"_meta\", default=None)\n    \"\"\"\n    See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)\n    for notes on _meta usage.\n    \"\"\"\n\n\nclass ToolResultContent(MCPModel):\n    \"\"\"Content representing the result of a tool execution.\n\n    This content type appears in user messages as a response to a ToolUseContent\n    from the assistant. It contains the output of executing the requested tool.\n    \"\"\"\n\n    type: Literal[\"tool_result\"] = \"tool_result\"\n    \"\"\"Discriminator for tool result content.\"\"\"\n\n    tool_use_id: str\n    \"\"\"The unique identifier that corresponds to the tool call's id field.\"\"\"\n\n    content: list[ContentBlock] = []\n    \"\"\"\n    A list of content objects representing the tool result.\n    Defaults to empty list if not provided.\n    \"\"\"\n\n    structured_content: dict[str, Any] | None = None\n    \"\"\"\n    Optional structured tool output that matches the tool's outputSchema (if defined).\n    \"\"\"\n\n    is_error: bool | None = None\n    \"\"\"Whether the tool execution resulted in an error.\"\"\"\n\n    meta: Meta | None = Field(alias=\"_meta\", default=None)\n    \"\"\"\n    See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)\n    for notes on _meta usage.\n    \"\"\"\n\n\nSamplingMessageContentBlock: TypeAlias = TextContent | ImageContent | AudioContent | ToolUseContent | ToolResultContent\n\"\"\"Content block types allowed in sampling messages.\"\"\"\n\nSamplingContent: TypeAlias = TextContent | ImageContent | AudioContent\n\"\"\"Basic content types for sampling responses (without tool use).\n\nUsed for backwards-compatible CreateMessageResult when tools are not used.\n\"\"\"\n\n\nclass SamplingMessage(MCPModel):\n    \"\"\"Describes a message issued to or received from an LLM API.\"\"\"\n\n    role: Role\n    content: SamplingMessageContentBlock | list[SamplingMessageContentBlock]\n    \"\"\"\n    Message content. Can be a single content block or an array of content blocks\n    for multi-modal messages and tool interactions.\n    \"\"\"\n    meta: Meta | None = Field(alias=\"_meta\", default=None)\n    \"\"\"\n    See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)\n    for notes on _meta usage.\n    \"\"\"\n\n    @property\n    def content_as_list(self) -> list[SamplingMessageContentBlock]:\n        \"\"\"Returns the content as a list of content blocks, regardless of whether\n        it was originally a single block or a list.\"\"\"\n        return self.content if isinstance(self.content, list) else [self.content]\n\n\nclass EmbeddedResource(MCPModel):\n    \"\"\"The contents of a resource, embedded into a prompt or tool call result.\n\n    It is up to the client how best to render embedded resources for the benefit\n    of the LLM and/or the user.\n    \"\"\"\n\n    type: Literal[\"resource\"] = \"resource\"\n    resource: TextResourceContents | BlobResourceContents\n    annotations: Annotations | None = None\n    meta: Meta | None = Field(alias=\"_meta\", default=None)\n    \"\"\"\n    See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)\n    for notes on _meta usage.\n    \"\"\"\n\n\nclass ResourceLink(Resource):\n    \"\"\"A resource that the server is capable of reading, included in a prompt or tool call result.\n\n    Note: resource links returned by tools are not guaranteed to appear in the results of `resources/list` requests.\n    \"\"\"\n\n    type: Literal[\"resource_link\"] = \"resource_link\"\n\n\nContentBlock = TextContent | ImageContent | AudioContent | ResourceLink | EmbeddedResource\n\"\"\"A content block that can be used in prompts and tool results.\"\"\"\n\n\nclass PromptMessage(MCPModel):\n    \"\"\"Describes a message returned as part of a prompt.\"\"\"\n\n    role: Role\n    content: ContentBlock\n\n\nclass GetPromptResult(Result):\n    \"\"\"The server's response to a prompts/get request from the client.\"\"\"\n\n    description: str | None = None\n    \"\"\"An optional description for the prompt.\"\"\"\n    messages: list[PromptMessage]\n\n\nclass PromptListChangedNotification(\n    Notification[NotificationParams | None, Literal[\"notifications/prompts/list_changed\"]]\n):\n    \"\"\"An optional notification from the server to the client, informing it that the list\n    of prompts it offers has changed.\n    \"\"\"\n\n    method: Literal[\"notifications/prompts/list_changed\"] = \"notifications/prompts/list_changed\"\n    params: NotificationParams | None = None\n\n\nclass ListToolsRequest(PaginatedRequest[Literal[\"tools/list\"]]):\n    \"\"\"Sent from the client to request a list of tools the server has.\"\"\"\n\n    method: Literal[\"tools/list\"] = \"tools/list\"\n\n\nclass ToolAnnotations(MCPModel):\n    \"\"\"Additional properties describing a Tool to clients.\n\n    NOTE: all properties in ToolAnnotations are **hints**.\n    They are not guaranteed to provide a faithful description of\n    tool behavior (including descriptive properties like `title`).\n\n    Clients should never make tool use decisions based on ToolAnnotations\n    received from untrusted servers.\n    \"\"\"\n\n    title: str | None = None\n    \"\"\"A human-readable title for the tool.\"\"\"\n\n    read_only_hint: bool | None = None\n    \"\"\"\n    If true, the tool does not modify its environment.\n    Default: false\n    \"\"\"\n\n    destructive_hint: bool | None = None\n    \"\"\"\n    If true, the tool may perform destructive updates to its environment.\n    If false, the tool performs only additive updates.\n    (This property is meaningful only when `read_only_hint == false`)\n    Default: true\n    \"\"\"\n\n    idempotent_hint: bool | None = None\n    \"\"\"\n    If true, calling the tool repeatedly with the same arguments\n    will have no additional effect on its environment.\n    (This property is meaningful only when `read_only_hint == false`)\n    Default: false\n    \"\"\"\n\n    open_world_hint: bool | None = None\n    \"\"\"\n    If true, this tool may interact with an \"open world\" of external\n    entities. If false, the tool's domain of interaction is closed.\n    For example, the world of a web search tool is open, whereas that\n    of a memory tool is not.\n    Default: true\n    \"\"\"\n\n\nclass ToolExecution(MCPModel):\n    \"\"\"Execution-related properties for a tool.\"\"\"\n\n    task_support: TaskExecutionMode | None = None\n    \"\"\"\n    Indicates whether this tool supports task-augmented execution.\n    This allows clients to handle long-running operations through polling\n    the task system.\n\n    - \"forbidden\": Tool does not support task-augmented execution (default when absent)\n    - \"optional\": Tool may support task-augmented execution\n    - \"required\": Tool requires task-augmented execution\n\n    Default: \"forbidden\"\n    \"\"\"\n\n\nclass Tool(BaseMetadata):\n    \"\"\"Definition for a tool the client can call.\"\"\"\n\n    description: str | None = None\n    \"\"\"A human-readable description of the tool.\"\"\"\n    input_schema: dict[str, Any]\n    \"\"\"A JSON Schema object defining the expected parameters for the tool.\"\"\"\n    output_schema: dict[str, Any] | None = None\n    \"\"\"\n    An optional JSON Schema object defining the structure of the tool's output\n    returned in the structured_content field of a CallToolResult.\n    \"\"\"\n    icons: list[Icon] | None = None\n    \"\"\"An optional list of icons for this tool.\"\"\"\n    annotations: ToolAnnotations | None = None\n    \"\"\"Optional additional tool information.\"\"\"\n    meta: Meta | None = Field(alias=\"_meta\", default=None)\n    \"\"\"\n    See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)\n    for notes on _meta usage.\n    \"\"\"\n\n    execution: ToolExecution | None = None\n\n\nclass ListToolsResult(PaginatedResult):\n    \"\"\"The server's response to a tools/list request from the client.\"\"\"\n\n    tools: list[Tool]\n\n\nclass CallToolRequestParams(RequestParams):\n    \"\"\"Parameters for calling a tool.\"\"\"\n\n    name: str\n    arguments: dict[str, Any] | None = None\n\n\nclass CallToolRequest(Request[CallToolRequestParams, Literal[\"tools/call\"]]):\n    \"\"\"Used by the client to invoke a tool provided by the server.\"\"\"\n\n    method: Literal[\"tools/call\"] = \"tools/call\"\n    params: CallToolRequestParams\n\n\nclass CallToolResult(Result):\n    \"\"\"The server's response to a tool call.\"\"\"\n\n    content: list[ContentBlock]\n    structured_content: dict[str, Any] | None = None\n    \"\"\"An optional JSON object that represents the structured result of the tool call.\"\"\"\n    is_error: bool = False\n\n\nclass ToolListChangedNotification(Notification[NotificationParams | None, Literal[\"notifications/tools/list_changed\"]]):\n    \"\"\"An optional notification from the server to the client, informing it that the list\n    of tools it offers has changed.\n    \"\"\"\n\n    method: Literal[\"notifications/tools/list_changed\"] = \"notifications/tools/list_changed\"\n    params: NotificationParams | None = None\n\n\nLoggingLevel = Literal[\"debug\", \"info\", \"notice\", \"warning\", \"error\", \"critical\", \"alert\", \"emergency\"]\n\n\nclass SetLevelRequestParams(RequestParams):\n    \"\"\"Parameters for setting the logging level.\"\"\"\n\n    level: LoggingLevel\n    \"\"\"The level of logging that the client wants to receive from the server.\"\"\"\n\n\nclass SetLevelRequest(Request[SetLevelRequestParams, Literal[\"logging/setLevel\"]]):\n    \"\"\"A request from the client to the server, to enable or adjust logging.\"\"\"\n\n    method: Literal[\"logging/setLevel\"] = \"logging/setLevel\"\n    params: SetLevelRequestParams\n\n\nclass LoggingMessageNotificationParams(NotificationParams):\n    \"\"\"Parameters for logging message notifications.\"\"\"\n\n    level: LoggingLevel\n    \"\"\"The severity of this log message.\"\"\"\n    logger: str | None = None\n    \"\"\"An optional name of the logger issuing this message.\"\"\"\n    data: Any\n    \"\"\"\n    The data to be logged, such as a string message or an object. Any JSON serializable\n    type is allowed here.\n    \"\"\"\n\n\nclass LoggingMessageNotification(Notification[LoggingMessageNotificationParams, Literal[\"notifications/message\"]]):\n    \"\"\"Notification of a log message passed from server to client.\"\"\"\n\n    method: Literal[\"notifications/message\"] = \"notifications/message\"\n    params: LoggingMessageNotificationParams\n\n\nIncludeContext = Literal[\"none\", \"thisServer\", \"allServers\"]\n\n\nclass ModelHint(MCPModel):\n    \"\"\"Hints to use for model selection.\"\"\"\n\n    name: str | None = None\n    \"\"\"A hint for a model name.\"\"\"\n\n\nclass ModelPreferences(MCPModel):\n    \"\"\"The server's preferences for model selection, requested by the client during\n    sampling.\n\n    Because LLMs can vary along multiple dimensions, choosing the \"best\" model is\n    rarely straightforward. Different models excel in different areas—some are\n    faster but less capable, others are more capable but more expensive, and so\n    on. This interface allows servers to express their priorities across multiple\n    dimensions to help clients make an appropriate selection for their use case.\n\n    These preferences are always advisory. The client MAY ignore them. It is also\n    up to the client to decide how to interpret these preferences and how to\n    balance them against other considerations.\n    \"\"\"\n\n    hints: list[ModelHint] | None = None\n    \"\"\"\n    Optional hints to use for model selection.\n\n    If multiple hints are specified, the client MUST evaluate them in order\n    (such that the first match is taken).\n\n    The client SHOULD prioritize these hints over the numeric priorities, but\n    MAY still use the priorities to select from ambiguous matches.\n    \"\"\"\n\n    cost_priority: float | None = None\n    \"\"\"\n    How much to prioritize cost when selecting a model. A value of 0 means cost\n    is not important, while a value of 1 means cost is the most important\n    factor.\n    \"\"\"\n\n    speed_priority: float | None = None\n    \"\"\"\n    How much to prioritize sampling speed (latency) when selecting a model. A\n    value of 0 means speed is not important, while a value of 1 means speed is\n    the most important factor.\n    \"\"\"\n\n    intelligence_priority: float | None = None\n    \"\"\"\n    How much to prioritize intelligence and capabilities when selecting a\n    model. A value of 0 means intelligence is not important, while a value of 1\n    means intelligence is the most important factor.\n    \"\"\"\n\n\nclass ToolChoice(MCPModel):\n    \"\"\"Controls tool usage behavior during sampling.\n\n    Allows the server to specify whether and how the LLM should use tools\n    in its response.\n    \"\"\"\n\n    mode: Literal[\"auto\", \"required\", \"none\"] | None = None\n    \"\"\"\n    Controls when tools are used:\n    - \"auto\": Model decides whether to use tools (default)\n    - \"required\": Model MUST use at least one tool before completing\n    - \"none\": Model should not use tools\n    \"\"\"\n\n\nclass CreateMessageRequestParams(RequestParams):\n    \"\"\"Parameters for creating a message.\"\"\"\n\n    messages: list[SamplingMessage]\n    model_preferences: ModelPreferences | None = None\n    \"\"\"\n    The server's preferences for which model to select. The client MAY ignore\n    these preferences.\n    \"\"\"\n    system_prompt: str | None = None\n    \"\"\"An optional system prompt the server wants to use for sampling.\"\"\"\n    include_context: IncludeContext | None = None\n    \"\"\"\n    A request to include context from one or more MCP servers (including the caller), to\n    be attached to the prompt.\n    \"\"\"\n    temperature: float | None = None\n    max_tokens: int\n    \"\"\"The maximum number of tokens to sample, as requested by the server.\"\"\"\n    stop_sequences: list[str] | None = None\n    metadata: dict[str, Any] | None = None\n    \"\"\"Optional metadata to pass through to the LLM provider.\"\"\"\n    tools: list[Tool] | None = None\n    \"\"\"\n    Tool definitions for the LLM to use during sampling.\n    Requires clientCapabilities.sampling.tools to be present.\n    \"\"\"\n    tool_choice: ToolChoice | None = None\n    \"\"\"\n    Controls tool usage behavior.\n    Requires clientCapabilities.sampling.tools and the tools parameter to be present.\n    \"\"\"\n\n\nclass CreateMessageRequest(Request[CreateMessageRequestParams, Literal[\"sampling/createMessage\"]]):\n    \"\"\"A request from the server to sample an LLM via the client.\"\"\"\n\n    method: Literal[\"sampling/createMessage\"] = \"sampling/createMessage\"\n    params: CreateMessageRequestParams\n\n\nStopReason = Literal[\"endTurn\", \"stopSequence\", \"maxTokens\", \"toolUse\"] | str\n\n\nclass CreateMessageResult(Result):\n    \"\"\"The client's response to a sampling/createMessage request from the server.\n\n    This is the backwards-compatible version that returns single content (no arrays).\n    Used when the request does not include tools.\n    \"\"\"\n\n    role: Role\n    \"\"\"The role of the message sender (typically 'assistant' for LLM responses).\"\"\"\n    content: SamplingContent\n    \"\"\"Response content. Single content block (text, image, or audio).\"\"\"\n    model: str\n    \"\"\"The name of the model that generated the message.\"\"\"\n    stop_reason: StopReason | None = None\n    \"\"\"The reason why sampling stopped, if known.\"\"\"\n\n\nclass CreateMessageResultWithTools(Result):\n    \"\"\"The client's response to a sampling/createMessage request when tools were provided.\n\n    This version supports array content for tool use flows.\n    \"\"\"\n\n    role: Role\n    \"\"\"The role of the message sender (typically 'assistant' for LLM responses).\"\"\"\n    content: SamplingMessageContentBlock | list[SamplingMessageContentBlock]\n    \"\"\"\n    Response content. May be a single content block or an array.\n    May include ToolUseContent if stop_reason is 'toolUse'.\n    \"\"\"\n    model: str\n    \"\"\"The name of the model that generated the message.\"\"\"\n    stop_reason: StopReason | None = None\n    \"\"\"\n    The reason why sampling stopped, if known.\n    'toolUse' indicates the model wants to use a tool.\n    \"\"\"\n\n    @property\n    def content_as_list(self) -> list[SamplingMessageContentBlock]:\n        \"\"\"Returns the content as a list of content blocks, regardless of whether\n        it was originally a single block or a list.\"\"\"\n        return self.content if isinstance(self.content, list) else [self.content]\n\n\nclass ResourceTemplateReference(MCPModel):\n    \"\"\"A reference to a resource or resource template definition.\"\"\"\n\n    type: Literal[\"ref/resource\"] = \"ref/resource\"\n    uri: str\n    \"\"\"The URI or URI template of the resource.\"\"\"\n\n\nclass PromptReference(MCPModel):\n    \"\"\"Identifies a prompt.\"\"\"\n\n    type: Literal[\"ref/prompt\"] = \"ref/prompt\"\n    name: str\n    \"\"\"The name of the prompt or prompt template.\"\"\"\n\n\nclass CompletionArgument(MCPModel):\n    \"\"\"The argument's information for completion requests.\"\"\"\n\n    name: str\n    \"\"\"The name of the argument.\"\"\"\n    value: str\n    \"\"\"The value of the argument to use for completion matching.\"\"\"\n\n\nclass CompletionContext(MCPModel):\n    \"\"\"Additional, optional context for completions.\"\"\"\n\n    arguments: dict[str, str] | None = None\n    \"\"\"Previously-resolved variables in a URI template or prompt.\"\"\"\n\n\nclass CompleteRequestParams(RequestParams):\n    \"\"\"Parameters for completion requests.\"\"\"\n\n    ref: ResourceTemplateReference | PromptReference\n    argument: CompletionArgument\n    context: CompletionContext | None = None\n    \"\"\"Additional, optional context for completions.\"\"\"\n\n\nclass CompleteRequest(Request[CompleteRequestParams, Literal[\"completion/complete\"]]):\n    \"\"\"A request from the client to the server, to ask for completion options.\"\"\"\n\n    method: Literal[\"completion/complete\"] = \"completion/complete\"\n    params: CompleteRequestParams\n\n\nclass Completion(MCPModel):\n    \"\"\"Completion information.\"\"\"\n\n    values: list[str]\n    \"\"\"An array of completion values. Must not exceed 100 items.\"\"\"\n    total: int | None = None\n    \"\"\"\n    The total number of completion options available. This can exceed the number of\n    values actually sent in the response.\n    \"\"\"\n    has_more: bool | None = None\n    \"\"\"\n    Indicates whether there are additional completion options beyond those provided in\n    the current response, even if the exact total is unknown.\n    \"\"\"\n\n\nclass CompleteResult(Result):\n    \"\"\"The server's response to a completion/complete request.\"\"\"\n\n    completion: Completion\n\n\nclass ListRootsRequest(Request[RequestParams | None, Literal[\"roots/list\"]]):\n    \"\"\"Sent from the server to request a list of root URIs from the client. Roots allow\n    servers to ask for specific directories or files to operate on. A common example\n    for roots is providing a set of repositories or directories a server should operate\n    on.\n\n    This request is typically used when the server needs to understand the file system\n    structure or access specific locations that the client has permission to read from.\n    \"\"\"\n\n    method: Literal[\"roots/list\"] = \"roots/list\"\n    params: RequestParams | None = None\n\n\nclass Root(MCPModel):\n    \"\"\"Represents a root directory or file that the server can operate on.\"\"\"\n\n    uri: FileUrl\n    \"\"\"\n    The URI identifying the root. This *must* start with file:// for now.\n    This restriction may be relaxed in future versions of the protocol to allow\n    other URI schemes.\n    \"\"\"\n    name: str | None = None\n    \"\"\"\n    An optional name for the root. This can be used to provide a human-readable\n    identifier for the root, which may be useful for display purposes or for\n    referencing the root in other parts of the application.\n    \"\"\"\n    meta: Meta | None = Field(alias=\"_meta\", default=None)\n    \"\"\"\n    See [MCP specification](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/47339c03c143bb4ec01a26e721a1b8fe66634ebe/docs/specification/draft/basic/index.mdx#general-fields)\n    for notes on _meta usage.\n    \"\"\"\n\n\nclass ListRootsResult(Result):\n    \"\"\"The client's response to a roots/list request from the server.\n\n    This result contains an array of Root objects, each representing a root directory\n    or file that the server can operate on.\n    \"\"\"\n\n    roots: list[Root]\n\n\nclass RootsListChangedNotification(\n    Notification[NotificationParams | None, Literal[\"notifications/roots/list_changed\"]]\n):\n    \"\"\"A notification from the client to the server, informing it that the list of\n    roots has changed.\n\n    This notification should be sent whenever the client adds, removes, or\n    modifies any root. The server should then request an updated list of roots\n    using the ListRootsRequest.\n    \"\"\"\n\n    method: Literal[\"notifications/roots/list_changed\"] = \"notifications/roots/list_changed\"\n    params: NotificationParams | None = None\n\n\nclass CancelledNotificationParams(NotificationParams):\n    \"\"\"Parameters for cancellation notifications.\"\"\"\n\n    request_id: RequestId | None = None\n    \"\"\"\n    The ID of the request to cancel.\n\n    This MUST correspond to the ID of a request previously issued in the same direction.\n    This MUST be provided for cancelling non-task requests.\n    This MUST NOT be used for cancelling tasks (use the `tasks/cancel` request instead).\n    \"\"\"\n    reason: str | None = None\n    \"\"\"An optional string describing the reason for the cancellation.\"\"\"\n\n\nclass CancelledNotification(Notification[CancelledNotificationParams, Literal[\"notifications/cancelled\"]]):\n    \"\"\"This notification can be sent by either side to indicate that it is canceling a\n    previously-issued request.\n    \"\"\"\n\n    method: Literal[\"notifications/cancelled\"] = \"notifications/cancelled\"\n    params: CancelledNotificationParams\n\n\nclass ElicitCompleteNotificationParams(NotificationParams):\n    \"\"\"Parameters for elicitation completion notifications.\"\"\"\n\n    elicitation_id: str\n    \"\"\"The unique identifier of the elicitation that was completed.\"\"\"\n\n\nclass ElicitCompleteNotification(\n    Notification[ElicitCompleteNotificationParams, Literal[\"notifications/elicitation/complete\"]]\n):\n    \"\"\"A notification from the server to the client, informing it that a URL mode\n    elicitation has been completed.\n\n    Clients MAY use the notification to automatically retry requests that received a\n    URLElicitationRequiredError, update the user interface, or otherwise continue\n    an interaction. However, because delivery of the notification is not guaranteed,\n    clients must not wait indefinitely for a notification from the server.\n    \"\"\"\n\n    method: Literal[\"notifications/elicitation/complete\"] = \"notifications/elicitation/complete\"\n    params: ElicitCompleteNotificationParams\n\n\nClientRequest = (\n    PingRequest\n    | InitializeRequest\n    | CompleteRequest\n    | SetLevelRequest\n    | GetPromptRequest\n    | ListPromptsRequest\n    | ListResourcesRequest\n    | ListResourceTemplatesRequest\n    | ReadResourceRequest\n    | SubscribeRequest\n    | UnsubscribeRequest\n    | CallToolRequest\n    | ListToolsRequest\n    | GetTaskRequest\n    | GetTaskPayloadRequest\n    | ListTasksRequest\n    | CancelTaskRequest\n)\nclient_request_adapter = TypeAdapter[ClientRequest](ClientRequest)\n\n\nClientNotification = (\n    CancelledNotification\n    | ProgressNotification\n    | InitializedNotification\n    | RootsListChangedNotification\n    | TaskStatusNotification\n)\nclient_notification_adapter = TypeAdapter[ClientNotification](ClientNotification)\n\n\n# Type for elicitation schema - a JSON Schema dict\nElicitRequestedSchema: TypeAlias = dict[str, Any]\n\"\"\"Schema for elicitation requests.\"\"\"\n\n\nclass ElicitRequestFormParams(RequestParams):\n    \"\"\"Parameters for form mode elicitation requests.\n\n    Form mode collects non-sensitive information from the user via an in-band form\n    rendered by the client.\n    \"\"\"\n\n    mode: Literal[\"form\"] = \"form\"\n    \"\"\"The elicitation mode (always \"form\" for this type).\"\"\"\n\n    message: str\n    \"\"\"The message to present to the user describing what information is being requested.\"\"\"\n\n    requested_schema: ElicitRequestedSchema\n    \"\"\"\n    A restricted subset of JSON Schema defining the structure of the expected response.\n    Only top-level properties are allowed, without nesting.\n    \"\"\"\n\n\nclass ElicitRequestURLParams(RequestParams):\n    \"\"\"Parameters for URL mode elicitation requests.\n\n    URL mode directs users to external URLs for sensitive out-of-band interactions\n    like OAuth flows, credential collection, or payment processing.\n    \"\"\"\n\n    mode: Literal[\"url\"] = \"url\"\n    \"\"\"The elicitation mode (always \"url\" for this type).\"\"\"\n\n    message: str\n    \"\"\"The message to present to the user explaining why the interaction is needed.\"\"\"\n\n    url: str\n    \"\"\"The URL that the user should navigate to.\"\"\"\n\n    elicitation_id: str\n    \"\"\"The ID of the elicitation, which must be unique within the context of the server.\n\n    The client MUST treat this ID as an opaque value.\n    \"\"\"\n\n\n# Union type for elicitation request parameters\nElicitRequestParams: TypeAlias = ElicitRequestURLParams | ElicitRequestFormParams\n\"\"\"Parameters for elicitation requests - either form or URL mode.\"\"\"\n\n\nclass ElicitRequest(Request[ElicitRequestParams, Literal[\"elicitation/create\"]]):\n    \"\"\"A request from the server to elicit information from the client.\"\"\"\n\n    method: Literal[\"elicitation/create\"] = \"elicitation/create\"\n    params: ElicitRequestParams\n\n\nclass ElicitResult(Result):\n    \"\"\"The client's response to an elicitation request.\"\"\"\n\n    action: Literal[\"accept\", \"decline\", \"cancel\"]\n    \"\"\"\n    The user action in response to the elicitation.\n    - \"accept\": User submitted the form/confirmed the action (or consented to URL navigation)\n    - \"decline\": User explicitly declined the action\n    - \"cancel\": User dismissed without making an explicit choice\n    \"\"\"\n\n    content: dict[str, str | int | float | bool | list[str] | None] | None = None\n    \"\"\"\n    The submitted form data, only present when action is \"accept\" in form mode.\n    Contains values matching the requested schema. Values can be strings, integers, floats,\n    booleans, arrays of strings, or null.\n    For URL mode, this field is omitted.\n    \"\"\"\n\n\nclass ElicitationRequiredErrorData(MCPModel):\n    \"\"\"Error data for URLElicitationRequiredError.\n\n    Servers return this when a request cannot be processed until one or more\n    URL mode elicitations are completed.\n    \"\"\"\n\n    elicitations: list[ElicitRequestURLParams]\n    \"\"\"List of URL mode elicitations that must be completed.\"\"\"\n\n\nClientResult = (\n    EmptyResult\n    | CreateMessageResult\n    | CreateMessageResultWithTools\n    | ListRootsResult\n    | ElicitResult\n    | GetTaskResult\n    | GetTaskPayloadResult\n    | ListTasksResult\n    | CancelTaskResult\n    | CreateTaskResult\n)\nclient_result_adapter = TypeAdapter[ClientResult](ClientResult)\n\n\nServerRequest = (\n    PingRequest\n    | CreateMessageRequest\n    | ListRootsRequest\n    | ElicitRequest\n    | GetTaskRequest\n    | GetTaskPayloadRequest\n    | ListTasksRequest\n    | CancelTaskRequest\n)\nserver_request_adapter = TypeAdapter[ServerRequest](ServerRequest)\n\n\nServerNotification = (\n    CancelledNotification\n    | ProgressNotification\n    | LoggingMessageNotification\n    | ResourceUpdatedNotification\n    | ResourceListChangedNotification\n    | ToolListChangedNotification\n    | PromptListChangedNotification\n    | ElicitCompleteNotification\n    | TaskStatusNotification\n)\nserver_notification_adapter = TypeAdapter[ServerNotification](ServerNotification)\n\n\nServerResult = (\n    EmptyResult\n    | InitializeResult\n    | CompleteResult\n    | GetPromptResult\n    | ListPromptsResult\n    | ListResourcesResult\n    | ListResourceTemplatesResult\n    | ReadResourceResult\n    | CallToolResult\n    | ListToolsResult\n    | GetTaskResult\n    | GetTaskPayloadResult\n    | ListTasksResult\n    | CancelTaskResult\n    | CreateTaskResult\n)\nserver_result_adapter = TypeAdapter[ServerResult](ServerResult)\n"
  },
  {
    "path": "src/mcp/types/jsonrpc.py",
    "content": "\"\"\"This module follows the JSON-RPC 2.0 specification: https://www.jsonrpc.org/specification.\"\"\"\n\nfrom __future__ import annotations\n\nfrom typing import Annotated, Any, Literal\n\nfrom pydantic import BaseModel, Field, TypeAdapter\n\nRequestId = Annotated[int, Field(strict=True)] | str\n\"\"\"The ID of a JSON-RPC request.\"\"\"\n\n\nclass JSONRPCRequest(BaseModel):\n    \"\"\"A JSON-RPC request that expects a response.\"\"\"\n\n    jsonrpc: Literal[\"2.0\"]\n    id: RequestId\n    method: str\n    params: dict[str, Any] | None = None\n\n\nclass JSONRPCNotification(BaseModel):\n    \"\"\"A JSON-RPC notification which does not expect a response.\"\"\"\n\n    jsonrpc: Literal[\"2.0\"]\n    method: str\n    params: dict[str, Any] | None = None\n\n\n# TODO(Marcelo): This is actually not correct. A JSONRPCResponse is the union of a successful response and an error.\nclass JSONRPCResponse(BaseModel):\n    \"\"\"A successful (non-error) response to a request.\"\"\"\n\n    jsonrpc: Literal[\"2.0\"]\n    id: RequestId\n    result: dict[str, Any]\n\n\n# MCP-specific error codes in the range [-32000, -32099]\nURL_ELICITATION_REQUIRED = -32042\n\"\"\"Error code indicating that a URL mode elicitation is required before the request can be processed.\"\"\"\n\n# SDK error codes\nCONNECTION_CLOSED = -32000\nREQUEST_TIMEOUT = -32001\n\n# Standard JSON-RPC error codes\nPARSE_ERROR = -32700\nINVALID_REQUEST = -32600\nMETHOD_NOT_FOUND = -32601\nINVALID_PARAMS = -32602\nINTERNAL_ERROR = -32603\n\n\nclass ErrorData(BaseModel):\n    \"\"\"Error information for JSON-RPC error responses.\"\"\"\n\n    code: int\n    \"\"\"The error type that occurred.\"\"\"\n\n    message: str\n    \"\"\"A short description of the error.\n\n    The message SHOULD be limited to a concise single sentence.\n    \"\"\"\n\n    data: Any = None\n    \"\"\"Additional information about the error.\n\n    The value of this member is defined by the sender (e.g. detailed error information, nested errors, etc.).\n    \"\"\"\n\n\nclass JSONRPCError(BaseModel):\n    \"\"\"A response to a request that indicates an error occurred.\"\"\"\n\n    jsonrpc: Literal[\"2.0\"]\n    id: RequestId | None\n    error: ErrorData\n\n\nJSONRPCMessage = JSONRPCRequest | JSONRPCNotification | JSONRPCResponse | JSONRPCError\njsonrpc_message_adapter: TypeAdapter[JSONRPCMessage] = TypeAdapter(JSONRPCMessage)\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/cli/__init__.py",
    "content": ""
  },
  {
    "path": "tests/cli/test_claude.py",
    "content": "\"\"\"Tests for mcp.cli.claude — Claude Desktop config file generation.\"\"\"\n\nimport json\nfrom pathlib import Path\nfrom typing import Any\n\nimport pytest\n\nfrom mcp.cli.claude import get_uv_path, update_claude_config\n\n\n@pytest.fixture\ndef config_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:\n    \"\"\"Temp Claude config dir with get_claude_config_path and get_uv_path mocked.\"\"\"\n    claude_dir = tmp_path / \"Claude\"\n    claude_dir.mkdir()\n    monkeypatch.setattr(\"mcp.cli.claude.get_claude_config_path\", lambda: claude_dir)\n    monkeypatch.setattr(\"mcp.cli.claude.get_uv_path\", lambda: \"/fake/bin/uv\")\n    return claude_dir\n\n\ndef _read_server(config_dir: Path, name: str) -> dict[str, Any]:\n    config = json.loads((config_dir / \"claude_desktop_config.json\").read_text())\n    return config[\"mcpServers\"][name]\n\n\ndef test_generates_uv_run_command(config_dir: Path):\n    \"\"\"Should write a uv run command that invokes mcp run on the resolved file spec.\"\"\"\n    assert update_claude_config(file_spec=\"server.py:app\", server_name=\"my_server\")\n\n    resolved = Path(\"server.py\").resolve()\n    assert _read_server(config_dir, \"my_server\") == {\n        \"command\": \"/fake/bin/uv\",\n        \"args\": [\"run\", \"--frozen\", \"--with\", \"mcp[cli]\", \"mcp\", \"run\", f\"{resolved}:app\"],\n    }\n\n\ndef test_file_spec_without_object_suffix(config_dir: Path):\n    \"\"\"File specs without :object should still resolve to an absolute path.\"\"\"\n    assert update_claude_config(file_spec=\"server.py\", server_name=\"s\")\n\n    assert _read_server(config_dir, \"s\")[\"args\"][-1] == str(Path(\"server.py\").resolve())\n\n\ndef test_with_packages_sorted_and_deduplicated(config_dir: Path):\n    \"\"\"Extra packages should appear as --with flags, sorted and deduplicated with mcp[cli].\"\"\"\n    assert update_claude_config(file_spec=\"s.py:app\", server_name=\"s\", with_packages=[\"zebra\", \"aardvark\", \"zebra\"])\n\n    args = _read_server(config_dir, \"s\")[\"args\"]\n    assert args[:8] == [\"run\", \"--frozen\", \"--with\", \"aardvark\", \"--with\", \"mcp[cli]\", \"--with\", \"zebra\"]\n\n\ndef test_with_editable_adds_flag(config_dir: Path, tmp_path: Path):\n    \"\"\"with_editable should add --with-editable after the --with flags.\"\"\"\n    editable = tmp_path / \"project\"\n    assert update_claude_config(file_spec=\"s.py:app\", server_name=\"s\", with_editable=editable)\n\n    args = _read_server(config_dir, \"s\")[\"args\"]\n    assert args[4:6] == [\"--with-editable\", str(editable)]\n\n\ndef test_env_vars_written(config_dir: Path):\n    \"\"\"env_vars should be written under the server's env key.\"\"\"\n    assert update_claude_config(file_spec=\"s.py:app\", server_name=\"s\", env_vars={\"KEY\": \"val\"})\n\n    assert _read_server(config_dir, \"s\")[\"env\"] == {\"KEY\": \"val\"}\n\n\ndef test_existing_env_vars_merged_new_wins(config_dir: Path):\n    \"\"\"Re-installing should merge env vars, with new values overriding existing ones.\"\"\"\n    (config_dir / \"claude_desktop_config.json\").write_text(\n        json.dumps({\"mcpServers\": {\"s\": {\"env\": {\"OLD\": \"keep\", \"KEY\": \"old\"}}}})\n    )\n\n    assert update_claude_config(file_spec=\"s.py:app\", server_name=\"s\", env_vars={\"KEY\": \"new\"})\n\n    assert _read_server(config_dir, \"s\")[\"env\"] == {\"OLD\": \"keep\", \"KEY\": \"new\"}\n\n\ndef test_existing_env_vars_preserved_without_new(config_dir: Path):\n    \"\"\"Re-installing without env_vars should keep the existing env block intact.\"\"\"\n    (config_dir / \"claude_desktop_config.json\").write_text(json.dumps({\"mcpServers\": {\"s\": {\"env\": {\"KEEP\": \"me\"}}}}))\n\n    assert update_claude_config(file_spec=\"s.py:app\", server_name=\"s\")\n\n    assert _read_server(config_dir, \"s\")[\"env\"] == {\"KEEP\": \"me\"}\n\n\ndef test_other_servers_preserved(config_dir: Path):\n    \"\"\"Installing a new server should not clobber existing mcpServers entries.\"\"\"\n    (config_dir / \"claude_desktop_config.json\").write_text(json.dumps({\"mcpServers\": {\"other\": {\"command\": \"x\"}}}))\n\n    assert update_claude_config(file_spec=\"s.py:app\", server_name=\"s\")\n\n    config = json.loads((config_dir / \"claude_desktop_config.json\").read_text())\n    assert set(config[\"mcpServers\"]) == {\"other\", \"s\"}\n    assert config[\"mcpServers\"][\"other\"] == {\"command\": \"x\"}\n\n\ndef test_raises_when_config_dir_missing(monkeypatch: pytest.MonkeyPatch):\n    \"\"\"Should raise RuntimeError when Claude Desktop config dir can't be found.\"\"\"\n    monkeypatch.setattr(\"mcp.cli.claude.get_claude_config_path\", lambda: None)\n    monkeypatch.setattr(\"mcp.cli.claude.get_uv_path\", lambda: \"/fake/bin/uv\")\n\n    with pytest.raises(RuntimeError, match=\"Claude Desktop config directory not found\"):\n        update_claude_config(file_spec=\"s.py:app\", server_name=\"s\")\n\n\n@pytest.mark.parametrize(\"which_result, expected\", [(\"/usr/local/bin/uv\", \"/usr/local/bin/uv\"), (None, \"uv\")])\ndef test_get_uv_path(monkeypatch: pytest.MonkeyPatch, which_result: str | None, expected: str):\n    \"\"\"Should return shutil.which's result, or fall back to bare 'uv' when not on PATH.\"\"\"\n\n    def fake_which(cmd: str) -> str | None:\n        return which_result\n\n    monkeypatch.setattr(\"shutil.which\", fake_which)\n    assert get_uv_path() == expected\n\n\n@pytest.mark.parametrize(\n    \"file_spec, expected_last_arg\",\n    [\n        (\"C:\\\\Users\\\\server.py\", \"C:\\\\Users\\\\server.py\"),\n        (\"C:\\\\Users\\\\server.py:app\", \"C:\\\\Users\\\\server.py:app\"),\n    ],\n)\ndef test_windows_drive_letter_not_split(\n    config_dir: Path, monkeypatch: pytest.MonkeyPatch, file_spec: str, expected_last_arg: str\n):\n    \"\"\"Drive-letter paths like 'C:\\\\server.py' must not be split on the drive colon.\n\n    Before the fix, a bare 'C:\\\\path\\\\server.py' would hit rsplit(\":\", 1) and yield\n    (\"C\", \"\\\\path\\\\server.py\"), calling resolve() on Path(\"C\") instead of the full path.\n    \"\"\"\n    seen: list[str] = []\n\n    def fake_resolve(self: Path) -> Path:\n        seen.append(str(self))\n        return self\n\n    monkeypatch.setattr(Path, \"resolve\", fake_resolve)\n\n    assert update_claude_config(file_spec=file_spec, server_name=\"s\")\n\n    assert seen == [\"C:\\\\Users\\\\server.py\"]\n    assert _read_server(config_dir, \"s\")[\"args\"][-1] == expected_last_arg\n"
  },
  {
    "path": "tests/cli/test_utils.py",
    "content": "import subprocess\nimport sys\nfrom pathlib import Path\nfrom typing import Any\n\nimport pytest\n\nfrom mcp.cli.cli import _build_uv_command, _get_npx_command, _parse_file_path  # type: ignore[reportPrivateUsage]\n\n\n@pytest.mark.parametrize(\n    \"spec, expected_obj\",\n    [\n        (\"server.py\", None),\n        (\"foo.py:srv_obj\", \"srv_obj\"),\n    ],\n)\ndef test_parse_file_path_accepts_valid_specs(tmp_path: Path, spec: str, expected_obj: str | None):\n    \"\"\"Should accept valid file specs.\"\"\"\n    file = tmp_path / spec.split(\":\")[0]\n    file.write_text(\"x = 1\")\n    path, obj = _parse_file_path(f\"{file}:{expected_obj}\" if \":\" in spec else str(file))\n    assert path == file.resolve()\n    assert obj == expected_obj\n\n\ndef test_parse_file_path_missing(tmp_path: Path):\n    \"\"\"Should system exit if a file is missing.\"\"\"\n    with pytest.raises(SystemExit):\n        _parse_file_path(str(tmp_path / \"missing.py\"))\n\n\ndef test_parse_file_exit_on_dir(tmp_path: Path):\n    \"\"\"Should system exit if a directory is passed\"\"\"\n    dir_path = tmp_path / \"dir\"\n    dir_path.mkdir()\n    with pytest.raises(SystemExit):\n        _parse_file_path(str(dir_path))\n\n\ndef test_build_uv_command_minimal():\n    \"\"\"Should emit core command when no extras specified.\"\"\"\n    cmd = _build_uv_command(\"foo.py\")\n    assert cmd == [\"uv\", \"run\", \"--with\", \"mcp\", \"mcp\", \"run\", \"foo.py\"]\n\n\ndef test_build_uv_command_adds_editable_and_packages():\n    \"\"\"Should include --with-editable and every --with pkg in correct order.\"\"\"\n    test_path = Path(\"/pkg\")\n    cmd = _build_uv_command(\n        \"foo.py\",\n        with_editable=test_path,\n        with_packages=[\"package1\", \"package2\"],\n    )\n    assert cmd == [\n        \"uv\",\n        \"run\",\n        \"--with\",\n        \"mcp\",\n        \"--with-editable\",\n        str(test_path),  # Use str() to match what the function does\n        \"--with\",\n        \"package1\",\n        \"--with\",\n        \"package2\",\n        \"mcp\",\n        \"run\",\n        \"foo.py\",\n    ]\n\n\ndef test_get_npx_unix_like(monkeypatch: pytest.MonkeyPatch):\n    \"\"\"Should return \"npx\" on unix-like systems.\"\"\"\n    monkeypatch.setattr(sys, \"platform\", \"linux\")\n    assert _get_npx_command() == \"npx\"\n\n\ndef test_get_npx_windows(monkeypatch: pytest.MonkeyPatch):\n    \"\"\"Should return one of the npx candidates on Windows.\"\"\"\n    candidates = [\"npx.cmd\", \"npx.exe\", \"npx\"]\n\n    def fake_run(cmd: list[str], **kw: Any) -> subprocess.CompletedProcess[bytes]:\n        if cmd[0] in candidates:\n            return subprocess.CompletedProcess(cmd, 0)\n        else:  # pragma: no cover\n            raise subprocess.CalledProcessError(1, cmd[0])\n\n    monkeypatch.setattr(sys, \"platform\", \"win32\")\n    monkeypatch.setattr(subprocess, \"run\", fake_run)\n    assert _get_npx_command() in candidates\n\n\ndef test_get_npx_returns_none_when_npx_missing(monkeypatch: pytest.MonkeyPatch):\n    \"\"\"Should give None if every candidate fails.\"\"\"\n    monkeypatch.setattr(sys, \"platform\", \"win32\", raising=False)\n\n    def always_fail(*args: Any, **kwargs: Any) -> subprocess.CompletedProcess[bytes]:\n        raise subprocess.CalledProcessError(1, args[0])\n\n    monkeypatch.setattr(subprocess, \"run\", always_fail)\n    assert _get_npx_command() is None\n"
  },
  {
    "path": "tests/client/__init__.py",
    "content": ""
  },
  {
    "path": "tests/client/auth/extensions/test_client_credentials.py",
    "content": "import urllib.parse\nimport warnings\n\nimport jwt\nimport pytest\nfrom pydantic import AnyHttpUrl, AnyUrl\n\nfrom mcp.client.auth.extensions.client_credentials import (\n    ClientCredentialsOAuthProvider,\n    JWTParameters,\n    PrivateKeyJWTOAuthProvider,\n    RFC7523OAuthClientProvider,\n    SignedJWTParameters,\n    static_assertion_provider,\n)\nfrom mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthMetadata, OAuthToken\n\n\nclass MockTokenStorage:\n    \"\"\"Mock token storage for testing.\"\"\"\n\n    def __init__(self):\n        self._tokens: OAuthToken | None = None\n        self._client_info: OAuthClientInformationFull | None = None\n\n    async def get_tokens(self) -> OAuthToken | None:\n        return self._tokens\n\n    async def set_tokens(self, tokens: OAuthToken) -> None:  # pragma: no cover\n        self._tokens = tokens\n\n    async def get_client_info(self) -> OAuthClientInformationFull | None:  # pragma: no cover\n        return self._client_info\n\n    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:  # pragma: no cover\n        self._client_info = client_info\n\n\n@pytest.fixture\ndef mock_storage():\n    return MockTokenStorage()\n\n\n@pytest.fixture\ndef client_metadata():\n    return OAuthClientMetadata(\n        client_name=\"Test Client\",\n        client_uri=AnyHttpUrl(\"https://example.com\"),\n        redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n        scope=\"read write\",\n    )\n\n\n@pytest.fixture\ndef rfc7523_oauth_provider(client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage):\n    async def redirect_handler(url: str) -> None:  # pragma: no cover\n        \"\"\"Mock redirect handler.\"\"\"\n        pass\n\n    async def callback_handler() -> tuple[str, str | None]:  # pragma: no cover\n        \"\"\"Mock callback handler.\"\"\"\n        return \"test_auth_code\", \"test_state\"\n\n    with warnings.catch_warnings():\n        warnings.simplefilter(\"ignore\", DeprecationWarning)\n        return RFC7523OAuthClientProvider(\n            server_url=\"https://api.example.com/v1/mcp\",\n            client_metadata=client_metadata,\n            storage=mock_storage,\n            redirect_handler=redirect_handler,\n            callback_handler=callback_handler,\n        )\n\n\nclass TestOAuthFlowClientCredentials:\n    \"\"\"Test OAuth flow behavior for client credentials flows.\"\"\"\n\n    @pytest.mark.anyio\n    async def test_token_exchange_request_jwt_predefined(self, rfc7523_oauth_provider: RFC7523OAuthClientProvider):\n        \"\"\"Test token exchange request building with a predefined JWT assertion.\"\"\"\n        # Set up required context\n        rfc7523_oauth_provider.context.client_info = OAuthClientInformationFull(\n            grant_types=[\"urn:ietf:params:oauth:grant-type:jwt-bearer\"],\n            token_endpoint_auth_method=\"private_key_jwt\",\n            redirect_uris=None,\n            scope=\"read write\",\n        )\n        rfc7523_oauth_provider.context.oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://api.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://api.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://api.example.com/token\"),\n            registration_endpoint=AnyHttpUrl(\"https://api.example.com/register\"),\n        )\n        rfc7523_oauth_provider.context.client_metadata = rfc7523_oauth_provider.context.client_info\n        rfc7523_oauth_provider.context.protocol_version = \"2025-06-18\"\n        rfc7523_oauth_provider.jwt_parameters = JWTParameters(\n            # https://www.jwt.io\n            assertion=\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30\"\n        )\n\n        request = await rfc7523_oauth_provider._exchange_token_jwt_bearer()\n\n        assert request.method == \"POST\"\n        assert str(request.url) == \"https://api.example.com/token\"\n        assert request.headers[\"Content-Type\"] == \"application/x-www-form-urlencoded\"\n\n        # Check form data\n        content = urllib.parse.unquote_plus(request.content.decode())\n        assert \"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer\" in content\n        assert \"scope=read write\" in content\n        assert \"resource=https://api.example.com/v1/mcp\" in content\n        assert (\n            \"assertion=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30\"\n            in content\n        )\n\n    @pytest.mark.anyio\n    async def test_token_exchange_request_jwt(self, rfc7523_oauth_provider: RFC7523OAuthClientProvider):\n        \"\"\"Test token exchange request building wiith a generated JWT assertion.\"\"\"\n        # Set up required context\n        rfc7523_oauth_provider.context.client_info = OAuthClientInformationFull(\n            grant_types=[\"urn:ietf:params:oauth:grant-type:jwt-bearer\"],\n            token_endpoint_auth_method=\"private_key_jwt\",\n            redirect_uris=None,\n            scope=\"read write\",\n        )\n        rfc7523_oauth_provider.context.oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://api.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://api.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://api.example.com/token\"),\n            registration_endpoint=AnyHttpUrl(\"https://api.example.com/register\"),\n        )\n        rfc7523_oauth_provider.context.client_metadata = rfc7523_oauth_provider.context.client_info\n        rfc7523_oauth_provider.context.protocol_version = \"2025-06-18\"\n        rfc7523_oauth_provider.jwt_parameters = JWTParameters(\n            issuer=\"foo\",\n            subject=\"1234567890\",\n            claims={\n                \"name\": \"John Doe\",\n                \"admin\": True,\n                \"iat\": 1516239022,\n            },\n            jwt_signing_algorithm=\"HS256\",\n            jwt_signing_key=\"a-string-secret-at-least-256-bits-long\",\n            jwt_lifetime_seconds=300,\n        )\n\n        request = await rfc7523_oauth_provider._exchange_token_jwt_bearer()\n\n        assert request.method == \"POST\"\n        assert str(request.url) == \"https://api.example.com/token\"\n        assert request.headers[\"Content-Type\"] == \"application/x-www-form-urlencoded\"\n\n        # Check form data\n        content = urllib.parse.unquote_plus(request.content.decode()).split(\"&\")\n        assert \"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer\" in content\n        assert \"scope=read write\" in content\n        assert \"resource=https://api.example.com/v1/mcp\" in content\n\n        # Check assertion\n        assertion = next(param for param in content if param.startswith(\"assertion=\"))[len(\"assertion=\") :]\n        claims = jwt.decode(\n            assertion,\n            key=\"a-string-secret-at-least-256-bits-long\",\n            algorithms=[\"HS256\"],\n            audience=\"https://api.example.com/\",\n            subject=\"1234567890\",\n            issuer=\"foo\",\n            verify=True,\n        )\n        assert claims[\"name\"] == \"John Doe\"\n        assert claims[\"admin\"]\n        assert claims[\"iat\"] == 1516239022\n\n\nclass TestClientCredentialsOAuthProvider:\n    \"\"\"Test ClientCredentialsOAuthProvider.\"\"\"\n\n    @pytest.mark.anyio\n    async def test_init_sets_client_info(self, mock_storage: MockTokenStorage):\n        \"\"\"Test that _initialize sets client_info.\"\"\"\n        provider = ClientCredentialsOAuthProvider(\n            server_url=\"https://api.example.com\",\n            storage=mock_storage,\n            client_id=\"test-client-id\",\n            client_secret=\"test-client-secret\",\n        )\n\n        # client_info is set during _initialize\n        await provider._initialize()\n\n        assert provider.context.client_info is not None\n        assert provider.context.client_info.client_id == \"test-client-id\"\n        assert provider.context.client_info.client_secret == \"test-client-secret\"\n        assert provider.context.client_info.grant_types == [\"client_credentials\"]\n        assert provider.context.client_info.token_endpoint_auth_method == \"client_secret_basic\"\n\n    @pytest.mark.anyio\n    async def test_init_with_scopes(self, mock_storage: MockTokenStorage):\n        \"\"\"Test that constructor accepts scopes.\"\"\"\n        provider = ClientCredentialsOAuthProvider(\n            server_url=\"https://api.example.com\",\n            storage=mock_storage,\n            client_id=\"test-client-id\",\n            client_secret=\"test-client-secret\",\n            scopes=\"read write\",\n        )\n\n        await provider._initialize()\n        assert provider.context.client_info is not None\n        assert provider.context.client_info.scope == \"read write\"\n\n    @pytest.mark.anyio\n    async def test_init_with_client_secret_post(self, mock_storage: MockTokenStorage):\n        \"\"\"Test that constructor accepts client_secret_post auth method.\"\"\"\n        provider = ClientCredentialsOAuthProvider(\n            server_url=\"https://api.example.com\",\n            storage=mock_storage,\n            client_id=\"test-client-id\",\n            client_secret=\"test-client-secret\",\n            token_endpoint_auth_method=\"client_secret_post\",\n        )\n\n        await provider._initialize()\n        assert provider.context.client_info is not None\n        assert provider.context.client_info.token_endpoint_auth_method == \"client_secret_post\"\n\n    @pytest.mark.anyio\n    async def test_exchange_token_client_credentials(self, mock_storage: MockTokenStorage):\n        \"\"\"Test token exchange request building.\"\"\"\n        provider = ClientCredentialsOAuthProvider(\n            server_url=\"https://api.example.com/v1/mcp\",\n            storage=mock_storage,\n            client_id=\"test-client-id\",\n            client_secret=\"test-client-secret\",\n            scopes=\"read write\",\n        )\n        provider.context.oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://api.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://api.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://api.example.com/token\"),\n        )\n        provider.context.protocol_version = \"2025-06-18\"\n\n        request = await provider._perform_authorization()\n\n        assert request.method == \"POST\"\n        assert str(request.url) == \"https://api.example.com/token\"\n\n        content = urllib.parse.unquote_plus(request.content.decode())\n        assert \"grant_type=client_credentials\" in content\n        assert \"scope=read write\" in content\n        assert \"resource=https://api.example.com/v1/mcp\" in content\n\n    @pytest.mark.anyio\n    async def test_exchange_token_client_secret_post_includes_client_id(self, mock_storage: MockTokenStorage):\n        \"\"\"Test that client_secret_post includes both client_id and client_secret in body (RFC 6749 §2.3.1).\"\"\"\n        provider = ClientCredentialsOAuthProvider(\n            server_url=\"https://api.example.com/v1/mcp\",\n            storage=mock_storage,\n            client_id=\"test-client-id\",\n            client_secret=\"test-client-secret\",\n            token_endpoint_auth_method=\"client_secret_post\",\n            scopes=\"read write\",\n        )\n        await provider._initialize()\n        provider.context.oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://api.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://api.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://api.example.com/token\"),\n        )\n        provider.context.protocol_version = \"2025-06-18\"\n\n        request = await provider._perform_authorization()\n\n        content = urllib.parse.unquote_plus(request.content.decode())\n        assert \"grant_type=client_credentials\" in content\n        assert \"client_id=test-client-id\" in content\n        assert \"client_secret=test-client-secret\" in content\n        # Should NOT have Basic auth header\n        assert \"Authorization\" not in request.headers\n\n    @pytest.mark.anyio\n    async def test_exchange_token_client_secret_post_without_client_id(self, mock_storage: MockTokenStorage):\n        \"\"\"Test client_secret_post skips body credentials when client_id is None.\"\"\"\n        provider = ClientCredentialsOAuthProvider(\n            server_url=\"https://api.example.com/v1/mcp\",\n            storage=mock_storage,\n            client_id=\"placeholder\",\n            client_secret=\"test-client-secret\",\n            token_endpoint_auth_method=\"client_secret_post\",\n            scopes=\"read write\",\n        )\n        await provider._initialize()\n        provider.context.oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://api.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://api.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://api.example.com/token\"),\n        )\n        provider.context.protocol_version = \"2025-06-18\"\n        # Override client_info to have client_id=None (edge case)\n        provider.context.client_info = OAuthClientInformationFull(\n            redirect_uris=None,\n            client_id=None,\n            client_secret=\"test-client-secret\",\n            grant_types=[\"client_credentials\"],\n            token_endpoint_auth_method=\"client_secret_post\",\n            scope=\"read write\",\n        )\n\n        request = await provider._perform_authorization()\n\n        content = urllib.parse.unquote_plus(request.content.decode())\n        assert \"grant_type=client_credentials\" in content\n        # Neither client_id nor client_secret should be in body since client_id is None\n        # (RFC 6749 §2.3.1 requires both for client_secret_post)\n        assert \"client_id=\" not in content\n        assert \"client_secret=\" not in content\n        assert \"Authorization\" not in request.headers\n\n    @pytest.mark.anyio\n    async def test_exchange_token_without_scopes(self, mock_storage: MockTokenStorage):\n        \"\"\"Test token exchange without scopes.\"\"\"\n        provider = ClientCredentialsOAuthProvider(\n            server_url=\"https://api.example.com/v1/mcp\",\n            storage=mock_storage,\n            client_id=\"test-client-id\",\n            client_secret=\"test-client-secret\",\n        )\n        provider.context.oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://api.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://api.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://api.example.com/token\"),\n        )\n        provider.context.protocol_version = \"2024-11-05\"  # Old version - no resource param\n\n        request = await provider._perform_authorization()\n\n        content = urllib.parse.unquote_plus(request.content.decode())\n        assert \"grant_type=client_credentials\" in content\n        assert \"scope=\" not in content\n        assert \"resource=\" not in content\n\n\nclass TestPrivateKeyJWTOAuthProvider:\n    \"\"\"Test PrivateKeyJWTOAuthProvider.\"\"\"\n\n    @pytest.mark.anyio\n    async def test_init_sets_client_info(self, mock_storage: MockTokenStorage):\n        \"\"\"Test that _initialize sets client_info.\"\"\"\n\n        async def mock_assertion_provider(audience: str) -> str:  # pragma: no cover\n            return \"mock-jwt\"\n\n        provider = PrivateKeyJWTOAuthProvider(\n            server_url=\"https://api.example.com\",\n            storage=mock_storage,\n            client_id=\"test-client-id\",\n            assertion_provider=mock_assertion_provider,\n        )\n\n        # client_info is set during _initialize\n        await provider._initialize()\n\n        assert provider.context.client_info is not None\n        assert provider.context.client_info.client_id == \"test-client-id\"\n        assert provider.context.client_info.grant_types == [\"client_credentials\"]\n        assert provider.context.client_info.token_endpoint_auth_method == \"private_key_jwt\"\n\n    @pytest.mark.anyio\n    async def test_exchange_token_client_credentials(self, mock_storage: MockTokenStorage):\n        \"\"\"Test token exchange request building with assertion provider.\"\"\"\n\n        async def mock_assertion_provider(audience: str) -> str:\n            return f\"jwt-for-{audience}\"\n\n        provider = PrivateKeyJWTOAuthProvider(\n            server_url=\"https://api.example.com/v1/mcp\",\n            storage=mock_storage,\n            client_id=\"test-client-id\",\n            assertion_provider=mock_assertion_provider,\n            scopes=\"read write\",\n        )\n        provider.context.oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://auth.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://auth.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://auth.example.com/token\"),\n        )\n        provider.context.protocol_version = \"2025-06-18\"\n\n        request = await provider._perform_authorization()\n\n        assert request.method == \"POST\"\n        assert str(request.url) == \"https://auth.example.com/token\"\n\n        content = urllib.parse.unquote_plus(request.content.decode())\n        assert \"grant_type=client_credentials\" in content\n        assert \"client_assertion=jwt-for-https://auth.example.com/\" in content\n        assert \"client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer\" in content\n        assert \"scope=read write\" in content\n\n    @pytest.mark.anyio\n    async def test_exchange_token_without_scopes(self, mock_storage: MockTokenStorage):\n        \"\"\"Test token exchange without scopes.\"\"\"\n\n        async def mock_assertion_provider(audience: str) -> str:\n            return f\"jwt-for-{audience}\"\n\n        provider = PrivateKeyJWTOAuthProvider(\n            server_url=\"https://api.example.com/v1/mcp\",\n            storage=mock_storage,\n            client_id=\"test-client-id\",\n            assertion_provider=mock_assertion_provider,\n        )\n        provider.context.oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://auth.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://auth.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://auth.example.com/token\"),\n        )\n        provider.context.protocol_version = \"2024-11-05\"  # Old version - no resource param\n\n        request = await provider._perform_authorization()\n\n        content = urllib.parse.unquote_plus(request.content.decode())\n        assert \"grant_type=client_credentials\" in content\n        assert \"scope=\" not in content\n        assert \"resource=\" not in content\n\n\nclass TestSignedJWTParameters:\n    \"\"\"Test SignedJWTParameters.\"\"\"\n\n    @pytest.mark.anyio\n    async def test_create_assertion_provider(self):\n        \"\"\"Test that create_assertion_provider creates valid JWTs.\"\"\"\n        params = SignedJWTParameters(\n            issuer=\"test-issuer\",\n            subject=\"test-subject\",\n            signing_key=\"a-string-secret-at-least-256-bits-long\",\n            signing_algorithm=\"HS256\",\n            lifetime_seconds=300,\n        )\n\n        provider = params.create_assertion_provider()\n        assertion = await provider(\"https://auth.example.com\")\n\n        claims = jwt.decode(\n            assertion,\n            key=\"a-string-secret-at-least-256-bits-long\",\n            algorithms=[\"HS256\"],\n            audience=\"https://auth.example.com\",\n        )\n        assert claims[\"iss\"] == \"test-issuer\"\n        assert claims[\"sub\"] == \"test-subject\"\n        assert claims[\"aud\"] == \"https://auth.example.com\"\n        assert \"exp\" in claims\n        assert \"iat\" in claims\n        assert \"jti\" in claims\n\n    @pytest.mark.anyio\n    async def test_create_assertion_provider_with_additional_claims(self):\n        \"\"\"Test that additional_claims are included in the JWT.\"\"\"\n        params = SignedJWTParameters(\n            issuer=\"test-issuer\",\n            subject=\"test-subject\",\n            signing_key=\"a-string-secret-at-least-256-bits-long\",\n            signing_algorithm=\"HS256\",\n            additional_claims={\"custom\": \"value\"},\n        )\n\n        provider = params.create_assertion_provider()\n        assertion = await provider(\"https://auth.example.com\")\n\n        claims = jwt.decode(\n            assertion,\n            key=\"a-string-secret-at-least-256-bits-long\",\n            algorithms=[\"HS256\"],\n            audience=\"https://auth.example.com\",\n        )\n        assert claims[\"custom\"] == \"value\"\n\n\nclass TestStaticAssertionProvider:\n    \"\"\"Test static_assertion_provider helper.\"\"\"\n\n    @pytest.mark.anyio\n    async def test_returns_static_token(self):\n        \"\"\"Test that static_assertion_provider returns the same token regardless of audience.\"\"\"\n        token = \"my-static-jwt-token\"\n        provider = static_assertion_provider(token)\n\n        result1 = await provider(\"https://auth1.example.com\")\n        result2 = await provider(\"https://auth2.example.com\")\n\n        assert result1 == token\n        assert result2 == token\n"
  },
  {
    "path": "tests/client/conftest.py",
    "content": "from collections.abc import Callable, Generator\nfrom contextlib import asynccontextmanager\nfrom typing import Any\nfrom unittest.mock import patch\n\nimport pytest\nfrom anyio.streams.memory import MemoryObjectSendStream\n\nimport mcp.shared.memory\nfrom mcp.shared.message import SessionMessage\nfrom mcp.types import JSONRPCNotification, JSONRPCRequest\n\n\nclass SpyMemoryObjectSendStream:\n    def __init__(self, original_stream: MemoryObjectSendStream[SessionMessage]):\n        self.original_stream = original_stream\n        self.sent_messages: list[SessionMessage] = []\n\n    async def send(self, message: SessionMessage):\n        self.sent_messages.append(message)\n        await self.original_stream.send(message)\n\n    async def aclose(self):\n        await self.original_stream.aclose()\n\n    async def __aenter__(self):\n        return self\n\n    async def __aexit__(self, *args: Any):\n        await self.aclose()\n\n\nclass StreamSpyCollection:\n    def __init__(self, client_spy: SpyMemoryObjectSendStream, server_spy: SpyMemoryObjectSendStream):\n        self.client = client_spy\n        self.server = server_spy\n\n    def clear(self) -> None:\n        \"\"\"Clear all captured messages.\"\"\"\n        self.client.sent_messages.clear()\n        self.server.sent_messages.clear()\n\n    def get_client_requests(self, method: str | None = None) -> list[JSONRPCRequest]:\n        \"\"\"Get client-sent requests, optionally filtered by method.\"\"\"\n        return [\n            req.message\n            for req in self.client.sent_messages\n            if isinstance(req.message, JSONRPCRequest) and (method is None or req.message.method == method)\n        ]\n\n    def get_server_requests(self, method: str | None = None) -> list[JSONRPCRequest]:  # pragma: no cover\n        \"\"\"Get server-sent requests, optionally filtered by method.\"\"\"\n        return [  # pragma: no cover\n            req.message\n            for req in self.server.sent_messages\n            if isinstance(req.message, JSONRPCRequest) and (method is None or req.message.method == method)\n        ]\n\n    def get_client_notifications(self, method: str | None = None) -> list[JSONRPCNotification]:  # pragma: no cover\n        \"\"\"Get client-sent notifications, optionally filtered by method.\"\"\"\n        return [\n            notif.message\n            for notif in self.client.sent_messages\n            if isinstance(notif.message, JSONRPCNotification) and (method is None or notif.message.method == method)\n        ]\n\n    def get_server_notifications(self, method: str | None = None) -> list[JSONRPCNotification]:  # pragma: no cover\n        \"\"\"Get server-sent notifications, optionally filtered by method.\"\"\"\n        return [\n            notif.message\n            for notif in self.server.sent_messages\n            if isinstance(notif.message, JSONRPCNotification) and (method is None or notif.message.method == method)\n        ]\n\n\n@pytest.fixture\ndef stream_spy() -> Generator[Callable[[], StreamSpyCollection], None, None]:\n    \"\"\"Fixture that provides spies for both client and server write streams.\n\n    Example:\n        ```python\n        async def test_something(stream_spy):\n            # ... set up server and client ...\n\n            spies = stream_spy()\n\n            # Run some operation that sends messages\n            await client.some_operation()\n\n            # Check the messages\n            requests = spies.get_client_requests(method=\"some/method\")\n            assert len(requests) == 1\n\n            # Clear for the next operation\n            spies.clear()\n        ```\n    \"\"\"\n    client_spy = None\n    server_spy = None\n\n    # Store references to our spy objects\n    def capture_spies(c_spy: SpyMemoryObjectSendStream, s_spy: SpyMemoryObjectSendStream):\n        nonlocal client_spy, server_spy\n        client_spy = c_spy\n        server_spy = s_spy\n\n    # Create patched version of stream creation\n    original_create_streams = mcp.shared.memory.create_client_server_memory_streams\n\n    @asynccontextmanager\n    async def patched_create_streams():\n        async with original_create_streams() as (client_streams, server_streams):\n            client_read, client_write = client_streams\n            server_read, server_write = server_streams\n\n            # Create spy wrappers\n            spy_client_write = SpyMemoryObjectSendStream(client_write)\n            spy_server_write = SpyMemoryObjectSendStream(server_write)\n\n            # Capture references for the test to use\n            capture_spies(spy_client_write, spy_server_write)\n\n            yield (client_read, spy_client_write), (server_read, spy_server_write)\n\n    # Apply the patch for the duration of the test\n    # Patch both locations since InMemoryTransport imports it directly\n    with patch(\"mcp.shared.memory.create_client_server_memory_streams\", patched_create_streams):\n        with patch(\"mcp.client._memory.create_client_server_memory_streams\", patched_create_streams):\n            # Return a collection with helper methods\n            def get_spy_collection() -> StreamSpyCollection:\n                assert client_spy is not None, \"client_spy was not initialized\"\n                assert server_spy is not None, \"server_spy was not initialized\"\n                return StreamSpyCollection(client_spy, server_spy)\n\n            yield get_spy_collection\n"
  },
  {
    "path": "tests/client/test_auth.py",
    "content": "\"\"\"Tests for refactored OAuth client authentication implementation.\"\"\"\n\nimport base64\nimport time\nfrom unittest import mock\nfrom urllib.parse import parse_qs, quote, unquote, urlparse\n\nimport httpx\nimport pytest\nfrom inline_snapshot import Is, snapshot\nfrom pydantic import AnyHttpUrl, AnyUrl\n\nfrom mcp.client.auth import OAuthClientProvider, PKCEParameters\nfrom mcp.client.auth.exceptions import OAuthFlowError\nfrom mcp.client.auth.utils import (\n    build_oauth_authorization_server_metadata_discovery_urls,\n    build_protected_resource_metadata_discovery_urls,\n    create_client_info_from_metadata_url,\n    create_client_registration_request,\n    create_oauth_metadata_request,\n    extract_field_from_www_auth,\n    extract_resource_metadata_from_www_auth,\n    extract_scope_from_www_auth,\n    get_client_metadata_scopes,\n    handle_registration_response,\n    is_valid_client_metadata_url,\n    should_use_client_metadata_url,\n)\nfrom mcp.server.auth.routes import build_metadata\nfrom mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions\nfrom mcp.shared.auth import (\n    OAuthClientInformationFull,\n    OAuthClientMetadata,\n    OAuthMetadata,\n    OAuthToken,\n    ProtectedResourceMetadata,\n)\n\n\nclass MockTokenStorage:\n    \"\"\"Mock token storage for testing.\"\"\"\n\n    def __init__(self):\n        self._tokens: OAuthToken | None = None\n        self._client_info: OAuthClientInformationFull | None = None\n\n    async def get_tokens(self) -> OAuthToken | None:\n        return self._tokens  # pragma: no cover\n\n    async def set_tokens(self, tokens: OAuthToken) -> None:\n        self._tokens = tokens\n\n    async def get_client_info(self) -> OAuthClientInformationFull | None:\n        return self._client_info  # pragma: no cover\n\n    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:\n        self._client_info = client_info\n\n\n@pytest.fixture\ndef mock_storage():\n    return MockTokenStorage()\n\n\n@pytest.fixture\ndef client_metadata():\n    return OAuthClientMetadata(\n        client_name=\"Test Client\",\n        client_uri=AnyHttpUrl(\"https://example.com\"),\n        redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n        scope=\"read write\",\n    )\n\n\n@pytest.fixture\ndef valid_tokens():\n    return OAuthToken(\n        access_token=\"test_access_token\",\n        token_type=\"Bearer\",\n        expires_in=3600,\n        refresh_token=\"test_refresh_token\",\n        scope=\"read write\",\n    )\n\n\n@pytest.fixture\ndef oauth_provider(client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage):\n    async def redirect_handler(url: str) -> None:\n        \"\"\"Mock redirect handler.\"\"\"\n        pass  # pragma: no cover\n\n    async def callback_handler() -> tuple[str, str | None]:\n        \"\"\"Mock callback handler.\"\"\"\n        return \"test_auth_code\", \"test_state\"  # pragma: no cover\n\n    return OAuthClientProvider(\n        server_url=\"https://api.example.com/v1/mcp\",\n        client_metadata=client_metadata,\n        storage=mock_storage,\n        redirect_handler=redirect_handler,\n        callback_handler=callback_handler,\n    )\n\n\n@pytest.fixture\ndef prm_metadata_response():\n    \"\"\"PRM metadata response with scopes.\"\"\"\n    return httpx.Response(\n        200,\n        content=(\n            b'{\"resource\": \"https://api.example.com/v1/mcp\", '\n            b'\"authorization_servers\": [\"https://auth.example.com\"], '\n            b'\"scopes_supported\": [\"resource:read\", \"resource:write\"]}'\n        ),\n    )\n\n\n@pytest.fixture\ndef prm_metadata_without_scopes_response():\n    \"\"\"PRM metadata response without scopes.\"\"\"\n    return httpx.Response(\n        200,\n        content=(\n            b'{\"resource\": \"https://api.example.com/v1/mcp\", '\n            b'\"authorization_servers\": [\"https://auth.example.com\"], '\n            b'\"scopes_supported\": null}'\n        ),\n    )\n\n\n@pytest.fixture\ndef init_response_with_www_auth_scope():\n    \"\"\"Initial 401 response with WWW-Authenticate header containing scope.\"\"\"\n    return httpx.Response(\n        401,\n        headers={\"WWW-Authenticate\": 'Bearer scope=\"special:scope from:www-authenticate\"'},\n        request=httpx.Request(\"GET\", \"https://api.example.com/test\"),\n    )\n\n\n@pytest.fixture\ndef init_response_without_www_auth_scope():\n    \"\"\"Initial 401 response without WWW-Authenticate scope.\"\"\"\n    return httpx.Response(\n        401,\n        headers={},\n        request=httpx.Request(\"GET\", \"https://api.example.com/test\"),\n    )\n\n\nclass TestPKCEParameters:\n    \"\"\"Test PKCE parameter generation.\"\"\"\n\n    def test_pkce_generation(self):\n        \"\"\"Test PKCE parameter generation creates valid values.\"\"\"\n        pkce = PKCEParameters.generate()\n\n        # Verify lengths\n        assert len(pkce.code_verifier) == 128\n        assert 43 <= len(pkce.code_challenge) <= 128\n\n        # Verify characters used in verifier\n        allowed_chars = set(\"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~\")\n        assert all(c in allowed_chars for c in pkce.code_verifier)\n\n        # Verify base64url encoding in challenge (no padding)\n        assert \"=\" not in pkce.code_challenge\n\n    def test_pkce_uniqueness(self):\n        \"\"\"Test PKCE generates unique values each time.\"\"\"\n        pkce1 = PKCEParameters.generate()\n        pkce2 = PKCEParameters.generate()\n\n        assert pkce1.code_verifier != pkce2.code_verifier\n        assert pkce1.code_challenge != pkce2.code_challenge\n\n\nclass TestOAuthContext:\n    \"\"\"Test OAuth context functionality.\"\"\"\n\n    @pytest.mark.anyio\n    async def test_oauth_provider_initialization(\n        self, oauth_provider: OAuthClientProvider, client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n    ):\n        \"\"\"Test OAuthClientProvider basic setup.\"\"\"\n        assert oauth_provider.context.server_url == \"https://api.example.com/v1/mcp\"\n        assert oauth_provider.context.client_metadata == client_metadata\n        assert oauth_provider.context.storage == mock_storage\n        assert oauth_provider.context.timeout == 300.0\n        assert oauth_provider.context is not None\n\n    def test_context_url_parsing(self, oauth_provider: OAuthClientProvider):\n        \"\"\"Test get_authorization_base_url() extracts base URLs correctly.\"\"\"\n        context = oauth_provider.context\n\n        # Test with path\n        assert context.get_authorization_base_url(\"https://api.example.com/v1/mcp\") == \"https://api.example.com\"\n\n        # Test with no path\n        assert context.get_authorization_base_url(\"https://api.example.com\") == \"https://api.example.com\"\n\n        # Test with port\n        assert (\n            context.get_authorization_base_url(\"https://api.example.com:8080/path/to/mcp\")\n            == \"https://api.example.com:8080\"\n        )\n\n        # Test with query params\n        assert (\n            context.get_authorization_base_url(\"https://api.example.com/path?param=value\") == \"https://api.example.com\"\n        )\n\n    @pytest.mark.anyio\n    async def test_token_validity_checking(self, oauth_provider: OAuthClientProvider, valid_tokens: OAuthToken):\n        \"\"\"Test is_token_valid() and can_refresh_token() logic.\"\"\"\n        context = oauth_provider.context\n\n        # No tokens - should be invalid\n        assert not context.is_token_valid()\n        assert not context.can_refresh_token()\n\n        # Set valid tokens and client info\n        context.current_tokens = valid_tokens\n        context.token_expiry_time = time.time() + 1800  # 30 minutes from now\n        context.client_info = OAuthClientInformationFull(\n            client_id=\"test_client_id\",\n            client_secret=\"test_client_secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n        )\n\n        # Should be valid\n        assert context.is_token_valid()\n        assert context.can_refresh_token()  # Has refresh token and client info\n\n        # Expire the token\n        context.token_expiry_time = time.time() - 100  # Expired 100 seconds ago\n        assert not context.is_token_valid()\n        assert context.can_refresh_token()  # Can still refresh\n\n        # Remove refresh token\n        context.current_tokens.refresh_token = None\n        assert not context.can_refresh_token()\n\n        # Remove client info\n        context.current_tokens.refresh_token = \"test_refresh_token\"\n        context.client_info = None\n        assert not context.can_refresh_token()\n\n    def test_clear_tokens(self, oauth_provider: OAuthClientProvider, valid_tokens: OAuthToken):\n        \"\"\"Test clear_tokens() removes token data.\"\"\"\n        context = oauth_provider.context\n        context.current_tokens = valid_tokens\n        context.token_expiry_time = time.time() + 1800\n\n        # Clear tokens\n        context.clear_tokens()\n\n        # Verify cleared\n        assert context.current_tokens is None\n        assert context.token_expiry_time is None\n\n\nclass TestOAuthFlow:\n    \"\"\"Test OAuth flow methods.\"\"\"\n\n    @pytest.mark.anyio\n    async def test_build_protected_resource_discovery_urls(\n        self, client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n    ):\n        \"\"\"Test protected resource metadata discovery URL building with fallback.\"\"\"\n\n        async def redirect_handler(url: str) -> None:\n            pass  # pragma: no cover\n\n        async def callback_handler() -> tuple[str, str | None]:\n            return \"test_auth_code\", \"test_state\"  # pragma: no cover\n\n        provider = OAuthClientProvider(\n            server_url=\"https://api.example.com\",\n            client_metadata=client_metadata,\n            storage=mock_storage,\n            redirect_handler=redirect_handler,\n            callback_handler=callback_handler,\n        )\n\n        # Test without WWW-Authenticate (fallback)\n        init_response = httpx.Response(\n            status_code=401, headers={}, request=httpx.Request(\"GET\", \"https://request-api.example.com\")\n        )\n\n        urls = build_protected_resource_metadata_discovery_urls(\n            extract_resource_metadata_from_www_auth(init_response), provider.context.server_url\n        )\n        assert len(urls) == 1\n        assert urls[0] == \"https://api.example.com/.well-known/oauth-protected-resource\"\n\n        # Test with WWW-Authenticate header\n        init_response.headers[\"WWW-Authenticate\"] = (\n            'Bearer resource_metadata=\"https://prm.example.com/.well-known/oauth-protected-resource/path\"'\n        )\n\n        urls = build_protected_resource_metadata_discovery_urls(\n            extract_resource_metadata_from_www_auth(init_response), provider.context.server_url\n        )\n        assert len(urls) == 2\n        assert urls[0] == \"https://prm.example.com/.well-known/oauth-protected-resource/path\"\n        assert urls[1] == \"https://api.example.com/.well-known/oauth-protected-resource\"\n\n    @pytest.mark.anyio\n    def test_create_oauth_metadata_request(self, oauth_provider: OAuthClientProvider):\n        \"\"\"Test OAuth metadata discovery request building.\"\"\"\n        request = create_oauth_metadata_request(\"https://example.com\")\n\n        # Ensure correct method and headers, and that the URL is unmodified\n        assert request.method == \"GET\"\n        assert str(request.url) == \"https://example.com\"\n        assert \"mcp-protocol-version\" in request.headers\n\n\nclass TestOAuthFallback:\n    \"\"\"Test OAuth discovery fallback behavior for legacy (act as AS not RS) servers.\"\"\"\n\n    @pytest.mark.anyio\n    async def test_oauth_discovery_legacy_fallback_when_no_prm(self):\n        \"\"\"Test that when PRM discovery fails, only root OAuth URL is tried (March 2025 spec).\"\"\"\n        # When auth_server_url is None (PRM failed), we use server_url and only try root\n        discovery_urls = build_oauth_authorization_server_metadata_discovery_urls(None, \"https://mcp.linear.app/sse\")\n\n        # Should only try the root URL (legacy behavior)\n        assert discovery_urls == [\n            \"https://mcp.linear.app/.well-known/oauth-authorization-server\",\n        ]\n\n    @pytest.mark.anyio\n    async def test_oauth_discovery_path_aware_when_auth_server_has_path(self):\n        \"\"\"Test that when auth server URL has a path, only path-based URLs are tried.\"\"\"\n        discovery_urls = build_oauth_authorization_server_metadata_discovery_urls(\n            \"https://auth.example.com/tenant1\", \"https://api.example.com/mcp\"\n        )\n\n        # Should try path-based URLs only (no root URLs)\n        assert discovery_urls == [\n            \"https://auth.example.com/.well-known/oauth-authorization-server/tenant1\",\n            \"https://auth.example.com/.well-known/openid-configuration/tenant1\",\n            \"https://auth.example.com/tenant1/.well-known/openid-configuration\",\n        ]\n\n    @pytest.mark.anyio\n    async def test_oauth_discovery_root_when_auth_server_has_no_path(self):\n        \"\"\"Test that when auth server URL has no path, only root URLs are tried.\"\"\"\n        discovery_urls = build_oauth_authorization_server_metadata_discovery_urls(\n            \"https://auth.example.com\", \"https://api.example.com/mcp\"\n        )\n\n        # Should try root URLs only\n        assert discovery_urls == [\n            \"https://auth.example.com/.well-known/oauth-authorization-server\",\n            \"https://auth.example.com/.well-known/openid-configuration\",\n        ]\n\n    @pytest.mark.anyio\n    async def test_oauth_discovery_root_when_auth_server_has_only_slash(self):\n        \"\"\"Test that when auth server URL has only trailing slash, treated as root.\"\"\"\n        discovery_urls = build_oauth_authorization_server_metadata_discovery_urls(\n            \"https://auth.example.com/\", \"https://api.example.com/mcp\"\n        )\n\n        # Should try root URLs only\n        assert discovery_urls == [\n            \"https://auth.example.com/.well-known/oauth-authorization-server\",\n            \"https://auth.example.com/.well-known/openid-configuration\",\n        ]\n\n    @pytest.mark.anyio\n    async def test_oauth_discovery_fallback_order(self, oauth_provider: OAuthClientProvider):\n        \"\"\"Test fallback URL construction order when auth server URL has a path.\"\"\"\n        # Simulate PRM discovery returning an auth server URL with a path\n        oauth_provider.context.auth_server_url = oauth_provider.context.server_url\n\n        discovery_urls = build_oauth_authorization_server_metadata_discovery_urls(\n            oauth_provider.context.auth_server_url, oauth_provider.context.server_url\n        )\n\n        assert discovery_urls == [\n            \"https://api.example.com/.well-known/oauth-authorization-server/v1/mcp\",\n            \"https://api.example.com/.well-known/openid-configuration/v1/mcp\",\n            \"https://api.example.com/v1/mcp/.well-known/openid-configuration\",\n        ]\n\n    @pytest.mark.anyio\n    async def test_oauth_discovery_fallback_conditions(self, oauth_provider: OAuthClientProvider):\n        \"\"\"Test the conditions during which an AS metadata discovery fallback will be attempted.\"\"\"\n        # Ensure no tokens are stored\n        oauth_provider.context.current_tokens = None\n        oauth_provider.context.token_expiry_time = None\n        oauth_provider._initialized = True\n\n        # Mock client info to skip DCR\n        oauth_provider.context.client_info = OAuthClientInformationFull(\n            client_id=\"existing_client\",\n            redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n        )\n\n        # Create a test request\n        test_request = httpx.Request(\"GET\", \"https://api.example.com/v1/mcp\")\n\n        # Mock the auth flow\n        auth_flow = oauth_provider.async_auth_flow(test_request)\n\n        # First request should be the original request without auth header\n        request = await auth_flow.__anext__()\n        assert \"Authorization\" not in request.headers\n\n        # Send a 401 response to trigger the OAuth flow\n        response = httpx.Response(\n            401,\n            headers={\n                \"WWW-Authenticate\": 'Bearer resource_metadata=\"https://api.example.com/.well-known/oauth-protected-resource\"'\n            },\n            request=test_request,\n        )\n\n        # Next request should be to discover protected resource metadata\n        discovery_request = await auth_flow.asend(response)\n        assert str(discovery_request.url) == \"https://api.example.com/.well-known/oauth-protected-resource\"\n        assert discovery_request.method == \"GET\"\n\n        # Send a successful discovery response with minimal protected resource metadata\n        # Note: auth server URL has a path (/v1/mcp), so only path-based URLs will be tried\n        discovery_response = httpx.Response(\n            200,\n            content=b'{\"resource\": \"https://api.example.com/v1/mcp\", \"authorization_servers\": [\"https://auth.example.com/v1/mcp\"]}',\n            request=discovery_request,\n        )\n\n        # Next request should be to discover OAuth metadata at path-aware OAuth URL\n        oauth_metadata_request_1 = await auth_flow.asend(discovery_response)\n        assert (\n            str(oauth_metadata_request_1.url)\n            == \"https://auth.example.com/.well-known/oauth-authorization-server/v1/mcp\"\n        )\n        assert oauth_metadata_request_1.method == \"GET\"\n\n        # Send a 404 response\n        oauth_metadata_response_1 = httpx.Response(\n            404,\n            content=b\"Not Found\",\n            request=oauth_metadata_request_1,\n        )\n\n        # Next request should be path-aware OIDC URL (not root URL since auth server has path)\n        oauth_metadata_request_2 = await auth_flow.asend(oauth_metadata_response_1)\n        assert str(oauth_metadata_request_2.url) == \"https://auth.example.com/.well-known/openid-configuration/v1/mcp\"\n        assert oauth_metadata_request_2.method == \"GET\"\n\n        # Send a 400 response\n        oauth_metadata_response_2 = httpx.Response(\n            400,\n            content=b\"Bad Request\",\n            request=oauth_metadata_request_2,\n        )\n\n        # Next request should be OIDC path-appended URL\n        oauth_metadata_request_3 = await auth_flow.asend(oauth_metadata_response_2)\n        assert str(oauth_metadata_request_3.url) == \"https://auth.example.com/v1/mcp/.well-known/openid-configuration\"\n        assert oauth_metadata_request_3.method == \"GET\"\n\n        # Send a 500 response\n        oauth_metadata_response_3 = httpx.Response(\n            500,\n            content=b\"Internal Server Error\",\n            request=oauth_metadata_request_3,\n        )\n\n        # Mock the authorization process to minimize unnecessary state in this test\n        oauth_provider._perform_authorization_code_grant = mock.AsyncMock(\n            return_value=(\"test_auth_code\", \"test_code_verifier\")\n        )\n\n        # All path-based URLs failed, flow continues with default endpoints\n        # Next request should be token exchange using MCP server base URL (fallback when OAuth metadata not found)\n        token_request = await auth_flow.asend(oauth_metadata_response_3)\n        assert str(token_request.url) == \"https://api.example.com/token\"\n        assert token_request.method == \"POST\"\n\n        # Send a successful token response\n        token_response = httpx.Response(\n            200,\n            content=(\n                b'{\"access_token\": \"new_access_token\", \"token_type\": \"Bearer\", \"expires_in\": 3600, '\n                b'\"refresh_token\": \"new_refresh_token\"}'\n            ),\n            request=token_request,\n        )\n\n        # After OAuth flow completes, the original request is retried with auth header\n        final_request = await auth_flow.asend(token_response)\n        assert final_request.headers[\"Authorization\"] == \"Bearer new_access_token\"\n        assert final_request.method == \"GET\"\n        assert str(final_request.url) == \"https://api.example.com/v1/mcp\"\n\n        # Send final success response to properly close the generator\n        final_response = httpx.Response(200, request=final_request)\n        try:\n            await auth_flow.asend(final_response)\n        except StopAsyncIteration:\n            pass  # Expected - generator should complete\n\n    @pytest.mark.anyio\n    async def test_handle_metadata_response_success(self, oauth_provider: OAuthClientProvider):\n        \"\"\"Test successful metadata response handling.\"\"\"\n        # Create minimal valid OAuth metadata\n        content = b\"\"\"{\n            \"issuer\": \"https://auth.example.com\",\n            \"authorization_endpoint\": \"https://auth.example.com/authorize\",\n            \"token_endpoint\": \"https://auth.example.com/token\"\n        }\"\"\"\n        response = httpx.Response(200, content=content)\n\n        # Should set metadata\n        await oauth_provider._handle_oauth_metadata_response(response)\n        assert oauth_provider.context.oauth_metadata is not None\n        assert str(oauth_provider.context.oauth_metadata.issuer) == \"https://auth.example.com/\"\n\n    @pytest.mark.anyio\n    async def test_prioritize_www_auth_scope_over_prm(\n        self,\n        oauth_provider: OAuthClientProvider,\n        prm_metadata_response: httpx.Response,\n        init_response_with_www_auth_scope: httpx.Response,\n    ):\n        \"\"\"Test that WWW-Authenticate scope is prioritized over PRM scopes.\"\"\"\n        # First, process PRM metadata to set protected_resource_metadata with scopes\n        await oauth_provider._handle_protected_resource_response(prm_metadata_response)\n\n        # Process the scope selection with WWW-Authenticate header\n        scopes = get_client_metadata_scopes(\n            extract_scope_from_www_auth(init_response_with_www_auth_scope),\n            oauth_provider.context.protected_resource_metadata,\n        )\n\n        # Verify that WWW-Authenticate scope is used (not PRM scopes)\n        assert scopes == \"special:scope from:www-authenticate\"\n\n    @pytest.mark.anyio\n    async def test_prioritize_prm_scopes_when_no_www_auth_scope(\n        self,\n        oauth_provider: OAuthClientProvider,\n        prm_metadata_response: httpx.Response,\n        init_response_without_www_auth_scope: httpx.Response,\n    ):\n        \"\"\"Test that PRM scopes are prioritized when WWW-Authenticate header has no scopes.\"\"\"\n        # Process the PRM metadata to set protected_resource_metadata with scopes\n        await oauth_provider._handle_protected_resource_response(prm_metadata_response)\n\n        # Process the scope selection without WWW-Authenticate scope\n        scopes = get_client_metadata_scopes(\n            extract_scope_from_www_auth(init_response_without_www_auth_scope),\n            oauth_provider.context.protected_resource_metadata,\n        )\n\n        # Verify that PRM scopes are used\n        assert scopes == \"resource:read resource:write\"\n\n    @pytest.mark.anyio\n    async def test_omit_scope_when_no_prm_scopes_or_www_auth(\n        self,\n        oauth_provider: OAuthClientProvider,\n        prm_metadata_without_scopes_response: httpx.Response,\n        init_response_without_www_auth_scope: httpx.Response,\n    ):\n        \"\"\"Test that scope is omitted when PRM has no scopes and WWW-Authenticate doesn't specify scope.\"\"\"\n        # Process the PRM metadata without scopes\n        await oauth_provider._handle_protected_resource_response(prm_metadata_without_scopes_response)\n\n        # Process the scope selection without WWW-Authenticate scope\n        scopes = get_client_metadata_scopes(\n            extract_scope_from_www_auth(init_response_without_www_auth_scope),\n            oauth_provider.context.protected_resource_metadata,\n        )\n        # Verify that scope is omitted\n        assert scopes is None\n\n    @pytest.mark.anyio\n    async def test_token_exchange_request_authorization_code(self, oauth_provider: OAuthClientProvider):\n        \"\"\"Test token exchange request building.\"\"\"\n        # Set up required context\n        oauth_provider.context.client_info = OAuthClientInformationFull(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n            token_endpoint_auth_method=\"client_secret_post\",\n        )\n\n        request = await oauth_provider._exchange_token_authorization_code(\"test_auth_code\", \"test_verifier\")\n\n        assert request.method == \"POST\"\n        assert str(request.url) == \"https://api.example.com/token\"\n        assert request.headers[\"Content-Type\"] == \"application/x-www-form-urlencoded\"\n\n        # Check form data\n        content = request.content.decode()\n        assert \"grant_type=authorization_code\" in content\n        assert \"code=test_auth_code\" in content\n        assert \"code_verifier=test_verifier\" in content\n        assert \"client_id=test_client\" in content\n        assert \"client_secret=test_secret\" in content\n\n    @pytest.mark.anyio\n    async def test_refresh_token_request(self, oauth_provider: OAuthClientProvider, valid_tokens: OAuthToken):\n        \"\"\"Test refresh token request building.\"\"\"\n        # Set up required context\n        oauth_provider.context.current_tokens = valid_tokens\n        oauth_provider.context.client_info = OAuthClientInformationFull(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n            token_endpoint_auth_method=\"client_secret_post\",\n        )\n\n        request = await oauth_provider._refresh_token()\n\n        assert request.method == \"POST\"\n        assert str(request.url) == \"https://api.example.com/token\"\n        assert request.headers[\"Content-Type\"] == \"application/x-www-form-urlencoded\"\n\n        # Check form data\n        content = request.content.decode()\n        assert \"grant_type=refresh_token\" in content\n        assert \"refresh_token=test_refresh_token\" in content\n        assert \"client_id=test_client\" in content\n        assert \"client_secret=test_secret\" in content\n\n    @pytest.mark.anyio\n    async def test_basic_auth_token_exchange(self, oauth_provider: OAuthClientProvider):\n        \"\"\"Test token exchange with client_secret_basic authentication.\"\"\"\n        # Set up OAuth metadata to support basic auth\n        oauth_provider.context.oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://auth.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://auth.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://auth.example.com/token\"),\n            token_endpoint_auth_methods_supported=[\"client_secret_basic\", \"client_secret_post\"],\n        )\n\n        client_id_raw = \"test@client\"  # Include special character to test URL encoding\n        client_secret_raw = \"test:secret\"  # Include colon to test URL encoding\n\n        oauth_provider.context.client_info = OAuthClientInformationFull(\n            client_id=client_id_raw,\n            client_secret=client_secret_raw,\n            redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n            token_endpoint_auth_method=\"client_secret_basic\",\n        )\n\n        request = await oauth_provider._exchange_token_authorization_code(\"test_auth_code\", \"test_verifier\")\n\n        # Should use basic auth (registered method)\n        assert \"Authorization\" in request.headers\n        assert request.headers[\"Authorization\"].startswith(\"Basic \")\n\n        # Decode and verify credentials are properly URL-encoded\n        encoded_creds = request.headers[\"Authorization\"][6:]  # Remove \"Basic \" prefix\n        decoded = base64.b64decode(encoded_creds).decode()\n        client_id, client_secret = decoded.split(\":\", 1)\n\n        # Check URL encoding was applied\n        assert client_id == \"test%40client\"  # @ should be encoded as %40\n        assert client_secret == \"test%3Asecret\"  # : should be encoded as %3A\n\n        # Verify decoded values match original\n        assert unquote(client_id) == client_id_raw\n        assert unquote(client_secret) == client_secret_raw\n\n        # client_secret should NOT be in body for basic auth\n        content = request.content.decode()\n        assert \"client_secret=\" not in content\n        assert \"client_id=test%40client\" in content  # client_id still in body\n\n    @pytest.mark.anyio\n    async def test_basic_auth_refresh_token(self, oauth_provider: OAuthClientProvider, valid_tokens: OAuthToken):\n        \"\"\"Test token refresh with client_secret_basic authentication.\"\"\"\n        oauth_provider.context.current_tokens = valid_tokens\n\n        # Set up OAuth metadata to only support basic auth\n        oauth_provider.context.oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://auth.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://auth.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://auth.example.com/token\"),\n            token_endpoint_auth_methods_supported=[\"client_secret_basic\"],\n        )\n\n        client_id = \"test_client\"\n        client_secret = \"test_secret\"\n        oauth_provider.context.client_info = OAuthClientInformationFull(\n            client_id=client_id,\n            client_secret=client_secret,\n            redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n            token_endpoint_auth_method=\"client_secret_basic\",\n        )\n\n        request = await oauth_provider._refresh_token()\n\n        assert \"Authorization\" in request.headers\n        assert request.headers[\"Authorization\"].startswith(\"Basic \")\n\n        encoded_creds = request.headers[\"Authorization\"][6:]\n        decoded = base64.b64decode(encoded_creds).decode()\n        assert decoded == f\"{client_id}:{client_secret}\"\n\n        # client_secret should NOT be in body\n        content = request.content.decode()\n        assert \"client_secret=\" not in content\n\n    @pytest.mark.anyio\n    async def test_none_auth_method(self, oauth_provider: OAuthClientProvider):\n        \"\"\"Test 'none' authentication method (public client).\"\"\"\n        oauth_provider.context.oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://auth.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://auth.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://auth.example.com/token\"),\n            token_endpoint_auth_methods_supported=[\"none\"],\n        )\n\n        client_id = \"public_client\"\n        oauth_provider.context.client_info = OAuthClientInformationFull(\n            client_id=client_id,\n            client_secret=None,  # No secret for public client\n            redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n            token_endpoint_auth_method=\"none\",\n        )\n\n        request = await oauth_provider._exchange_token_authorization_code(\"test_auth_code\", \"test_verifier\")\n\n        # Should NOT have Authorization header\n        assert \"Authorization\" not in request.headers\n\n        # Should NOT have client_secret in body\n        content = request.content.decode()\n        assert \"client_secret=\" not in content\n        assert \"client_id=public_client\" in content\n\n\nclass TestProtectedResourceMetadata:\n    \"\"\"Test protected resource handling.\"\"\"\n\n    @pytest.mark.anyio\n    async def test_resource_param_included_with_recent_protocol_version(self, oauth_provider: OAuthClientProvider):\n        \"\"\"Test resource parameter is included for protocol version >= 2025-06-18.\"\"\"\n        # Set protocol version to 2025-06-18\n        oauth_provider.context.protocol_version = \"2025-06-18\"\n        oauth_provider.context.client_info = OAuthClientInformationFull(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n        )\n\n        # Test in token exchange\n        request = await oauth_provider._exchange_token_authorization_code(\"test_code\", \"test_verifier\")\n        content = request.content.decode()\n        assert \"resource=\" in content\n        # Check URL-encoded resource parameter\n        expected_resource = quote(oauth_provider.context.get_resource_url(), safe=\"\")\n        assert f\"resource={expected_resource}\" in content\n\n        # Test in refresh token\n        oauth_provider.context.current_tokens = OAuthToken(\n            access_token=\"test_access\",\n            token_type=\"Bearer\",\n            refresh_token=\"test_refresh\",\n        )\n        refresh_request = await oauth_provider._refresh_token()\n        refresh_content = refresh_request.content.decode()\n        assert \"resource=\" in refresh_content\n\n    @pytest.mark.anyio\n    async def test_resource_param_excluded_with_old_protocol_version(self, oauth_provider: OAuthClientProvider):\n        \"\"\"Test resource parameter is excluded for protocol version < 2025-06-18.\"\"\"\n        # Set protocol version to older version\n        oauth_provider.context.protocol_version = \"2025-03-26\"\n        oauth_provider.context.client_info = OAuthClientInformationFull(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n        )\n\n        # Test in token exchange\n        request = await oauth_provider._exchange_token_authorization_code(\"test_code\", \"test_verifier\")\n        content = request.content.decode()\n        assert \"resource=\" not in content\n\n        # Test in refresh token\n        oauth_provider.context.current_tokens = OAuthToken(\n            access_token=\"test_access\",\n            token_type=\"Bearer\",\n            refresh_token=\"test_refresh\",\n        )\n        refresh_request = await oauth_provider._refresh_token()\n        refresh_content = refresh_request.content.decode()\n        assert \"resource=\" not in refresh_content\n\n    @pytest.mark.anyio\n    async def test_resource_param_included_with_protected_resource_metadata(self, oauth_provider: OAuthClientProvider):\n        \"\"\"Test resource parameter is always included when protected resource metadata exists.\"\"\"\n        # Set old protocol version but with protected resource metadata\n        oauth_provider.context.protocol_version = \"2025-03-26\"\n        oauth_provider.context.protected_resource_metadata = ProtectedResourceMetadata(\n            resource=AnyHttpUrl(\"https://api.example.com/v1/mcp\"),\n            authorization_servers=[AnyHttpUrl(\"https://api.example.com\")],\n        )\n        oauth_provider.context.client_info = OAuthClientInformationFull(\n            client_id=\"test_client\",\n            client_secret=\"test_secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n        )\n\n        # Test in token exchange\n        request = await oauth_provider._exchange_token_authorization_code(\"test_code\", \"test_verifier\")\n        content = request.content.decode()\n        assert \"resource=\" in content\n\n\n@pytest.mark.anyio\nasync def test_validate_resource_rejects_mismatched_resource(\n    client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n) -> None:\n    \"\"\"Client must reject PRM resource that doesn't match server URL.\"\"\"\n    provider = OAuthClientProvider(\n        server_url=\"https://api.example.com/v1/mcp\",\n        client_metadata=client_metadata,\n        storage=mock_storage,\n    )\n    provider._initialized = True\n\n    prm = ProtectedResourceMetadata(\n        resource=AnyHttpUrl(\"https://evil.example.com/mcp\"),\n        authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n    )\n    with pytest.raises(OAuthFlowError, match=\"does not match expected\"):\n        await provider._validate_resource_match(prm)\n\n\n@pytest.mark.anyio\nasync def test_validate_resource_accepts_matching_resource(\n    client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n) -> None:\n    \"\"\"Client must accept PRM resource that matches server URL.\"\"\"\n    provider = OAuthClientProvider(\n        server_url=\"https://api.example.com/v1/mcp\",\n        client_metadata=client_metadata,\n        storage=mock_storage,\n    )\n    provider._initialized = True\n\n    prm = ProtectedResourceMetadata(\n        resource=AnyHttpUrl(\"https://api.example.com/v1/mcp\"),\n        authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n    )\n    # Should not raise\n    await provider._validate_resource_match(prm)\n\n\n@pytest.mark.anyio\nasync def test_validate_resource_custom_callback(\n    client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n) -> None:\n    \"\"\"Custom callback overrides default validation.\"\"\"\n    callback_called_with: list[tuple[str, str | None]] = []\n\n    async def custom_validate(server_url: str, prm_resource: str | None) -> None:\n        callback_called_with.append((server_url, prm_resource))\n\n    provider = OAuthClientProvider(\n        server_url=\"https://api.example.com/v1/mcp\",\n        client_metadata=client_metadata,\n        storage=mock_storage,\n        validate_resource_url=custom_validate,\n    )\n    provider._initialized = True\n\n    # This would normally fail default validation (different origin),\n    # but custom callback accepts it\n    prm = ProtectedResourceMetadata(\n        resource=AnyHttpUrl(\"https://evil.example.com/mcp\"),\n        authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n    )\n    await provider._validate_resource_match(prm)\n    assert callback_called_with == snapshot([(\"https://api.example.com/v1/mcp\", \"https://evil.example.com/mcp\")])\n\n\n@pytest.mark.anyio\nasync def test_validate_resource_accepts_root_url_with_trailing_slash(\n    client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n) -> None:\n    \"\"\"Root URLs with trailing slash normalization should match.\"\"\"\n    provider = OAuthClientProvider(\n        server_url=\"https://api.example.com\",\n        client_metadata=client_metadata,\n        storage=mock_storage,\n    )\n    provider._initialized = True\n\n    prm = ProtectedResourceMetadata(\n        resource=AnyHttpUrl(\"https://api.example.com/\"),\n        authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n    )\n    # Should not raise despite trailing slash difference\n    await provider._validate_resource_match(prm)\n\n\n@pytest.mark.anyio\nasync def test_validate_resource_accepts_server_url_with_trailing_slash(\n    client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n) -> None:\n    \"\"\"Server URL with trailing slash should match PRM resource.\"\"\"\n    provider = OAuthClientProvider(\n        server_url=\"https://api.example.com/v1/mcp/\",\n        client_metadata=client_metadata,\n        storage=mock_storage,\n    )\n    provider._initialized = True\n\n    prm = ProtectedResourceMetadata(\n        resource=AnyHttpUrl(\"https://api.example.com/v1/mcp\"),\n        authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n    )\n    # Should not raise - both normalize to the same URL with trailing slash\n    await provider._validate_resource_match(prm)\n\n\n@pytest.mark.anyio\nasync def test_get_resource_url_uses_canonical_when_prm_mismatches(\n    client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n) -> None:\n    \"\"\"get_resource_url falls back to canonical URL when PRM resource doesn't match.\"\"\"\n    provider = OAuthClientProvider(\n        server_url=\"https://api.example.com/v1/mcp\",\n        client_metadata=client_metadata,\n        storage=mock_storage,\n    )\n    provider._initialized = True\n\n    # Set PRM with a resource that is NOT a parent of the server URL\n    provider.context.protected_resource_metadata = ProtectedResourceMetadata(\n        resource=AnyHttpUrl(\"https://other.example.com/mcp\"),\n        authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n    )\n\n    # get_resource_url should return the canonical server URL, not the PRM resource\n    assert provider.context.get_resource_url() == snapshot(\"https://api.example.com/v1/mcp\")\n\n\nclass TestRegistrationResponse:\n    \"\"\"Test client registration response handling.\"\"\"\n\n    @pytest.mark.anyio\n    async def test_handle_registration_response_reads_before_accessing_text(self):\n        \"\"\"Test that response.aread() is called before accessing response.text.\"\"\"\n\n        # Track if aread() was called\n        class MockResponse(httpx.Response):\n            def __init__(self):\n                self.status_code = 400\n                self._aread_called = False\n                self._text = \"Registration failed with error\"\n\n            async def aread(self):\n                self._aread_called = True\n                return b\"test content\"\n\n            @property\n            def text(self):\n                if not self._aread_called:\n                    raise RuntimeError(\"Response.text accessed before response.aread()\")  # pragma: no cover\n                return self._text\n\n        mock_response = MockResponse()\n\n        # This should call aread() before accessing text\n        with pytest.raises(Exception) as exc_info:\n            await handle_registration_response(mock_response)\n\n        # Verify aread() was called\n        assert mock_response._aread_called\n        # Verify the error message includes the response text\n        assert \"Registration failed: 400\" in str(exc_info.value)\n\n\nclass TestCreateClientRegistrationRequest:\n    \"\"\"Test client registration request creation.\"\"\"\n\n    def test_uses_registration_endpoint_from_metadata(self):\n        \"\"\"Test that registration URL comes from metadata when available.\"\"\"\n        oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://auth.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://auth.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://auth.example.com/token\"),\n            registration_endpoint=AnyHttpUrl(\"https://auth.example.com/register\"),\n        )\n        client_metadata = OAuthClientMetadata(redirect_uris=[AnyHttpUrl(\"http://localhost:3000/callback\")])\n\n        request = create_client_registration_request(oauth_metadata, client_metadata, \"https://auth.example.com\")\n\n        assert str(request.url) == \"https://auth.example.com/register\"\n        assert request.method == \"POST\"\n\n    def test_falls_back_to_default_register_endpoint_when_no_metadata(self):\n        \"\"\"Test that registration uses fallback URL when auth_server_metadata is None.\"\"\"\n        client_metadata = OAuthClientMetadata(redirect_uris=[AnyHttpUrl(\"http://localhost:3000/callback\")])\n\n        request = create_client_registration_request(None, client_metadata, \"https://auth.example.com\")\n\n        assert str(request.url) == \"https://auth.example.com/register\"\n        assert request.method == \"POST\"\n\n    def test_falls_back_when_metadata_has_no_registration_endpoint(self):\n        \"\"\"Test fallback when metadata exists but lacks registration_endpoint.\"\"\"\n        oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://auth.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://auth.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://auth.example.com/token\"),\n            # No registration_endpoint\n        )\n        client_metadata = OAuthClientMetadata(redirect_uris=[AnyHttpUrl(\"http://localhost:3000/callback\")])\n\n        request = create_client_registration_request(oauth_metadata, client_metadata, \"https://auth.example.com\")\n\n        assert str(request.url) == \"https://auth.example.com/register\"\n        assert request.method == \"POST\"\n\n\nclass TestAuthFlow:\n    \"\"\"Test the auth flow in httpx.\"\"\"\n\n    @pytest.mark.anyio\n    async def test_auth_flow_with_valid_tokens(\n        self, oauth_provider: OAuthClientProvider, mock_storage: MockTokenStorage, valid_tokens: OAuthToken\n    ):\n        \"\"\"Test auth flow when tokens are already valid.\"\"\"\n        # Pre-store valid tokens\n        await mock_storage.set_tokens(valid_tokens)\n        oauth_provider.context.current_tokens = valid_tokens\n        oauth_provider.context.token_expiry_time = time.time() + 1800\n        oauth_provider._initialized = True\n\n        # Create a test request\n        test_request = httpx.Request(\"GET\", \"https://api.example.com/test\")\n\n        # Mock the auth flow\n        auth_flow = oauth_provider.async_auth_flow(test_request)\n\n        # Should get the request with auth header added\n        request = await auth_flow.__anext__()\n        assert request.headers[\"Authorization\"] == \"Bearer test_access_token\"\n\n        # Send a successful response\n        response = httpx.Response(200)\n        try:\n            await auth_flow.asend(response)\n        except StopAsyncIteration:\n            pass  # Expected\n\n    @pytest.mark.anyio\n    async def test_auth_flow_with_no_tokens(self, oauth_provider: OAuthClientProvider, mock_storage: MockTokenStorage):\n        \"\"\"Test auth flow when no tokens are available, triggering the full OAuth flow.\"\"\"\n        # Ensure no tokens are stored\n        oauth_provider.context.current_tokens = None\n        oauth_provider.context.token_expiry_time = None\n        oauth_provider._initialized = True\n\n        # Create a test request\n        test_request = httpx.Request(\"GET\", \"https://api.example.com/mcp\")\n\n        # Mock the auth flow\n        auth_flow = oauth_provider.async_auth_flow(test_request)\n\n        # First request should be the original request without auth header\n        request = await auth_flow.__anext__()\n        assert \"Authorization\" not in request.headers\n\n        # Send a 401 response to trigger the OAuth flow\n        response = httpx.Response(\n            401,\n            headers={\n                \"WWW-Authenticate\": 'Bearer resource_metadata=\"https://api.example.com/.well-known/oauth-protected-resource\"'\n            },\n            request=test_request,\n        )\n\n        # Next request should be to discover protected resource metadata\n        discovery_request = await auth_flow.asend(response)\n        assert discovery_request.method == \"GET\"\n        assert str(discovery_request.url) == \"https://api.example.com/.well-known/oauth-protected-resource\"\n\n        # Send a successful discovery response with minimal protected resource metadata\n        discovery_response = httpx.Response(\n            200,\n            content=b'{\"resource\": \"https://api.example.com/v1/mcp\", \"authorization_servers\": [\"https://auth.example.com\"]}',\n            request=discovery_request,\n        )\n\n        # Next request should be to discover OAuth metadata\n        oauth_metadata_request = await auth_flow.asend(discovery_response)\n        assert oauth_metadata_request.method == \"GET\"\n        assert str(oauth_metadata_request.url).startswith(\"https://auth.example.com/\")\n        assert \"mcp-protocol-version\" in oauth_metadata_request.headers\n\n        # Send a successful OAuth metadata response\n        oauth_metadata_response = httpx.Response(\n            200,\n            content=(\n                b'{\"issuer\": \"https://auth.example.com\", '\n                b'\"authorization_endpoint\": \"https://auth.example.com/authorize\", '\n                b'\"token_endpoint\": \"https://auth.example.com/token\", '\n                b'\"registration_endpoint\": \"https://auth.example.com/register\"}'\n            ),\n            request=oauth_metadata_request,\n        )\n\n        # Next request should be to register client\n        registration_request = await auth_flow.asend(oauth_metadata_response)\n        assert registration_request.method == \"POST\"\n        assert str(registration_request.url) == \"https://auth.example.com/register\"\n\n        # Send a successful registration response\n        registration_response = httpx.Response(\n            201,\n            content=b'{\"client_id\": \"test_client_id\", \"client_secret\": \"test_client_secret\", \"redirect_uris\": [\"http://localhost:3030/callback\"]}',\n            request=registration_request,\n        )\n\n        # Mock the authorization process\n        oauth_provider._perform_authorization_code_grant = mock.AsyncMock(\n            return_value=(\"test_auth_code\", \"test_code_verifier\")\n        )\n\n        # Next request should be to exchange token\n        token_request = await auth_flow.asend(registration_response)\n        assert token_request.method == \"POST\"\n        assert str(token_request.url) == \"https://auth.example.com/token\"\n        assert \"code=test_auth_code\" in token_request.content.decode()\n\n        # Send a successful token response\n        token_response = httpx.Response(\n            200,\n            content=(\n                b'{\"access_token\": \"new_access_token\", \"token_type\": \"Bearer\", \"expires_in\": 3600, '\n                b'\"refresh_token\": \"new_refresh_token\"}'\n            ),\n            request=token_request,\n        )\n\n        # Final request should be the original request with auth header\n        final_request = await auth_flow.asend(token_response)\n        assert final_request.headers[\"Authorization\"] == \"Bearer new_access_token\"\n        assert final_request.method == \"GET\"\n        assert str(final_request.url) == \"https://api.example.com/mcp\"\n\n        # Send final success response to properly close the generator\n        final_response = httpx.Response(200, request=final_request)\n        try:\n            await auth_flow.asend(final_response)\n        except StopAsyncIteration:\n            pass  # Expected - generator should complete\n\n        # Verify tokens were stored\n        assert oauth_provider.context.current_tokens is not None\n        assert oauth_provider.context.current_tokens.access_token == \"new_access_token\"\n        assert oauth_provider.context.token_expiry_time is not None\n\n    @pytest.mark.anyio\n    async def test_auth_flow_no_unnecessary_retry_after_oauth(\n        self, oauth_provider: OAuthClientProvider, mock_storage: MockTokenStorage, valid_tokens: OAuthToken\n    ):\n        \"\"\"Test that requests are not retried unnecessarily - the core bug that caused 2x performance degradation.\"\"\"\n        # Pre-store valid tokens so no OAuth flow is needed\n        await mock_storage.set_tokens(valid_tokens)\n        oauth_provider.context.current_tokens = valid_tokens\n        oauth_provider.context.token_expiry_time = time.time() + 1800\n        oauth_provider._initialized = True\n\n        test_request = httpx.Request(\"GET\", \"https://api.example.com/mcp\")\n        auth_flow = oauth_provider.async_auth_flow(test_request)\n\n        # Count how many times the request is yielded\n        request_yields = 0\n\n        # First request - should have auth header already\n        request = await auth_flow.__anext__()\n        request_yields += 1\n        assert request.headers[\"Authorization\"] == \"Bearer test_access_token\"\n\n        # Send a successful 200 response\n        response = httpx.Response(200, request=request)\n\n        # In the buggy version, this would yield the request AGAIN unconditionally\n        # In the fixed version, this should end the generator\n        try:\n            await auth_flow.asend(response)  # extra request\n            request_yields += 1  # pragma: no cover\n            # If we reach here, the bug is present\n            pytest.fail(\n                f\"Unnecessary retry detected! Request was yielded {request_yields} times. \"\n                f\"This indicates the retry logic bug that caused 2x performance degradation. \"\n                f\"The request should only be yielded once for successful responses.\"\n            )  # pragma: no cover\n        except StopAsyncIteration:\n            # This is the expected behavior - no unnecessary retry\n            pass\n\n        # Verify exactly one request was yielded (no double-sending)\n        assert request_yields == 1, f\"Expected 1 request yield, got {request_yields}\"\n\n    @pytest.mark.anyio\n    async def test_token_exchange_accepts_201_status(\n        self, oauth_provider: OAuthClientProvider, mock_storage: MockTokenStorage\n    ):\n        \"\"\"Test that token exchange accepts both 200 and 201 status codes.\"\"\"\n        # Ensure no tokens are stored\n        oauth_provider.context.current_tokens = None\n        oauth_provider.context.token_expiry_time = None\n        oauth_provider._initialized = True\n\n        # Create a test request\n        test_request = httpx.Request(\"GET\", \"https://api.example.com/mcp\")\n\n        # Mock the auth flow\n        auth_flow = oauth_provider.async_auth_flow(test_request)\n\n        # First request should be the original request without auth header\n        request = await auth_flow.__anext__()\n        assert \"Authorization\" not in request.headers\n\n        # Send a 401 response to trigger the OAuth flow\n        response = httpx.Response(\n            401,\n            headers={\n                \"WWW-Authenticate\": 'Bearer resource_metadata=\"https://api.example.com/.well-known/oauth-protected-resource\"'\n            },\n            request=test_request,\n        )\n\n        # Next request should be to discover protected resource metadata\n        discovery_request = await auth_flow.asend(response)\n        assert discovery_request.method == \"GET\"\n        assert str(discovery_request.url) == \"https://api.example.com/.well-known/oauth-protected-resource\"\n\n        # Send a successful discovery response with minimal protected resource metadata\n        discovery_response = httpx.Response(\n            200,\n            content=b'{\"resource\": \"https://api.example.com/v1/mcp\", \"authorization_servers\": [\"https://auth.example.com\"]}',\n            request=discovery_request,\n        )\n\n        # Next request should be to discover OAuth metadata\n        oauth_metadata_request = await auth_flow.asend(discovery_response)\n        assert oauth_metadata_request.method == \"GET\"\n        assert str(oauth_metadata_request.url).startswith(\"https://auth.example.com/\")\n        assert \"mcp-protocol-version\" in oauth_metadata_request.headers\n\n        # Send a successful OAuth metadata response\n        oauth_metadata_response = httpx.Response(\n            200,\n            content=(\n                b'{\"issuer\": \"https://auth.example.com\", '\n                b'\"authorization_endpoint\": \"https://auth.example.com/authorize\", '\n                b'\"token_endpoint\": \"https://auth.example.com/token\", '\n                b'\"registration_endpoint\": \"https://auth.example.com/register\"}'\n            ),\n            request=oauth_metadata_request,\n        )\n\n        # Next request should be to register client\n        registration_request = await auth_flow.asend(oauth_metadata_response)\n        assert registration_request.method == \"POST\"\n        assert str(registration_request.url) == \"https://auth.example.com/register\"\n\n        # Send a successful registration response with 201 status\n        registration_response = httpx.Response(\n            201,\n            content=b'{\"client_id\": \"test_client_id\", \"client_secret\": \"test_client_secret\", \"redirect_uris\": [\"http://localhost:3030/callback\"]}',\n            request=registration_request,\n        )\n\n        # Mock the authorization process\n        oauth_provider._perform_authorization_code_grant = mock.AsyncMock(\n            return_value=(\"test_auth_code\", \"test_code_verifier\")\n        )\n\n        # Next request should be to exchange token\n        token_request = await auth_flow.asend(registration_response)\n        assert token_request.method == \"POST\"\n        assert str(token_request.url) == \"https://auth.example.com/token\"\n        assert \"code=test_auth_code\" in token_request.content.decode()\n\n        # Send a successful token response with 201 status code (test both 200 and 201 are accepted)\n        token_response = httpx.Response(\n            201,\n            content=(\n                b'{\"access_token\": \"new_access_token\", \"token_type\": \"Bearer\", \"expires_in\": 3600, '\n                b'\"refresh_token\": \"new_refresh_token\"}'\n            ),\n            request=token_request,\n        )\n\n        # Final request should be the original request with auth header\n        final_request = await auth_flow.asend(token_response)\n        assert final_request.headers[\"Authorization\"] == \"Bearer new_access_token\"\n        assert final_request.method == \"GET\"\n        assert str(final_request.url) == \"https://api.example.com/mcp\"\n\n        # Send final success response to properly close the generator\n        final_response = httpx.Response(200, request=final_request)\n        try:\n            await auth_flow.asend(final_response)\n        except StopAsyncIteration:\n            pass  # Expected - generator should complete\n\n        # Verify tokens were stored\n        assert oauth_provider.context.current_tokens is not None\n        assert oauth_provider.context.current_tokens.access_token == \"new_access_token\"\n        assert oauth_provider.context.token_expiry_time is not None\n\n    @pytest.mark.anyio\n    async def test_403_insufficient_scope_updates_scope_from_header(\n        self,\n        oauth_provider: OAuthClientProvider,\n        mock_storage: MockTokenStorage,\n        valid_tokens: OAuthToken,\n    ):\n        \"\"\"Test that 403 response correctly updates scope from WWW-Authenticate header.\"\"\"\n        # Pre-store valid tokens and client info\n        client_info = OAuthClientInformationFull(\n            client_id=\"test_client_id\",\n            client_secret=\"test_client_secret\",\n            redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n        )\n        await mock_storage.set_tokens(valid_tokens)\n        await mock_storage.set_client_info(client_info)\n        oauth_provider.context.current_tokens = valid_tokens\n        oauth_provider.context.token_expiry_time = time.time() + 1800\n        oauth_provider.context.client_info = client_info\n        oauth_provider._initialized = True\n\n        # Original scope\n        assert oauth_provider.context.client_metadata.scope == \"read write\"\n\n        redirect_captured = False\n        captured_state = None\n\n        async def capture_redirect(url: str) -> None:\n            nonlocal redirect_captured, captured_state\n            redirect_captured = True\n            # Verify the new scope is included in authorization URL\n            assert \"scope=admin%3Awrite+admin%3Adelete\" in url or \"scope=admin:write+admin:delete\" in url.replace(\n                \"%3A\", \":\"\n            ).replace(\"+\", \" \")\n            # Extract state from redirect URL\n            parsed = urlparse(url)\n            params = parse_qs(parsed.query)\n            captured_state = params.get(\"state\", [None])[0]\n\n        oauth_provider.context.redirect_handler = capture_redirect\n\n        # Mock callback\n        async def mock_callback() -> tuple[str, str | None]:\n            return \"auth_code\", captured_state\n\n        oauth_provider.context.callback_handler = mock_callback\n\n        test_request = httpx.Request(\"GET\", \"https://api.example.com/mcp\")\n        auth_flow = oauth_provider.async_auth_flow(test_request)\n\n        # First request\n        request = await auth_flow.__anext__()\n\n        # Send 403 with new scope requirement\n        response_403 = httpx.Response(\n            403,\n            headers={\"WWW-Authenticate\": 'Bearer error=\"insufficient_scope\", scope=\"admin:write admin:delete\"'},\n            request=request,\n        )\n\n        # Trigger step-up - should get token exchange request\n        token_exchange_request = await auth_flow.asend(response_403)\n\n        # Verify scope was updated\n        assert oauth_provider.context.client_metadata.scope == \"admin:write admin:delete\"\n        assert redirect_captured\n\n        # Complete the flow with successful token response\n        token_response = httpx.Response(\n            200,\n            json={\n                \"access_token\": \"new_token_with_new_scope\",\n                \"token_type\": \"Bearer\",\n                \"expires_in\": 3600,\n                \"scope\": \"admin:write admin:delete\",\n            },\n            request=token_exchange_request,\n        )\n\n        # Should get final retry request\n        final_request = await auth_flow.asend(token_response)\n\n        # Send success response - flow should complete\n        success_response = httpx.Response(200, request=final_request)\n        try:\n            await auth_flow.asend(success_response)\n            pytest.fail(\"Should have stopped after successful response\")  # pragma: no cover\n        except StopAsyncIteration:\n            pass  # Expected\n\n\n@pytest.mark.parametrize(\n    (\n        \"issuer_url\",\n        \"service_documentation_url\",\n        \"authorization_endpoint\",\n        \"token_endpoint\",\n        \"registration_endpoint\",\n        \"revocation_endpoint\",\n    ),\n    (\n        # Pydantic's AnyUrl incorrectly adds trailing slash to base URLs\n        # This is being fixed in https://github.com/pydantic/pydantic-core/pull/1719 (Pydantic 2.12+)\n        pytest.param(\n            \"https://auth.example.com\",\n            \"https://auth.example.com/docs\",\n            \"https://auth.example.com/authorize\",\n            \"https://auth.example.com/token\",\n            \"https://auth.example.com/register\",\n            \"https://auth.example.com/revoke\",\n            id=\"simple-url\",\n            marks=pytest.mark.xfail(\n                reason=\"Pydantic AnyUrl adds trailing slash to base URLs - fixed in Pydantic 2.12+\"\n            ),\n        ),\n        pytest.param(\n            \"https://auth.example.com/\",\n            \"https://auth.example.com/docs\",\n            \"https://auth.example.com/authorize\",\n            \"https://auth.example.com/token\",\n            \"https://auth.example.com/register\",\n            \"https://auth.example.com/revoke\",\n            id=\"with-trailing-slash\",\n        ),\n        pytest.param(\n            \"https://auth.example.com/v1/mcp\",\n            \"https://auth.example.com/v1/mcp/docs\",\n            \"https://auth.example.com/v1/mcp/authorize\",\n            \"https://auth.example.com/v1/mcp/token\",\n            \"https://auth.example.com/v1/mcp/register\",\n            \"https://auth.example.com/v1/mcp/revoke\",\n            id=\"with-path-param\",\n        ),\n    ),\n)\ndef test_build_metadata(\n    issuer_url: str,\n    service_documentation_url: str,\n    authorization_endpoint: str,\n    token_endpoint: str,\n    registration_endpoint: str,\n    revocation_endpoint: str,\n):\n    metadata = build_metadata(\n        issuer_url=AnyHttpUrl(issuer_url),\n        service_documentation_url=AnyHttpUrl(service_documentation_url),\n        client_registration_options=ClientRegistrationOptions(enabled=True, valid_scopes=[\"read\", \"write\", \"admin\"]),\n        revocation_options=RevocationOptions(enabled=True),\n    )\n\n    assert metadata.model_dump(exclude_defaults=True, mode=\"json\") == snapshot(\n        {\n            \"issuer\": Is(issuer_url),\n            \"authorization_endpoint\": Is(authorization_endpoint),\n            \"token_endpoint\": Is(token_endpoint),\n            \"registration_endpoint\": Is(registration_endpoint),\n            \"scopes_supported\": [\"read\", \"write\", \"admin\"],\n            \"grant_types_supported\": [\"authorization_code\", \"refresh_token\"],\n            \"token_endpoint_auth_methods_supported\": [\"client_secret_post\", \"client_secret_basic\"],\n            \"service_documentation\": Is(service_documentation_url),\n            \"revocation_endpoint\": Is(revocation_endpoint),\n            \"revocation_endpoint_auth_methods_supported\": [\"client_secret_post\", \"client_secret_basic\"],\n            \"code_challenge_methods_supported\": [\"S256\"],\n        }\n    )\n\n\nclass TestLegacyServerFallback:\n    \"\"\"Test backward compatibility with legacy servers that don't support PRM (issue #1495).\"\"\"\n\n    @pytest.mark.anyio\n    async def test_legacy_server_no_prm_falls_back_to_root_oauth_discovery(\n        self, client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n    ):\n        \"\"\"Test that when PRM discovery fails completely, we fall back to root OAuth discovery (March 2025 spec).\"\"\"\n\n        async def redirect_handler(url: str) -> None:\n            pass  # pragma: no cover\n\n        async def callback_handler() -> tuple[str, str | None]:\n            return \"test_auth_code\", \"test_state\"  # pragma: no cover\n\n        # Simulate a legacy server like Linear\n        provider = OAuthClientProvider(\n            server_url=\"https://mcp.linear.app/sse\",\n            client_metadata=client_metadata,\n            storage=mock_storage,\n            redirect_handler=redirect_handler,\n            callback_handler=callback_handler,\n        )\n\n        provider.context.current_tokens = None\n        provider.context.token_expiry_time = None\n        provider._initialized = True\n\n        # Mock client info to skip DCR\n        provider.context.client_info = OAuthClientInformationFull(\n            client_id=\"existing_client\",\n            redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n        )\n\n        test_request = httpx.Request(\"GET\", \"https://mcp.linear.app/sse\")\n        auth_flow = provider.async_auth_flow(test_request)\n\n        # First request\n        request = await auth_flow.__anext__()\n        assert \"Authorization\" not in request.headers\n\n        # Send 401 without WWW-Authenticate header (typical legacy server)\n        response = httpx.Response(401, headers={}, request=test_request)\n\n        # Should try path-based PRM first\n        prm_request_1 = await auth_flow.asend(response)\n        assert str(prm_request_1.url) == \"https://mcp.linear.app/.well-known/oauth-protected-resource/sse\"\n\n        # PRM returns 404\n        prm_response_1 = httpx.Response(404, request=prm_request_1)\n\n        # Should try root-based PRM\n        prm_request_2 = await auth_flow.asend(prm_response_1)\n        assert str(prm_request_2.url) == \"https://mcp.linear.app/.well-known/oauth-protected-resource\"\n\n        # PRM returns 404 again - all PRM URLs failed\n        prm_response_2 = httpx.Response(404, request=prm_request_2)\n\n        # Should fall back to root OAuth discovery (March 2025 spec behavior)\n        oauth_metadata_request = await auth_flow.asend(prm_response_2)\n        assert str(oauth_metadata_request.url) == \"https://mcp.linear.app/.well-known/oauth-authorization-server\"\n        assert oauth_metadata_request.method == \"GET\"\n\n        # Send successful OAuth metadata response\n        oauth_metadata_response = httpx.Response(\n            200,\n            content=(\n                b'{\"issuer\": \"https://mcp.linear.app\", '\n                b'\"authorization_endpoint\": \"https://mcp.linear.app/authorize\", '\n                b'\"token_endpoint\": \"https://mcp.linear.app/token\"}'\n            ),\n            request=oauth_metadata_request,\n        )\n\n        # Mock authorization\n        provider._perform_authorization_code_grant = mock.AsyncMock(\n            return_value=(\"test_auth_code\", \"test_code_verifier\")\n        )\n\n        # Next should be token exchange\n        token_request = await auth_flow.asend(oauth_metadata_response)\n        assert str(token_request.url) == \"https://mcp.linear.app/token\"\n\n        # Send successful token response\n        token_response = httpx.Response(\n            200,\n            content=b'{\"access_token\": \"linear_token\", \"token_type\": \"Bearer\", \"expires_in\": 3600}',\n            request=token_request,\n        )\n\n        # Final request with auth header\n        final_request = await auth_flow.asend(token_response)\n        assert final_request.headers[\"Authorization\"] == \"Bearer linear_token\"\n        assert str(final_request.url) == \"https://mcp.linear.app/sse\"\n\n        # Complete flow\n        final_response = httpx.Response(200, request=final_request)\n        try:\n            await auth_flow.asend(final_response)\n        except StopAsyncIteration:\n            pass\n\n    @pytest.mark.anyio\n    async def test_legacy_server_with_different_prm_and_root_urls(\n        self, client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n    ):\n        \"\"\"Test PRM fallback with different WWW-Authenticate and root URLs.\"\"\"\n\n        async def redirect_handler(url: str) -> None:\n            pass  # pragma: no cover\n\n        async def callback_handler() -> tuple[str, str | None]:\n            return \"test_auth_code\", \"test_state\"  # pragma: no cover\n\n        provider = OAuthClientProvider(\n            server_url=\"https://api.example.com/v1/mcp\",\n            client_metadata=client_metadata,\n            storage=mock_storage,\n            redirect_handler=redirect_handler,\n            callback_handler=callback_handler,\n        )\n\n        provider.context.current_tokens = None\n        provider.context.token_expiry_time = None\n        provider._initialized = True\n\n        provider.context.client_info = OAuthClientInformationFull(\n            client_id=\"existing_client\",\n            redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n        )\n\n        test_request = httpx.Request(\"GET\", \"https://api.example.com/v1/mcp\")\n        auth_flow = provider.async_auth_flow(test_request)\n\n        await auth_flow.__anext__()\n\n        # 401 with custom WWW-Authenticate PRM URL\n        response = httpx.Response(\n            401,\n            headers={\n                \"WWW-Authenticate\": 'Bearer resource_metadata=\"https://custom.prm.com/.well-known/oauth-protected-resource\"'\n            },\n            request=test_request,\n        )\n\n        # Try custom PRM URL first\n        prm_request_1 = await auth_flow.asend(response)\n        assert str(prm_request_1.url) == \"https://custom.prm.com/.well-known/oauth-protected-resource\"\n\n        # Returns 500\n        prm_response_1 = httpx.Response(500, request=prm_request_1)\n\n        # Try path-based fallback\n        prm_request_2 = await auth_flow.asend(prm_response_1)\n        assert str(prm_request_2.url) == \"https://api.example.com/.well-known/oauth-protected-resource/v1/mcp\"\n\n        # Returns 404\n        prm_response_2 = httpx.Response(404, request=prm_request_2)\n\n        # Try root fallback\n        prm_request_3 = await auth_flow.asend(prm_response_2)\n        assert str(prm_request_3.url) == \"https://api.example.com/.well-known/oauth-protected-resource\"\n\n        # Also returns 404 - all PRM URLs failed\n        prm_response_3 = httpx.Response(404, request=prm_request_3)\n\n        # Should fall back to root OAuth discovery\n        oauth_metadata_request = await auth_flow.asend(prm_response_3)\n        assert str(oauth_metadata_request.url) == \"https://api.example.com/.well-known/oauth-authorization-server\"\n\n        # Complete the flow\n        oauth_metadata_response = httpx.Response(\n            200,\n            content=(\n                b'{\"issuer\": \"https://api.example.com\", '\n                b'\"authorization_endpoint\": \"https://api.example.com/authorize\", '\n                b'\"token_endpoint\": \"https://api.example.com/token\"}'\n            ),\n            request=oauth_metadata_request,\n        )\n\n        provider._perform_authorization_code_grant = mock.AsyncMock(\n            return_value=(\"test_auth_code\", \"test_code_verifier\")\n        )\n\n        token_request = await auth_flow.asend(oauth_metadata_response)\n        assert str(token_request.url) == \"https://api.example.com/token\"\n\n        token_response = httpx.Response(\n            200,\n            content=b'{\"access_token\": \"test_token\", \"token_type\": \"Bearer\", \"expires_in\": 3600}',\n            request=token_request,\n        )\n\n        final_request = await auth_flow.asend(token_response)\n        assert final_request.headers[\"Authorization\"] == \"Bearer test_token\"\n\n        final_response = httpx.Response(200, request=final_request)\n        try:\n            await auth_flow.asend(final_response)\n        except StopAsyncIteration:\n            pass\n\n\nclass TestSEP985Discovery:\n    \"\"\"Test SEP-985 protected resource metadata discovery with fallback.\"\"\"\n\n    @pytest.mark.anyio\n    async def test_path_based_fallback_when_no_www_authenticate(\n        self, client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n    ):\n        \"\"\"Test that client falls back to path-based well-known URI when WWW-Authenticate is absent.\"\"\"\n\n        async def redirect_handler(url: str) -> None:\n            pass  # pragma: no cover\n\n        async def callback_handler() -> tuple[str, str | None]:\n            return \"test_auth_code\", \"test_state\"  # pragma: no cover\n\n        provider = OAuthClientProvider(\n            server_url=\"https://api.example.com/v1/mcp\",\n            client_metadata=client_metadata,\n            storage=mock_storage,\n            redirect_handler=redirect_handler,\n            callback_handler=callback_handler,\n        )\n\n        # Test with 401 response without WWW-Authenticate header\n        init_response = httpx.Response(\n            status_code=401, headers={}, request=httpx.Request(\"GET\", \"https://api.example.com/v1/mcp\")\n        )\n\n        # Build discovery URLs\n        discovery_urls = build_protected_resource_metadata_discovery_urls(\n            extract_resource_metadata_from_www_auth(init_response), provider.context.server_url\n        )\n\n        # Should have path-based URL first, then root-based URL\n        assert len(discovery_urls) == 2\n        assert discovery_urls[0] == \"https://api.example.com/.well-known/oauth-protected-resource/v1/mcp\"\n        assert discovery_urls[1] == \"https://api.example.com/.well-known/oauth-protected-resource\"\n\n    @pytest.mark.anyio\n    async def test_root_based_fallback_after_path_based_404(\n        self, client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n    ):\n        \"\"\"Test that client falls back to root-based URI when path-based returns 404.\"\"\"\n\n        async def redirect_handler(url: str) -> None:\n            pass  # pragma: no cover\n\n        async def callback_handler() -> tuple[str, str | None]:\n            return \"test_auth_code\", \"test_state\"  # pragma: no cover\n\n        provider = OAuthClientProvider(\n            server_url=\"https://api.example.com/v1/mcp\",\n            client_metadata=client_metadata,\n            storage=mock_storage,\n            redirect_handler=redirect_handler,\n            callback_handler=callback_handler,\n        )\n\n        # Ensure no tokens are stored\n        provider.context.current_tokens = None\n        provider.context.token_expiry_time = None\n        provider._initialized = True\n\n        # Mock client info to skip DCR\n        provider.context.client_info = OAuthClientInformationFull(\n            client_id=\"existing_client\",\n            redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n        )\n\n        # Create a test request\n        test_request = httpx.Request(\"GET\", \"https://api.example.com/v1/mcp\")\n\n        # Mock the auth flow\n        auth_flow = provider.async_auth_flow(test_request)\n\n        # First request should be the original request without auth header\n        request = await auth_flow.__anext__()\n        assert \"Authorization\" not in request.headers\n\n        # Send a 401 response without WWW-Authenticate header\n        response = httpx.Response(401, headers={}, request=test_request)\n\n        # Next request should be to discover protected resource metadata (path-based)\n        discovery_request_1 = await auth_flow.asend(response)\n        assert str(discovery_request_1.url) == \"https://api.example.com/.well-known/oauth-protected-resource/v1/mcp\"\n        assert discovery_request_1.method == \"GET\"\n\n        # Send 404 response for path-based discovery\n        discovery_response_1 = httpx.Response(404, request=discovery_request_1)\n\n        # Next request should be to root-based well-known URI\n        discovery_request_2 = await auth_flow.asend(discovery_response_1)\n        assert str(discovery_request_2.url) == \"https://api.example.com/.well-known/oauth-protected-resource\"\n        assert discovery_request_2.method == \"GET\"\n\n        # Send successful discovery response\n        discovery_response_2 = httpx.Response(\n            200,\n            content=(\n                b'{\"resource\": \"https://api.example.com/v1/mcp\", \"authorization_servers\": [\"https://auth.example.com\"]}'\n            ),\n            request=discovery_request_2,\n        )\n\n        # Mock the rest of the OAuth flow\n        provider._perform_authorization = mock.AsyncMock(return_value=(\"test_auth_code\", \"test_code_verifier\"))\n\n        # Next should be OAuth metadata discovery\n        oauth_metadata_request = await auth_flow.asend(discovery_response_2)\n        assert oauth_metadata_request.method == \"GET\"\n\n        # Complete the flow\n        oauth_metadata_response = httpx.Response(\n            200,\n            content=(\n                b'{\"issuer\": \"https://auth.example.com\", '\n                b'\"authorization_endpoint\": \"https://auth.example.com/authorize\", '\n                b'\"token_endpoint\": \"https://auth.example.com/token\"}'\n            ),\n            request=oauth_metadata_request,\n        )\n\n        token_request = await auth_flow.asend(oauth_metadata_response)\n        token_response = httpx.Response(\n            200,\n            content=(\n                b'{\"access_token\": \"new_access_token\", \"token_type\": \"Bearer\", \"expires_in\": 3600, '\n                b'\"refresh_token\": \"new_refresh_token\"}'\n            ),\n            request=token_request,\n        )\n\n        final_request = await auth_flow.asend(token_response)\n        final_response = httpx.Response(200, request=final_request)\n        try:\n            await auth_flow.asend(final_response)\n        except StopAsyncIteration:\n            pass\n\n    @pytest.mark.anyio\n    async def test_www_authenticate_takes_priority_over_well_known(\n        self, client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n    ):\n        \"\"\"Test that WWW-Authenticate header resource_metadata takes priority over well-known URIs.\"\"\"\n\n        async def redirect_handler(url: str) -> None:\n            pass  # pragma: no cover\n\n        async def callback_handler() -> tuple[str, str | None]:\n            return \"test_auth_code\", \"test_state\"  # pragma: no cover\n\n        provider = OAuthClientProvider(\n            server_url=\"https://api.example.com/v1/mcp\",\n            client_metadata=client_metadata,\n            storage=mock_storage,\n            redirect_handler=redirect_handler,\n            callback_handler=callback_handler,\n        )\n\n        # Test with 401 response with WWW-Authenticate header\n        init_response = httpx.Response(\n            status_code=401,\n            headers={\n                \"WWW-Authenticate\": 'Bearer resource_metadata=\"https://custom.example.com/.well-known/oauth-protected-resource\"'\n            },\n            request=httpx.Request(\"GET\", \"https://api.example.com/v1/mcp\"),\n        )\n\n        # Build discovery URLs\n        discovery_urls = build_protected_resource_metadata_discovery_urls(\n            extract_resource_metadata_from_www_auth(init_response), provider.context.server_url\n        )\n\n        # Should have WWW-Authenticate URL first, then fallback URLs\n        assert len(discovery_urls) == 3\n        assert discovery_urls[0] == \"https://custom.example.com/.well-known/oauth-protected-resource\"\n        assert discovery_urls[1] == \"https://api.example.com/.well-known/oauth-protected-resource/v1/mcp\"\n        assert discovery_urls[2] == \"https://api.example.com/.well-known/oauth-protected-resource\"\n\n\nclass TestWWWAuthenticate:\n    \"\"\"Test WWW-Authenticate header parsing functionality.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"www_auth_header,field_name,expected_value\",\n        [\n            # Quoted values\n            ('Bearer scope=\"read write\"', \"scope\", \"read write\"),\n            (\n                'Bearer resource_metadata=\"https://api.example.com/.well-known/oauth-protected-resource\"',\n                \"resource_metadata\",\n                \"https://api.example.com/.well-known/oauth-protected-resource\",\n            ),\n            ('Bearer error=\"insufficient_scope\"', \"error\", \"insufficient_scope\"),\n            # Unquoted values\n            (\"Bearer scope=read\", \"scope\", \"read\"),\n            (\n                \"Bearer resource_metadata=https://api.example.com/.well-known/oauth-protected-resource\",\n                \"resource_metadata\",\n                \"https://api.example.com/.well-known/oauth-protected-resource\",\n            ),\n            (\"Bearer error=invalid_token\", \"error\", \"invalid_token\"),\n            # Multiple parameters with quoted value\n            (\n                'Bearer realm=\"api\", scope=\"admin:write resource:read\", error=\"insufficient_scope\"',\n                \"scope\",\n                \"admin:write resource:read\",\n            ),\n            (\n                'Bearer realm=\"api\", resource_metadata=\"https://api.example.com/.well-known/oauth-protected-resource\", '\n                'error=\"insufficient_scope\"',\n                \"resource_metadata\",\n                \"https://api.example.com/.well-known/oauth-protected-resource\",\n            ),\n            # Multiple parameters with unquoted value\n            ('Bearer realm=\"api\", scope=basic', \"scope\", \"basic\"),\n            # Values with special characters\n            (\n                'Bearer scope=\"resource:read resource:write user_profile\"',\n                \"scope\",\n                \"resource:read resource:write user_profile\",\n            ),\n            (\n                'Bearer resource_metadata=\"https://api.example.com/auth/metadata?version=1\"',\n                \"resource_metadata\",\n                \"https://api.example.com/auth/metadata?version=1\",\n            ),\n        ],\n    )\n    def test_extract_field_from_www_auth_valid_cases(\n        self,\n        client_metadata: OAuthClientMetadata,\n        mock_storage: MockTokenStorage,\n        www_auth_header: str,\n        field_name: str,\n        expected_value: str,\n    ):\n        \"\"\"Test extraction of various fields from valid WWW-Authenticate headers.\"\"\"\n\n        init_response = httpx.Response(\n            status_code=401,\n            headers={\"WWW-Authenticate\": www_auth_header},\n            request=httpx.Request(\"GET\", \"https://api.example.com/test\"),\n        )\n\n        result = extract_field_from_www_auth(init_response, field_name)\n        assert result == expected_value\n\n    @pytest.mark.parametrize(\n        \"www_auth_header,field_name,description\",\n        [\n            # No header\n            (None, \"scope\", \"no WWW-Authenticate header\"),\n            # Empty header\n            (\"\", \"scope\", \"empty WWW-Authenticate header\"),\n            # Header without requested field\n            ('Bearer realm=\"api\", error=\"insufficient_scope\"', \"scope\", \"no scope parameter\"),\n            ('Bearer realm=\"api\", scope=\"read write\"', \"resource_metadata\", \"no resource_metadata parameter\"),\n            # Malformed field (empty value)\n            (\"Bearer scope=\", \"scope\", \"malformed scope parameter\"),\n            (\"Bearer resource_metadata=\", \"resource_metadata\", \"malformed resource_metadata parameter\"),\n        ],\n    )\n    def test_extract_field_from_www_auth_invalid_cases(\n        self,\n        client_metadata: OAuthClientMetadata,\n        mock_storage: MockTokenStorage,\n        www_auth_header: str | None,\n        field_name: str,\n        description: str,\n    ):\n        \"\"\"Test extraction returns None for invalid cases.\"\"\"\n\n        headers = {\"WWW-Authenticate\": www_auth_header} if www_auth_header is not None else {}\n        init_response = httpx.Response(\n            status_code=401, headers=headers, request=httpx.Request(\"GET\", \"https://api.example.com/test\")\n        )\n\n        result = extract_field_from_www_auth(init_response, field_name)\n        assert result is None, f\"Should return None for {description}\"\n\n\nclass TestCIMD:\n    \"\"\"Test Client ID Metadata Document (CIMD) support.\"\"\"\n\n    @pytest.mark.parametrize(\n        \"url,expected\",\n        [\n            # Valid CIMD URLs\n            (\"https://example.com/client\", True),\n            (\"https://example.com/client-metadata.json\", True),\n            (\"https://example.com/path/to/client\", True),\n            (\"https://example.com:8443/client\", True),\n            # Invalid URLs - HTTP (not HTTPS)\n            (\"http://example.com/client\", False),\n            # Invalid URLs - root path\n            (\"https://example.com\", False),\n            (\"https://example.com/\", False),\n            # Invalid URLs - None or empty\n            (None, False),\n            (\"\", False),\n            # Invalid URLs - malformed (triggers urlparse exception)\n            (\"http://[::1/foo/\", False),\n        ],\n    )\n    def test_is_valid_client_metadata_url(self, url: str | None, expected: bool):\n        \"\"\"Test CIMD URL validation.\"\"\"\n        assert is_valid_client_metadata_url(url) == expected\n\n    def test_should_use_client_metadata_url_when_server_supports(self):\n        \"\"\"Test that CIMD is used when server supports it and URL is provided.\"\"\"\n        oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://auth.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://auth.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://auth.example.com/token\"),\n            client_id_metadata_document_supported=True,\n        )\n        assert should_use_client_metadata_url(oauth_metadata, \"https://example.com/client\") is True\n\n    def test_should_not_use_client_metadata_url_when_server_does_not_support(self):\n        \"\"\"Test that CIMD is not used when server doesn't support it.\"\"\"\n        oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://auth.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://auth.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://auth.example.com/token\"),\n            client_id_metadata_document_supported=False,\n        )\n        assert should_use_client_metadata_url(oauth_metadata, \"https://example.com/client\") is False\n\n    def test_should_not_use_client_metadata_url_when_not_provided(self):\n        \"\"\"Test that CIMD is not used when no URL is provided.\"\"\"\n        oauth_metadata = OAuthMetadata(\n            issuer=AnyHttpUrl(\"https://auth.example.com\"),\n            authorization_endpoint=AnyHttpUrl(\"https://auth.example.com/authorize\"),\n            token_endpoint=AnyHttpUrl(\"https://auth.example.com/token\"),\n            client_id_metadata_document_supported=True,\n        )\n        assert should_use_client_metadata_url(oauth_metadata, None) is False\n\n    def test_should_not_use_client_metadata_url_when_no_metadata(self):\n        \"\"\"Test that CIMD is not used when OAuth metadata is None.\"\"\"\n        assert should_use_client_metadata_url(None, \"https://example.com/client\") is False\n\n    def test_create_client_info_from_metadata_url(self):\n        \"\"\"Test creating client info from CIMD URL.\"\"\"\n        client_info = create_client_info_from_metadata_url(\n            \"https://example.com/client\",\n            redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n        )\n        assert client_info.client_id == \"https://example.com/client\"\n        assert client_info.token_endpoint_auth_method == \"none\"\n        assert client_info.redirect_uris == [AnyUrl(\"http://localhost:3030/callback\")]\n        assert client_info.client_secret is None\n\n    def test_oauth_provider_with_valid_client_metadata_url(\n        self, client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n    ):\n        \"\"\"Test OAuthClientProvider initialization with valid client_metadata_url.\"\"\"\n\n        async def redirect_handler(url: str) -> None:\n            pass  # pragma: no cover\n\n        async def callback_handler() -> tuple[str, str | None]:\n            return \"test_auth_code\", \"test_state\"  # pragma: no cover\n\n        provider = OAuthClientProvider(\n            server_url=\"https://api.example.com/v1/mcp\",\n            client_metadata=client_metadata,\n            storage=mock_storage,\n            redirect_handler=redirect_handler,\n            callback_handler=callback_handler,\n            client_metadata_url=\"https://example.com/client\",\n        )\n        assert provider.context.client_metadata_url == \"https://example.com/client\"\n\n    def test_oauth_provider_with_invalid_client_metadata_url_raises_error(\n        self, client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n    ):\n        \"\"\"Test OAuthClientProvider raises error for invalid client_metadata_url.\"\"\"\n\n        async def redirect_handler(url: str) -> None:\n            pass  # pragma: no cover\n\n        async def callback_handler() -> tuple[str, str | None]:\n            return \"test_auth_code\", \"test_state\"  # pragma: no cover\n\n        with pytest.raises(ValueError) as exc_info:\n            OAuthClientProvider(\n                server_url=\"https://api.example.com/v1/mcp\",\n                client_metadata=client_metadata,\n                storage=mock_storage,\n                redirect_handler=redirect_handler,\n                callback_handler=callback_handler,\n                client_metadata_url=\"http://example.com/client\",  # HTTP instead of HTTPS\n            )\n        assert \"HTTPS URL with a non-root pathname\" in str(exc_info.value)\n\n    @pytest.mark.anyio\n    async def test_auth_flow_uses_cimd_when_server_supports(\n        self, client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n    ):\n        \"\"\"Test that auth flow uses CIMD URL as client_id when server supports it.\"\"\"\n\n        async def redirect_handler(url: str) -> None:\n            pass  # pragma: no cover\n\n        async def callback_handler() -> tuple[str, str | None]:\n            return \"test_auth_code\", \"test_state\"  # pragma: no cover\n\n        provider = OAuthClientProvider(\n            server_url=\"https://api.example.com/v1/mcp\",\n            client_metadata=client_metadata,\n            storage=mock_storage,\n            redirect_handler=redirect_handler,\n            callback_handler=callback_handler,\n            client_metadata_url=\"https://example.com/client\",\n        )\n\n        provider.context.current_tokens = None\n        provider.context.token_expiry_time = None\n        provider._initialized = True\n\n        test_request = httpx.Request(\"GET\", \"https://api.example.com/v1/mcp\")\n        auth_flow = provider.async_auth_flow(test_request)\n\n        # First request\n        request = await auth_flow.__anext__()\n        assert \"Authorization\" not in request.headers\n\n        # Send 401 response\n        response = httpx.Response(401, headers={}, request=test_request)\n\n        # PRM discovery\n        prm_request = await auth_flow.asend(response)\n        prm_response = httpx.Response(\n            200,\n            content=b'{\"resource\": \"https://api.example.com/v1/mcp\", \"authorization_servers\": [\"https://auth.example.com\"]}',\n            request=prm_request,\n        )\n\n        # OAuth metadata discovery\n        oauth_request = await auth_flow.asend(prm_response)\n        oauth_response = httpx.Response(\n            200,\n            content=(\n                b'{\"issuer\": \"https://auth.example.com\", '\n                b'\"authorization_endpoint\": \"https://auth.example.com/authorize\", '\n                b'\"token_endpoint\": \"https://auth.example.com/token\", '\n                b'\"client_id_metadata_document_supported\": true}'\n            ),\n            request=oauth_request,\n        )\n\n        # Mock authorization\n        provider._perform_authorization_code_grant = mock.AsyncMock(\n            return_value=(\"test_auth_code\", \"test_code_verifier\")\n        )\n\n        # Should skip DCR and go directly to token exchange\n        token_request = await auth_flow.asend(oauth_response)\n        assert token_request.method == \"POST\"\n        assert str(token_request.url) == \"https://auth.example.com/token\"\n\n        # Verify client_id is the CIMD URL\n        content = token_request.content.decode()\n        assert \"client_id=https%3A%2F%2Fexample.com%2Fclient\" in content\n\n        # Verify client info was set correctly\n        assert provider.context.client_info is not None\n        assert provider.context.client_info.client_id == \"https://example.com/client\"\n        assert provider.context.client_info.token_endpoint_auth_method == \"none\"\n\n        # Complete the flow\n        token_response = httpx.Response(\n            200,\n            content=b'{\"access_token\": \"test_token\", \"token_type\": \"Bearer\", \"expires_in\": 3600}',\n            request=token_request,\n        )\n\n        final_request = await auth_flow.asend(token_response)\n        assert final_request.headers[\"Authorization\"] == \"Bearer test_token\"\n\n        final_response = httpx.Response(200, request=final_request)\n        try:\n            await auth_flow.asend(final_response)\n        except StopAsyncIteration:\n            pass\n\n    @pytest.mark.anyio\n    async def test_auth_flow_falls_back_to_dcr_when_no_cimd_support(\n        self, client_metadata: OAuthClientMetadata, mock_storage: MockTokenStorage\n    ):\n        \"\"\"Test that auth flow falls back to DCR when server doesn't support CIMD.\"\"\"\n\n        async def redirect_handler(url: str) -> None:\n            pass  # pragma: no cover\n\n        async def callback_handler() -> tuple[str, str | None]:\n            return \"test_auth_code\", \"test_state\"  # pragma: no cover\n\n        provider = OAuthClientProvider(\n            server_url=\"https://api.example.com/v1/mcp\",\n            client_metadata=client_metadata,\n            storage=mock_storage,\n            redirect_handler=redirect_handler,\n            callback_handler=callback_handler,\n            client_metadata_url=\"https://example.com/client\",\n        )\n\n        provider.context.current_tokens = None\n        provider.context.token_expiry_time = None\n        provider._initialized = True\n\n        test_request = httpx.Request(\"GET\", \"https://api.example.com/v1/mcp\")\n        auth_flow = provider.async_auth_flow(test_request)\n\n        # First request\n        await auth_flow.__anext__()\n\n        # Send 401 response\n        response = httpx.Response(401, headers={}, request=test_request)\n\n        # PRM discovery\n        prm_request = await auth_flow.asend(response)\n        prm_response = httpx.Response(\n            200,\n            content=b'{\"resource\": \"https://api.example.com/v1/mcp\", \"authorization_servers\": [\"https://auth.example.com\"]}',\n            request=prm_request,\n        )\n\n        # OAuth metadata discovery - server does NOT support CIMD\n        oauth_request = await auth_flow.asend(prm_response)\n        oauth_response = httpx.Response(\n            200,\n            content=(\n                b'{\"issuer\": \"https://auth.example.com\", '\n                b'\"authorization_endpoint\": \"https://auth.example.com/authorize\", '\n                b'\"token_endpoint\": \"https://auth.example.com/token\", '\n                b'\"registration_endpoint\": \"https://auth.example.com/register\"}'\n            ),\n            request=oauth_request,\n        )\n\n        # Should proceed to DCR instead of skipping it\n        registration_request = await auth_flow.asend(oauth_response)\n        assert registration_request.method == \"POST\"\n        assert str(registration_request.url) == \"https://auth.example.com/register\"\n\n        # Complete the flow to avoid generator cleanup issues\n        registration_response = httpx.Response(\n            201,\n            content=b'{\"client_id\": \"dcr_client_id\", \"redirect_uris\": [\"http://localhost:3030/callback\"]}',\n            request=registration_request,\n        )\n\n        # Mock authorization\n        provider._perform_authorization_code_grant = mock.AsyncMock(\n            return_value=(\"test_auth_code\", \"test_code_verifier\")\n        )\n\n        token_request = await auth_flow.asend(registration_response)\n        token_response = httpx.Response(\n            200,\n            content=b'{\"access_token\": \"test_token\", \"token_type\": \"Bearer\", \"expires_in\": 3600}',\n            request=token_request,\n        )\n\n        final_request = await auth_flow.asend(token_response)\n        final_response = httpx.Response(200, request=final_request)\n        try:\n            await auth_flow.asend(final_response)\n        except StopAsyncIteration:\n            pass\n"
  },
  {
    "path": "tests/client/test_client.py",
    "content": "\"\"\"Tests for the unified Client class.\"\"\"\n\nfrom __future__ import annotations\n\nfrom unittest.mock import patch\n\nimport anyio\nimport pytest\nfrom inline_snapshot import snapshot\n\nfrom mcp import MCPError, types\nfrom mcp.client._memory import InMemoryTransport\nfrom mcp.client.client import Client\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.types import (\n    CallToolResult,\n    EmptyResult,\n    GetPromptResult,\n    ListPromptsResult,\n    ListResourcesResult,\n    ListResourceTemplatesResult,\n    ListToolsResult,\n    Prompt,\n    PromptArgument,\n    PromptMessage,\n    PromptsCapability,\n    ReadResourceResult,\n    Resource,\n    ResourcesCapability,\n    ServerCapabilities,\n    TextContent,\n    TextResourceContents,\n    Tool,\n    ToolsCapability,\n)\n\npytestmark = pytest.mark.anyio\n\n\n@pytest.fixture\ndef simple_server() -> Server:\n    \"\"\"Create a simple MCP server for testing.\"\"\"\n\n    async def handle_list_resources(\n        ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n    ) -> ListResourcesResult:\n        return ListResourcesResult(\n            resources=[Resource(uri=\"memory://test\", name=\"Test Resource\", description=\"A test resource\")]\n        )\n\n    async def handle_subscribe_resource(ctx: ServerRequestContext, params: types.SubscribeRequestParams) -> EmptyResult:\n        return EmptyResult()\n\n    async def handle_unsubscribe_resource(\n        ctx: ServerRequestContext, params: types.UnsubscribeRequestParams\n    ) -> EmptyResult:\n        return EmptyResult()\n\n    async def handle_set_logging_level(ctx: ServerRequestContext, params: types.SetLevelRequestParams) -> EmptyResult:\n        return EmptyResult()\n\n    async def handle_completion(ctx: ServerRequestContext, params: types.CompleteRequestParams) -> types.CompleteResult:\n        return types.CompleteResult(completion=types.Completion(values=[]))\n\n    return Server(\n        name=\"test_server\",\n        on_list_resources=handle_list_resources,\n        on_subscribe_resource=handle_subscribe_resource,\n        on_unsubscribe_resource=handle_unsubscribe_resource,\n        on_set_logging_level=handle_set_logging_level,\n        on_completion=handle_completion,\n    )\n\n\n@pytest.fixture\ndef app() -> MCPServer:\n    \"\"\"Create an MCPServer server for testing.\"\"\"\n    server = MCPServer(\"test\")\n\n    @server.tool()\n    def greet(name: str) -> str:\n        \"\"\"Greet someone by name.\"\"\"\n        return f\"Hello, {name}!\"\n\n    @server.resource(\"test://resource\")\n    def test_resource() -> str:\n        \"\"\"A test resource.\"\"\"\n        return \"Test content\"\n\n    @server.prompt()\n    def greeting_prompt(name: str) -> str:\n        \"\"\"A greeting prompt.\"\"\"\n        return f\"Please greet {name} warmly.\"\n\n    return server\n\n\nasync def test_client_is_initialized(app: MCPServer):\n    \"\"\"Test that the client is initialized after entering context.\"\"\"\n    async with Client(app) as client:\n        assert client.initialize_result.capabilities == snapshot(\n            ServerCapabilities(\n                experimental={},\n                prompts=PromptsCapability(list_changed=False),\n                resources=ResourcesCapability(subscribe=False, list_changed=False),\n                tools=ToolsCapability(list_changed=False),\n            )\n        )\n        assert client.initialize_result.server_info.name == \"test\"\n\n\nasync def test_client_with_simple_server(simple_server: Server):\n    \"\"\"Test that from_server works with a basic Server instance.\"\"\"\n    async with Client(simple_server) as client:\n        resources = await client.list_resources()\n        assert resources == snapshot(\n            ListResourcesResult(\n                resources=[Resource(name=\"Test Resource\", uri=\"memory://test\", description=\"A test resource\")]\n            )\n        )\n\n\nasync def test_client_send_ping(app: MCPServer):\n    async with Client(app) as client:\n        result = await client.send_ping()\n        assert result == snapshot(EmptyResult())\n\n\nasync def test_client_list_tools(app: MCPServer):\n    async with Client(app) as client:\n        result = await client.list_tools()\n        assert result == snapshot(\n            ListToolsResult(\n                tools=[\n                    Tool(\n                        name=\"greet\",\n                        description=\"Greet someone by name.\",\n                        input_schema={\n                            \"properties\": {\"name\": {\"title\": \"Name\", \"type\": \"string\"}},\n                            \"required\": [\"name\"],\n                            \"title\": \"greetArguments\",\n                            \"type\": \"object\",\n                        },\n                        output_schema={\n                            \"properties\": {\"result\": {\"title\": \"Result\", \"type\": \"string\"}},\n                            \"required\": [\"result\"],\n                            \"title\": \"greetOutput\",\n                            \"type\": \"object\",\n                        },\n                    )\n                ]\n            )\n        )\n\n\nasync def test_client_call_tool(app: MCPServer):\n    async with Client(app) as client:\n        result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n        assert result == snapshot(\n            CallToolResult(\n                content=[TextContent(text=\"Hello, World!\")],\n                structured_content={\"result\": \"Hello, World!\"},\n            )\n        )\n\n\nasync def test_read_resource(app: MCPServer):\n    \"\"\"Test reading a resource.\"\"\"\n    async with Client(app) as client:\n        result = await client.read_resource(\"test://resource\")\n        assert result == snapshot(\n            ReadResourceResult(\n                contents=[TextResourceContents(uri=\"test://resource\", mime_type=\"text/plain\", text=\"Test content\")]\n            )\n        )\n\n\nasync def test_read_resource_error_propagates():\n    \"\"\"MCPError raised by a server handler propagates to the client with its code intact.\"\"\"\n\n    async def handle_read_resource(\n        ctx: ServerRequestContext, params: types.ReadResourceRequestParams\n    ) -> ReadResourceResult:\n        raise MCPError(code=404, message=\"no resource with that URI was found\")\n\n    server = Server(\"test\", on_read_resource=handle_read_resource)\n    async with Client(server) as client:\n        with pytest.raises(MCPError) as exc_info:\n            await client.read_resource(\"unknown://example\")\n        assert exc_info.value.error.code == 404\n\n\nasync def test_get_prompt(app: MCPServer):\n    \"\"\"Test getting a prompt.\"\"\"\n    async with Client(app) as client:\n        result = await client.get_prompt(\"greeting_prompt\", {\"name\": \"Alice\"})\n        assert result == snapshot(\n            GetPromptResult(\n                description=\"A greeting prompt.\",\n                messages=[PromptMessage(role=\"user\", content=TextContent(text=\"Please greet Alice warmly.\"))],\n            )\n        )\n\n\ndef test_client_session_property_before_enter(app: MCPServer):\n    \"\"\"Test that accessing session before context manager raises RuntimeError.\"\"\"\n    client = Client(app)\n    with pytest.raises(RuntimeError, match=\"Client must be used within an async context manager\"):\n        client.session\n\n\nasync def test_client_reentry_raises_runtime_error(app: MCPServer):\n    \"\"\"Test that reentering a client raises RuntimeError.\"\"\"\n    async with Client(app) as client:\n        with pytest.raises(RuntimeError, match=\"Client is already entered\"):\n            await client.__aenter__()\n\n\nasync def test_client_send_progress_notification():\n    \"\"\"Test sending progress notification.\"\"\"\n    received_from_client = None\n    event = anyio.Event()\n\n    async def handle_progress(ctx: ServerRequestContext, params: types.ProgressNotificationParams) -> None:\n        nonlocal received_from_client\n        received_from_client = {\"progress_token\": params.progress_token, \"progress\": params.progress}\n        event.set()\n\n    server = Server(name=\"test_server\", on_progress=handle_progress)\n\n    async with Client(server) as client:\n        await client.send_progress_notification(progress_token=\"token123\", progress=50.0)\n        await event.wait()\n        assert received_from_client == snapshot({\"progress_token\": \"token123\", \"progress\": 50.0})\n\n\nasync def test_client_subscribe_resource(simple_server: Server):\n    async with Client(simple_server) as client:\n        result = await client.subscribe_resource(\"memory://test\")\n        assert result == snapshot(EmptyResult())\n\n\nasync def test_client_unsubscribe_resource(simple_server: Server):\n    async with Client(simple_server) as client:\n        result = await client.unsubscribe_resource(\"memory://test\")\n        assert result == snapshot(EmptyResult())\n\n\nasync def test_client_set_logging_level(simple_server: Server):\n    \"\"\"Test setting logging level.\"\"\"\n    async with Client(simple_server) as client:\n        result = await client.set_logging_level(\"debug\")\n        assert result == snapshot(EmptyResult())\n\n\nasync def test_client_list_resources_with_params(app: MCPServer):\n    \"\"\"Test listing resources with params parameter.\"\"\"\n    async with Client(app) as client:\n        result = await client.list_resources()\n        assert result == snapshot(\n            ListResourcesResult(\n                resources=[\n                    Resource(\n                        name=\"test_resource\",\n                        uri=\"test://resource\",\n                        description=\"A test resource.\",\n                        mime_type=\"text/plain\",\n                    )\n                ]\n            )\n        )\n\n\nasync def test_client_list_resource_templates(app: MCPServer):\n    \"\"\"Test listing resource templates with params parameter.\"\"\"\n    async with Client(app) as client:\n        result = await client.list_resource_templates()\n        assert result == snapshot(ListResourceTemplatesResult(resource_templates=[]))\n\n\nasync def test_list_prompts(app: MCPServer):\n    \"\"\"Test listing prompts with params parameter.\"\"\"\n    async with Client(app) as client:\n        result = await client.list_prompts()\n        assert result == snapshot(\n            ListPromptsResult(\n                prompts=[\n                    Prompt(\n                        name=\"greeting_prompt\",\n                        description=\"A greeting prompt.\",\n                        arguments=[PromptArgument(name=\"name\", required=True)],\n                    )\n                ]\n            )\n        )\n\n\nasync def test_complete_with_prompt_reference(simple_server: Server):\n    \"\"\"Test getting completions for a prompt argument.\"\"\"\n    async with Client(simple_server) as client:\n        ref = types.PromptReference(type=\"ref/prompt\", name=\"test_prompt\")\n        result = await client.complete(ref=ref, argument={\"name\": \"arg\", \"value\": \"test\"})\n        assert result == snapshot(types.CompleteResult(completion=types.Completion(values=[])))\n\n\ndef test_client_with_url_initializes_streamable_http_transport():\n    with patch(\"mcp.client.client.streamable_http_client\") as mock:\n        _ = Client(\"http://localhost:8000/mcp\")\n    mock.assert_called_once_with(\"http://localhost:8000/mcp\")\n\n\nasync def test_client_uses_transport_directly(app: MCPServer):\n    transport = InMemoryTransport(app)\n    async with Client(transport) as client:\n        result = await client.call_tool(\"greet\", {\"name\": \"Transport\"})\n        assert result == snapshot(\n            CallToolResult(\n                content=[TextContent(text=\"Hello, Transport!\")],\n                structured_content={\"result\": \"Hello, Transport!\"},\n            )\n        )\n"
  },
  {
    "path": "tests/client/test_http_unicode.py",
    "content": "\"\"\"Tests for Unicode handling in streamable HTTP transport.\n\nVerifies that Unicode text is correctly transmitted and received in both directions\n(server→client and client→server) using the streamable HTTP transport.\n\"\"\"\n\nimport multiprocessing\nimport socket\nfrom collections.abc import AsyncGenerator, Generator\nfrom contextlib import asynccontextmanager\n\nimport pytest\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom mcp import types\nfrom mcp.client.session import ClientSession\nfrom mcp.client.streamable_http import streamable_http_client\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.server.streamable_http_manager import StreamableHTTPSessionManager\nfrom mcp.types import TextContent, Tool\nfrom tests.test_helpers import wait_for_server\n\n# Test constants with various Unicode characters\nUNICODE_TEST_STRINGS = {\n    \"cyrillic\": \"Слой хранилища, где располагаются\",\n    \"cyrillic_short\": \"Привет мир\",\n    \"chinese\": \"你好世界 - 这是一个测试\",\n    \"japanese\": \"こんにちは世界 - これはテストです\",\n    \"korean\": \"안녕하세요 세계 - 이것은 테스트입니다\",\n    \"arabic\": \"مرحبا بالعالم - هذا اختبار\",\n    \"hebrew\": \"שלום עולם - זה מבחן\",\n    \"greek\": \"Γεια σου κόσμε - αυτό είναι δοκιμή\",\n    \"emoji\": \"Hello 👋 World 🌍 - Testing 🧪 Unicode ✨\",\n    \"math\": \"∑ ∫ √ ∞ ≠ ≤ ≥ ∈ ∉ ⊆ ⊇\",\n    \"accented\": \"Café, naïve, résumé, piñata, Zürich\",\n    \"mixed\": \"Hello世界🌍Привет안녕مرحباשלום\",\n    \"special\": \"Line\\nbreak\\ttab\\r\\nCRLF\",\n    \"quotes\": '«French» „German\" \"English\" 「Japanese」',\n    \"currency\": \"€100 £50 ¥1000 ₹500 ₽200 ¢99\",\n}\n\n\ndef run_unicode_server(port: int) -> None:  # pragma: no cover\n    \"\"\"Run the Unicode test server in a separate process.\"\"\"\n    import uvicorn\n\n    # Need to recreate the server setup in this process\n    async def handle_list_tools(\n        ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n    ) -> types.ListToolsResult:\n        return types.ListToolsResult(\n            tools=[\n                Tool(\n                    name=\"echo_unicode\",\n                    description=\"🔤 Echo Unicode text - Hello 👋 World 🌍 - Testing 🧪 Unicode ✨\",\n                    input_schema={\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"text\": {\"type\": \"string\", \"description\": \"Text to echo back\"},\n                        },\n                        \"required\": [\"text\"],\n                    },\n                ),\n            ]\n        )\n\n    async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:\n        if params.name == \"echo_unicode\":\n            text = params.arguments.get(\"text\", \"\") if params.arguments else \"\"\n            return types.CallToolResult(\n                content=[\n                    TextContent(\n                        type=\"text\",\n                        text=f\"Echo: {text}\",\n                    )\n                ]\n            )\n        else:\n            raise ValueError(f\"Unknown tool: {params.name}\")\n\n    async def handle_list_prompts(\n        ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n    ) -> types.ListPromptsResult:\n        return types.ListPromptsResult(\n            prompts=[\n                types.Prompt(\n                    name=\"unicode_prompt\",\n                    description=\"Unicode prompt - Слой хранилища, где располагаются\",\n                    arguments=[],\n                )\n            ]\n        )\n\n    async def handle_get_prompt(\n        ctx: ServerRequestContext, params: types.GetPromptRequestParams\n    ) -> types.GetPromptResult:\n        if params.name == \"unicode_prompt\":\n            return types.GetPromptResult(\n                messages=[\n                    types.PromptMessage(\n                        role=\"user\",\n                        content=types.TextContent(\n                            type=\"text\",\n                            text=\"Hello世界🌍Привет안녕مرحباשלום\",\n                        ),\n                    )\n                ]\n            )\n        raise ValueError(f\"Unknown prompt: {params.name}\")\n\n    server = Server(\n        name=\"unicode_test_server\",\n        on_list_tools=handle_list_tools,\n        on_call_tool=handle_call_tool,\n        on_list_prompts=handle_list_prompts,\n        on_get_prompt=handle_get_prompt,\n    )\n\n    # Create the session manager\n    session_manager = StreamableHTTPSessionManager(\n        app=server,\n        json_response=False,  # Use SSE for testing\n    )\n\n    @asynccontextmanager\n    async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:\n        async with session_manager.run():\n            yield\n\n    # Create an ASGI application\n    app = Starlette(\n        debug=True,\n        routes=[\n            Mount(\"/mcp\", app=session_manager.handle_request),\n        ],\n        lifespan=lifespan,\n    )\n\n    # Run the server\n    config = uvicorn.Config(\n        app=app,\n        host=\"127.0.0.1\",\n        port=port,\n        log_level=\"error\",\n    )\n    uvicorn_server = uvicorn.Server(config)\n    uvicorn_server.run()\n\n\n@pytest.fixture\ndef unicode_server_port() -> int:\n    \"\"\"Find an available port for the Unicode test server.\"\"\"\n    with socket.socket() as s:\n        s.bind((\"127.0.0.1\", 0))\n        return s.getsockname()[1]\n\n\n@pytest.fixture\ndef running_unicode_server(unicode_server_port: int) -> Generator[str, None, None]:\n    \"\"\"Start a Unicode test server in a separate process.\"\"\"\n    proc = multiprocessing.Process(target=run_unicode_server, kwargs={\"port\": unicode_server_port}, daemon=True)\n    proc.start()\n\n    # Wait for server to be ready\n    wait_for_server(unicode_server_port)\n\n    try:\n        yield f\"http://127.0.0.1:{unicode_server_port}\"\n    finally:\n        # Clean up - try graceful termination first\n        proc.terminate()\n        proc.join(timeout=2)\n        if proc.is_alive():  # pragma: no cover\n            proc.kill()\n            proc.join(timeout=1)\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_unicode_tool_call(running_unicode_server: str) -> None:\n    \"\"\"Test that Unicode text is correctly handled in tool calls via streamable HTTP.\"\"\"\n    base_url = running_unicode_server\n    endpoint_url = f\"{base_url}/mcp\"\n\n    async with streamable_http_client(endpoint_url) as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            await session.initialize()\n\n            # Test 1: List tools (server→client Unicode in descriptions)\n            tools = await session.list_tools()\n            assert len(tools.tools) == 1\n\n            # Check Unicode in tool descriptions\n            echo_tool = tools.tools[0]\n            assert echo_tool.name == \"echo_unicode\"\n            assert echo_tool.description is not None\n            assert \"🔤\" in echo_tool.description\n            assert \"👋\" in echo_tool.description\n\n            # Test 2: Send Unicode text in tool call (client→server→client)\n            for test_name, test_string in UNICODE_TEST_STRINGS.items():\n                result = await session.call_tool(\"echo_unicode\", arguments={\"text\": test_string})\n\n                # Verify server correctly received and echoed back Unicode\n                assert len(result.content) == 1\n                content = result.content[0]\n                assert content.type == \"text\"\n                assert f\"Echo: {test_string}\" == content.text, f\"Failed for {test_name}\"\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_unicode_prompts(running_unicode_server: str) -> None:\n    \"\"\"Test that Unicode text is correctly handled in prompts via streamable HTTP.\"\"\"\n    base_url = running_unicode_server\n    endpoint_url = f\"{base_url}/mcp\"\n\n    async with streamable_http_client(endpoint_url) as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            await session.initialize()\n\n            # Test 1: List prompts (server→client Unicode in descriptions)\n            prompts = await session.list_prompts()\n            assert len(prompts.prompts) == 1\n\n            prompt = prompts.prompts[0]\n            assert prompt.name == \"unicode_prompt\"\n            assert prompt.description is not None\n            assert \"Слой хранилища, где располагаются\" in prompt.description\n\n            # Test 2: Get prompt with Unicode content (server→client)\n            result = await session.get_prompt(\"unicode_prompt\", arguments={})\n            assert len(result.messages) == 1\n\n            message = result.messages[0]\n            assert message.role == \"user\"\n            assert message.content.type == \"text\"\n            assert message.content.text == \"Hello世界🌍Привет안녕مرحباשלום\"\n"
  },
  {
    "path": "tests/client/test_list_methods_cursor.py",
    "content": "from collections.abc import Callable\n\nimport pytest\n\nfrom mcp import Client, types\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.types import ListToolsResult\n\nfrom .conftest import StreamSpyCollection\n\npytestmark = pytest.mark.anyio\n\n\n@pytest.fixture\nasync def full_featured_server():\n    \"\"\"Create a server with tools, resources, prompts, and templates.\"\"\"\n    server = MCPServer(\"test\")\n\n    # pragma: no cover on handlers below - these exist only to register items with the\n    # server so list_* methods return results. The handlers themselves are never called\n    # because these tests only verify pagination/cursor behavior, not tool/resource invocation.\n    @server.tool()\n    def greet(name: str) -> str:  # pragma: no cover\n        \"\"\"Greet someone by name.\"\"\"\n        return f\"Hello, {name}!\"\n\n    @server.resource(\"test://resource\")\n    def test_resource() -> str:  # pragma: no cover\n        \"\"\"A test resource.\"\"\"\n        return \"Test content\"\n\n    @server.resource(\"test://template/{id}\")\n    def test_template(id: str) -> str:  # pragma: no cover\n        \"\"\"A test resource template.\"\"\"\n        return f\"Template content for {id}\"\n\n    @server.prompt()\n    def greeting_prompt(name: str) -> str:  # pragma: no cover\n        \"\"\"A greeting prompt.\"\"\"\n        return f\"Please greet {name}.\"\n\n    return server\n\n\n@pytest.mark.parametrize(\n    \"method_name,request_method\",\n    [\n        (\"list_tools\", \"tools/list\"),\n        (\"list_resources\", \"resources/list\"),\n        (\"list_prompts\", \"prompts/list\"),\n        (\"list_resource_templates\", \"resources/templates/list\"),\n    ],\n)\nasync def test_list_methods_params_parameter(\n    stream_spy: Callable[[], StreamSpyCollection],\n    full_featured_server: MCPServer,\n    method_name: str,\n    request_method: str,\n):\n    \"\"\"Test that the params parameter is accepted and correctly passed to the server.\n\n    Covers: list_tools, list_resources, list_prompts, list_resource_templates\n\n    See: https://modelcontextprotocol.io/specification/2025-03-26/server/utilities/pagination#request-format\n    \"\"\"\n    async with Client(full_featured_server) as client:\n        spies = stream_spy()\n\n        # Test without params (omitted)\n        method = getattr(client, method_name)\n        _ = await method()\n        requests = spies.get_client_requests(method=request_method)\n        assert len(requests) == 1\n        assert requests[0].params is None or \"cursor\" not in requests[0].params\n\n        spies.clear()\n\n        # Test with params containing cursor\n        _ = await method(cursor=\"from_params\")\n        requests = spies.get_client_requests(method=request_method)\n        assert len(requests) == 1\n        assert requests[0].params is not None\n        assert requests[0].params[\"cursor\"] == \"from_params\"\n\n        spies.clear()\n\n        # Test with empty params\n        _ = await method()\n        requests = spies.get_client_requests(method=request_method)\n        assert len(requests) == 1\n        # Empty params means no cursor\n        assert requests[0].params is None or \"cursor\" not in requests[0].params\n\n\nasync def test_list_tools_with_strict_server_validation(\n    full_featured_server: MCPServer,\n):\n    \"\"\"Test pagination with a server that validates request format strictly.\"\"\"\n    async with Client(full_featured_server) as client:\n        result = await client.list_tools()\n        assert isinstance(result, ListToolsResult)\n        assert len(result.tools) > 0\n\n\nasync def test_list_tools_with_lowlevel_server():\n    \"\"\"Test that list_tools works with a lowlevel Server using params.\"\"\"\n\n    async def handle_list_tools(\n        ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n    ) -> ListToolsResult:\n        # Echo back what cursor we received in the tool description\n        cursor = params.cursor if params else None\n        return ListToolsResult(tools=[types.Tool(name=\"test_tool\", description=f\"cursor={cursor}\", input_schema={})])\n\n    server = Server(\"test-lowlevel\", on_list_tools=handle_list_tools)\n\n    async with Client(server) as client:\n        result = await client.list_tools()\n        assert result.tools[0].description == \"cursor=None\"\n\n        result = await client.list_tools(cursor=\"page2\")\n        assert result.tools[0].description == \"cursor=page2\"\n"
  },
  {
    "path": "tests/client/test_list_roots_callback.py",
    "content": "import pytest\nfrom pydantic import FileUrl\n\nfrom mcp import Client\nfrom mcp.client.session import ClientSession\nfrom mcp.server.mcpserver import Context, MCPServer\nfrom mcp.shared._context import RequestContext\nfrom mcp.types import ListRootsResult, Root, TextContent\n\n\n@pytest.mark.anyio\nasync def test_list_roots_callback():\n    server = MCPServer(\"test\")\n\n    callback_return = ListRootsResult(\n        roots=[\n            Root(uri=FileUrl(\"file://users/fake/test\"), name=\"Test Root 1\"),\n            Root(uri=FileUrl(\"file://users/fake/test/2\"), name=\"Test Root 2\"),\n        ]\n    )\n\n    async def list_roots_callback(\n        context: RequestContext[ClientSession],\n    ) -> ListRootsResult:\n        return callback_return\n\n    @server.tool(\"test_list_roots\")\n    async def test_list_roots(context: Context, message: str):\n        roots = await context.session.list_roots()\n        assert roots == callback_return\n        return True\n\n    # Test with list_roots callback\n    async with Client(server, list_roots_callback=list_roots_callback) as client:\n        # Make a request to trigger sampling callback\n        result = await client.call_tool(\"test_list_roots\", {\"message\": \"test message\"})\n        assert result.is_error is False\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"true\"\n\n    # Test without list_roots callback\n    async with Client(server) as client:\n        # Make a request to trigger sampling callback\n        result = await client.call_tool(\"test_list_roots\", {\"message\": \"test message\"})\n        assert result.is_error is True\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Error executing tool test_list_roots: List roots not supported\"\n"
  },
  {
    "path": "tests/client/test_logging_callback.py",
    "content": "from typing import Any, Literal\n\nimport pytest\n\nfrom mcp import Client, types\nfrom mcp.server.mcpserver import Context, MCPServer\nfrom mcp.shared.session import RequestResponder\nfrom mcp.types import (\n    LoggingMessageNotificationParams,\n    TextContent,\n)\n\n\nclass LoggingCollector:\n    def __init__(self):\n        self.log_messages: list[LoggingMessageNotificationParams] = []\n\n    async def __call__(self, params: LoggingMessageNotificationParams) -> None:\n        self.log_messages.append(params)\n\n\n@pytest.mark.anyio\nasync def test_logging_callback():\n    server = MCPServer(\"test\")\n    logging_collector = LoggingCollector()\n\n    # Create a simple test tool\n    @server.tool(\"test_tool\")\n    async def test_tool() -> bool:\n        # The actual tool is very simple and just returns True\n        return True\n\n    # Create a function that can send a log notification\n    @server.tool(\"test_tool_with_log\")\n    async def test_tool_with_log(\n        message: str, level: Literal[\"debug\", \"info\", \"warning\", \"error\"], logger: str, ctx: Context\n    ) -> bool:\n        \"\"\"Send a log notification to the client.\"\"\"\n        await ctx.log(level=level, message=message, logger_name=logger)\n        return True\n\n    @server.tool(\"test_tool_with_log_extra\")\n    async def test_tool_with_log_extra(\n        message: str,\n        level: Literal[\"debug\", \"info\", \"warning\", \"error\"],\n        logger: str,\n        extra_string: str,\n        extra_dict: dict[str, Any],\n        ctx: Context,\n    ) -> bool:\n        \"\"\"Send a log notification to the client with extra fields.\"\"\"\n        await ctx.log(\n            level=level,\n            message=message,\n            logger_name=logger,\n            extra={\"extra_string\": extra_string, \"extra_dict\": extra_dict},\n        )\n        return True\n\n    # Create a message handler to catch exceptions\n    async def message_handler(\n        message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,\n    ) -> None:\n        if isinstance(message, Exception):  # pragma: no cover\n            raise message\n\n    async with Client(\n        server,\n        logging_callback=logging_collector,\n        message_handler=message_handler,\n    ) as client:\n        # First verify our test tool works\n        result = await client.call_tool(\"test_tool\", {})\n        assert result.is_error is False\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"true\"\n\n        # Now send a log message via our tool\n        log_result = await client.call_tool(\n            \"test_tool_with_log\",\n            {\n                \"message\": \"Test log message\",\n                \"level\": \"info\",\n                \"logger\": \"test_logger\",\n            },\n        )\n        log_result_with_extra = await client.call_tool(\n            \"test_tool_with_log_extra\",\n            {\n                \"message\": \"Test log message\",\n                \"level\": \"info\",\n                \"logger\": \"test_logger\",\n                \"extra_string\": \"example\",\n                \"extra_dict\": {\"a\": 1, \"b\": 2, \"c\": 3},\n            },\n        )\n        assert log_result.is_error is False\n        assert log_result_with_extra.is_error is False\n        assert len(logging_collector.log_messages) == 2\n        # Create meta object with related_request_id added dynamically\n        log = logging_collector.log_messages[0]\n        assert log.level == \"info\"\n        assert log.logger == \"test_logger\"\n        assert log.data == \"Test log message\"\n\n        log_with_extra = logging_collector.log_messages[1]\n        assert log_with_extra.level == \"info\"\n        assert log_with_extra.logger == \"test_logger\"\n        assert log_with_extra.data == {\n            \"message\": \"Test log message\",\n            \"extra_string\": \"example\",\n            \"extra_dict\": {\"a\": 1, \"b\": 2, \"c\": 3},\n        }\n"
  },
  {
    "path": "tests/client/test_notification_response.py",
    "content": "\"\"\"Tests for StreamableHTTP client transport with non-SDK servers.\n\nThese tests verify client behavior when interacting with servers\nthat don't follow SDK conventions.\n\"\"\"\n\nimport json\n\nimport httpx\nimport pytest\nfrom starlette.applications import Starlette\nfrom starlette.requests import Request\nfrom starlette.responses import JSONResponse, Response\nfrom starlette.routing import Route\n\nfrom mcp import ClientSession, MCPError, types\nfrom mcp.client.streamable_http import streamable_http_client\nfrom mcp.shared.session import RequestResponder\nfrom mcp.types import RootsListChangedNotification\n\npytestmark = pytest.mark.anyio\n\nINIT_RESPONSE = {\n    \"serverInfo\": {\"name\": \"test-non-sdk-server\", \"version\": \"1.0.0\"},\n    \"protocolVersion\": \"2024-11-05\",\n    \"capabilities\": {},\n}\n\n\ndef _init_json_response(data: dict[str, object]) -> JSONResponse:\n    return JSONResponse({\"jsonrpc\": \"2.0\", \"id\": data[\"id\"], \"result\": INIT_RESPONSE})\n\n\ndef _create_non_sdk_server_app() -> Starlette:\n    \"\"\"Create a minimal server that doesn't follow SDK conventions.\"\"\"\n\n    async def handle_mcp_request(request: Request) -> Response:\n        body = await request.body()\n        data = json.loads(body)\n\n        if data.get(\"method\") == \"initialize\":\n            return _init_json_response(data)\n\n        # For notifications, return 204 No Content (non-SDK behavior)\n        if \"id\" not in data:\n            return Response(status_code=204, headers={\"Content-Type\": \"application/json\"})\n\n        return JSONResponse(  # pragma: no cover\n            {\"jsonrpc\": \"2.0\", \"id\": data.get(\"id\"), \"error\": {\"code\": -32601, \"message\": \"Method not found\"}}\n        )\n\n    return Starlette(debug=True, routes=[Route(\"/mcp\", handle_mcp_request, methods=[\"POST\"])])\n\n\ndef _create_unexpected_content_type_app() -> Starlette:\n    \"\"\"Create a server that returns an unexpected content type for requests.\"\"\"\n\n    async def handle_mcp_request(request: Request) -> Response:\n        body = await request.body()\n        data = json.loads(body)\n\n        if data.get(\"method\") == \"initialize\":\n            return _init_json_response(data)\n\n        if \"id\" not in data:\n            return Response(status_code=202)\n\n        # Return text/plain for all other requests — an unexpected content type.\n        return Response(content=\"this is plain text, not json or sse\", status_code=200, media_type=\"text/plain\")\n\n    return Starlette(debug=True, routes=[Route(\"/mcp\", handle_mcp_request, methods=[\"POST\"])])\n\n\nasync def test_non_compliant_notification_response() -> None:\n    \"\"\"Verify the client ignores unexpected responses to notifications.\n\n    The spec states notifications should get either 202 + no response body, or 4xx + optional error body\n    (https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#sending-messages-to-the-server),\n    but some servers wrongly return other 2xx codes (e.g. 204). For now we simply ignore unexpected responses\n    (aligning behaviour w/ the TS SDK).\n    \"\"\"\n    returned_exception = None\n\n    async def message_handler(  # pragma: no cover\n        message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,\n    ) -> None:\n        nonlocal returned_exception\n        if isinstance(message, Exception):\n            returned_exception = message\n\n    async with httpx.AsyncClient(transport=httpx.ASGITransport(app=_create_non_sdk_server_app())) as client:\n        async with streamable_http_client(\"http://localhost/mcp\", http_client=client) as (read_stream, write_stream):\n            async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:\n                await session.initialize()\n\n                # The test server returns a 204 instead of the expected 202\n                await session.send_notification(RootsListChangedNotification(method=\"notifications/roots/list_changed\"))\n\n    if returned_exception:  # pragma: no cover\n        pytest.fail(f\"Server encountered an exception: {returned_exception}\")\n\n\nasync def test_unexpected_content_type_sends_jsonrpc_error() -> None:\n    \"\"\"Verify unexpected content types unblock the pending request with an MCPError.\n\n    When a server returns a content type that is neither application/json nor text/event-stream,\n    the client should send a JSONRPCError so the pending request resolves immediately\n    instead of hanging until timeout.\n    \"\"\"\n    async with httpx.AsyncClient(transport=httpx.ASGITransport(app=_create_unexpected_content_type_app())) as client:\n        async with streamable_http_client(\"http://localhost/mcp\", http_client=client) as (read_stream, write_stream):\n            async with ClientSession(read_stream, write_stream) as session:  # pragma: no branch\n                await session.initialize()\n\n                with pytest.raises(MCPError, match=\"Unexpected content type: text/plain\"):  # pragma: no branch\n                    await session.list_tools()\n\n\ndef _create_http_error_app(error_status: int, *, error_on_notifications: bool = False) -> Starlette:\n    \"\"\"Create a server that returns an HTTP error for non-init requests.\"\"\"\n\n    async def handle_mcp_request(request: Request) -> Response:\n        body = await request.body()\n        data = json.loads(body)\n\n        if data.get(\"method\") == \"initialize\":\n            return _init_json_response(data)\n\n        if \"id\" not in data:\n            if error_on_notifications:\n                return Response(status_code=error_status)\n            return Response(status_code=202)\n\n        return Response(status_code=error_status)\n\n    return Starlette(debug=True, routes=[Route(\"/mcp\", handle_mcp_request, methods=[\"POST\"])])\n\n\nasync def test_http_error_status_sends_jsonrpc_error() -> None:\n    \"\"\"Verify HTTP 5xx errors unblock the pending request with an MCPError.\n\n    When a server returns a non-2xx status code (e.g. 500), the client should\n    send a JSONRPCError so the pending request resolves immediately instead of\n    raising an unhandled httpx.HTTPStatusError that causes the caller to hang.\n    \"\"\"\n    async with httpx.AsyncClient(transport=httpx.ASGITransport(app=_create_http_error_app(500))) as client:\n        async with streamable_http_client(\"http://localhost/mcp\", http_client=client) as (read_stream, write_stream):\n            async with ClientSession(read_stream, write_stream) as session:  # pragma: no branch\n                await session.initialize()\n\n                with pytest.raises(MCPError, match=\"Server returned an error response\"):  # pragma: no branch\n                    await session.list_tools()\n\n\nasync def test_http_error_on_notification_does_not_hang() -> None:\n    \"\"\"Verify HTTP errors on notifications are silently ignored.\n\n    When a notification gets an HTTP error, there is no pending request to\n    unblock, so the client should just return without sending a JSONRPCError.\n    \"\"\"\n    app = _create_http_error_app(500, error_on_notifications=True)\n    async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app)) as client:\n        async with streamable_http_client(\"http://localhost/mcp\", http_client=client) as (read_stream, write_stream):\n            async with ClientSession(read_stream, write_stream) as session:  # pragma: no branch\n                await session.initialize()\n\n                # Should not raise or hang — the error is silently ignored for notifications\n                await session.send_notification(RootsListChangedNotification(method=\"notifications/roots/list_changed\"))\n\n\ndef _create_invalid_json_response_app() -> Starlette:\n    \"\"\"Create a server that returns invalid JSON for requests.\"\"\"\n\n    async def handle_mcp_request(request: Request) -> Response:\n        body = await request.body()\n        data = json.loads(body)\n\n        if data.get(\"method\") == \"initialize\":\n            return _init_json_response(data)\n\n        if \"id\" not in data:\n            return Response(status_code=202)\n\n        # Return application/json content type but with invalid JSON body.\n        return Response(content=\"not valid json{{{\", status_code=200, media_type=\"application/json\")\n\n    return Starlette(debug=True, routes=[Route(\"/mcp\", handle_mcp_request, methods=[\"POST\"])])\n\n\nasync def test_invalid_json_response_sends_jsonrpc_error() -> None:\n    \"\"\"Verify invalid JSON responses unblock the pending request with an MCPError.\n\n    When a server returns application/json with an unparseable body, the client\n    should send a JSONRPCError so the pending request resolves immediately\n    instead of hanging until timeout.\n    \"\"\"\n    async with httpx.AsyncClient(transport=httpx.ASGITransport(app=_create_invalid_json_response_app())) as client:\n        async with streamable_http_client(\"http://localhost/mcp\", http_client=client) as (read_stream, write_stream):\n            async with ClientSession(read_stream, write_stream) as session:  # pragma: no branch\n                await session.initialize()\n\n                with pytest.raises(MCPError, match=\"Failed to parse JSON response\"):  # pragma: no branch\n                    await session.list_tools()\n"
  },
  {
    "path": "tests/client/test_output_schema_validation.py",
    "content": "import logging\nfrom typing import Any\n\nimport pytest\n\nfrom mcp import Client\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.types import (\n    CallToolRequestParams,\n    CallToolResult,\n    ListToolsResult,\n    PaginatedRequestParams,\n    TextContent,\n    Tool,\n)\n\n\ndef _make_server(\n    tools: list[Tool],\n    structured_content: dict[str, Any],\n) -> Server:\n    \"\"\"Create a low-level server that returns the given structured_content for any tool call.\"\"\"\n\n    async def on_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        return ListToolsResult(tools=tools)\n\n    async def on_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:\n        return CallToolResult(\n            content=[TextContent(type=\"text\", text=\"result\")],\n            structured_content=structured_content,\n        )\n\n    return Server(\"test-server\", on_list_tools=on_list_tools, on_call_tool=on_call_tool)\n\n\n@pytest.mark.anyio\nasync def test_tool_structured_output_client_side_validation_basemodel():\n    \"\"\"Test that client validates structured content against schema for BaseModel outputs\"\"\"\n    output_schema = {\n        \"type\": \"object\",\n        \"properties\": {\"name\": {\"type\": \"string\", \"title\": \"Name\"}, \"age\": {\"type\": \"integer\", \"title\": \"Age\"}},\n        \"required\": [\"name\", \"age\"],\n        \"title\": \"UserOutput\",\n    }\n\n    server = _make_server(\n        tools=[\n            Tool(\n                name=\"get_user\",\n                description=\"Get user data\",\n                input_schema={\"type\": \"object\"},\n                output_schema=output_schema,\n            )\n        ],\n        structured_content={\"name\": \"John\", \"age\": \"invalid\"},  # Invalid: age should be int\n    )\n\n    async with Client(server) as client:\n        with pytest.raises(RuntimeError) as exc_info:\n            await client.call_tool(\"get_user\", {})\n        assert \"Invalid structured content returned by tool get_user\" in str(exc_info.value)\n\n\n@pytest.mark.anyio\nasync def test_tool_structured_output_client_side_validation_primitive():\n    \"\"\"Test that client validates structured content for primitive outputs\"\"\"\n    output_schema = {\n        \"type\": \"object\",\n        \"properties\": {\"result\": {\"type\": \"integer\", \"title\": \"Result\"}},\n        \"required\": [\"result\"],\n        \"title\": \"calculate_Output\",\n    }\n\n    server = _make_server(\n        tools=[\n            Tool(\n                name=\"calculate\",\n                description=\"Calculate something\",\n                input_schema={\"type\": \"object\"},\n                output_schema=output_schema,\n            )\n        ],\n        structured_content={\"result\": \"not_a_number\"},  # Invalid: should be int\n    )\n\n    async with Client(server) as client:\n        with pytest.raises(RuntimeError) as exc_info:\n            await client.call_tool(\"calculate\", {})\n        assert \"Invalid structured content returned by tool calculate\" in str(exc_info.value)\n\n\n@pytest.mark.anyio\nasync def test_tool_structured_output_client_side_validation_dict_typed():\n    \"\"\"Test that client validates dict[str, T] structured content\"\"\"\n    output_schema = {\"type\": \"object\", \"additionalProperties\": {\"type\": \"integer\"}, \"title\": \"get_scores_Output\"}\n\n    server = _make_server(\n        tools=[\n            Tool(\n                name=\"get_scores\",\n                description=\"Get scores\",\n                input_schema={\"type\": \"object\"},\n                output_schema=output_schema,\n            )\n        ],\n        structured_content={\"alice\": \"100\", \"bob\": \"85\"},  # Invalid: values should be int\n    )\n\n    async with Client(server) as client:\n        with pytest.raises(RuntimeError) as exc_info:\n            await client.call_tool(\"get_scores\", {})\n        assert \"Invalid structured content returned by tool get_scores\" in str(exc_info.value)\n\n\n@pytest.mark.anyio\nasync def test_tool_structured_output_client_side_validation_missing_required():\n    \"\"\"Test that client validates missing required fields\"\"\"\n    output_schema = {\n        \"type\": \"object\",\n        \"properties\": {\"name\": {\"type\": \"string\"}, \"age\": {\"type\": \"integer\"}, \"email\": {\"type\": \"string\"}},\n        \"required\": [\"name\", \"age\", \"email\"],\n        \"title\": \"PersonOutput\",\n    }\n\n    server = _make_server(\n        tools=[\n            Tool(\n                name=\"get_person\",\n                description=\"Get person data\",\n                input_schema={\"type\": \"object\"},\n                output_schema=output_schema,\n            )\n        ],\n        structured_content={\"name\": \"John\", \"age\": 30},  # Missing required 'email'\n    )\n\n    async with Client(server) as client:\n        with pytest.raises(RuntimeError) as exc_info:\n            await client.call_tool(\"get_person\", {})\n        assert \"Invalid structured content returned by tool get_person\" in str(exc_info.value)\n\n\n@pytest.mark.anyio\nasync def test_tool_not_listed_warning(caplog: pytest.LogCaptureFixture):\n    \"\"\"Test that client logs warning when tool is not in list_tools but has output_schema\"\"\"\n\n    async def on_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        return ListToolsResult(tools=[])\n\n    async def on_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:\n        return CallToolResult(\n            content=[TextContent(type=\"text\", text=\"result\")],\n            structured_content={\"result\": 42},\n        )\n\n    server = Server(\"test-server\", on_list_tools=on_list_tools, on_call_tool=on_call_tool)\n\n    caplog.set_level(logging.WARNING)\n\n    async with Client(server) as client:\n        result = await client.call_tool(\"mystery_tool\", {})\n        assert result.structured_content == {\"result\": 42}\n        assert result.is_error is False\n\n        assert \"Tool mystery_tool not listed\" in caplog.text\n"
  },
  {
    "path": "tests/client/test_resource_cleanup.py",
    "content": "from typing import Any\nfrom unittest.mock import patch\n\nimport anyio\nimport pytest\nfrom pydantic import TypeAdapter\n\nfrom mcp.shared.message import SessionMessage\nfrom mcp.shared.session import BaseSession, RequestId, SendResultT\nfrom mcp.types import ClientNotification, ClientRequest, ClientResult, EmptyResult, ErrorData, PingRequest\n\n\n@pytest.mark.anyio\nasync def test_send_request_stream_cleanup():\n    \"\"\"Test that send_request properly cleans up streams when an exception occurs.\n\n    This test mocks out most of the session functionality to focus on stream cleanup.\n    \"\"\"\n\n    # Create a mock session with the minimal required functionality\n    class TestSession(BaseSession[ClientRequest, ClientNotification, ClientResult, Any, Any]):\n        async def _send_response(\n            self, request_id: RequestId, response: SendResultT | ErrorData\n        ) -> None:  # pragma: no cover\n            pass\n\n        @property\n        def _receive_request_adapter(self) -> TypeAdapter[Any]:\n            return TypeAdapter(object)  # pragma: no cover\n\n        @property\n        def _receive_notification_adapter(self) -> TypeAdapter[Any]:\n            return TypeAdapter(object)  # pragma: no cover\n\n    # Create streams\n    write_stream_send, write_stream_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    read_stream_send, read_stream_receive = anyio.create_memory_object_stream[SessionMessage](1)\n\n    # Create the session\n    session = TestSession(read_stream_receive, write_stream_send)\n\n    # Create a test request\n    request = PingRequest()\n\n    # Patch the _write_stream.send method to raise an exception\n    async def mock_send(*args: Any, **kwargs: Any):\n        raise RuntimeError(\"Simulated network error\")\n\n    # Record the response streams before the test\n    initial_stream_count = len(session._response_streams)\n\n    # Run the test with the patched method\n    with patch.object(session._write_stream, \"send\", mock_send):\n        with pytest.raises(RuntimeError):\n            await session.send_request(request, EmptyResult)\n\n    # Verify that no response streams were leaked\n    assert len(session._response_streams) == initial_stream_count, (\n        f\"Expected {initial_stream_count} response streams after request, but found {len(session._response_streams)}\"\n    )\n\n    # Clean up\n    await write_stream_send.aclose()\n    await write_stream_receive.aclose()\n    await read_stream_send.aclose()\n    await read_stream_receive.aclose()\n"
  },
  {
    "path": "tests/client/test_sampling_callback.py",
    "content": "import pytest\n\nfrom mcp import Client\nfrom mcp.client.session import ClientSession\nfrom mcp.server.mcpserver import Context, MCPServer\nfrom mcp.shared._context import RequestContext\nfrom mcp.types import (\n    CreateMessageRequestParams,\n    CreateMessageResult,\n    CreateMessageResultWithTools,\n    SamplingMessage,\n    TextContent,\n    ToolUseContent,\n)\n\n\n@pytest.mark.anyio\nasync def test_sampling_callback():\n    server = MCPServer(\"test\")\n\n    callback_return = CreateMessageResult(\n        role=\"assistant\",\n        content=TextContent(type=\"text\", text=\"This is a response from the sampling callback\"),\n        model=\"test-model\",\n        stop_reason=\"endTurn\",\n    )\n\n    async def sampling_callback(\n        context: RequestContext[ClientSession],\n        params: CreateMessageRequestParams,\n    ) -> CreateMessageResult:\n        return callback_return\n\n    @server.tool(\"test_sampling\")\n    async def test_sampling_tool(message: str, ctx: Context) -> bool:\n        value = await ctx.session.create_message(\n            messages=[SamplingMessage(role=\"user\", content=TextContent(type=\"text\", text=message))],\n            max_tokens=100,\n        )\n        assert value == callback_return\n        return True\n\n    # Test with sampling callback\n    async with Client(server, sampling_callback=sampling_callback) as client:\n        # Make a request to trigger sampling callback\n        result = await client.call_tool(\"test_sampling\", {\"message\": \"Test message for sampling\"})\n        assert result.is_error is False\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"true\"\n\n    # Test without sampling callback\n    async with Client(server) as client:\n        # Make a request to trigger sampling callback\n        result = await client.call_tool(\"test_sampling\", {\"message\": \"Test message for sampling\"})\n        assert result.is_error is True\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Error executing tool test_sampling: Sampling not supported\"\n\n\n@pytest.mark.anyio\nasync def test_create_message_backwards_compat_single_content():\n    \"\"\"Test backwards compatibility: create_message without tools returns single content.\"\"\"\n    server = MCPServer(\"test\")\n\n    # Callback returns single content (text)\n    callback_return = CreateMessageResult(\n        role=\"assistant\",\n        content=TextContent(type=\"text\", text=\"Hello from LLM\"),\n        model=\"test-model\",\n        stop_reason=\"endTurn\",\n    )\n\n    async def sampling_callback(\n        context: RequestContext[ClientSession],\n        params: CreateMessageRequestParams,\n    ) -> CreateMessageResult:\n        return callback_return\n\n    @server.tool(\"test_backwards_compat\")\n    async def test_tool(message: str, ctx: Context) -> bool:\n        # Call create_message WITHOUT tools\n        result = await ctx.session.create_message(\n            messages=[SamplingMessage(role=\"user\", content=TextContent(type=\"text\", text=message))],\n            max_tokens=100,\n        )\n        # Backwards compat: result should be CreateMessageResult\n        assert isinstance(result, CreateMessageResult)\n        # Content should be single (not a list) - this is the key backwards compat check\n        assert isinstance(result.content, TextContent)\n        assert result.content.text == \"Hello from LLM\"\n        # CreateMessageResult should NOT have content_as_list (that's on WithTools)\n        assert not hasattr(result, \"content_as_list\") or not callable(getattr(result, \"content_as_list\", None))\n        return True\n\n    async with Client(server, sampling_callback=sampling_callback) as client:\n        result = await client.call_tool(\"test_backwards_compat\", {\"message\": \"Test\"})\n        assert result.is_error is False\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"true\"\n\n\n@pytest.mark.anyio\nasync def test_create_message_result_with_tools_type():\n    \"\"\"Test that CreateMessageResultWithTools supports content_as_list.\"\"\"\n    # Test the type itself, not the overload (overload requires client capability setup)\n    result = CreateMessageResultWithTools(\n        role=\"assistant\",\n        content=ToolUseContent(type=\"tool_use\", id=\"call_123\", name=\"get_weather\", input={\"city\": \"SF\"}),\n        model=\"test-model\",\n        stop_reason=\"toolUse\",\n    )\n\n    # CreateMessageResultWithTools should have content_as_list\n    content_list = result.content_as_list\n    assert len(content_list) == 1\n    assert content_list[0].type == \"tool_use\"\n\n    # It should also work with array content\n    result_array = CreateMessageResultWithTools(\n        role=\"assistant\",\n        content=[\n            TextContent(type=\"text\", text=\"Let me check the weather\"),\n            ToolUseContent(type=\"tool_use\", id=\"call_456\", name=\"get_weather\", input={\"city\": \"NYC\"}),\n        ],\n        model=\"test-model\",\n        stop_reason=\"toolUse\",\n    )\n    content_list_array = result_array.content_as_list\n    assert len(content_list_array) == 2\n    assert content_list_array[0].type == \"text\"\n    assert content_list_array[1].type == \"tool_use\"\n"
  },
  {
    "path": "tests/client/test_scope_bug_1630.py",
    "content": "\"\"\"Regression test for issue #1630: OAuth2 scope incorrectly set to resource_metadata URL.\n\nThis test verifies that when a 401 response contains both resource_metadata and scope\nin the WWW-Authenticate header, the actual scope is used (not the resource_metadata URL).\n\"\"\"\n\nfrom unittest import mock\n\nimport httpx\nimport pytest\nfrom pydantic import AnyUrl\n\nfrom mcp.client.auth import OAuthClientProvider\nfrom mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken\n\n\nclass MockTokenStorage:\n    \"\"\"Mock token storage for testing.\"\"\"\n\n    def __init__(self) -> None:\n        self._tokens: OAuthToken | None = None\n        self._client_info: OAuthClientInformationFull | None = None\n\n    async def get_tokens(self) -> OAuthToken | None:\n        return self._tokens  # pragma: no cover\n\n    async def set_tokens(self, tokens: OAuthToken) -> None:\n        self._tokens = tokens\n\n    async def get_client_info(self) -> OAuthClientInformationFull | None:\n        return self._client_info  # pragma: no cover\n\n    async def set_client_info(self, client_info: OAuthClientInformationFull) -> None:\n        self._client_info = client_info  # pragma: no cover\n\n\n@pytest.mark.anyio\nasync def test_401_uses_www_auth_scope_not_resource_metadata_url():\n    \"\"\"Regression test for #1630: Ensure scope is extracted from WWW-Authenticate header,\n    not the resource_metadata URL.\n\n    When a 401 response contains:\n        WWW-Authenticate: Bearer resource_metadata=\"https://...\", scope=\"read write\"\n\n    The client should use \"read write\" as the scope, NOT the resource_metadata URL.\n    \"\"\"\n\n    async def redirect_handler(url: str) -> None:\n        pass  # pragma: no cover\n\n    async def callback_handler() -> tuple[str, str | None]:\n        return \"test_auth_code\", \"test_state\"  # pragma: no cover\n\n    client_metadata = OAuthClientMetadata(\n        redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n        client_name=\"Test Client\",\n    )\n\n    provider = OAuthClientProvider(\n        server_url=\"https://api.example.com/mcp\",\n        client_metadata=client_metadata,\n        storage=MockTokenStorage(),\n        redirect_handler=redirect_handler,\n        callback_handler=callback_handler,\n    )\n\n    provider.context.current_tokens = None\n    provider.context.token_expiry_time = None\n    provider._initialized = True\n\n    # Pre-set client info to skip DCR\n    provider.context.client_info = OAuthClientInformationFull(\n        client_id=\"test_client\",\n        redirect_uris=[AnyUrl(\"http://localhost:3030/callback\")],\n    )\n\n    test_request = httpx.Request(\"GET\", \"https://api.example.com/mcp\")\n    auth_flow = provider.async_auth_flow(test_request)\n\n    # First request (no auth header yet)\n    await auth_flow.__anext__()\n\n    # 401 response with BOTH resource_metadata URL and scope in WWW-Authenticate\n    # This is the key: the bug would use the URL as scope instead of \"read write\"\n    resource_metadata_url = \"https://api.example.com/.well-known/oauth-protected-resource\"\n    expected_scope = \"read write\"\n\n    response_401 = httpx.Response(\n        401,\n        headers={\"WWW-Authenticate\": (f'Bearer resource_metadata=\"{resource_metadata_url}\", scope=\"{expected_scope}\"')},\n        request=test_request,\n    )\n\n    # Send 401, expect PRM discovery request\n    prm_request = await auth_flow.asend(response_401)\n    assert \".well-known/oauth-protected-resource\" in str(prm_request.url)\n\n    # PRM response with scopes_supported (these should be overridden by WWW-Auth scope)\n    prm_response = httpx.Response(\n        200,\n        content=(\n            b'{\"resource\": \"https://api.example.com/mcp\", '\n            b'\"authorization_servers\": [\"https://auth.example.com\"], '\n            b'\"scopes_supported\": [\"fallback:scope1\", \"fallback:scope2\"]}'\n        ),\n        request=prm_request,\n    )\n\n    # Send PRM response, expect OAuth metadata discovery\n    oauth_metadata_request = await auth_flow.asend(prm_response)\n    assert \".well-known/oauth-authorization-server\" in str(oauth_metadata_request.url)\n\n    # OAuth metadata response\n    oauth_metadata_response = httpx.Response(\n        200,\n        content=(\n            b'{\"issuer\": \"https://auth.example.com\", '\n            b'\"authorization_endpoint\": \"https://auth.example.com/authorize\", '\n            b'\"token_endpoint\": \"https://auth.example.com/token\"}'\n        ),\n        request=oauth_metadata_request,\n    )\n\n    # Mock authorization to skip interactive flow\n    provider._perform_authorization_code_grant = mock.AsyncMock(return_value=(\"test_auth_code\", \"test_code_verifier\"))\n\n    # Send OAuth metadata response, expect token request\n    token_request = await auth_flow.asend(oauth_metadata_response)\n    assert \"token\" in str(token_request.url)\n\n    # NOW CHECK: The scope should be the WWW-Authenticate scope, NOT the URL\n    # This is where the bug manifested - scope was set to resource_metadata_url\n    actual_scope = provider.context.client_metadata.scope\n\n    # This assertion would FAIL on main (scope would be the URL)\n    # but PASS on the fix branch (scope is \"read write\")\n    assert actual_scope == expected_scope, (\n        f\"Expected scope to be '{expected_scope}' from WWW-Authenticate header, \"\n        f\"but got '{actual_scope}'. \"\n        f\"If scope is '{resource_metadata_url}', the bug from #1630 is present.\"\n    )\n\n    # Verify it's definitely not the URL (explicit check for the bug)\n    assert actual_scope != resource_metadata_url, (\n        f\"BUG #1630: Scope was incorrectly set to resource_metadata URL '{resource_metadata_url}' \"\n        f\"instead of the actual scope '{expected_scope}'\"\n    )\n\n    # Complete the flow to properly release the lock\n    token_response = httpx.Response(\n        200,\n        content=b'{\"access_token\": \"test_token\", \"token_type\": \"Bearer\", \"expires_in\": 3600}',\n        request=token_request,\n    )\n\n    final_request = await auth_flow.asend(token_response)\n    assert final_request.headers[\"Authorization\"] == \"Bearer test_token\"\n\n    # Finish the flow\n    final_response = httpx.Response(200, request=final_request)\n    try:\n        await auth_flow.asend(final_response)\n    except StopAsyncIteration:\n        pass\n"
  },
  {
    "path": "tests/client/test_session.py",
    "content": "from __future__ import annotations\n\nimport anyio\nimport pytest\n\nfrom mcp import types\nfrom mcp.client.session import DEFAULT_CLIENT_INFO, ClientSession\nfrom mcp.shared._context import RequestContext\nfrom mcp.shared.message import SessionMessage\nfrom mcp.shared.session import RequestResponder\nfrom mcp.shared.version import SUPPORTED_PROTOCOL_VERSIONS\nfrom mcp.types import (\n    LATEST_PROTOCOL_VERSION,\n    CallToolResult,\n    Implementation,\n    InitializedNotification,\n    InitializeRequest,\n    InitializeResult,\n    JSONRPCNotification,\n    JSONRPCRequest,\n    JSONRPCResponse,\n    RequestParamsMeta,\n    ServerCapabilities,\n    TextContent,\n    client_notification_adapter,\n    client_request_adapter,\n)\n\n\n@pytest.mark.anyio\nasync def test_client_session_initialize():\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n\n    initialized_notification = None\n    result = None\n\n    async def mock_server():\n        nonlocal initialized_notification\n\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n        request = client_request_adapter.validate_python(\n            jsonrpc_request.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n        )\n        assert isinstance(request, InitializeRequest)\n\n        result = InitializeResult(\n            protocol_version=LATEST_PROTOCOL_VERSION,\n            capabilities=ServerCapabilities(\n                logging=None,\n                resources=None,\n                tools=None,\n                experimental=None,\n                prompts=None,\n            ),\n            server_info=Implementation(name=\"mock-server\", version=\"0.1.0\"),\n            instructions=\"The server instructions.\",\n        )\n\n        async with server_to_client_send:\n            await server_to_client_send.send(\n                SessionMessage(\n                    JSONRPCResponse(\n                        jsonrpc=\"2.0\",\n                        id=jsonrpc_request.id,\n                        result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                    )\n                )\n            )\n            session_notification = await client_to_server_receive.receive()\n            jsonrpc_notification = session_notification.message\n            assert isinstance(jsonrpc_notification, JSONRPCNotification)\n            initialized_notification = client_notification_adapter.validate_python(\n                jsonrpc_notification.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n            )\n\n    # Create a message handler to catch exceptions\n    async def message_handler(  # pragma: no cover\n        message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,\n    ) -> None:\n        if isinstance(message, Exception):\n            raise message\n\n    async with (\n        ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n            message_handler=message_handler,\n        ) as session,\n        anyio.create_task_group() as tg,\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        tg.start_soon(mock_server)\n        result = await session.initialize()\n\n    # Assert the result\n    assert isinstance(result, InitializeResult)\n    assert result.protocol_version == LATEST_PROTOCOL_VERSION\n    assert isinstance(result.capabilities, ServerCapabilities)\n    assert result.server_info == Implementation(name=\"mock-server\", version=\"0.1.0\")\n    assert result.instructions == \"The server instructions.\"\n\n    # Check that the client sent the initialized notification\n    assert initialized_notification\n    assert isinstance(initialized_notification, InitializedNotification)\n\n\n@pytest.mark.anyio\nasync def test_client_session_custom_client_info():\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n\n    custom_client_info = Implementation(name=\"test-client\", version=\"1.2.3\")\n    received_client_info = None\n\n    async def mock_server():\n        nonlocal received_client_info\n\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n        request = client_request_adapter.validate_python(\n            jsonrpc_request.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n        )\n        assert isinstance(request, InitializeRequest)\n        received_client_info = request.params.client_info\n\n        result = InitializeResult(\n            protocol_version=LATEST_PROTOCOL_VERSION,\n            capabilities=ServerCapabilities(),\n            server_info=Implementation(name=\"mock-server\", version=\"0.1.0\"),\n        )\n\n        async with server_to_client_send:\n            await server_to_client_send.send(\n                SessionMessage(\n                    JSONRPCResponse(\n                        jsonrpc=\"2.0\",\n                        id=jsonrpc_request.id,\n                        result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                    )\n                )\n            )\n            # Receive initialized notification\n            await client_to_server_receive.receive()\n\n    async with (\n        ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n            client_info=custom_client_info,\n        ) as session,\n        anyio.create_task_group() as tg,\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        tg.start_soon(mock_server)\n        await session.initialize()\n\n    # Assert that the custom client info was sent\n    assert received_client_info == custom_client_info\n\n\n@pytest.mark.anyio\nasync def test_client_session_default_client_info():\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n\n    received_client_info = None\n\n    async def mock_server():\n        nonlocal received_client_info\n\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n        request = client_request_adapter.validate_python(\n            jsonrpc_request.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n        )\n        assert isinstance(request, InitializeRequest)\n        received_client_info = request.params.client_info\n\n        result = InitializeResult(\n            protocol_version=LATEST_PROTOCOL_VERSION,\n            capabilities=ServerCapabilities(),\n            server_info=Implementation(name=\"mock-server\", version=\"0.1.0\"),\n        )\n\n        async with server_to_client_send:\n            await server_to_client_send.send(\n                SessionMessage(\n                    JSONRPCResponse(\n                        jsonrpc=\"2.0\",\n                        id=jsonrpc_request.id,\n                        result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                    )\n                )\n            )\n            # Receive initialized notification\n            await client_to_server_receive.receive()\n\n    async with (\n        ClientSession(server_to_client_receive, client_to_server_send) as session,\n        anyio.create_task_group() as tg,\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        tg.start_soon(mock_server)\n        await session.initialize()\n\n    # Assert that the default client info was sent\n    assert received_client_info == DEFAULT_CLIENT_INFO\n\n\n@pytest.mark.anyio\nasync def test_client_session_version_negotiation_success():\n    \"\"\"Test successful version negotiation with supported version\"\"\"\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    result = None\n\n    async def mock_server():\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n        request = client_request_adapter.validate_python(\n            jsonrpc_request.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n        )\n        assert isinstance(request, InitializeRequest)\n\n        # Verify client sent the latest protocol version\n        assert request.params.protocol_version == LATEST_PROTOCOL_VERSION\n\n        # Server responds with a supported older version\n        result = InitializeResult(\n            protocol_version=\"2024-11-05\",\n            capabilities=ServerCapabilities(),\n            server_info=Implementation(name=\"mock-server\", version=\"0.1.0\"),\n        )\n\n        async with server_to_client_send:\n            await server_to_client_send.send(\n                SessionMessage(\n                    JSONRPCResponse(\n                        jsonrpc=\"2.0\",\n                        id=jsonrpc_request.id,\n                        result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                    )\n                )\n            )\n            # Receive initialized notification\n            await client_to_server_receive.receive()\n\n    async with (\n        ClientSession(server_to_client_receive, client_to_server_send) as session,\n        anyio.create_task_group() as tg,\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        tg.start_soon(mock_server)\n        result = await session.initialize()\n\n    # Assert the result with negotiated version\n    assert isinstance(result, InitializeResult)\n    assert result.protocol_version == \"2024-11-05\"\n    assert result.protocol_version in SUPPORTED_PROTOCOL_VERSIONS\n\n\n@pytest.mark.anyio\nasync def test_client_session_version_negotiation_failure():\n    \"\"\"Test version negotiation failure with unsupported version\"\"\"\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n\n    async def mock_server():\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n        request = client_request_adapter.validate_python(\n            jsonrpc_request.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n        )\n        assert isinstance(request, InitializeRequest)\n\n        # Server responds with an unsupported version\n        result = InitializeResult(\n            protocol_version=\"2020-01-01\",  # Unsupported old version\n            capabilities=ServerCapabilities(),\n            server_info=Implementation(name=\"mock-server\", version=\"0.1.0\"),\n        )\n\n        async with server_to_client_send:\n            await server_to_client_send.send(\n                SessionMessage(\n                    JSONRPCResponse(\n                        jsonrpc=\"2.0\",\n                        id=jsonrpc_request.id,\n                        result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                    )\n                )\n            )\n\n    async with (\n        ClientSession(server_to_client_receive, client_to_server_send) as session,\n        anyio.create_task_group() as tg,\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        tg.start_soon(mock_server)\n\n        # Should raise RuntimeError for unsupported version\n        with pytest.raises(RuntimeError, match=\"Unsupported protocol version\"):\n            await session.initialize()\n\n\n@pytest.mark.anyio\nasync def test_client_capabilities_default():\n    \"\"\"Test that client capabilities are properly set with default callbacks\"\"\"\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n\n    received_capabilities = None\n\n    async def mock_server():\n        nonlocal received_capabilities\n\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n        request = client_request_adapter.validate_python(\n            jsonrpc_request.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n        )\n        assert isinstance(request, InitializeRequest)\n        received_capabilities = request.params.capabilities\n\n        result = InitializeResult(\n            protocol_version=LATEST_PROTOCOL_VERSION,\n            capabilities=ServerCapabilities(),\n            server_info=Implementation(name=\"mock-server\", version=\"0.1.0\"),\n        )\n\n        async with server_to_client_send:\n            await server_to_client_send.send(\n                SessionMessage(\n                    JSONRPCResponse(\n                        jsonrpc=\"2.0\",\n                        id=jsonrpc_request.id,\n                        result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                    )\n                )\n            )\n            # Receive initialized notification\n            await client_to_server_receive.receive()\n\n    async with (\n        ClientSession(server_to_client_receive, client_to_server_send) as session,\n        anyio.create_task_group() as tg,\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        tg.start_soon(mock_server)\n        await session.initialize()\n\n    # Assert that capabilities are properly set with defaults\n    assert received_capabilities is not None\n    assert received_capabilities.sampling is None  # No custom sampling callback\n    assert received_capabilities.roots is None  # No custom list_roots callback\n\n\n@pytest.mark.anyio\nasync def test_client_capabilities_with_custom_callbacks():\n    \"\"\"Test that client capabilities are properly set with custom callbacks\"\"\"\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n\n    received_capabilities = None\n\n    async def custom_sampling_callback(  # pragma: no cover\n        context: RequestContext[ClientSession],\n        params: types.CreateMessageRequestParams,\n    ) -> types.CreateMessageResult | types.ErrorData:\n        return types.CreateMessageResult(\n            role=\"assistant\",\n            content=types.TextContent(type=\"text\", text=\"test\"),\n            model=\"test-model\",\n        )\n\n    async def custom_list_roots_callback(  # pragma: no cover\n        context: RequestContext[ClientSession],\n    ) -> types.ListRootsResult | types.ErrorData:\n        return types.ListRootsResult(roots=[])\n\n    async def mock_server():\n        nonlocal received_capabilities\n\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n        request = client_request_adapter.validate_python(\n            jsonrpc_request.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n        )\n        assert isinstance(request, InitializeRequest)\n        received_capabilities = request.params.capabilities\n\n        result = InitializeResult(\n            protocol_version=LATEST_PROTOCOL_VERSION,\n            capabilities=ServerCapabilities(),\n            server_info=Implementation(name=\"mock-server\", version=\"0.1.0\"),\n        )\n\n        async with server_to_client_send:\n            await server_to_client_send.send(\n                SessionMessage(\n                    JSONRPCResponse(\n                        jsonrpc=\"2.0\",\n                        id=jsonrpc_request.id,\n                        result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                    )\n                )\n            )\n            # Receive initialized notification\n            await client_to_server_receive.receive()\n\n    async with (\n        ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n            sampling_callback=custom_sampling_callback,\n            list_roots_callback=custom_list_roots_callback,\n        ) as session,\n        anyio.create_task_group() as tg,\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        tg.start_soon(mock_server)\n        await session.initialize()\n\n    # Assert that capabilities are properly set with custom callbacks\n    assert received_capabilities is not None\n    # Custom sampling callback provided\n    assert received_capabilities.sampling is not None\n    assert isinstance(received_capabilities.sampling, types.SamplingCapability)\n    # Default sampling capabilities (no tools)\n    assert received_capabilities.sampling.tools is None\n    # Custom list_roots callback provided\n    assert received_capabilities.roots is not None\n    assert isinstance(received_capabilities.roots, types.RootsCapability)\n    # Should be True for custom callback\n    assert received_capabilities.roots.list_changed is True\n\n\n@pytest.mark.anyio\nasync def test_client_capabilities_with_sampling_tools():\n    \"\"\"Test that sampling capabilities with tools are properly advertised\"\"\"\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n\n    received_capabilities = None\n\n    async def custom_sampling_callback(  # pragma: no cover\n        context: RequestContext[ClientSession],\n        params: types.CreateMessageRequestParams,\n    ) -> types.CreateMessageResult | types.ErrorData:\n        return types.CreateMessageResult(\n            role=\"assistant\",\n            content=types.TextContent(type=\"text\", text=\"test\"),\n            model=\"test-model\",\n        )\n\n    async def mock_server():\n        nonlocal received_capabilities\n\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n        request = client_request_adapter.validate_python(\n            jsonrpc_request.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n        )\n        assert isinstance(request, InitializeRequest)\n        received_capabilities = request.params.capabilities\n\n        result = InitializeResult(\n            protocol_version=LATEST_PROTOCOL_VERSION,\n            capabilities=ServerCapabilities(),\n            server_info=Implementation(name=\"mock-server\", version=\"0.1.0\"),\n        )\n\n        async with server_to_client_send:\n            await server_to_client_send.send(\n                SessionMessage(\n                    JSONRPCResponse(\n                        jsonrpc=\"2.0\",\n                        id=jsonrpc_request.id,\n                        result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                    )\n                )\n            )\n            # Receive initialized notification\n            await client_to_server_receive.receive()\n\n    async with (\n        ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n            sampling_callback=custom_sampling_callback,\n            sampling_capabilities=types.SamplingCapability(tools=types.SamplingToolsCapability()),\n        ) as session,\n        anyio.create_task_group() as tg,\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        tg.start_soon(mock_server)\n        await session.initialize()\n\n    # Assert that sampling capabilities with tools are properly advertised\n    assert received_capabilities is not None\n    assert received_capabilities.sampling is not None\n    assert isinstance(received_capabilities.sampling, types.SamplingCapability)\n    # Tools capability should be present\n    assert received_capabilities.sampling.tools is not None\n    assert isinstance(received_capabilities.sampling.tools, types.SamplingToolsCapability)\n\n\n@pytest.mark.anyio\nasync def test_initialize_result():\n    \"\"\"Test that initialize_result is None before init and contains the full result after.\"\"\"\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n\n    expected_capabilities = ServerCapabilities(\n        logging=types.LoggingCapability(),\n        prompts=types.PromptsCapability(list_changed=True),\n        resources=types.ResourcesCapability(subscribe=True, list_changed=True),\n        tools=types.ToolsCapability(list_changed=False),\n    )\n    expected_server_info = Implementation(name=\"mock-server\", version=\"0.1.0\")\n    expected_instructions = \"Use the tools wisely.\"\n\n    async def mock_server():\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n        request = client_request_adapter.validate_python(\n            jsonrpc_request.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n        )\n        assert isinstance(request, InitializeRequest)\n\n        result = InitializeResult(\n            protocol_version=LATEST_PROTOCOL_VERSION,\n            capabilities=expected_capabilities,\n            server_info=expected_server_info,\n            instructions=expected_instructions,\n        )\n\n        async with server_to_client_send:\n            await server_to_client_send.send(\n                SessionMessage(\n                    JSONRPCResponse(\n                        jsonrpc=\"2.0\",\n                        id=jsonrpc_request.id,\n                        result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                    )\n                )\n            )\n            await client_to_server_receive.receive()\n\n    async with (\n        ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n        ) as session,\n        anyio.create_task_group() as tg,\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        assert session.initialize_result is None\n\n        tg.start_soon(mock_server)\n        await session.initialize()\n\n        result = session.initialize_result\n        assert result is not None\n        assert result.server_info == expected_server_info\n        assert result.capabilities == expected_capabilities\n        assert result.instructions == expected_instructions\n        assert result.protocol_version == LATEST_PROTOCOL_VERSION\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(argnames=\"meta\", argvalues=[None, {\"toolMeta\": \"value\"}])\nasync def test_client_tool_call_with_meta(meta: RequestParamsMeta | None):\n    \"\"\"Test that client tool call requests can include metadata\"\"\"\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n\n    mocked_tool = types.Tool(name=\"sample_tool\", input_schema={})\n\n    async def mock_server():\n        # Receive initialization request from client\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n        request = client_request_adapter.validate_python(\n            jsonrpc_request.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n        )\n        assert isinstance(request, InitializeRequest)\n\n        result = InitializeResult(\n            protocol_version=LATEST_PROTOCOL_VERSION,\n            capabilities=ServerCapabilities(),\n            server_info=Implementation(name=\"mock-server\", version=\"0.1.0\"),\n        )\n\n        # Answer initialization request\n        await server_to_client_send.send(\n            SessionMessage(\n                JSONRPCResponse(\n                    jsonrpc=\"2.0\",\n                    id=jsonrpc_request.id,\n                    result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                )\n            )\n        )\n\n        # Receive initialized notification\n        await client_to_server_receive.receive()\n\n        # Wait for the client to send a 'tools/call' request\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n\n        assert jsonrpc_request.method == \"tools/call\"\n\n        if meta is not None:\n            assert jsonrpc_request.params\n            assert \"_meta\" in jsonrpc_request.params\n            assert jsonrpc_request.params[\"_meta\"] == meta\n\n        result = CallToolResult(content=[TextContent(type=\"text\", text=\"Called successfully\")], is_error=False)\n\n        # Send the tools/call result\n        await server_to_client_send.send(\n            SessionMessage(\n                JSONRPCResponse(\n                    jsonrpc=\"2.0\",\n                    id=jsonrpc_request.id,\n                    result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                )\n            )\n        )\n\n        # Wait for the tools/list request from the client\n        # The client requires this step to validate the tool output schema\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n\n        assert jsonrpc_request.method == \"tools/list\"\n\n        result = types.ListToolsResult(tools=[mocked_tool])\n\n        await server_to_client_send.send(\n            SessionMessage(\n                JSONRPCResponse(\n                    jsonrpc=\"2.0\",\n                    id=jsonrpc_request.id,\n                    result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                )\n            )\n        )\n\n        server_to_client_send.close()\n\n    async with (\n        ClientSession(server_to_client_receive, client_to_server_send) as session,\n        anyio.create_task_group() as tg,\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        tg.start_soon(mock_server)\n\n        await session.initialize()\n\n        await session.call_tool(name=mocked_tool.name, arguments={\"foo\": \"bar\"}, meta=meta)\n"
  },
  {
    "path": "tests/client/test_session_group.py",
    "content": "import contextlib\nfrom unittest import mock\n\nimport httpx\nimport pytest\n\nimport mcp\nfrom mcp import types\nfrom mcp.client.session_group import (\n    ClientSessionGroup,\n    ClientSessionParameters,\n    SseServerParameters,\n    StreamableHttpParameters,\n)\nfrom mcp.client.stdio import StdioServerParameters\nfrom mcp.shared.exceptions import MCPError\n\n\n@pytest.fixture\ndef mock_exit_stack():\n    \"\"\"Fixture for a mocked AsyncExitStack.\"\"\"\n    # Use unittest.mock.Mock directly if needed, or just a plain object\n    # if only attribute access/existence is needed.\n    # For AsyncExitStack, Mock or MagicMock is usually fine.\n    return mock.MagicMock(spec=contextlib.AsyncExitStack)\n\n\ndef test_client_session_group_init():\n    mcp_session_group = ClientSessionGroup()\n    assert not mcp_session_group._tools\n    assert not mcp_session_group._resources\n    assert not mcp_session_group._prompts\n    assert not mcp_session_group._tool_to_session\n\n\ndef test_client_session_group_component_properties():\n    # --- Mock Dependencies ---\n    mock_prompt = mock.Mock()\n    mock_resource = mock.Mock()\n    mock_tool = mock.Mock()\n\n    # --- Prepare Session Group ---\n    mcp_session_group = ClientSessionGroup()\n    mcp_session_group._prompts = {\"my_prompt\": mock_prompt}\n    mcp_session_group._resources = {\"my_resource\": mock_resource}\n    mcp_session_group._tools = {\"my_tool\": mock_tool}\n\n    # --- Assertions ---\n    assert mcp_session_group.prompts == {\"my_prompt\": mock_prompt}\n    assert mcp_session_group.resources == {\"my_resource\": mock_resource}\n    assert mcp_session_group.tools == {\"my_tool\": mock_tool}\n\n\n@pytest.mark.anyio\nasync def test_client_session_group_call_tool():\n    # --- Mock Dependencies ---\n    mock_session = mock.AsyncMock()\n\n    # --- Prepare Session Group ---\n    def hook(name: str, server_info: types.Implementation) -> str:  # pragma: no cover\n        return f\"{(server_info.name)}-{name}\"\n\n    mcp_session_group = ClientSessionGroup(component_name_hook=hook)\n    mcp_session_group._tools = {\"server1-my_tool\": types.Tool(name=\"my_tool\", input_schema={})}\n    mcp_session_group._tool_to_session = {\"server1-my_tool\": mock_session}\n    text_content = types.TextContent(type=\"text\", text=\"OK\")\n    mock_session.call_tool.return_value = types.CallToolResult(content=[text_content])\n\n    # --- Test Execution ---\n    result = await mcp_session_group.call_tool(\n        name=\"server1-my_tool\",\n        arguments={\n            \"name\": \"value1\",\n            \"args\": {},\n        },\n    )\n\n    # --- Assertions ---\n    assert result.content == [text_content]\n    mock_session.call_tool.assert_called_once_with(\n        \"my_tool\",\n        arguments={\"name\": \"value1\", \"args\": {}},\n        read_timeout_seconds=None,\n        progress_callback=None,\n        meta=None,\n    )\n\n\n@pytest.mark.anyio\nasync def test_client_session_group_connect_to_server(mock_exit_stack: contextlib.AsyncExitStack):\n    \"\"\"Test connecting to a server and aggregating components.\"\"\"\n    # --- Mock Dependencies ---\n    mock_server_info = mock.Mock(spec=types.Implementation)\n    mock_server_info.name = \"TestServer1\"\n    mock_session = mock.AsyncMock(spec=mcp.ClientSession)\n    mock_tool1 = mock.Mock(spec=types.Tool)\n    mock_tool1.name = \"tool_a\"\n    mock_resource1 = mock.Mock(spec=types.Resource)\n    mock_resource1.name = \"resource_b\"\n    mock_prompt1 = mock.Mock(spec=types.Prompt)\n    mock_prompt1.name = \"prompt_c\"\n    mock_session.list_tools.return_value = mock.AsyncMock(tools=[mock_tool1])\n    mock_session.list_resources.return_value = mock.AsyncMock(resources=[mock_resource1])\n    mock_session.list_prompts.return_value = mock.AsyncMock(prompts=[mock_prompt1])\n\n    # --- Test Execution ---\n    group = ClientSessionGroup(exit_stack=mock_exit_stack)\n    with mock.patch.object(group, \"_establish_session\", return_value=(mock_server_info, mock_session)):\n        await group.connect_to_server(StdioServerParameters(command=\"test\"))\n\n    # --- Assertions ---\n    assert mock_session in group._sessions\n    assert len(group.tools) == 1\n    assert \"tool_a\" in group.tools\n    assert group.tools[\"tool_a\"] == mock_tool1\n    assert group._tool_to_session[\"tool_a\"] == mock_session\n    assert len(group.resources) == 1\n    assert \"resource_b\" in group.resources\n    assert group.resources[\"resource_b\"] == mock_resource1\n    assert len(group.prompts) == 1\n    assert \"prompt_c\" in group.prompts\n    assert group.prompts[\"prompt_c\"] == mock_prompt1\n    mock_session.list_tools.assert_awaited_once()\n    mock_session.list_resources.assert_awaited_once()\n    mock_session.list_prompts.assert_awaited_once()\n\n\n@pytest.mark.anyio\nasync def test_client_session_group_connect_to_server_with_name_hook(mock_exit_stack: contextlib.AsyncExitStack):\n    \"\"\"Test connecting with a component name hook.\"\"\"\n    # --- Mock Dependencies ---\n    mock_server_info = mock.Mock(spec=types.Implementation)\n    mock_server_info.name = \"HookServer\"\n    mock_session = mock.AsyncMock(spec=mcp.ClientSession)\n    mock_tool = mock.Mock(spec=types.Tool)\n    mock_tool.name = \"base_tool\"\n    mock_session.list_tools.return_value = mock.AsyncMock(tools=[mock_tool])\n    mock_session.list_resources.return_value = mock.AsyncMock(resources=[])\n    mock_session.list_prompts.return_value = mock.AsyncMock(prompts=[])\n\n    # --- Test Setup ---\n    def name_hook(name: str, server_info: types.Implementation) -> str:\n        return f\"{server_info.name}.{name}\"\n\n    # --- Test Execution ---\n    group = ClientSessionGroup(exit_stack=mock_exit_stack, component_name_hook=name_hook)\n    with mock.patch.object(group, \"_establish_session\", return_value=(mock_server_info, mock_session)):\n        await group.connect_to_server(StdioServerParameters(command=\"test\"))\n\n    # --- Assertions ---\n    assert mock_session in group._sessions\n    assert len(group.tools) == 1\n    expected_tool_name = \"HookServer.base_tool\"\n    assert expected_tool_name in group.tools\n    assert group.tools[expected_tool_name] == mock_tool\n    assert group._tool_to_session[expected_tool_name] == mock_session\n\n\n@pytest.mark.anyio\nasync def test_client_session_group_disconnect_from_server():\n    \"\"\"Test disconnecting from a server.\"\"\"\n    # --- Test Setup ---\n    group = ClientSessionGroup()\n    server_name = \"ServerToDisconnect\"\n\n    # Manually populate state using standard mocks\n    mock_session1 = mock.MagicMock(spec=mcp.ClientSession)\n    mock_session2 = mock.MagicMock(spec=mcp.ClientSession)\n    mock_tool1 = mock.Mock(spec=types.Tool)\n    mock_tool1.name = \"tool1\"\n    mock_resource1 = mock.Mock(spec=types.Resource)\n    mock_resource1.name = \"res1\"\n    mock_prompt1 = mock.Mock(spec=types.Prompt)\n    mock_prompt1.name = \"prm1\"\n    mock_tool2 = mock.Mock(spec=types.Tool)\n    mock_tool2.name = \"tool2\"\n    mock_component_named_like_server = mock.Mock()\n    mock_session = mock.Mock(spec=mcp.ClientSession)\n\n    group._tools = {\n        \"tool1\": mock_tool1,\n        \"tool2\": mock_tool2,\n        server_name: mock_component_named_like_server,\n    }\n    group._tool_to_session = {\n        \"tool1\": mock_session1,\n        \"tool2\": mock_session2,\n        server_name: mock_session1,\n    }\n    group._resources = {\n        \"res1\": mock_resource1,\n        server_name: mock_component_named_like_server,\n    }\n    group._prompts = {\n        \"prm1\": mock_prompt1,\n        server_name: mock_component_named_like_server,\n    }\n    group._sessions = {\n        mock_session: ClientSessionGroup._ComponentNames(\n            prompts=set({\"prm1\"}),\n            resources=set({\"res1\"}),\n            tools=set({\"tool1\", \"tool2\"}),\n        )\n    }\n\n    # --- Assertions ---\n    assert mock_session in group._sessions\n    assert \"tool1\" in group._tools\n    assert \"tool2\" in group._tools\n    assert \"res1\" in group._resources\n    assert \"prm1\" in group._prompts\n\n    # --- Test Execution ---\n    await group.disconnect_from_server(mock_session)\n\n    # --- Assertions ---\n    assert mock_session not in group._sessions\n    assert \"tool1\" not in group._tools\n    assert \"tool2\" not in group._tools\n    assert \"res1\" not in group._resources\n    assert \"prm1\" not in group._prompts\n\n\n@pytest.mark.anyio\nasync def test_client_session_group_connect_to_server_duplicate_tool_raises_error(\n    mock_exit_stack: contextlib.AsyncExitStack,\n):\n    \"\"\"Test MCPError raised when connecting a server with a dup name.\"\"\"\n    # --- Setup Pre-existing State ---\n    group = ClientSessionGroup(exit_stack=mock_exit_stack)\n    existing_tool_name = \"shared_tool\"\n    # Manually add a tool to simulate a previous connection\n    group._tools[existing_tool_name] = mock.Mock(spec=types.Tool)\n    group._tools[existing_tool_name].name = existing_tool_name\n    # Need a dummy session associated with the existing tool\n    mock_session = mock.MagicMock(spec=mcp.ClientSession)\n    group._tool_to_session[existing_tool_name] = mock_session\n    group._session_exit_stacks[mock_session] = mock.Mock(spec=contextlib.AsyncExitStack)\n\n    # --- Mock New Connection Attempt ---\n    mock_server_info_new = mock.Mock(spec=types.Implementation)\n    mock_server_info_new.name = \"ServerWithDuplicate\"\n    mock_session_new = mock.AsyncMock(spec=mcp.ClientSession)\n\n    # Configure the new session to return a tool with the *same name*\n    duplicate_tool = mock.Mock(spec=types.Tool)\n    duplicate_tool.name = existing_tool_name\n    mock_session_new.list_tools.return_value = mock.AsyncMock(tools=[duplicate_tool])\n    # Keep other lists empty for simplicity\n    mock_session_new.list_resources.return_value = mock.AsyncMock(resources=[])\n    mock_session_new.list_prompts.return_value = mock.AsyncMock(prompts=[])\n\n    # --- Test Execution and Assertion ---\n    with pytest.raises(MCPError) as excinfo:\n        with mock.patch.object(\n            group,\n            \"_establish_session\",\n            return_value=(mock_server_info_new, mock_session_new),\n        ):\n            await group.connect_to_server(StdioServerParameters(command=\"test\"))\n\n    # Assert details about the raised error\n    assert excinfo.value.error.code == types.INVALID_PARAMS\n    assert existing_tool_name in excinfo.value.error.message\n    assert \"already exist \" in excinfo.value.error.message\n\n    # Verify the duplicate tool was *not* added again (state should be unchanged)\n    assert len(group._tools) == 1  # Should still only have the original\n    assert group._tools[existing_tool_name] is not duplicate_tool  # Ensure it's the original mock\n\n\n@pytest.mark.anyio\nasync def test_client_session_group_disconnect_non_existent_server():\n    \"\"\"Test disconnecting a server that isn't connected.\"\"\"\n    session = mock.Mock(spec=mcp.ClientSession)\n    group = ClientSessionGroup()\n    with pytest.raises(MCPError):\n        await group.disconnect_from_server(session)\n\n\n# TODO(Marcelo): This is horrible. We should drop this test.\n@pytest.mark.anyio\n@pytest.mark.parametrize(\n    \"server_params_instance, client_type_name, patch_target_for_client_func\",\n    [\n        (\n            StdioServerParameters(command=\"test_stdio_cmd\"),\n            \"stdio\",\n            \"mcp.client.session_group.mcp.stdio_client\",\n        ),\n        (\n            SseServerParameters(url=\"http://test.com/sse\", timeout=10.0),\n            \"sse\",\n            \"mcp.client.session_group.sse_client\",\n        ),  # url, headers, timeout, sse_read_timeout\n        (\n            StreamableHttpParameters(url=\"http://test.com/stream\", terminate_on_close=False),\n            \"streamablehttp\",\n            \"mcp.client.session_group.streamable_http_client\",\n        ),  # url, headers, timeout, sse_read_timeout, terminate_on_close\n    ],\n)\nasync def test_client_session_group_establish_session_parameterized(\n    server_params_instance: StdioServerParameters | SseServerParameters | StreamableHttpParameters,\n    client_type_name: str,  # Just for clarity or conditional logic if needed\n    patch_target_for_client_func: str,\n):\n    with mock.patch(\"mcp.client.session_group.mcp.ClientSession\") as mock_ClientSession_class:\n        with mock.patch(patch_target_for_client_func) as mock_specific_client_func:\n            mock_client_cm_instance = mock.AsyncMock(name=f\"{client_type_name}ClientCM\")\n            mock_read_stream = mock.AsyncMock(name=f\"{client_type_name}Read\")\n            mock_write_stream = mock.AsyncMock(name=f\"{client_type_name}Write\")\n\n            # All client context managers return (read_stream, write_stream)\n            mock_client_cm_instance.__aenter__.return_value = (mock_read_stream, mock_write_stream)\n\n            mock_client_cm_instance.__aexit__ = mock.AsyncMock(return_value=None)\n            mock_specific_client_func.return_value = mock_client_cm_instance\n\n            # --- Mock mcp.ClientSession (class) ---\n            # mock_ClientSession_class is already provided by the outer patch\n            mock_raw_session_cm = mock.AsyncMock(name=\"RawSessionCM\")\n            mock_ClientSession_class.return_value = mock_raw_session_cm\n\n            mock_entered_session = mock.AsyncMock(name=\"EnteredSessionInstance\")\n            mock_raw_session_cm.__aenter__.return_value = mock_entered_session\n            mock_raw_session_cm.__aexit__ = mock.AsyncMock(return_value=None)\n\n            # Mock session.initialize()\n            mock_initialize_result = mock.AsyncMock(name=\"InitializeResult\")\n            mock_initialize_result.server_info = types.Implementation(name=\"foo\", version=\"1\")\n            mock_entered_session.initialize.return_value = mock_initialize_result\n\n            # --- Test Execution ---\n            group = ClientSessionGroup()\n            returned_server_info = None\n            returned_session = None\n\n            async with contextlib.AsyncExitStack() as stack:\n                group._exit_stack = stack\n                (\n                    returned_server_info,\n                    returned_session,\n                ) = await group._establish_session(server_params_instance, ClientSessionParameters())\n\n            # --- Assertions ---\n            # 1. Assert the correct specific client function was called\n            if client_type_name == \"stdio\":\n                assert isinstance(server_params_instance, StdioServerParameters)\n                mock_specific_client_func.assert_called_once_with(server_params_instance)\n            elif client_type_name == \"sse\":\n                assert isinstance(server_params_instance, SseServerParameters)\n                mock_specific_client_func.assert_called_once_with(\n                    url=server_params_instance.url,\n                    headers=server_params_instance.headers,\n                    timeout=server_params_instance.timeout,\n                    sse_read_timeout=server_params_instance.sse_read_timeout,\n                )\n            elif client_type_name == \"streamablehttp\":  # pragma: no branch\n                assert isinstance(server_params_instance, StreamableHttpParameters)\n                # Verify streamable_http_client was called with url, httpx_client, and terminate_on_close\n                # The http_client is created by the real create_mcp_http_client\n                call_args = mock_specific_client_func.call_args\n                assert call_args.kwargs[\"url\"] == server_params_instance.url\n                assert call_args.kwargs[\"terminate_on_close\"] == server_params_instance.terminate_on_close\n                assert isinstance(call_args.kwargs[\"http_client\"], httpx.AsyncClient)\n\n            mock_client_cm_instance.__aenter__.assert_awaited_once()\n\n            # 2. Assert ClientSession was called correctly\n            mock_ClientSession_class.assert_called_once_with(\n                mock_read_stream,\n                mock_write_stream,\n                read_timeout_seconds=None,\n                sampling_callback=None,\n                elicitation_callback=None,\n                list_roots_callback=None,\n                logging_callback=None,\n                message_handler=None,\n                client_info=None,\n            )\n            mock_raw_session_cm.__aenter__.assert_awaited_once()\n            mock_entered_session.initialize.assert_awaited_once()\n\n            # 3. Assert returned values\n            assert returned_server_info is mock_initialize_result.server_info\n            assert returned_session is mock_entered_session\n"
  },
  {
    "path": "tests/client/test_stdio.py",
    "content": "import errno\nimport shutil\nimport sys\nimport textwrap\nimport time\nfrom contextlib import AsyncExitStack, suppress\n\nimport anyio\nimport anyio.abc\nimport pytest\n\nfrom mcp.client.session import ClientSession\nfrom mcp.client.stdio import (\n    StdioServerParameters,\n    _create_platform_compatible_process,\n    _terminate_process_tree,\n    stdio_client,\n)\nfrom mcp.os.win32.utilities import FallbackProcess\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.message import SessionMessage\nfrom mcp.types import CONNECTION_CLOSED, JSONRPCMessage, JSONRPCRequest, JSONRPCResponse\n\n# Timeout for cleanup of processes that ignore SIGTERM\n# This timeout ensures the test fails quickly if the cleanup logic doesn't have\n# proper fallback mechanisms (SIGINT/SIGKILL) for processes that ignore SIGTERM\nSIGTERM_IGNORING_PROCESS_TIMEOUT = 5.0\n\ntee = shutil.which(\"tee\")\n\n\n@pytest.mark.anyio\n@pytest.mark.skipif(tee is None, reason=\"could not find tee command\")\nasync def test_stdio_context_manager_exiting():\n    assert tee is not None\n    async with stdio_client(StdioServerParameters(command=tee)) as (_, _):\n        pass\n\n\n@pytest.mark.anyio\n@pytest.mark.skipif(tee is None, reason=\"could not find tee command\")\nasync def test_stdio_client():\n    assert tee is not None\n    server_parameters = StdioServerParameters(command=tee)\n\n    async with stdio_client(server_parameters) as (read_stream, write_stream):\n        # Test sending and receiving messages\n        messages = [\n            JSONRPCRequest(jsonrpc=\"2.0\", id=1, method=\"ping\"),\n            JSONRPCResponse(jsonrpc=\"2.0\", id=2, result={}),\n        ]\n\n        async with write_stream:\n            for message in messages:\n                session_message = SessionMessage(message)\n                await write_stream.send(session_message)\n\n        read_messages: list[JSONRPCMessage] = []\n        async with read_stream:\n            async for message in read_stream:\n                if isinstance(message, Exception):  # pragma: no cover\n                    raise message\n\n                read_messages.append(message.message)\n                if len(read_messages) == 2:\n                    break\n\n        assert len(read_messages) == 2\n        assert read_messages[0] == JSONRPCRequest(jsonrpc=\"2.0\", id=1, method=\"ping\")\n        assert read_messages[1] == JSONRPCResponse(jsonrpc=\"2.0\", id=2, result={})\n\n\n@pytest.mark.anyio\nasync def test_stdio_client_bad_path():\n    \"\"\"Check that the connection doesn't hang if process errors.\"\"\"\n    server_params = StdioServerParameters(command=sys.executable, args=[\"-c\", \"non-existent-file.py\"])\n    async with stdio_client(server_params) as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            # The session should raise an error when the connection closes\n            with pytest.raises(MCPError) as exc_info:\n                await session.initialize()\n\n            # Check that we got a connection closed error\n            assert exc_info.value.error.code == CONNECTION_CLOSED\n            assert \"Connection closed\" in exc_info.value.error.message\n\n\n@pytest.mark.anyio\nasync def test_stdio_client_nonexistent_command():\n    \"\"\"Test that stdio_client raises an error for non-existent commands.\"\"\"\n    # Create a server with a non-existent command\n    server_params = StdioServerParameters(\n        command=\"/path/to/nonexistent/command\",\n        args=[\"--help\"],\n    )\n\n    # Should raise an error when trying to start the process\n    with pytest.raises(OSError) as exc_info:\n        async with stdio_client(server_params) as (_, _):\n            pass  # pragma: no cover\n\n    # The error should indicate the command was not found (ENOENT: No such file or directory)\n    assert exc_info.value.errno == errno.ENOENT\n\n\n@pytest.mark.anyio\nasync def test_stdio_client_universal_cleanup():\n    \"\"\"Test that stdio_client completes cleanup within reasonable time\n    even when connected to processes that exit slowly.\n    \"\"\"\n\n    # Use a Python script that simulates a long-running process\n    # This ensures consistent behavior across platforms\n    long_running_script = textwrap.dedent(\n        \"\"\"\n        import time\n        import sys\n\n        # Simulate a long-running process\n        for i in range(100):\n            time.sleep(0.1)\n            # Flush to ensure output is visible\n            sys.stdout.flush()\n            sys.stderr.flush()\n        \"\"\"\n    )\n\n    server_params = StdioServerParameters(\n        command=sys.executable,\n        args=[\"-c\", long_running_script],\n    )\n\n    start_time = time.time()\n\n    with anyio.move_on_after(8.0) as cancel_scope:\n        async with stdio_client(server_params) as (_, _):\n            # Immediately exit - this triggers cleanup while process is still running\n            pass\n\n        end_time = time.time()\n        elapsed = end_time - start_time\n\n        # On Windows: 2s (stdin wait) + 2s (terminate wait) + overhead = ~5s expected\n        assert elapsed < 6.0, (\n            f\"stdio_client cleanup took {elapsed:.1f} seconds, expected < 6.0 seconds. \"\n            f\"This suggests the timeout mechanism may not be working properly.\"\n        )\n\n    # Check if we timed out\n    if cancel_scope.cancelled_caught:  # pragma: no cover\n        pytest.fail(\n            \"stdio_client cleanup timed out after 8.0 seconds. \"\n            \"This indicates the cleanup mechanism is hanging and needs fixing.\"\n        )\n\n\n@pytest.mark.anyio\n@pytest.mark.skipif(sys.platform == \"win32\", reason=\"Windows signal handling is different\")\nasync def test_stdio_client_sigint_only_process():  # pragma: lax no cover\n    \"\"\"Test cleanup with a process that ignores SIGTERM but responds to SIGINT.\"\"\"\n    # Create a Python script that ignores SIGTERM but handles SIGINT\n    script_content = textwrap.dedent(\n        \"\"\"\n        import signal\n        import sys\n        import time\n\n        # Ignore SIGTERM (what process.terminate() sends)\n        signal.signal(signal.SIGTERM, signal.SIG_IGN)\n\n        # Handle SIGINT (Ctrl+C signal) by exiting cleanly\n        def sigint_handler(signum, frame):\n            sys.exit(0)\n\n        signal.signal(signal.SIGINT, sigint_handler)\n\n        # Keep running until SIGINT received\n        while True:\n            time.sleep(0.1)\n        \"\"\"\n    )\n\n    server_params = StdioServerParameters(\n        command=sys.executable,\n        args=[\"-c\", script_content],\n    )\n\n    start_time = time.time()\n\n    try:\n        # Use anyio timeout to prevent test from hanging forever\n        with anyio.move_on_after(5.0) as cancel_scope:\n            async with stdio_client(server_params) as (_, _):\n                # Let the process start and begin ignoring SIGTERM\n                await anyio.sleep(0.5)\n                # Exit context triggers cleanup - this should not hang\n                pass\n\n        if cancel_scope.cancelled_caught:  # pragma: no cover\n            raise TimeoutError(\"Test timed out\")\n\n        end_time = time.time()\n        elapsed = end_time - start_time\n\n        # Should complete quickly even with SIGTERM-ignoring process\n        # This will fail if cleanup only uses process.terminate() without fallback\n        assert elapsed < SIGTERM_IGNORING_PROCESS_TIMEOUT, (\n            f\"stdio_client cleanup took {elapsed:.1f} seconds with SIGTERM-ignoring process. \"\n            f\"Expected < {SIGTERM_IGNORING_PROCESS_TIMEOUT} seconds. \"\n            \"This suggests the cleanup needs SIGINT/SIGKILL fallback.\"\n        )\n    except (TimeoutError, Exception) as e:  # pragma: no cover\n        if isinstance(e, TimeoutError) or \"timed out\" in str(e):\n            pytest.fail(\n                f\"stdio_client cleanup timed out after {SIGTERM_IGNORING_PROCESS_TIMEOUT} seconds \"\n                \"with SIGTERM-ignoring process. \"\n                \"This confirms the cleanup needs SIGINT/SIGKILL fallback for processes that ignore SIGTERM.\"\n            )\n        else:\n            raise\n\n\n# ---------------------------------------------------------------------------\n# TestChildProcessCleanup — socket-based deterministic child liveness probe\n# ---------------------------------------------------------------------------\n#\n# These tests verify that `_terminate_process_tree()` kills the *entire* process\n# tree (not just the immediate child), which is critical for cleaning up tools\n# like `npx` that spawn their own subprocesses.\n#\n# Mechanism: each subprocess in the tree connects a TCP socket back to a\n# listener owned by the test. We then use two kernel-guaranteed blocking-I/O\n# signals — neither requires any `sleep()` or polling loop:\n#\n#   1. `await listener.accept()` blocks until the subprocess connects,\n#      proving it is running.\n#   2. After `_terminate_process_tree()`, `await stream.receive(1)` raises\n#      `EndOfStream` (clean close / FIN) or `BrokenResourceError` (abrupt\n#      close / RST — typical on Windows after TerminateJobObject) because the\n#      kernel closes all file descriptors when a process terminates. Either\n#      is the direct, OS-level proof that the child is dead.\n#\n# This replaces an older file-growth-watching approach whose fixed `sleep()`\n# durations raced against slow Python interpreter startup on loaded CI runners.\n\n\ndef _connect_back_script(port: int) -> str:\n    \"\"\"Return a ``python -c`` script body that connects to the given port,\n    sends ``b'alive'``, then blocks forever. Used by TestChildProcessCleanup\n    subprocesses as a liveness probe.\"\"\"\n    return (\n        f\"import socket, time\\n\"\n        f\"s = socket.create_connection(('127.0.0.1', {port}))\\n\"\n        f\"s.sendall(b'alive')\\n\"\n        f\"time.sleep(3600)\\n\"\n    )\n\n\ndef _spawn_then_block(child_script: str) -> str:\n    \"\"\"Return a ``python -c`` script body that spawns ``child_script`` as a\n    subprocess, then blocks forever. The ``!r`` injection avoids nested-quote\n    escaping for arbitrary child script content.\"\"\"\n    return (\n        f\"import subprocess, sys, time\\nsubprocess.Popen([sys.executable, '-c', {child_script!r}])\\ntime.sleep(3600)\\n\"\n    )\n\n\nasync def _open_liveness_listener() -> tuple[anyio.abc.SocketListener, int]:\n    \"\"\"Open a TCP listener on localhost and return it along with its port.\"\"\"\n    multi = await anyio.create_tcp_listener(local_host=\"127.0.0.1\")\n    sock = multi.listeners[0]\n    assert isinstance(sock, anyio.abc.SocketListener)\n    addr = sock.extra(anyio.abc.SocketAttribute.local_address)\n    # IPv4 local_address is (host: str, port: int)\n    assert isinstance(addr, tuple) and len(addr) >= 2 and isinstance(addr[1], int)\n    return sock, addr[1]\n\n\nasync def _accept_alive(sock: anyio.abc.SocketListener) -> anyio.abc.SocketStream:\n    \"\"\"Accept one connection and assert the peer sent ``b'alive'``.\n\n    Blocks deterministically until a subprocess connects (no polling). The\n    outer test bounds this with ``anyio.fail_after`` to catch the case where\n    the subprocess chain failed to start.\n    \"\"\"\n    stream = await sock.accept()\n    msg = await stream.receive(5)\n    assert msg == b\"alive\", f\"expected b'alive', got {msg!r}\"\n    return stream\n\n\nasync def _assert_stream_closed(stream: anyio.abc.SocketStream) -> None:\n    \"\"\"Assert the peer holding the other end of ``stream`` has terminated.\n\n    When a process dies, the kernel closes its file descriptors including\n    sockets. The next ``receive()`` on the peer socket unblocks with one of:\n\n    - ``anyio.EndOfStream`` — clean close (FIN), typical after graceful exit\n      or POSIX ``SIGTERM``.\n    - ``anyio.BrokenResourceError`` — abrupt close (RST), typical after\n      Windows ``TerminateJobObject`` or POSIX ``SIGKILL``.\n\n    Either is a deterministic, kernel-level signal that the process is dead —\n    no sleeps or polling required.\n    \"\"\"\n    with anyio.fail_after(5.0), pytest.raises((anyio.EndOfStream, anyio.BrokenResourceError)):\n        await stream.receive(1)\n\n\nasync def _terminate_and_reap(proc: anyio.abc.Process | FallbackProcess) -> None:\n    \"\"\"Terminate the process tree, reap, and tear down pipe transports.\n\n    ``_terminate_process_tree`` kills the OS process group / Job Object but does\n    not call ``process.wait()`` or clean up the asyncio pipe transports. On\n    Windows those transports leak and emit ``ResourceWarning`` when GC'd in a\n    later test, causing ``PytestUnraisableExceptionWarning`` knock-on failures.\n\n    Production ``stdio.py`` avoids this via its ``stdout_reader`` task which\n    reads stdout to EOF (triggering ``_ProactorReadPipeTransport._eof_received``\n    → ``close()``) plus ``async with process:`` which waits and closes stdin.\n    These tests call ``_terminate_process_tree`` directly, so they replicate\n    both parts here: ``wait()`` + close stdin + drain stdout to EOF.\n\n    The stdout drain is the non-obvious part: anyio's ``StreamReaderWrapper.aclose()``\n    only marks the Python-level reader closed — it never touches the underlying\n    ``_ProactorReadPipeTransport``. That transport starts paused and only detects\n    pipe EOF when someone reads, so without a drain it lives until ``__del__``.\n\n    Idempotent: the ``returncode`` guard skips termination if already reaped\n    (avoids spurious WARNING/ERROR logs from ``terminate_posix_process_tree``'s\n    fallback path, visible because ``log_cli = true``); ``wait()`` and stream\n    ``aclose()`` no-op on subsequent calls; the drain raises ``ClosedResourceError``\n    on the second call, caught by the suppress. The tests call this explicitly\n    as the action under test and ``AsyncExitStack`` calls it again on exit as a\n    safety net. Bounded by ``move_on_after`` to prevent hangs.\n    \"\"\"\n    with anyio.move_on_after(5.0):\n        if proc.returncode is None:\n            await _terminate_process_tree(proc)\n        await proc.wait()\n        assert proc.stdin is not None\n        assert proc.stdout is not None\n        await proc.stdin.aclose()\n        with suppress(anyio.EndOfStream, anyio.BrokenResourceError, anyio.ClosedResourceError):\n            await proc.stdout.receive(65536)\n        await proc.stdout.aclose()\n\n\nclass TestChildProcessCleanup:\n    \"\"\"Integration tests for ``_terminate_process_tree`` covering basic,\n    nested, and early-parent-exit process tree scenarios. See module-level\n    comment above for the socket-based liveness probe mechanism.\n    \"\"\"\n\n    @pytest.mark.anyio\n    async def test_basic_child_process_cleanup(self):\n        \"\"\"Parent spawns one child; terminating the tree kills both.\"\"\"\n        async with AsyncExitStack() as stack:\n            sock, port = await _open_liveness_listener()\n            stack.push_async_callback(sock.aclose)\n\n            # Parent spawns a child; the child connects back to us.\n            parent_script = _spawn_then_block(_connect_back_script(port))\n            proc = await _create_platform_compatible_process(sys.executable, [\"-c\", parent_script])\n            stack.push_async_callback(_terminate_and_reap, proc)\n\n            # Deterministic: accept() blocks until the child connects. No sleep.\n            with anyio.fail_after(10.0):\n                stream = await _accept_alive(sock)\n            stack.push_async_callback(stream.aclose)\n\n            # Terminate, reap and close transports (wraps _terminate_process_tree,\n            # the behavior under test).\n            await _terminate_and_reap(proc)\n\n            # Deterministic: kernel closed child's socket when it died.\n            await _assert_stream_closed(stream)\n\n    @pytest.mark.anyio\n    async def test_nested_process_tree(self):\n        \"\"\"Parent → child → grandchild; terminating the tree kills all three.\"\"\"\n        async with AsyncExitStack() as stack:\n            sock, port = await _open_liveness_listener()\n            stack.push_async_callback(sock.aclose)\n\n            # Build a three-level chain: parent spawns child, child spawns\n            # grandchild. Every level connects back to our socket.\n            grandchild = _connect_back_script(port)\n            child = (\n                f\"import subprocess, sys\\n\"\n                f\"subprocess.Popen([sys.executable, '-c', {grandchild!r}])\\n\" + _connect_back_script(port)\n            )\n            parent_script = (\n                f\"import subprocess, sys\\n\"\n                f\"subprocess.Popen([sys.executable, '-c', {child!r}])\\n\" + _connect_back_script(port)\n            )\n            proc = await _create_platform_compatible_process(sys.executable, [\"-c\", parent_script])\n            stack.push_async_callback(_terminate_and_reap, proc)\n\n            # Deterministic: three blocking accepts, one per tree level.\n            streams: list[anyio.abc.SocketStream] = []\n            with anyio.fail_after(10.0):\n                for _ in range(3):\n                    stream = await _accept_alive(sock)\n                    stack.push_async_callback(stream.aclose)\n                    streams.append(stream)\n\n            # Terminate the entire tree (wraps _terminate_process_tree).\n            await _terminate_and_reap(proc)\n\n            # Every level of the tree must be dead: three kernel-level EOFs.\n            for stream in streams:\n                await _assert_stream_closed(stream)\n\n    @pytest.mark.anyio\n    async def test_early_parent_exit(self):\n        \"\"\"Parent exits immediately on SIGTERM; process-group termination still\n        catches the child (exercises the race where the parent dies mid-cleanup).\n        \"\"\"\n        async with AsyncExitStack() as stack:\n            sock, port = await _open_liveness_listener()\n            stack.push_async_callback(sock.aclose)\n\n            # Parent installs a SIGTERM handler that exits immediately, spawns a\n            # child that connects back to us, then blocks.\n            child = _connect_back_script(port)\n            parent_script = (\n                f\"import signal, subprocess, sys, time\\n\"\n                f\"signal.signal(signal.SIGTERM, lambda *_: sys.exit(0))\\n\"\n                f\"subprocess.Popen([sys.executable, '-c', {child!r}])\\n\"\n                f\"time.sleep(3600)\\n\"\n            )\n            proc = await _create_platform_compatible_process(sys.executable, [\"-c\", parent_script])\n            stack.push_async_callback(_terminate_and_reap, proc)\n\n            # Deterministic: child connected means both parent and child are up.\n            with anyio.fail_after(10.0):\n                stream = await _accept_alive(sock)\n            stack.push_async_callback(stream.aclose)\n\n            # Parent will sys.exit(0) on SIGTERM, but the process-group kill\n            # (POSIX killpg / Windows Job Object) must still terminate the child.\n            await _terminate_and_reap(proc)\n\n            # Child must be dead despite parent's early exit.\n            await _assert_stream_closed(stream)\n\n\n@pytest.mark.anyio\nasync def test_stdio_client_graceful_stdin_exit():\n    \"\"\"Test that a process exits gracefully when stdin is closed,\n    without needing SIGTERM or SIGKILL.\n    \"\"\"\n    # Create a Python script that exits when stdin is closed\n    script_content = textwrap.dedent(\n        \"\"\"\n        import sys\n\n        # Read from stdin until it's closed\n        try:\n            while True:\n                line = sys.stdin.readline()\n                if not line:  # EOF/stdin closed\n                    break\n        except:\n            pass\n\n        # Exit gracefully\n        sys.exit(0)\n        \"\"\"\n    )\n\n    server_params = StdioServerParameters(\n        command=sys.executable,\n        args=[\"-c\", script_content],\n    )\n\n    start_time = time.time()\n\n    # Use anyio timeout to prevent test from hanging forever\n    with anyio.move_on_after(5.0) as cancel_scope:\n        async with stdio_client(server_params) as (_, _):\n            # Let the process start and begin reading stdin\n            await anyio.sleep(0.2)\n            # Exit context triggers cleanup - process should exit from stdin closure\n            pass\n\n    if cancel_scope.cancelled_caught:\n        pytest.fail(\n            \"stdio_client cleanup timed out after 5.0 seconds. \"\n            \"Process should have exited gracefully when stdin was closed.\"\n        )  # pragma: no cover\n\n    end_time = time.time()\n    elapsed = end_time - start_time\n\n    # Should complete quickly with just stdin closure (no signals needed)\n    assert elapsed < 3.0, (\n        f\"stdio_client cleanup took {elapsed:.1f} seconds for stdin-aware process. \"\n        f\"Expected < 3.0 seconds since process should exit on stdin closure.\"\n    )\n\n\n@pytest.mark.anyio\nasync def test_stdio_client_stdin_close_ignored():\n    \"\"\"Test that when a process ignores stdin closure, the shutdown sequence\n    properly escalates to SIGTERM.\n    \"\"\"\n    # Create a Python script that ignores stdin closure but responds to SIGTERM\n    script_content = textwrap.dedent(\n        \"\"\"\n        import signal\n        import sys\n        import time\n\n        # Set up SIGTERM handler to exit cleanly\n        def sigterm_handler(signum, frame):\n            sys.exit(0)\n\n        signal.signal(signal.SIGTERM, sigterm_handler)\n\n        # Close stdin immediately to simulate ignoring it\n        sys.stdin.close()\n\n        # Keep running until SIGTERM\n        while True:\n            time.sleep(0.1)\n        \"\"\"\n    )\n\n    server_params = StdioServerParameters(\n        command=sys.executable,\n        args=[\"-c\", script_content],\n    )\n\n    start_time = time.time()\n\n    # Use anyio timeout to prevent test from hanging forever\n    with anyio.move_on_after(7.0) as cancel_scope:\n        async with stdio_client(server_params) as (_, _):\n            # Let the process start\n            await anyio.sleep(0.2)\n            # Exit context triggers cleanup\n            pass\n\n    if cancel_scope.cancelled_caught:\n        pytest.fail(\n            \"stdio_client cleanup timed out after 7.0 seconds. \"\n            \"Process should have been terminated via SIGTERM escalation.\"\n        )  # pragma: no cover\n\n    end_time = time.time()\n    elapsed = end_time - start_time\n\n    # Should take ~2 seconds (stdin close timeout) before SIGTERM is sent\n    # Total time should be between 2-4 seconds\n    assert 1.5 < elapsed < 4.5, (\n        f\"stdio_client cleanup took {elapsed:.1f} seconds for stdin-ignoring process. \"\n        f\"Expected between 2-4 seconds (2s stdin timeout + termination time).\"\n    )\n"
  },
  {
    "path": "tests/client/test_transport_stream_cleanup.py",
    "content": "\"\"\"Regression tests for memory stream leaks in client transports.\n\nWhen a connection error occurs (404, 403, ConnectError), transport context\nmanagers must close ALL 4 memory stream ends they created. anyio memory streams\nare paired but independent — closing the writer does NOT close the reader.\nUnclosed stream ends emit ResourceWarning on GC, which pytest promotes to a\ntest failure in whatever test happens to be running when GC triggers.\n\nThese tests force GC after the transport context exits, so any leaked stream\ntriggers a ResourceWarning immediately and deterministically here, rather than\nnondeterministically in an unrelated later test.\n\"\"\"\n\nimport gc\nimport sys\nfrom collections.abc import Iterator\nfrom contextlib import contextmanager\n\nimport httpx\nimport pytest\n\nfrom mcp.client.sse import sse_client\nfrom mcp.client.streamable_http import streamable_http_client\nfrom mcp.client.websocket import websocket_client\n\n\n@contextmanager\ndef _assert_no_memory_stream_leak() -> Iterator[None]:\n    \"\"\"Fail if any anyio MemoryObject stream emits ResourceWarning during the block.\n\n    Uses a custom sys.unraisablehook to capture ONLY MemoryObject stream leaks,\n    ignoring unrelated resources (e.g. PipeHandle from flaky stdio tests on the\n    same xdist worker). gc.collect() is forced after the block to make leaks\n    deterministic.\n    \"\"\"\n    leaked: list[str] = []\n    old_hook = sys.unraisablehook\n\n    def hook(args: \"sys.UnraisableHookArgs\") -> None:  # pragma: no cover\n        # Only executes if a leak occurs (i.e. the bug is present).\n        # args.object is the __del__ function (not the stream instance) when\n        # unraisablehook fires from a finalizer, so check exc_value — the\n        # actual ResourceWarning(\"Unclosed <MemoryObjectSendStream at ...>\").\n        # Non-MemoryObject unraisables (e.g. PipeHandle leaked by an earlier\n        # flaky test on the same xdist worker) are deliberately ignored —\n        # this test should not fail for another test's resource leak.\n        if \"MemoryObject\" in str(args.exc_value):\n            leaked.append(str(args.exc_value))\n\n    sys.unraisablehook = hook\n    try:\n        yield\n        gc.collect()\n        assert not leaked, f\"Memory streams leaked: {leaked}\"\n    finally:\n        sys.unraisablehook = old_hook\n\n\n@pytest.mark.anyio\nasync def test_sse_client_closes_all_streams_on_connection_error(free_tcp_port: int) -> None:\n    \"\"\"sse_client creates streams only after the SSE connection succeeds, so a\n    ConnectError propagates directly with nothing to leak.\n\n    Before the fix, streams were created before connecting and only 2 of 4 were\n    closed in the finally block.\n    \"\"\"\n    with _assert_no_memory_stream_leak():\n        with pytest.raises(httpx.ConnectError):\n            async with sse_client(f\"http://127.0.0.1:{free_tcp_port}/sse\"):\n                pytest.fail(\"should not reach here\")  # pragma: no cover\n\n\n@pytest.mark.anyio\nasync def test_sse_client_closes_all_streams_on_http_error() -> None:\n    \"\"\"sse_client creates streams only after raise_for_status() passes, so an\n    HTTPStatusError from a 4xx/5xx response propagates bare (not wrapped in an\n    ExceptionGroup) with nothing to leak — the task group is never entered.\n    \"\"\"\n\n    def return_403(request: httpx.Request) -> httpx.Response:\n        return httpx.Response(403)\n\n    def mock_factory(\n        headers: dict[str, str] | None = None,\n        timeout: httpx.Timeout | None = None,\n        auth: httpx.Auth | None = None,\n    ) -> httpx.AsyncClient:\n        return httpx.AsyncClient(transport=httpx.MockTransport(return_403))\n\n    with _assert_no_memory_stream_leak():\n        with pytest.raises(httpx.HTTPStatusError):\n            async with sse_client(\"http://test/sse\", httpx_client_factory=mock_factory):\n                pytest.fail(\"should not reach here\")  # pragma: no cover\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_closes_all_streams_on_exit() -> None:\n    \"\"\"streamable_http_client must close all 4 stream ends on exit.\n\n    Before the fix, read_stream was never closed — not even on the happy path.\n    This test enters and exits the context without sending any messages, so no\n    network connection is ever attempted (streamable_http connects lazily).\n    \"\"\"\n    with _assert_no_memory_stream_leak():\n        async with streamable_http_client(\"http://127.0.0.1:1/mcp\"):\n            pass\n\n\n@pytest.mark.anyio\nasync def test_websocket_client_closes_all_streams_on_connection_error(free_tcp_port: int) -> None:\n    \"\"\"websocket_client must close all 4 stream ends when ws_connect fails.\n\n    Before the fix, there was no try/finally at all — if ws_connect raised,\n    all 4 streams were leaked.\n    \"\"\"\n    with _assert_no_memory_stream_leak():\n        with pytest.raises(OSError):\n            async with websocket_client(f\"ws://127.0.0.1:{free_tcp_port}/ws\"):\n                pytest.fail(\"should not reach here\")  # pragma: no cover\n"
  },
  {
    "path": "tests/client/transports/__init__.py",
    "content": ""
  },
  {
    "path": "tests/client/transports/test_memory.py",
    "content": "\"\"\"Tests for InMemoryTransport.\"\"\"\n\nimport pytest\n\nfrom mcp import Client, types\nfrom mcp.client._memory import InMemoryTransport\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.types import ListResourcesResult, Resource\n\n\n@pytest.fixture\ndef simple_server() -> Server:\n    \"\"\"Create a simple MCP server for testing.\"\"\"\n\n    async def handle_list_resources(\n        ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n    ) -> ListResourcesResult:  # pragma: no cover\n        return ListResourcesResult(\n            resources=[\n                Resource(\n                    uri=\"memory://test\",\n                    name=\"Test Resource\",\n                    description=\"A test resource\",\n                )\n            ]\n        )\n\n    return Server(name=\"test_server\", on_list_resources=handle_list_resources)\n\n\n@pytest.fixture\ndef mcpserver_server() -> MCPServer:\n    \"\"\"Create an MCPServer server for testing.\"\"\"\n    server = MCPServer(\"test\")\n\n    @server.tool()\n    def greet(name: str) -> str:\n        \"\"\"Greet someone by name.\"\"\"\n        return f\"Hello, {name}!\"\n\n    @server.resource(\"test://resource\")\n    def test_resource() -> str:  # pragma: no cover\n        \"\"\"A test resource.\"\"\"\n        return \"Test content\"\n\n    return server\n\n\npytestmark = pytest.mark.anyio\n\n\nasync def test_with_server(simple_server: Server):\n    \"\"\"Test creating transport with a Server instance.\"\"\"\n    transport = InMemoryTransport(simple_server)\n    async with transport as (read_stream, write_stream):\n        assert read_stream is not None\n        assert write_stream is not None\n\n\nasync def test_with_mcpserver(mcpserver_server: MCPServer):\n    \"\"\"Test creating transport with an MCPServer instance.\"\"\"\n    transport = InMemoryTransport(mcpserver_server)\n    async with transport as (read_stream, write_stream):\n        assert read_stream is not None\n        assert write_stream is not None\n\n\nasync def test_server_is_running(mcpserver_server: MCPServer):\n    \"\"\"Test that the server is running and responding to requests.\"\"\"\n    async with Client(mcpserver_server) as client:\n        assert client.initialize_result.capabilities.tools is not None\n\n\nasync def test_list_tools(mcpserver_server: MCPServer):\n    \"\"\"Test listing tools through the transport.\"\"\"\n    async with Client(mcpserver_server) as client:\n        tools_result = await client.list_tools()\n        assert len(tools_result.tools) > 0\n        tool_names = [t.name for t in tools_result.tools]\n        assert \"greet\" in tool_names\n\n\nasync def test_call_tool(mcpserver_server: MCPServer):\n    \"\"\"Test calling a tool through the transport.\"\"\"\n    async with Client(mcpserver_server) as client:\n        result = await client.call_tool(\"greet\", {\"name\": \"World\"})\n        assert result is not None\n        assert len(result.content) > 0\n        assert \"Hello, World!\" in str(result.content[0])\n\n\nasync def test_raise_exceptions(mcpserver_server: MCPServer):\n    \"\"\"Test that raise_exceptions parameter is passed through.\"\"\"\n    transport = InMemoryTransport(mcpserver_server, raise_exceptions=True)\n    async with transport as (read_stream, _write_stream):\n        assert read_stream is not None\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "import pytest\n\n\n@pytest.fixture\ndef anyio_backend():\n    return \"asyncio\"\n"
  },
  {
    "path": "tests/experimental/__init__.py",
    "content": ""
  },
  {
    "path": "tests/experimental/tasks/__init__.py",
    "content": "\"\"\"Tests for MCP task support.\"\"\"\n"
  },
  {
    "path": "tests/experimental/tasks/client/__init__.py",
    "content": ""
  },
  {
    "path": "tests/experimental/tasks/client/test_capabilities.py",
    "content": "\"\"\"Tests for client task capabilities declaration during initialization.\"\"\"\n\nimport anyio\nimport pytest\n\nfrom mcp import ClientCapabilities, types\nfrom mcp.client.experimental.task_handlers import ExperimentalTaskHandlers\nfrom mcp.client.session import ClientSession\nfrom mcp.shared._context import RequestContext\nfrom mcp.shared.message import SessionMessage\nfrom mcp.types import (\n    LATEST_PROTOCOL_VERSION,\n    Implementation,\n    InitializeRequest,\n    InitializeResult,\n    JSONRPCRequest,\n    JSONRPCResponse,\n    ServerCapabilities,\n    client_request_adapter,\n)\n\n\n@pytest.mark.anyio\nasync def test_client_capabilities_without_tasks():\n    \"\"\"Test that tasks capability is None when not provided.\"\"\"\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n\n    received_capabilities = None\n\n    async def mock_server():\n        nonlocal received_capabilities\n\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n        request = client_request_adapter.validate_python(\n            jsonrpc_request.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n        )\n        assert isinstance(request, InitializeRequest)\n        received_capabilities = request.params.capabilities\n\n        result = InitializeResult(\n            protocol_version=LATEST_PROTOCOL_VERSION,\n            capabilities=ServerCapabilities(),\n            server_info=Implementation(name=\"mock-server\", version=\"0.1.0\"),\n        )\n\n        async with server_to_client_send:\n            await server_to_client_send.send(\n                SessionMessage(\n                    JSONRPCResponse(\n                        jsonrpc=\"2.0\",\n                        id=jsonrpc_request.id,\n                        result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                    )\n                )\n            )\n            await client_to_server_receive.receive()\n\n    async with (\n        ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n        ) as session,\n        anyio.create_task_group() as tg,\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        tg.start_soon(mock_server)\n        await session.initialize()\n\n    # Assert that tasks capability is None when not provided\n    assert received_capabilities is not None\n    assert received_capabilities.tasks is None\n\n\n@pytest.mark.anyio\nasync def test_client_capabilities_with_tasks():\n    \"\"\"Test that tasks capability is properly set when handlers are provided.\"\"\"\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n\n    received_capabilities: ClientCapabilities | None = None\n\n    # Define custom handlers to trigger capability building (never actually called)\n    async def my_list_tasks_handler(\n        context: RequestContext[ClientSession],\n        params: types.PaginatedRequestParams | None,\n    ) -> types.ListTasksResult | types.ErrorData:\n        raise NotImplementedError\n\n    async def my_cancel_task_handler(\n        context: RequestContext[ClientSession],\n        params: types.CancelTaskRequestParams,\n    ) -> types.CancelTaskResult | types.ErrorData:\n        raise NotImplementedError\n\n    async def mock_server():\n        nonlocal received_capabilities\n\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n        request = client_request_adapter.validate_python(\n            jsonrpc_request.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n        )\n        assert isinstance(request, InitializeRequest)\n        received_capabilities = request.params.capabilities\n\n        result = InitializeResult(\n            protocol_version=LATEST_PROTOCOL_VERSION,\n            capabilities=ServerCapabilities(),\n            server_info=Implementation(name=\"mock-server\", version=\"0.1.0\"),\n        )\n\n        async with server_to_client_send:\n            await server_to_client_send.send(\n                SessionMessage(\n                    JSONRPCResponse(\n                        jsonrpc=\"2.0\",\n                        id=jsonrpc_request.id,\n                        result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                    )\n                )\n            )\n            await client_to_server_receive.receive()\n\n    # Create handlers container\n    task_handlers = ExperimentalTaskHandlers(\n        list_tasks=my_list_tasks_handler,\n        cancel_task=my_cancel_task_handler,\n    )\n\n    async with (\n        ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n            experimental_task_handlers=task_handlers,\n        ) as session,\n        anyio.create_task_group() as tg,\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        tg.start_soon(mock_server)\n        await session.initialize()\n\n    # Assert that tasks capability is properly set from handlers\n    assert received_capabilities is not None\n    assert received_capabilities.tasks is not None\n    assert isinstance(received_capabilities.tasks, types.ClientTasksCapability)\n    assert received_capabilities.tasks.list is not None\n    assert received_capabilities.tasks.cancel is not None\n\n\n@pytest.mark.anyio\nasync def test_client_capabilities_auto_built_from_handlers():\n    \"\"\"Test that tasks capability is automatically built from provided handlers.\"\"\"\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n\n    received_capabilities: ClientCapabilities | None = None\n\n    # Define custom handlers (not defaults)\n    async def my_list_tasks_handler(\n        context: RequestContext[ClientSession],\n        params: types.PaginatedRequestParams | None,\n    ) -> types.ListTasksResult | types.ErrorData:\n        raise NotImplementedError\n\n    async def my_cancel_task_handler(\n        context: RequestContext[ClientSession],\n        params: types.CancelTaskRequestParams,\n    ) -> types.CancelTaskResult | types.ErrorData:\n        raise NotImplementedError\n\n    async def mock_server():\n        nonlocal received_capabilities\n\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n        request = client_request_adapter.validate_python(\n            jsonrpc_request.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n        )\n        assert isinstance(request, InitializeRequest)\n        received_capabilities = request.params.capabilities\n\n        result = InitializeResult(\n            protocol_version=LATEST_PROTOCOL_VERSION,\n            capabilities=ServerCapabilities(),\n            server_info=Implementation(name=\"mock-server\", version=\"0.1.0\"),\n        )\n\n        async with server_to_client_send:\n            await server_to_client_send.send(\n                SessionMessage(\n                    JSONRPCResponse(\n                        jsonrpc=\"2.0\",\n                        id=jsonrpc_request.id,\n                        result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                    )\n                )\n            )\n            await client_to_server_receive.receive()\n\n    # Provide handlers via ExperimentalTaskHandlers\n    task_handlers = ExperimentalTaskHandlers(\n        list_tasks=my_list_tasks_handler,\n        cancel_task=my_cancel_task_handler,\n    )\n\n    async with (\n        ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n            experimental_task_handlers=task_handlers,\n        ) as session,\n        anyio.create_task_group() as tg,\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        tg.start_soon(mock_server)\n        await session.initialize()\n\n    # Assert that tasks capability was auto-built from handlers\n    assert received_capabilities is not None\n    assert received_capabilities.tasks is not None\n    assert received_capabilities.tasks.list is not None\n    assert received_capabilities.tasks.cancel is not None\n    # requests should be None since we didn't provide task-augmented handlers\n    assert received_capabilities.tasks.requests is None\n\n\n@pytest.mark.anyio\nasync def test_client_capabilities_with_task_augmented_handlers():\n    \"\"\"Test that requests capability is built when augmented handlers are provided.\"\"\"\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n\n    received_capabilities: ClientCapabilities | None = None\n\n    # Define task-augmented handler\n    async def my_augmented_sampling_handler(\n        context: RequestContext[ClientSession],\n        params: types.CreateMessageRequestParams,\n        task_metadata: types.TaskMetadata,\n    ) -> types.CreateTaskResult | types.ErrorData:\n        raise NotImplementedError\n\n    async def mock_server():\n        nonlocal received_capabilities\n\n        session_message = await client_to_server_receive.receive()\n        jsonrpc_request = session_message.message\n        assert isinstance(jsonrpc_request, JSONRPCRequest)\n        request = client_request_adapter.validate_python(\n            jsonrpc_request.model_dump(by_alias=True, mode=\"json\", exclude_none=True)\n        )\n        assert isinstance(request, InitializeRequest)\n        received_capabilities = request.params.capabilities\n\n        result = InitializeResult(\n            protocol_version=LATEST_PROTOCOL_VERSION,\n            capabilities=ServerCapabilities(),\n            server_info=Implementation(name=\"mock-server\", version=\"0.1.0\"),\n        )\n\n        async with server_to_client_send:\n            await server_to_client_send.send(\n                SessionMessage(\n                    JSONRPCResponse(\n                        jsonrpc=\"2.0\",\n                        id=jsonrpc_request.id,\n                        result=result.model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                    )\n                )\n            )\n            await client_to_server_receive.receive()\n\n    # Provide task-augmented sampling handler\n    task_handlers = ExperimentalTaskHandlers(\n        augmented_sampling=my_augmented_sampling_handler,\n    )\n\n    async with (\n        ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n            experimental_task_handlers=task_handlers,\n        ) as session,\n        anyio.create_task_group() as tg,\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        tg.start_soon(mock_server)\n        await session.initialize()\n\n    # Assert that tasks capability includes requests.sampling\n    assert received_capabilities is not None\n    assert received_capabilities.tasks is not None\n    assert received_capabilities.tasks.requests is not None\n    assert received_capabilities.tasks.requests.sampling is not None\n    assert received_capabilities.tasks.requests.elicitation is None  # Not provided\n"
  },
  {
    "path": "tests/experimental/tasks/client/test_handlers.py",
    "content": "\"\"\"Tests for client-side task management handlers (server -> client requests).\n\nThese tests verify that clients can handle task-related requests from servers:\n- GetTaskRequest - server polling client's task status\n- GetTaskPayloadRequest - server getting result from client's task\n- ListTasksRequest - server listing client's tasks\n- CancelTaskRequest - server cancelling client's task\n\nThis is the inverse of the existing tests in test_tasks.py, which test\nclient -> server task requests.\n\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom dataclasses import dataclass\n\nimport anyio\nimport pytest\nfrom anyio import Event\nfrom anyio.abc import TaskGroup\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\n\nfrom mcp import types\nfrom mcp.client.experimental.task_handlers import ExperimentalTaskHandlers\nfrom mcp.client.session import ClientSession\nfrom mcp.shared._context import RequestContext\nfrom mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore\nfrom mcp.shared.message import SessionMessage\nfrom mcp.shared.session import RequestResponder\nfrom mcp.types import (\n    CancelTaskRequest,\n    CancelTaskRequestParams,\n    CancelTaskResult,\n    ClientResult,\n    CreateMessageRequest,\n    CreateMessageRequestParams,\n    CreateMessageResult,\n    CreateTaskResult,\n    ElicitRequest,\n    ElicitRequestFormParams,\n    ElicitRequestParams,\n    ElicitResult,\n    ErrorData,\n    GetTaskPayloadRequest,\n    GetTaskPayloadRequestParams,\n    GetTaskPayloadResult,\n    GetTaskRequest,\n    GetTaskRequestParams,\n    GetTaskResult,\n    ListTasksRequest,\n    ListTasksResult,\n    SamplingMessage,\n    ServerNotification,\n    ServerRequest,\n    TaskMetadata,\n    TextContent,\n)\n\n# Buffer size for test streams\nSTREAM_BUFFER_SIZE = 10\n\n\n@dataclass\nclass ClientTestStreams:\n    \"\"\"Bidirectional message streams for client/server communication in tests.\"\"\"\n\n    server_send: MemoryObjectSendStream[SessionMessage]\n    server_receive: MemoryObjectReceiveStream[SessionMessage]\n    client_send: MemoryObjectSendStream[SessionMessage]\n    client_receive: MemoryObjectReceiveStream[SessionMessage]\n\n\n@pytest.fixture\nasync def client_streams() -> AsyncIterator[ClientTestStreams]:\n    \"\"\"Create bidirectional message streams for client tests.\n\n    Automatically closes all streams after the test completes.\n    \"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](\n        STREAM_BUFFER_SIZE\n    )\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](\n        STREAM_BUFFER_SIZE\n    )\n\n    streams = ClientTestStreams(\n        server_send=server_to_client_send,\n        server_receive=client_to_server_receive,\n        client_send=client_to_server_send,\n        client_receive=server_to_client_receive,\n    )\n\n    yield streams\n\n    # Cleanup\n    await server_to_client_send.aclose()\n    await server_to_client_receive.aclose()\n    await client_to_server_send.aclose()\n    await client_to_server_receive.aclose()\n\n\nasync def _default_message_handler(\n    message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception,\n) -> None:\n    \"\"\"Default message handler that ignores messages (tests handle them explicitly).\"\"\"\n    ...\n\n\n@pytest.mark.anyio\nasync def test_client_handles_get_task_request(client_streams: ClientTestStreams) -> None:\n    \"\"\"Test that client can respond to GetTaskRequest from server.\"\"\"\n    with anyio.fail_after(10):\n        store = InMemoryTaskStore()\n        received_task_id: str | None = None\n\n        async def get_task_handler(\n            context: RequestContext[ClientSession],\n            params: GetTaskRequestParams,\n        ) -> GetTaskResult | ErrorData:\n            nonlocal received_task_id\n            received_task_id = params.task_id\n            task = await store.get_task(params.task_id)\n            assert task is not None, f\"Test setup error: task {params.task_id} should exist\"\n            return GetTaskResult(\n                task_id=task.task_id,\n                status=task.status,\n                status_message=task.status_message,\n                created_at=task.created_at,\n                last_updated_at=task.last_updated_at,\n                ttl=task.ttl,\n                poll_interval=task.poll_interval,\n            )\n\n        await store.create_task(TaskMetadata(ttl=60000), task_id=\"test-task-123\")\n\n        task_handlers = ExperimentalTaskHandlers(get_task=get_task_handler)\n        client_ready = anyio.Event()\n\n        async with anyio.create_task_group() as tg:\n\n            async def run_client() -> None:\n                async with ClientSession(\n                    client_streams.client_receive,\n                    client_streams.client_send,\n                    message_handler=_default_message_handler,\n                    experimental_task_handlers=task_handlers,\n                ):\n                    client_ready.set()\n                    await anyio.sleep_forever()\n\n            tg.start_soon(run_client)\n            await client_ready.wait()\n\n            typed_request = GetTaskRequest(params=GetTaskRequestParams(task_id=\"test-task-123\"))\n            request = types.JSONRPCRequest(jsonrpc=\"2.0\", id=\"req-1\", **typed_request.model_dump(by_alias=True))\n            await client_streams.server_send.send(SessionMessage(request))\n\n            response_msg = await client_streams.server_receive.receive()\n            response = response_msg.message\n            assert isinstance(response, types.JSONRPCResponse)\n            assert response.id == \"req-1\"\n\n            result = GetTaskResult.model_validate(response.result)\n            assert result.task_id == \"test-task-123\"\n            assert result.status == \"working\"\n            assert received_task_id == \"test-task-123\"\n\n            tg.cancel_scope.cancel()\n\n        store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_client_handles_get_task_result_request(client_streams: ClientTestStreams) -> None:\n    \"\"\"Test that client can respond to GetTaskPayloadRequest from server.\"\"\"\n    with anyio.fail_after(10):\n        store = InMemoryTaskStore()\n\n        async def get_task_result_handler(\n            context: RequestContext[ClientSession],\n            params: GetTaskPayloadRequestParams,\n        ) -> GetTaskPayloadResult | ErrorData:\n            result = await store.get_result(params.task_id)\n            assert result is not None, f\"Test setup error: result for {params.task_id} should exist\"\n            assert isinstance(result, types.CallToolResult)\n            return GetTaskPayloadResult(**result.model_dump())\n\n        await store.create_task(TaskMetadata(ttl=60000), task_id=\"test-task-456\")\n        await store.store_result(\n            \"test-task-456\",\n            types.CallToolResult(content=[TextContent(type=\"text\", text=\"Task completed successfully!\")]),\n        )\n        await store.update_task(\"test-task-456\", status=\"completed\")\n\n        task_handlers = ExperimentalTaskHandlers(get_task_result=get_task_result_handler)\n        client_ready = anyio.Event()\n\n        async with anyio.create_task_group() as tg:\n\n            async def run_client() -> None:\n                async with ClientSession(\n                    client_streams.client_receive,\n                    client_streams.client_send,\n                    message_handler=_default_message_handler,\n                    experimental_task_handlers=task_handlers,\n                ):\n                    client_ready.set()\n                    await anyio.sleep_forever()\n\n            tg.start_soon(run_client)\n            await client_ready.wait()\n\n            typed_request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=\"test-task-456\"))\n            request = types.JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                id=\"req-2\",\n                **typed_request.model_dump(by_alias=True),\n            )\n            await client_streams.server_send.send(SessionMessage(request))\n\n            response_msg = await client_streams.server_receive.receive()\n            response = response_msg.message\n            assert isinstance(response, types.JSONRPCResponse)\n\n            assert isinstance(response.result, dict)\n            result_dict = response.result\n            assert \"content\" in result_dict\n            assert len(result_dict[\"content\"]) == 1\n            assert result_dict[\"content\"][0][\"text\"] == \"Task completed successfully!\"\n\n            tg.cancel_scope.cancel()\n\n        store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_client_handles_list_tasks_request(client_streams: ClientTestStreams) -> None:\n    \"\"\"Test that client can respond to ListTasksRequest from server.\"\"\"\n    with anyio.fail_after(10):\n        store = InMemoryTaskStore()\n\n        async def list_tasks_handler(\n            context: RequestContext[ClientSession],\n            params: types.PaginatedRequestParams | None,\n        ) -> ListTasksResult | ErrorData:\n            cursor = params.cursor if params else None\n            tasks_list, next_cursor = await store.list_tasks(cursor=cursor)\n            return ListTasksResult(tasks=tasks_list, next_cursor=next_cursor)\n\n        await store.create_task(TaskMetadata(ttl=60000), task_id=\"task-1\")\n        await store.create_task(TaskMetadata(ttl=60000), task_id=\"task-2\")\n\n        task_handlers = ExperimentalTaskHandlers(list_tasks=list_tasks_handler)\n        client_ready = anyio.Event()\n\n        async with anyio.create_task_group() as tg:\n\n            async def run_client() -> None:\n                async with ClientSession(\n                    client_streams.client_receive,\n                    client_streams.client_send,\n                    message_handler=_default_message_handler,\n                    experimental_task_handlers=task_handlers,\n                ):\n                    client_ready.set()\n                    await anyio.sleep_forever()\n\n            tg.start_soon(run_client)\n            await client_ready.wait()\n\n            typed_request = ListTasksRequest()\n            request = types.JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                id=\"req-3\",\n                **typed_request.model_dump(by_alias=True),\n            )\n            await client_streams.server_send.send(SessionMessage(request))\n\n            response_msg = await client_streams.server_receive.receive()\n            response = response_msg.message\n            assert isinstance(response, types.JSONRPCResponse)\n\n            result = ListTasksResult.model_validate(response.result)\n            assert len(result.tasks) == 2\n\n            tg.cancel_scope.cancel()\n\n        store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_client_handles_cancel_task_request(client_streams: ClientTestStreams) -> None:\n    \"\"\"Test that client can respond to CancelTaskRequest from server.\"\"\"\n    with anyio.fail_after(10):\n        store = InMemoryTaskStore()\n\n        async def cancel_task_handler(\n            context: RequestContext[ClientSession],\n            params: CancelTaskRequestParams,\n        ) -> CancelTaskResult | ErrorData:\n            task = await store.get_task(params.task_id)\n            assert task is not None, f\"Test setup error: task {params.task_id} should exist\"\n            await store.update_task(params.task_id, status=\"cancelled\")\n            updated = await store.get_task(params.task_id)\n            assert updated is not None\n            return CancelTaskResult(\n                task_id=updated.task_id,\n                status=updated.status,\n                created_at=updated.created_at,\n                last_updated_at=updated.last_updated_at,\n                ttl=updated.ttl,\n            )\n\n        await store.create_task(TaskMetadata(ttl=60000), task_id=\"task-to-cancel\")\n\n        task_handlers = ExperimentalTaskHandlers(cancel_task=cancel_task_handler)\n        client_ready = anyio.Event()\n\n        async with anyio.create_task_group() as tg:\n\n            async def run_client() -> None:\n                async with ClientSession(\n                    client_streams.client_receive,\n                    client_streams.client_send,\n                    message_handler=_default_message_handler,\n                    experimental_task_handlers=task_handlers,\n                ):\n                    client_ready.set()\n                    await anyio.sleep_forever()\n\n            tg.start_soon(run_client)\n            await client_ready.wait()\n\n            typed_request = CancelTaskRequest(params=CancelTaskRequestParams(task_id=\"task-to-cancel\"))\n            request = types.JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                id=\"req-4\",\n                **typed_request.model_dump(by_alias=True),\n            )\n            await client_streams.server_send.send(SessionMessage(request))\n\n            response_msg = await client_streams.server_receive.receive()\n            response = response_msg.message\n            assert isinstance(response, types.JSONRPCResponse)\n\n            result = CancelTaskResult.model_validate(response.result)\n            assert result.task_id == \"task-to-cancel\"\n            assert result.status == \"cancelled\"\n\n            tg.cancel_scope.cancel()\n\n        store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_client_task_augmented_sampling(client_streams: ClientTestStreams) -> None:\n    \"\"\"Test that client can handle task-augmented sampling request from server.\"\"\"\n    with anyio.fail_after(10):\n        store = InMemoryTaskStore()\n        sampling_completed = Event()\n        created_task_id: list[str | None] = [None]\n        background_tg: list[TaskGroup | None] = [None]\n\n        async def task_augmented_sampling_callback(\n            context: RequestContext[ClientSession],\n            params: CreateMessageRequestParams,\n            task_metadata: TaskMetadata,\n        ) -> CreateTaskResult:\n            task = await store.create_task(task_metadata)\n            created_task_id[0] = task.task_id\n\n            async def do_sampling() -> None:\n                result = CreateMessageResult(\n                    role=\"assistant\",\n                    content=TextContent(type=\"text\", text=\"Sampled response\"),\n                    model=\"test-model\",\n                    stop_reason=\"endTurn\",\n                )\n                await store.store_result(task.task_id, result)\n                await store.update_task(task.task_id, status=\"completed\")\n                sampling_completed.set()\n\n            assert background_tg[0] is not None\n            background_tg[0].start_soon(do_sampling)\n            return CreateTaskResult(task=task)\n\n        async def get_task_handler(\n            context: RequestContext[ClientSession],\n            params: GetTaskRequestParams,\n        ) -> GetTaskResult | ErrorData:\n            task = await store.get_task(params.task_id)\n            assert task is not None, f\"Test setup error: task {params.task_id} should exist\"\n            return GetTaskResult(\n                task_id=task.task_id,\n                status=task.status,\n                status_message=task.status_message,\n                created_at=task.created_at,\n                last_updated_at=task.last_updated_at,\n                ttl=task.ttl,\n                poll_interval=task.poll_interval,\n            )\n\n        async def get_task_result_handler(\n            context: RequestContext[ClientSession],\n            params: GetTaskPayloadRequestParams,\n        ) -> GetTaskPayloadResult | ErrorData:\n            result = await store.get_result(params.task_id)\n            assert result is not None, f\"Test setup error: result for {params.task_id} should exist\"\n            assert isinstance(result, CreateMessageResult)\n            return GetTaskPayloadResult(**result.model_dump())\n\n        task_handlers = ExperimentalTaskHandlers(\n            augmented_sampling=task_augmented_sampling_callback,\n            get_task=get_task_handler,\n            get_task_result=get_task_result_handler,\n        )\n        client_ready = anyio.Event()\n\n        async with anyio.create_task_group() as tg:\n            background_tg[0] = tg\n\n            async def run_client() -> None:\n                async with ClientSession(\n                    client_streams.client_receive,\n                    client_streams.client_send,\n                    message_handler=_default_message_handler,\n                    experimental_task_handlers=task_handlers,\n                ):\n                    client_ready.set()\n                    await anyio.sleep_forever()\n\n            tg.start_soon(run_client)\n            await client_ready.wait()\n\n            # Step 1: Server sends task-augmented CreateMessageRequest\n            typed_request = CreateMessageRequest(\n                params=CreateMessageRequestParams(\n                    messages=[SamplingMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hello\"))],\n                    max_tokens=100,\n                    task=TaskMetadata(ttl=60000),\n                )\n            )\n            request = types.JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                id=\"req-sampling\",\n                **typed_request.model_dump(by_alias=True),\n            )\n            await client_streams.server_send.send(SessionMessage(request))\n\n            # Step 2: Client responds with CreateTaskResult\n            response_msg = await client_streams.server_receive.receive()\n            response = response_msg.message\n            assert isinstance(response, types.JSONRPCResponse)\n\n            task_result = CreateTaskResult.model_validate(response.result)\n            task_id = task_result.task.task_id\n            assert task_id == created_task_id[0]\n\n            # Step 3: Wait for background sampling\n            await sampling_completed.wait()\n\n            # Step 4: Server polls task status\n            typed_poll = GetTaskRequest(params=GetTaskRequestParams(task_id=task_id))\n            poll_request = types.JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                id=\"req-poll\",\n                **typed_poll.model_dump(by_alias=True),\n            )\n            await client_streams.server_send.send(SessionMessage(poll_request))\n\n            poll_response_msg = await client_streams.server_receive.receive()\n            poll_response = poll_response_msg.message\n            assert isinstance(poll_response, types.JSONRPCResponse)\n\n            status = GetTaskResult.model_validate(poll_response.result)\n            assert status.status == \"completed\"\n\n            # Step 5: Server gets result\n            typed_result_req = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=task_id))\n            result_request = types.JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                id=\"req-result\",\n                **typed_result_req.model_dump(by_alias=True),\n            )\n            await client_streams.server_send.send(SessionMessage(result_request))\n\n            result_response_msg = await client_streams.server_receive.receive()\n            result_response = result_response_msg.message\n            assert isinstance(result_response, types.JSONRPCResponse)\n\n            assert isinstance(result_response.result, dict)\n            assert result_response.result[\"role\"] == \"assistant\"\n\n            tg.cancel_scope.cancel()\n\n        store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_client_task_augmented_elicitation(client_streams: ClientTestStreams) -> None:\n    \"\"\"Test that client can handle task-augmented elicitation request from server.\"\"\"\n    with anyio.fail_after(10):\n        store = InMemoryTaskStore()\n        elicitation_completed = Event()\n        created_task_id: list[str | None] = [None]\n        background_tg: list[TaskGroup | None] = [None]\n\n        async def task_augmented_elicitation_callback(\n            context: RequestContext[ClientSession],\n            params: ElicitRequestParams,\n            task_metadata: TaskMetadata,\n        ) -> CreateTaskResult | ErrorData:\n            task = await store.create_task(task_metadata)\n            created_task_id[0] = task.task_id\n\n            async def do_elicitation() -> None:\n                # Simulate user providing elicitation response\n                result = ElicitResult(action=\"accept\", content={\"name\": \"Test User\"})\n                await store.store_result(task.task_id, result)\n                await store.update_task(task.task_id, status=\"completed\")\n                elicitation_completed.set()\n\n            assert background_tg[0] is not None\n            background_tg[0].start_soon(do_elicitation)\n            return CreateTaskResult(task=task)\n\n        async def get_task_handler(\n            context: RequestContext[ClientSession],\n            params: GetTaskRequestParams,\n        ) -> GetTaskResult | ErrorData:\n            task = await store.get_task(params.task_id)\n            assert task is not None, f\"Test setup error: task {params.task_id} should exist\"\n            return GetTaskResult(\n                task_id=task.task_id,\n                status=task.status,\n                status_message=task.status_message,\n                created_at=task.created_at,\n                last_updated_at=task.last_updated_at,\n                ttl=task.ttl,\n                poll_interval=task.poll_interval,\n            )\n\n        async def get_task_result_handler(\n            context: RequestContext[ClientSession],\n            params: GetTaskPayloadRequestParams,\n        ) -> GetTaskPayloadResult | ErrorData:\n            result = await store.get_result(params.task_id)\n            assert result is not None, f\"Test setup error: result for {params.task_id} should exist\"\n            assert isinstance(result, ElicitResult)\n            return GetTaskPayloadResult(**result.model_dump())\n\n        task_handlers = ExperimentalTaskHandlers(\n            augmented_elicitation=task_augmented_elicitation_callback,\n            get_task=get_task_handler,\n            get_task_result=get_task_result_handler,\n        )\n        client_ready = anyio.Event()\n\n        async with anyio.create_task_group() as tg:\n            background_tg[0] = tg\n\n            async def run_client() -> None:\n                async with ClientSession(\n                    client_streams.client_receive,\n                    client_streams.client_send,\n                    message_handler=_default_message_handler,\n                    experimental_task_handlers=task_handlers,\n                ):\n                    client_ready.set()\n                    await anyio.sleep_forever()\n\n            tg.start_soon(run_client)\n            await client_ready.wait()\n\n            # Step 1: Server sends task-augmented ElicitRequest\n            typed_request = ElicitRequest(\n                params=ElicitRequestFormParams(\n                    message=\"What is your name?\",\n                    requested_schema={\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}},\n                    task=TaskMetadata(ttl=60000),\n                )\n            )\n            request = types.JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                id=\"req-elicit\",\n                **typed_request.model_dump(by_alias=True),\n            )\n            await client_streams.server_send.send(SessionMessage(request))\n\n            # Step 2: Client responds with CreateTaskResult\n            response_msg = await client_streams.server_receive.receive()\n            response = response_msg.message\n            assert isinstance(response, types.JSONRPCResponse)\n\n            task_result = CreateTaskResult.model_validate(response.result)\n            task_id = task_result.task.task_id\n            assert task_id == created_task_id[0]\n\n            # Step 3: Wait for background elicitation\n            await elicitation_completed.wait()\n\n            # Step 4: Server polls task status\n            typed_poll = GetTaskRequest(params=GetTaskRequestParams(task_id=task_id))\n            poll_request = types.JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                id=\"req-poll\",\n                **typed_poll.model_dump(by_alias=True),\n            )\n            await client_streams.server_send.send(SessionMessage(poll_request))\n\n            poll_response_msg = await client_streams.server_receive.receive()\n            poll_response = poll_response_msg.message\n            assert isinstance(poll_response, types.JSONRPCResponse)\n\n            status = GetTaskResult.model_validate(poll_response.result)\n            assert status.status == \"completed\"\n\n            # Step 5: Server gets result\n            typed_result_req = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=task_id))\n            result_request = types.JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                id=\"req-result\",\n                **typed_result_req.model_dump(by_alias=True),\n            )\n            await client_streams.server_send.send(SessionMessage(result_request))\n\n            result_response_msg = await client_streams.server_receive.receive()\n            result_response = result_response_msg.message\n            assert isinstance(result_response, types.JSONRPCResponse)\n\n            # Verify the elicitation result\n            assert isinstance(result_response.result, dict)\n            assert result_response.result[\"action\"] == \"accept\"\n            assert result_response.result[\"content\"] == {\"name\": \"Test User\"}\n\n            tg.cancel_scope.cancel()\n\n        store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_client_returns_error_for_unhandled_task_request(client_streams: ClientTestStreams) -> None:\n    \"\"\"Test that client returns error when no handler is registered for task request.\"\"\"\n    with anyio.fail_after(10):\n        client_ready = anyio.Event()\n\n        async with anyio.create_task_group() as tg:\n\n            async def run_client() -> None:\n                async with ClientSession(\n                    client_streams.client_receive,\n                    client_streams.client_send,\n                    message_handler=_default_message_handler,\n                ):\n                    client_ready.set()\n                    await anyio.sleep_forever()\n\n            tg.start_soon(run_client)\n            await client_ready.wait()\n\n            typed_request = GetTaskRequest(params=GetTaskRequestParams(task_id=\"nonexistent\"))\n            request = types.JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                id=\"req-unhandled\",\n                **typed_request.model_dump(by_alias=True),\n            )\n            await client_streams.server_send.send(SessionMessage(request))\n\n            response_msg = await client_streams.server_receive.receive()\n            response = response_msg.message\n            assert isinstance(response, types.JSONRPCError)\n            assert (\n                \"not supported\" in response.error.message.lower()\n                or \"method not found\" in response.error.message.lower()\n            )\n\n            tg.cancel_scope.cancel()\n\n\n@pytest.mark.anyio\nasync def test_client_returns_error_for_unhandled_task_result_request(client_streams: ClientTestStreams) -> None:\n    \"\"\"Test that client returns error for unhandled tasks/result request.\"\"\"\n    with anyio.fail_after(10):\n        client_ready = anyio.Event()\n\n        async with anyio.create_task_group() as tg:\n\n            async def run_client() -> None:\n                async with ClientSession(\n                    client_streams.client_receive,\n                    client_streams.client_send,\n                    message_handler=_default_message_handler,\n                ):\n                    client_ready.set()\n                    await anyio.sleep_forever()\n\n            tg.start_soon(run_client)\n            await client_ready.wait()\n\n            typed_request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=\"nonexistent\"))\n            request = types.JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                id=\"req-result\",\n                **typed_request.model_dump(by_alias=True),\n            )\n            await client_streams.server_send.send(SessionMessage(request))\n\n            response_msg = await client_streams.server_receive.receive()\n            response = response_msg.message\n            assert isinstance(response, types.JSONRPCError)\n            assert \"not supported\" in response.error.message.lower()\n\n            tg.cancel_scope.cancel()\n\n\n@pytest.mark.anyio\nasync def test_client_returns_error_for_unhandled_list_tasks_request(client_streams: ClientTestStreams) -> None:\n    \"\"\"Test that client returns error for unhandled tasks/list request.\"\"\"\n    with anyio.fail_after(10):\n        client_ready = anyio.Event()\n\n        async with anyio.create_task_group() as tg:\n\n            async def run_client() -> None:\n                async with ClientSession(\n                    client_streams.client_receive,\n                    client_streams.client_send,\n                    message_handler=_default_message_handler,\n                ):\n                    client_ready.set()\n                    await anyio.sleep_forever()\n\n            tg.start_soon(run_client)\n            await client_ready.wait()\n\n            typed_request = ListTasksRequest()\n            request = types.JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                id=\"req-list\",\n                **typed_request.model_dump(by_alias=True),\n            )\n            await client_streams.server_send.send(SessionMessage(request))\n\n            response_msg = await client_streams.server_receive.receive()\n            response = response_msg.message\n            assert isinstance(response, types.JSONRPCError)\n            assert \"not supported\" in response.error.message.lower()\n\n            tg.cancel_scope.cancel()\n\n\n@pytest.mark.anyio\nasync def test_client_returns_error_for_unhandled_cancel_task_request(client_streams: ClientTestStreams) -> None:\n    \"\"\"Test that client returns error for unhandled tasks/cancel request.\"\"\"\n    with anyio.fail_after(10):\n        client_ready = anyio.Event()\n\n        async with anyio.create_task_group() as tg:\n\n            async def run_client() -> None:\n                async with ClientSession(\n                    client_streams.client_receive,\n                    client_streams.client_send,\n                    message_handler=_default_message_handler,\n                ):\n                    client_ready.set()\n                    await anyio.sleep_forever()\n\n            tg.start_soon(run_client)\n            await client_ready.wait()\n\n            typed_request = CancelTaskRequest(params=CancelTaskRequestParams(task_id=\"nonexistent\"))\n            request = types.JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                id=\"req-cancel\",\n                **typed_request.model_dump(by_alias=True),\n            )\n            await client_streams.server_send.send(SessionMessage(request))\n\n            response_msg = await client_streams.server_receive.receive()\n            response = response_msg.message\n            assert isinstance(response, types.JSONRPCError)\n            assert \"not supported\" in response.error.message.lower()\n\n            tg.cancel_scope.cancel()\n\n\n@pytest.mark.anyio\nasync def test_client_returns_error_for_unhandled_task_augmented_sampling(client_streams: ClientTestStreams) -> None:\n    \"\"\"Test that client returns error for task-augmented sampling without handler.\"\"\"\n    with anyio.fail_after(10):\n        client_ready = anyio.Event()\n\n        async with anyio.create_task_group() as tg:\n\n            async def run_client() -> None:\n                # No task handlers provided - uses defaults\n                async with ClientSession(\n                    client_streams.client_receive,\n                    client_streams.client_send,\n                    message_handler=_default_message_handler,\n                ):\n                    client_ready.set()\n                    await anyio.sleep_forever()\n\n            tg.start_soon(run_client)\n            await client_ready.wait()\n\n            # Send task-augmented sampling request\n            typed_request = CreateMessageRequest(\n                params=CreateMessageRequestParams(\n                    messages=[SamplingMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hello\"))],\n                    max_tokens=100,\n                    task=TaskMetadata(ttl=60000),\n                )\n            )\n            request = types.JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                id=\"req-sampling\",\n                **typed_request.model_dump(by_alias=True),\n            )\n            await client_streams.server_send.send(SessionMessage(request))\n\n            response_msg = await client_streams.server_receive.receive()\n            response = response_msg.message\n            assert isinstance(response, types.JSONRPCError)\n            assert \"not supported\" in response.error.message.lower()\n\n            tg.cancel_scope.cancel()\n\n\n@pytest.mark.anyio\nasync def test_client_returns_error_for_unhandled_task_augmented_elicitation(\n    client_streams: ClientTestStreams,\n) -> None:\n    \"\"\"Test that client returns error for task-augmented elicitation without handler.\"\"\"\n    with anyio.fail_after(10):\n        client_ready = anyio.Event()\n\n        async with anyio.create_task_group() as tg:\n\n            async def run_client() -> None:\n                # No task handlers provided - uses defaults\n                async with ClientSession(\n                    client_streams.client_receive,\n                    client_streams.client_send,\n                    message_handler=_default_message_handler,\n                ):\n                    client_ready.set()\n                    await anyio.sleep_forever()\n\n            tg.start_soon(run_client)\n            await client_ready.wait()\n\n            # Send task-augmented elicitation request\n            typed_request = ElicitRequest(\n                params=ElicitRequestFormParams(\n                    message=\"What is your name?\",\n                    requested_schema={\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}}},\n                    task=TaskMetadata(ttl=60000),\n                )\n            )\n            request = types.JSONRPCRequest(\n                jsonrpc=\"2.0\",\n                id=\"req-elicit\",\n                **typed_request.model_dump(by_alias=True),\n            )\n            await client_streams.server_send.send(SessionMessage(request))\n\n            response_msg = await client_streams.server_receive.receive()\n            response = response_msg.message\n            assert isinstance(response, types.JSONRPCError)\n            assert \"not supported\" in response.error.message.lower()\n\n            tg.cancel_scope.cancel()\n"
  },
  {
    "path": "tests/experimental/tasks/client/test_poll_task.py",
    "content": "\"\"\"Tests for poll_task async iterator.\"\"\"\n\nfrom collections.abc import Callable, Coroutine\nfrom datetime import datetime, timezone\nfrom typing import Any\nfrom unittest.mock import AsyncMock\n\nimport pytest\n\nfrom mcp.client.experimental.tasks import ExperimentalClientFeatures\nfrom mcp.types import GetTaskResult, TaskStatus\n\n\ndef make_task_result(\n    status: TaskStatus = \"working\",\n    poll_interval: int = 0,\n    task_id: str = \"test-task\",\n    status_message: str | None = None,\n) -> GetTaskResult:\n    \"\"\"Create GetTaskResult with sensible defaults.\"\"\"\n    now = datetime.now(timezone.utc)\n    return GetTaskResult(\n        task_id=task_id,\n        status=status,\n        status_message=status_message,\n        created_at=now,\n        last_updated_at=now,\n        ttl=60000,\n        poll_interval=poll_interval,\n    )\n\n\ndef make_status_sequence(\n    *statuses: TaskStatus,\n    task_id: str = \"test-task\",\n) -> Callable[[str], Coroutine[Any, Any, GetTaskResult]]:\n    \"\"\"Create mock get_task that returns statuses in sequence.\"\"\"\n    status_iter = iter(statuses)\n\n    async def mock_get_task(tid: str) -> GetTaskResult:\n        return make_task_result(status=next(status_iter), task_id=tid)\n\n    return mock_get_task\n\n\n@pytest.fixture\ndef mock_session() -> AsyncMock:\n    return AsyncMock()\n\n\n@pytest.fixture\ndef features(mock_session: AsyncMock) -> ExperimentalClientFeatures:\n    return ExperimentalClientFeatures(mock_session)\n\n\n@pytest.mark.anyio\nasync def test_poll_task_yields_until_completed(features: ExperimentalClientFeatures) -> None:\n    \"\"\"poll_task yields each status until terminal.\"\"\"\n    features.get_task = make_status_sequence(\"working\", \"working\", \"completed\")  # type: ignore[method-assign]\n\n    statuses = [s.status async for s in features.poll_task(\"test-task\")]\n\n    assert statuses == [\"working\", \"working\", \"completed\"]\n\n\n@pytest.mark.anyio\n@pytest.mark.parametrize(\"terminal_status\", [\"completed\", \"failed\", \"cancelled\"])\nasync def test_poll_task_exits_on_terminal(features: ExperimentalClientFeatures, terminal_status: TaskStatus) -> None:\n    \"\"\"poll_task exits immediately when task is already terminal.\"\"\"\n    features.get_task = make_status_sequence(terminal_status)  # type: ignore[method-assign]\n\n    statuses = [s.status async for s in features.poll_task(\"test-task\")]\n\n    assert statuses == [terminal_status]\n\n\n@pytest.mark.anyio\nasync def test_poll_task_continues_through_input_required(features: ExperimentalClientFeatures) -> None:\n    \"\"\"poll_task yields input_required and continues (non-terminal).\"\"\"\n    features.get_task = make_status_sequence(\"working\", \"input_required\", \"working\", \"completed\")  # type: ignore[method-assign]\n\n    statuses = [s.status async for s in features.poll_task(\"test-task\")]\n\n    assert statuses == [\"working\", \"input_required\", \"working\", \"completed\"]\n\n\n@pytest.mark.anyio\nasync def test_poll_task_passes_task_id(features: ExperimentalClientFeatures) -> None:\n    \"\"\"poll_task passes correct task_id to get_task.\"\"\"\n    received_ids: list[str] = []\n\n    async def mock_get_task(task_id: str) -> GetTaskResult:\n        received_ids.append(task_id)\n        return make_task_result(status=\"completed\", task_id=task_id)\n\n    features.get_task = mock_get_task  # type: ignore[method-assign]\n\n    _ = [s async for s in features.poll_task(\"my-task-123\")]\n\n    assert received_ids == [\"my-task-123\"]\n\n\n@pytest.mark.anyio\nasync def test_poll_task_yields_full_result(features: ExperimentalClientFeatures) -> None:\n    \"\"\"poll_task yields complete GetTaskResult objects.\"\"\"\n\n    async def mock_get_task(task_id: str) -> GetTaskResult:\n        return make_task_result(\n            status=\"completed\",\n            task_id=task_id,\n            status_message=\"All done!\",\n        )\n\n    features.get_task = mock_get_task  # type: ignore[method-assign]\n\n    results = [r async for r in features.poll_task(\"test-task\")]\n\n    assert len(results) == 1\n    assert results[0].status == \"completed\"\n    assert results[0].status_message == \"All done!\"\n    assert results[0].task_id == \"test-task\"\n"
  },
  {
    "path": "tests/experimental/tasks/client/test_tasks.py",
    "content": "\"\"\"Tests for the experimental client task methods (session.experimental).\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom dataclasses import dataclass, field\n\nimport anyio\nimport pytest\nfrom anyio import Event\nfrom anyio.abc import TaskGroup\n\nfrom mcp import Client\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.shared.experimental.tasks.helpers import task_execution\nfrom mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore\nfrom mcp.types import (\n    CallToolRequest,\n    CallToolRequestParams,\n    CallToolResult,\n    CancelTaskRequestParams,\n    CancelTaskResult,\n    CreateTaskResult,\n    GetTaskPayloadRequestParams,\n    GetTaskPayloadResult,\n    GetTaskRequestParams,\n    GetTaskResult,\n    ListTasksResult,\n    ListToolsResult,\n    PaginatedRequestParams,\n    TaskMetadata,\n    TextContent,\n)\n\npytestmark = pytest.mark.anyio\n\n\n@dataclass\nclass AppContext:\n    \"\"\"Application context passed via lifespan_context.\"\"\"\n\n    task_group: TaskGroup\n    store: InMemoryTaskStore\n    task_done_events: dict[str, Event] = field(default_factory=lambda: {})\n\n\nasync def _handle_list_tools(\n    ctx: ServerRequestContext[AppContext], params: PaginatedRequestParams | None\n) -> ListToolsResult:\n    raise NotImplementedError\n\n\nasync def _handle_call_tool_with_done_event(\n    ctx: ServerRequestContext[AppContext], params: CallToolRequestParams, *, result_text: str = \"Done\"\n) -> CallToolResult | CreateTaskResult:\n    app = ctx.lifespan_context\n    if ctx.experimental.is_task:\n        task_metadata = ctx.experimental.task_metadata\n        assert task_metadata is not None\n        task = await app.store.create_task(task_metadata)\n\n        done_event = Event()\n        app.task_done_events[task.task_id] = done_event\n\n        async def do_work() -> None:\n            async with task_execution(task.task_id, app.store) as task_ctx:\n                await task_ctx.complete(CallToolResult(content=[TextContent(type=\"text\", text=result_text)]))\n            done_event.set()\n\n        app.task_group.start_soon(do_work)\n        return CreateTaskResult(task=task)\n\n    raise NotImplementedError\n\n\ndef _make_lifespan(store: InMemoryTaskStore, task_done_events: dict[str, Event]):\n    @asynccontextmanager\n    async def app_lifespan(server: Server[AppContext]) -> AsyncIterator[AppContext]:\n        async with anyio.create_task_group() as tg:\n            yield AppContext(task_group=tg, store=store, task_done_events=task_done_events)\n\n    return app_lifespan\n\n\nasync def test_session_experimental_get_task() -> None:\n    \"\"\"Test session.experimental.get_task() method.\"\"\"\n    store = InMemoryTaskStore()\n    task_done_events: dict[str, Event] = {}\n\n    async def handle_get_task(ctx: ServerRequestContext[AppContext], params: GetTaskRequestParams) -> GetTaskResult:\n        app = ctx.lifespan_context\n        task = await app.store.get_task(params.task_id)\n        assert task is not None, f\"Test setup error: task {params.task_id} should exist\"\n        return GetTaskResult(\n            task_id=task.task_id,\n            status=task.status,\n            status_message=task.status_message,\n            created_at=task.created_at,\n            last_updated_at=task.last_updated_at,\n            ttl=task.ttl,\n            poll_interval=task.poll_interval,\n        )\n\n    server: Server[AppContext] = Server(\n        \"test-server\",\n        lifespan=_make_lifespan(store, task_done_events),\n        on_list_tools=_handle_list_tools,\n        on_call_tool=_handle_call_tool_with_done_event,\n    )\n    server.experimental.enable_tasks(on_get_task=handle_get_task)\n\n    async with Client(server) as client:\n        # Create a task\n        create_result = await client.session.send_request(\n            CallToolRequest(\n                params=CallToolRequestParams(\n                    name=\"test_tool\",\n                    arguments={},\n                    task=TaskMetadata(ttl=60000),\n                )\n            ),\n            CreateTaskResult,\n        )\n        task_id = create_result.task.task_id\n\n        # Wait for task to complete\n        await task_done_events[task_id].wait()\n\n        # Use session.experimental to get task status\n        task_status = await client.session.experimental.get_task(task_id)\n\n        assert task_status.task_id == task_id\n        assert task_status.status == \"completed\"\n\n\nasync def test_session_experimental_get_task_result() -> None:\n    \"\"\"Test session.experimental.get_task_result() method.\"\"\"\n    store = InMemoryTaskStore()\n    task_done_events: dict[str, Event] = {}\n\n    async def handle_call_tool(\n        ctx: ServerRequestContext[AppContext], params: CallToolRequestParams\n    ) -> CallToolResult | CreateTaskResult:\n        return await _handle_call_tool_with_done_event(ctx, params, result_text=\"Task result content\")\n\n    async def handle_get_task_result(\n        ctx: ServerRequestContext[AppContext], params: GetTaskPayloadRequestParams\n    ) -> GetTaskPayloadResult:\n        app = ctx.lifespan_context\n        result = await app.store.get_result(params.task_id)\n        assert result is not None, f\"Test setup error: result for {params.task_id} should exist\"\n        assert isinstance(result, CallToolResult)\n        return GetTaskPayloadResult(**result.model_dump())\n\n    server: Server[AppContext] = Server(\n        \"test-server\",\n        lifespan=_make_lifespan(store, task_done_events),\n        on_list_tools=_handle_list_tools,\n        on_call_tool=handle_call_tool,\n    )\n    server.experimental.enable_tasks(on_task_result=handle_get_task_result)\n\n    async with Client(server) as client:\n        # Create a task\n        create_result = await client.session.send_request(\n            CallToolRequest(\n                params=CallToolRequestParams(\n                    name=\"test_tool\",\n                    arguments={},\n                    task=TaskMetadata(ttl=60000),\n                )\n            ),\n            CreateTaskResult,\n        )\n        task_id = create_result.task.task_id\n\n        # Wait for task to complete\n        await task_done_events[task_id].wait()\n\n        # Use TaskClient to get task result\n        task_result = await client.session.experimental.get_task_result(task_id, CallToolResult)\n\n        assert len(task_result.content) == 1\n        content = task_result.content[0]\n        assert isinstance(content, TextContent)\n        assert content.text == \"Task result content\"\n\n\nasync def test_session_experimental_list_tasks() -> None:\n    \"\"\"Test TaskClient.list_tasks() method.\"\"\"\n    store = InMemoryTaskStore()\n    task_done_events: dict[str, Event] = {}\n\n    async def handle_list_tasks(\n        ctx: ServerRequestContext[AppContext], params: PaginatedRequestParams | None\n    ) -> ListTasksResult:\n        app = ctx.lifespan_context\n        cursor = params.cursor if params else None\n        tasks_list, next_cursor = await app.store.list_tasks(cursor=cursor)\n        return ListTasksResult(tasks=tasks_list, next_cursor=next_cursor)\n\n    server: Server[AppContext] = Server(\n        \"test-server\",\n        lifespan=_make_lifespan(store, task_done_events),\n        on_list_tools=_handle_list_tools,\n        on_call_tool=_handle_call_tool_with_done_event,\n    )\n    server.experimental.enable_tasks(on_list_tasks=handle_list_tasks)\n\n    async with Client(server) as client:\n        # Create two tasks\n        for _ in range(2):\n            create_result = await client.session.send_request(\n                CallToolRequest(\n                    params=CallToolRequestParams(\n                        name=\"test_tool\",\n                        arguments={},\n                        task=TaskMetadata(ttl=60000),\n                    )\n                ),\n                CreateTaskResult,\n            )\n            await task_done_events[create_result.task.task_id].wait()\n\n        # Use TaskClient to list tasks\n        list_result = await client.session.experimental.list_tasks()\n\n        assert len(list_result.tasks) == 2\n\n\nasync def test_session_experimental_cancel_task() -> None:\n    \"\"\"Test TaskClient.cancel_task() method.\"\"\"\n    store = InMemoryTaskStore()\n    task_done_events: dict[str, Event] = {}\n\n    async def handle_call_tool_no_work(\n        ctx: ServerRequestContext[AppContext], params: CallToolRequestParams\n    ) -> CallToolResult | CreateTaskResult:\n        app = ctx.lifespan_context\n        if ctx.experimental.is_task:\n            task_metadata = ctx.experimental.task_metadata\n            assert task_metadata is not None\n            task = await app.store.create_task(task_metadata)\n            # Don't start any work - task stays in \"working\" status\n            return CreateTaskResult(task=task)\n        raise NotImplementedError\n\n    async def handle_get_task(ctx: ServerRequestContext[AppContext], params: GetTaskRequestParams) -> GetTaskResult:\n        app = ctx.lifespan_context\n        task = await app.store.get_task(params.task_id)\n        assert task is not None, f\"Test setup error: task {params.task_id} should exist\"\n        return GetTaskResult(\n            task_id=task.task_id,\n            status=task.status,\n            status_message=task.status_message,\n            created_at=task.created_at,\n            last_updated_at=task.last_updated_at,\n            ttl=task.ttl,\n            poll_interval=task.poll_interval,\n        )\n\n    async def handle_cancel_task(\n        ctx: ServerRequestContext[AppContext], params: CancelTaskRequestParams\n    ) -> CancelTaskResult:\n        app = ctx.lifespan_context\n        task = await app.store.get_task(params.task_id)\n        assert task is not None, f\"Test setup error: task {params.task_id} should exist\"\n        await app.store.update_task(params.task_id, status=\"cancelled\")\n        updated_task = await app.store.get_task(params.task_id)\n        assert updated_task is not None\n        return CancelTaskResult(\n            task_id=updated_task.task_id,\n            status=updated_task.status,\n            created_at=updated_task.created_at,\n            last_updated_at=updated_task.last_updated_at,\n            ttl=updated_task.ttl,\n        )\n\n    server: Server[AppContext] = Server(\n        \"test-server\",\n        lifespan=_make_lifespan(store, task_done_events),\n        on_list_tools=_handle_list_tools,\n        on_call_tool=handle_call_tool_no_work,\n    )\n    server.experimental.enable_tasks(on_get_task=handle_get_task, on_cancel_task=handle_cancel_task)\n\n    async with Client(server) as client:\n        # Create a task (but don't complete it)\n        create_result = await client.session.send_request(\n            CallToolRequest(\n                params=CallToolRequestParams(\n                    name=\"test_tool\",\n                    arguments={},\n                    task=TaskMetadata(ttl=60000),\n                )\n            ),\n            CreateTaskResult,\n        )\n        task_id = create_result.task.task_id\n\n        # Verify task is working\n        status_before = await client.session.experimental.get_task(task_id)\n        assert status_before.status == \"working\"\n\n        # Cancel the task\n        await client.session.experimental.cancel_task(task_id)\n\n        # Verify task is cancelled\n        status_after = await client.session.experimental.get_task(task_id)\n        assert status_after.status == \"cancelled\"\n"
  },
  {
    "path": "tests/experimental/tasks/server/__init__.py",
    "content": ""
  },
  {
    "path": "tests/experimental/tasks/server/test_context.py",
    "content": "\"\"\"Tests for TaskContext and helper functions.\"\"\"\n\nimport pytest\n\nfrom mcp.shared.experimental.tasks.context import TaskContext\nfrom mcp.shared.experimental.tasks.helpers import create_task_state, task_execution\nfrom mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore\nfrom mcp.types import CallToolResult, TaskMetadata, TextContent\n\n\n@pytest.mark.anyio\nasync def test_task_context_properties() -> None:\n    \"\"\"Test TaskContext basic properties.\"\"\"\n    store = InMemoryTaskStore()\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n    ctx = TaskContext(task, store)\n\n    assert ctx.task_id == task.task_id\n    assert ctx.task.task_id == task.task_id\n    assert ctx.task.status == \"working\"\n    assert ctx.is_cancelled is False\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_task_context_update_status() -> None:\n    \"\"\"Test TaskContext.update_status.\"\"\"\n    store = InMemoryTaskStore()\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n    ctx = TaskContext(task, store)\n\n    await ctx.update_status(\"Processing step 1...\")\n\n    # Check status message was updated\n    updated = await store.get_task(task.task_id)\n    assert updated is not None\n    assert updated.status_message == \"Processing step 1...\"\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_task_context_complete() -> None:\n    \"\"\"Test TaskContext.complete.\"\"\"\n    store = InMemoryTaskStore()\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n    ctx = TaskContext(task, store)\n\n    result = CallToolResult(content=[TextContent(type=\"text\", text=\"Done!\")])\n    await ctx.complete(result)\n\n    # Check task status\n    updated = await store.get_task(task.task_id)\n    assert updated is not None\n    assert updated.status == \"completed\"\n\n    # Check result is stored\n    stored_result = await store.get_result(task.task_id)\n    assert stored_result is not None\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_task_context_fail() -> None:\n    \"\"\"Test TaskContext.fail.\"\"\"\n    store = InMemoryTaskStore()\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n    ctx = TaskContext(task, store)\n\n    await ctx.fail(\"Something went wrong!\")\n\n    # Check task status\n    updated = await store.get_task(task.task_id)\n    assert updated is not None\n    assert updated.status == \"failed\"\n    assert updated.status_message == \"Something went wrong!\"\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_task_context_cancellation() -> None:\n    \"\"\"Test TaskContext cancellation request.\"\"\"\n    store = InMemoryTaskStore()\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n    ctx = TaskContext(task, store)\n\n    assert ctx.is_cancelled is False\n\n    ctx.request_cancellation()\n\n    assert ctx.is_cancelled is True\n\n    store.cleanup()\n\n\ndef test_create_task_state_generates_id() -> None:\n    \"\"\"create_task_state generates a unique task ID when none provided.\"\"\"\n    task1 = create_task_state(TaskMetadata(ttl=60000))\n    task2 = create_task_state(TaskMetadata(ttl=60000))\n\n    assert task1.task_id != task2.task_id\n\n\ndef test_create_task_state_uses_provided_id() -> None:\n    \"\"\"create_task_state uses the provided task ID.\"\"\"\n    task = create_task_state(TaskMetadata(ttl=60000), task_id=\"my-task-123\")\n    assert task.task_id == \"my-task-123\"\n\n\ndef test_create_task_state_null_ttl() -> None:\n    \"\"\"create_task_state handles null TTL.\"\"\"\n    task = create_task_state(TaskMetadata(ttl=None))\n    assert task.ttl is None\n\n\ndef test_create_task_state_has_created_at() -> None:\n    \"\"\"create_task_state sets createdAt timestamp.\"\"\"\n    task = create_task_state(TaskMetadata(ttl=60000))\n    assert task.created_at is not None\n\n\n@pytest.mark.anyio\nasync def test_task_execution_provides_context() -> None:\n    \"\"\"task_execution provides a TaskContext for the task.\"\"\"\n    store = InMemoryTaskStore()\n    await store.create_task(TaskMetadata(ttl=60000), task_id=\"exec-test-1\")\n\n    async with task_execution(\"exec-test-1\", store) as ctx:\n        assert ctx.task_id == \"exec-test-1\"\n        assert ctx.task.status == \"working\"\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_task_execution_auto_fails_on_exception() -> None:\n    \"\"\"task_execution automatically fails task on unhandled exception.\"\"\"\n    store = InMemoryTaskStore()\n    await store.create_task(TaskMetadata(ttl=60000), task_id=\"exec-fail-1\")\n\n    async with task_execution(\"exec-fail-1\", store):\n        raise RuntimeError(\"Oops!\")\n\n    # Task should be failed\n    failed_task = await store.get_task(\"exec-fail-1\")\n    assert failed_task is not None\n    assert failed_task.status == \"failed\"\n    assert \"Oops!\" in (failed_task.status_message or \"\")\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_task_execution_doesnt_fail_if_already_terminal() -> None:\n    \"\"\"task_execution doesn't re-fail if task already terminal.\"\"\"\n    store = InMemoryTaskStore()\n    await store.create_task(TaskMetadata(ttl=60000), task_id=\"exec-term-1\")\n\n    async with task_execution(\"exec-term-1\", store) as ctx:\n        # Complete the task first\n        await ctx.complete(CallToolResult(content=[TextContent(type=\"text\", text=\"Done\")]))\n        # Then raise - shouldn't change status\n        raise RuntimeError(\"This shouldn't matter\")\n\n    # Task should remain completed\n    final_task = await store.get_task(\"exec-term-1\")\n    assert final_task is not None\n    assert final_task.status == \"completed\"\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_task_execution_not_found() -> None:\n    \"\"\"task_execution raises ValueError for non-existent task.\"\"\"\n    store = InMemoryTaskStore()\n\n    with pytest.raises(ValueError, match=\"not found\"):\n        async with task_execution(\"nonexistent\", store):\n            ...\n"
  },
  {
    "path": "tests/experimental/tasks/server/test_integration.py",
    "content": "\"\"\"End-to-end integration tests for tasks functionality.\n\nThese tests demonstrate the full task lifecycle:\n1. Client sends task-augmented request (tools/call with task metadata)\n2. Server creates task and returns CreateTaskResult immediately\n3. Background work executes (using task_execution context manager)\n4. Client polls with tasks/get\n5. Client retrieves result with tasks/result\n\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom dataclasses import dataclass, field\n\nimport anyio\nimport pytest\nfrom anyio import Event\nfrom anyio.abc import TaskGroup\n\nfrom mcp import Client\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.shared.experimental.tasks.helpers import task_execution\nfrom mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore\nfrom mcp.types import (\n    CallToolRequest,\n    CallToolRequestParams,\n    CallToolResult,\n    CreateTaskResult,\n    GetTaskPayloadRequestParams,\n    GetTaskPayloadResult,\n    GetTaskRequestParams,\n    GetTaskResult,\n    ListTasksResult,\n    ListToolsResult,\n    PaginatedRequestParams,\n    TaskMetadata,\n    TextContent,\n)\n\npytestmark = pytest.mark.anyio\n\n\n@dataclass\nclass AppContext:\n    \"\"\"Application context passed via lifespan_context.\"\"\"\n\n    task_group: TaskGroup\n    store: InMemoryTaskStore\n    task_done_events: dict[str, Event] = field(default_factory=lambda: {})\n\n\ndef _make_lifespan(store: InMemoryTaskStore, task_done_events: dict[str, Event]):\n    @asynccontextmanager\n    async def app_lifespan(server: Server[AppContext]) -> AsyncIterator[AppContext]:\n        async with anyio.create_task_group() as tg:\n            yield AppContext(task_group=tg, store=store, task_done_events=task_done_events)\n\n    return app_lifespan\n\n\nasync def test_task_lifecycle_with_task_execution() -> None:\n    \"\"\"Test the complete task lifecycle using the task_execution pattern.\"\"\"\n    store = InMemoryTaskStore()\n    task_done_events: dict[str, Event] = {}\n\n    async def handle_list_tools(\n        ctx: ServerRequestContext[AppContext], params: PaginatedRequestParams | None\n    ) -> ListToolsResult:\n        raise NotImplementedError\n\n    async def handle_call_tool(\n        ctx: ServerRequestContext[AppContext], params: CallToolRequestParams\n    ) -> CallToolResult | CreateTaskResult:\n        app = ctx.lifespan_context\n        if params.name == \"process_data\" and ctx.experimental.is_task:\n            task_metadata = ctx.experimental.task_metadata\n            assert task_metadata is not None\n            task = await app.store.create_task(task_metadata)\n\n            done_event = Event()\n            app.task_done_events[task.task_id] = done_event\n\n            async def do_work() -> None:\n                async with task_execution(task.task_id, app.store) as task_ctx:\n                    await task_ctx.update_status(\"Processing input...\")\n                    input_value = (params.arguments or {}).get(\"input\", \"\")\n                    result_text = f\"Processed: {input_value.upper()}\"\n                    await task_ctx.complete(CallToolResult(content=[TextContent(type=\"text\", text=result_text)]))\n                done_event.set()\n\n            app.task_group.start_soon(do_work)\n            return CreateTaskResult(task=task)\n\n        raise NotImplementedError\n\n    async def handle_get_task(ctx: ServerRequestContext[AppContext], params: GetTaskRequestParams) -> GetTaskResult:\n        app = ctx.lifespan_context\n        task = await app.store.get_task(params.task_id)\n        assert task is not None, f\"Test setup error: task {params.task_id} should exist\"\n        return GetTaskResult(\n            task_id=task.task_id,\n            status=task.status,\n            status_message=task.status_message,\n            created_at=task.created_at,\n            last_updated_at=task.last_updated_at,\n            ttl=task.ttl,\n            poll_interval=task.poll_interval,\n        )\n\n    async def handle_get_task_result(\n        ctx: ServerRequestContext[AppContext], params: GetTaskPayloadRequestParams\n    ) -> GetTaskPayloadResult:\n        app = ctx.lifespan_context\n        result = await app.store.get_result(params.task_id)\n        assert result is not None, f\"Test setup error: result for {params.task_id} should exist\"\n        assert isinstance(result, CallToolResult)\n        return GetTaskPayloadResult(**result.model_dump())\n\n    async def handle_list_tasks(\n        ctx: ServerRequestContext[AppContext], params: PaginatedRequestParams | None\n    ) -> ListTasksResult:\n        raise NotImplementedError\n\n    server: Server[AppContext] = Server(\n        \"test-tasks\",\n        lifespan=_make_lifespan(store, task_done_events),\n        on_list_tools=handle_list_tools,\n        on_call_tool=handle_call_tool,\n    )\n    server.experimental.enable_tasks(\n        on_get_task=handle_get_task,\n        on_task_result=handle_get_task_result,\n        on_list_tasks=handle_list_tasks,\n    )\n\n    async with Client(server) as client:\n        # Step 1: Send task-augmented tool call\n        create_result = await client.session.send_request(\n            CallToolRequest(\n                params=CallToolRequestParams(\n                    name=\"process_data\",\n                    arguments={\"input\": \"hello world\"},\n                    task=TaskMetadata(ttl=60000),\n                ),\n            ),\n            CreateTaskResult,\n        )\n\n        assert isinstance(create_result, CreateTaskResult)\n        assert create_result.task.status == \"working\"\n        task_id = create_result.task.task_id\n\n        # Step 2: Wait for task to complete\n        await task_done_events[task_id].wait()\n\n        task_status = await client.session.experimental.get_task(task_id)\n        assert task_status.task_id == task_id\n        assert task_status.status == \"completed\"\n\n        # Step 3: Retrieve the actual result\n        task_result = await client.session.experimental.get_task_result(task_id, CallToolResult)\n\n        assert len(task_result.content) == 1\n        content = task_result.content[0]\n        assert isinstance(content, TextContent)\n        assert content.text == \"Processed: HELLO WORLD\"\n\n\nasync def test_task_auto_fails_on_exception() -> None:\n    \"\"\"Test that task_execution automatically fails the task on unhandled exception.\"\"\"\n    store = InMemoryTaskStore()\n    task_done_events: dict[str, Event] = {}\n\n    async def handle_list_tools(\n        ctx: ServerRequestContext[AppContext], params: PaginatedRequestParams | None\n    ) -> ListToolsResult:\n        raise NotImplementedError\n\n    async def handle_call_tool(\n        ctx: ServerRequestContext[AppContext], params: CallToolRequestParams\n    ) -> CallToolResult | CreateTaskResult:\n        app = ctx.lifespan_context\n        if params.name == \"failing_task\" and ctx.experimental.is_task:\n            task_metadata = ctx.experimental.task_metadata\n            assert task_metadata is not None\n            task = await app.store.create_task(task_metadata)\n\n            done_event = Event()\n            app.task_done_events[task.task_id] = done_event\n\n            async def do_failing_work() -> None:\n                async with task_execution(task.task_id, app.store) as task_ctx:\n                    await task_ctx.update_status(\"About to fail...\")\n                    raise RuntimeError(\"Something went wrong!\")\n                # This line is reached because task_execution suppresses the exception\n                done_event.set()\n\n            app.task_group.start_soon(do_failing_work)\n            return CreateTaskResult(task=task)\n\n        raise NotImplementedError\n\n    async def handle_get_task(ctx: ServerRequestContext[AppContext], params: GetTaskRequestParams) -> GetTaskResult:\n        app = ctx.lifespan_context\n        task = await app.store.get_task(params.task_id)\n        assert task is not None, f\"Test setup error: task {params.task_id} should exist\"\n        return GetTaskResult(\n            task_id=task.task_id,\n            status=task.status,\n            status_message=task.status_message,\n            created_at=task.created_at,\n            last_updated_at=task.last_updated_at,\n            ttl=task.ttl,\n            poll_interval=task.poll_interval,\n        )\n\n    server: Server[AppContext] = Server(\n        \"test-tasks-failure\",\n        lifespan=_make_lifespan(store, task_done_events),\n        on_list_tools=handle_list_tools,\n        on_call_tool=handle_call_tool,\n    )\n    server.experimental.enable_tasks(on_get_task=handle_get_task)\n\n    async with Client(server) as client:\n        # Send task request\n        create_result = await client.session.send_request(\n            CallToolRequest(\n                params=CallToolRequestParams(\n                    name=\"failing_task\",\n                    arguments={},\n                    task=TaskMetadata(ttl=60000),\n                ),\n            ),\n            CreateTaskResult,\n        )\n\n        task_id = create_result.task.task_id\n\n        # Wait for task to complete (even though it fails)\n        await task_done_events[task_id].wait()\n\n        # Check that task was auto-failed\n        task_status = await client.session.experimental.get_task(task_id)\n\n        assert task_status.status == \"failed\"\n        assert task_status.status_message == \"Something went wrong!\"\n"
  },
  {
    "path": "tests/experimental/tasks/server/test_run_task_flow.py",
    "content": "\"\"\"Tests for the simplified task API: enable_tasks() + run_task()\n\nThis tests the recommended user flow:\n1. server.experimental.enable_tasks() - one-line setup\n2. ctx.experimental.run_task(work) - spawns work, returns CreateTaskResult\n3. work function uses ServerTaskContext for elicit/create_message\n\nThese are integration tests that verify the complete flow works end-to-end.\n\"\"\"\n\nfrom unittest.mock import Mock\n\nimport anyio\nimport pytest\nfrom anyio import Event\n\nfrom mcp import Client\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.server.experimental.request_context import Experimental\nfrom mcp.server.experimental.task_context import ServerTaskContext\nfrom mcp.server.experimental.task_support import TaskSupport\nfrom mcp.server.lowlevel import NotificationOptions\nfrom mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore\nfrom mcp.shared.experimental.tasks.message_queue import InMemoryTaskMessageQueue\nfrom mcp.types import (\n    TASK_REQUIRED,\n    CallToolRequestParams,\n    CallToolResult,\n    CreateTaskResult,\n    GetTaskRequestParams,\n    GetTaskResult,\n    ListToolsResult,\n    PaginatedRequestParams,\n    TextContent,\n)\n\npytestmark = pytest.mark.anyio\n\n\nasync def _handle_list_tools_simple_task(\n    ctx: ServerRequestContext, params: PaginatedRequestParams | None\n) -> ListToolsResult:\n    raise NotImplementedError\n\n\nasync def test_run_task_basic_flow() -> None:\n    \"\"\"Test the basic run_task flow without elicitation.\"\"\"\n    work_completed = Event()\n    received_meta: list[str | None] = [None]\n\n    async def handle_call_tool(\n        ctx: ServerRequestContext, params: CallToolRequestParams\n    ) -> CallToolResult | CreateTaskResult:\n        ctx.experimental.validate_task_mode(TASK_REQUIRED)\n\n        if ctx.meta is not None:  # pragma: no branch\n            received_meta[0] = ctx.meta.get(\"custom_field\")\n\n        async def work(task: ServerTaskContext) -> CallToolResult:\n            await task.update_status(\"Working...\")\n            input_val = (params.arguments or {}).get(\"input\", \"default\")\n            result = CallToolResult(content=[TextContent(type=\"text\", text=f\"Processed: {input_val}\")])\n            work_completed.set()\n            return result\n\n        return await ctx.experimental.run_task(work)\n\n    server = Server(\n        \"test-run-task\",\n        on_list_tools=_handle_list_tools_simple_task,\n        on_call_tool=handle_call_tool,\n    )\n    server.experimental.enable_tasks()\n\n    async with Client(server) as client:\n        result = await client.session.experimental.call_tool_as_task(\n            \"simple_task\",\n            {\"input\": \"hello\"},\n            meta={\"custom_field\": \"test_value\"},\n        )\n\n        task_id = result.task.task_id\n        assert result.task.status == \"working\"\n\n        with anyio.fail_after(5):\n            await work_completed.wait()\n\n        with anyio.fail_after(5):\n            while True:\n                task_status = await client.session.experimental.get_task(task_id)\n                if task_status.status == \"completed\":  # pragma: no branch\n                    break\n\n    assert received_meta[0] == \"test_value\"\n\n\nasync def test_run_task_auto_fails_on_exception() -> None:\n    \"\"\"Test that run_task automatically fails the task when work raises.\"\"\"\n    work_failed = Event()\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        raise NotImplementedError\n\n    async def handle_call_tool(\n        ctx: ServerRequestContext, params: CallToolRequestParams\n    ) -> CallToolResult | CreateTaskResult:\n        ctx.experimental.validate_task_mode(TASK_REQUIRED)\n\n        async def work(task: ServerTaskContext) -> CallToolResult:\n            work_failed.set()\n            raise RuntimeError(\"Something went wrong!\")\n\n        return await ctx.experimental.run_task(work)\n\n    server = Server(\n        \"test-run-task-fail\",\n        on_list_tools=handle_list_tools,\n        on_call_tool=handle_call_tool,\n    )\n    server.experimental.enable_tasks()\n\n    async with Client(server) as client:\n        result = await client.session.experimental.call_tool_as_task(\"failing_task\", {})\n        task_id = result.task.task_id\n\n        with anyio.fail_after(5):\n            await work_failed.wait()\n\n        with anyio.fail_after(5):\n            while True:\n                task_status = await client.session.experimental.get_task(task_id)\n                if task_status.status == \"failed\":  # pragma: no branch\n                    break\n\n        assert \"Something went wrong\" in (task_status.status_message or \"\")\n\n\nasync def test_enable_tasks_auto_registers_handlers() -> None:\n    \"\"\"Test that enable_tasks() auto-registers get_task, list_tasks, cancel_task handlers.\"\"\"\n    server = Server(\"test-enable-tasks\")\n\n    # Before enable_tasks, no task capabilities\n    caps_before = server.get_capabilities(NotificationOptions(), {})\n    assert caps_before.tasks is None\n\n    # Enable tasks\n    server.experimental.enable_tasks()\n\n    # After enable_tasks, should have task capabilities\n    caps_after = server.get_capabilities(NotificationOptions(), {})\n    assert caps_after.tasks is not None\n    assert caps_after.tasks.list is not None\n    assert caps_after.tasks.cancel is not None\n    assert caps_after.tasks.requests is not None\n    assert caps_after.tasks.requests.tools is not None\n    assert caps_after.tasks.requests.tools.call is not None\n\n\nasync def test_enable_tasks_with_custom_store_and_queue() -> None:\n    \"\"\"Test that enable_tasks() uses provided store and queue instead of defaults.\"\"\"\n    server = Server(\"test-custom-store-queue\")\n\n    custom_store = InMemoryTaskStore()\n    custom_queue = InMemoryTaskMessageQueue()\n\n    task_support = server.experimental.enable_tasks(store=custom_store, queue=custom_queue)\n\n    assert task_support.store is custom_store\n    assert task_support.queue is custom_queue\n\n\nasync def test_enable_tasks_skips_default_handlers_when_custom_registered() -> None:\n    \"\"\"Test that enable_tasks() doesn't override already-registered handlers.\"\"\"\n    server = Server(\"test-custom-handlers\")\n\n    # Register custom handlers via enable_tasks kwargs\n    async def custom_get_task(ctx: ServerRequestContext, params: GetTaskRequestParams) -> GetTaskResult:\n        raise NotImplementedError\n\n    server.experimental.enable_tasks(on_get_task=custom_get_task)\n\n    # Verify handler is registered\n    assert server._has_handler(\"tasks/get\")\n    assert server._has_handler(\"tasks/list\")\n    assert server._has_handler(\"tasks/cancel\")\n    assert server._has_handler(\"tasks/result\")\n\n\nasync def test_run_task_without_enable_tasks_raises() -> None:\n    \"\"\"Test that run_task raises when enable_tasks() wasn't called.\"\"\"\n    experimental = Experimental(\n        task_metadata=None,\n        _client_capabilities=None,\n        _session=None,\n        _task_support=None,  # Not enabled\n    )\n\n    async def work(task: ServerTaskContext) -> CallToolResult:\n        raise NotImplementedError\n\n    with pytest.raises(RuntimeError, match=\"Task support not enabled\"):\n        await experimental.run_task(work)\n\n\nasync def test_task_support_task_group_before_run_raises() -> None:\n    \"\"\"Test that accessing task_group before run() raises RuntimeError.\"\"\"\n    task_support = TaskSupport.in_memory()\n\n    with pytest.raises(RuntimeError, match=\"TaskSupport not running\"):\n        _ = task_support.task_group\n\n\nasync def test_run_task_without_session_raises() -> None:\n    \"\"\"Test that run_task raises when session is not available.\"\"\"\n    task_support = TaskSupport.in_memory()\n\n    experimental = Experimental(\n        task_metadata=None,\n        _client_capabilities=None,\n        _session=None,  # No session\n        _task_support=task_support,\n    )\n\n    async def work(task: ServerTaskContext) -> CallToolResult:\n        raise NotImplementedError\n\n    with pytest.raises(RuntimeError, match=\"Session not available\"):\n        await experimental.run_task(work)\n\n\nasync def test_run_task_without_task_metadata_raises() -> None:\n    \"\"\"Test that run_task raises when request is not task-augmented.\"\"\"\n    task_support = TaskSupport.in_memory()\n    mock_session = Mock()\n\n    experimental = Experimental(\n        task_metadata=None,  # Not a task-augmented request\n        _client_capabilities=None,\n        _session=mock_session,\n        _task_support=task_support,\n    )\n\n    async def work(task: ServerTaskContext) -> CallToolResult:\n        raise NotImplementedError\n\n    with pytest.raises(RuntimeError, match=\"Request is not task-augmented\"):\n        await experimental.run_task(work)\n\n\nasync def test_run_task_with_model_immediate_response() -> None:\n    \"\"\"Test that run_task includes model_immediate_response in CreateTaskResult._meta.\"\"\"\n    work_completed = Event()\n    immediate_response_text = \"Processing your request...\"\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        raise NotImplementedError\n\n    async def handle_call_tool(\n        ctx: ServerRequestContext, params: CallToolRequestParams\n    ) -> CallToolResult | CreateTaskResult:\n        ctx.experimental.validate_task_mode(TASK_REQUIRED)\n\n        async def work(task: ServerTaskContext) -> CallToolResult:\n            work_completed.set()\n            return CallToolResult(content=[TextContent(type=\"text\", text=\"Done\")])\n\n        return await ctx.experimental.run_task(work, model_immediate_response=immediate_response_text)\n\n    server = Server(\n        \"test-run-task-immediate\",\n        on_list_tools=handle_list_tools,\n        on_call_tool=handle_call_tool,\n    )\n    server.experimental.enable_tasks()\n\n    async with Client(server) as client:\n        result = await client.session.experimental.call_tool_as_task(\"task_with_immediate\", {})\n\n        assert result.meta is not None\n        assert \"io.modelcontextprotocol/model-immediate-response\" in result.meta\n        assert result.meta[\"io.modelcontextprotocol/model-immediate-response\"] == immediate_response_text\n\n        with anyio.fail_after(5):\n            await work_completed.wait()\n\n\nasync def test_run_task_doesnt_complete_if_already_terminal() -> None:\n    \"\"\"Test that run_task doesn't auto-complete if work manually completed the task.\"\"\"\n    work_completed = Event()\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        raise NotImplementedError\n\n    async def handle_call_tool(\n        ctx: ServerRequestContext, params: CallToolRequestParams\n    ) -> CallToolResult | CreateTaskResult:\n        ctx.experimental.validate_task_mode(TASK_REQUIRED)\n\n        async def work(task: ServerTaskContext) -> CallToolResult:\n            manual_result = CallToolResult(content=[TextContent(type=\"text\", text=\"Manually completed\")])\n            await task.complete(manual_result, notify=False)\n            work_completed.set()\n            return CallToolResult(content=[TextContent(type=\"text\", text=\"This should be ignored\")])\n\n        return await ctx.experimental.run_task(work)\n\n    server = Server(\n        \"test-already-complete\",\n        on_list_tools=handle_list_tools,\n        on_call_tool=handle_call_tool,\n    )\n    server.experimental.enable_tasks()\n\n    async with Client(server) as client:\n        result = await client.session.experimental.call_tool_as_task(\"manual_complete_task\", {})\n        task_id = result.task.task_id\n\n        with anyio.fail_after(5):\n            await work_completed.wait()\n\n        with anyio.fail_after(5):\n            while True:\n                status = await client.session.experimental.get_task(task_id)\n                if status.status == \"completed\":  # pragma: no branch\n                    break\n\n\nasync def test_run_task_doesnt_fail_if_already_terminal() -> None:\n    \"\"\"Test that run_task doesn't auto-fail if work manually failed/cancelled the task.\"\"\"\n    work_completed = Event()\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        raise NotImplementedError\n\n    async def handle_call_tool(\n        ctx: ServerRequestContext, params: CallToolRequestParams\n    ) -> CallToolResult | CreateTaskResult:\n        ctx.experimental.validate_task_mode(TASK_REQUIRED)\n\n        async def work(task: ServerTaskContext) -> CallToolResult:\n            await task.fail(\"Manually failed\", notify=False)\n            work_completed.set()\n            raise RuntimeError(\"This error should not change status\")\n\n        return await ctx.experimental.run_task(work)\n\n    server = Server(\n        \"test-already-failed\",\n        on_list_tools=handle_list_tools,\n        on_call_tool=handle_call_tool,\n    )\n    server.experimental.enable_tasks()\n\n    async with Client(server) as client:\n        result = await client.session.experimental.call_tool_as_task(\"manual_cancel_task\", {})\n        task_id = result.task.task_id\n\n        with anyio.fail_after(5):\n            await work_completed.wait()\n\n        with anyio.fail_after(5):\n            while True:\n                status = await client.session.experimental.get_task(task_id)\n                if status.status == \"failed\":  # pragma: no branch\n                    break\n\n        assert status.status_message == \"Manually failed\"\n"
  },
  {
    "path": "tests/experimental/tasks/server/test_server.py",
    "content": "\"\"\"Tests for server-side task support (handlers, capabilities, integration).\"\"\"\n\nfrom datetime import datetime, timezone\nfrom typing import Any\n\nimport anyio\nimport pytest\n\nfrom mcp import Client\nfrom mcp.client.session import ClientSession\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.server.lowlevel import NotificationOptions\nfrom mcp.server.models import InitializationOptions\nfrom mcp.server.session import ServerSession\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.message import ServerMessageMetadata, SessionMessage\nfrom mcp.shared.response_router import ResponseRouter\nfrom mcp.shared.session import RequestResponder\nfrom mcp.types import (\n    INVALID_REQUEST,\n    TASK_FORBIDDEN,\n    TASK_OPTIONAL,\n    TASK_REQUIRED,\n    CallToolRequest,\n    CallToolRequestParams,\n    CallToolResult,\n    CancelTaskRequestParams,\n    CancelTaskResult,\n    ClientResult,\n    ErrorData,\n    GetTaskPayloadRequest,\n    GetTaskPayloadRequestParams,\n    GetTaskPayloadResult,\n    GetTaskRequestParams,\n    GetTaskResult,\n    JSONRPCError,\n    JSONRPCNotification,\n    JSONRPCResponse,\n    ListTasksResult,\n    ListToolsResult,\n    PaginatedRequestParams,\n    SamplingMessage,\n    ServerCapabilities,\n    ServerNotification,\n    ServerRequest,\n    Task,\n    TaskMetadata,\n    TextContent,\n    Tool,\n    ToolExecution,\n)\n\npytestmark = pytest.mark.anyio\n\n\nasync def test_list_tasks_handler() -> None:\n    \"\"\"Test that experimental list_tasks handler works via Client.\"\"\"\n    now = datetime.now(timezone.utc)\n    test_tasks = [\n        Task(task_id=\"task-1\", status=\"working\", created_at=now, last_updated_at=now, ttl=60000, poll_interval=1000),\n        Task(task_id=\"task-2\", status=\"completed\", created_at=now, last_updated_at=now, ttl=60000, poll_interval=1000),\n    ]\n\n    async def handle_list_tasks(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListTasksResult:\n        return ListTasksResult(tasks=test_tasks)\n\n    server = Server(\"test\")\n    server.experimental.enable_tasks(on_list_tasks=handle_list_tasks)\n\n    async with Client(server) as client:\n        result = await client.session.experimental.list_tasks()\n        assert len(result.tasks) == 2\n        assert result.tasks[0].task_id == \"task-1\"\n        assert result.tasks[1].task_id == \"task-2\"\n\n\nasync def test_get_task_handler() -> None:\n    \"\"\"Test that experimental get_task handler works via Client.\"\"\"\n\n    async def handle_get_task(ctx: ServerRequestContext, params: GetTaskRequestParams) -> GetTaskResult:\n        now = datetime.now(timezone.utc)\n        return GetTaskResult(\n            task_id=params.task_id,\n            status=\"working\",\n            created_at=now,\n            last_updated_at=now,\n            ttl=60000,\n            poll_interval=1000,\n        )\n\n    server = Server(\"test\")\n    server.experimental.enable_tasks(on_get_task=handle_get_task)\n\n    async with Client(server) as client:\n        result = await client.session.experimental.get_task(\"test-task-123\")\n        assert result.task_id == \"test-task-123\"\n        assert result.status == \"working\"\n\n\nasync def test_get_task_result_handler() -> None:\n    \"\"\"Test that experimental get_task_result handler works via Client.\"\"\"\n\n    async def handle_get_task_result(\n        ctx: ServerRequestContext, params: GetTaskPayloadRequestParams\n    ) -> GetTaskPayloadResult:\n        return GetTaskPayloadResult()\n\n    server = Server(\"test\")\n    server.experimental.enable_tasks(on_task_result=handle_get_task_result)\n\n    async with Client(server) as client:\n        result = await client.session.send_request(\n            GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=\"test-task-123\")),\n            GetTaskPayloadResult,\n        )\n        assert isinstance(result, GetTaskPayloadResult)\n\n\nasync def test_cancel_task_handler() -> None:\n    \"\"\"Test that experimental cancel_task handler works via Client.\"\"\"\n\n    async def handle_cancel_task(ctx: ServerRequestContext, params: CancelTaskRequestParams) -> CancelTaskResult:\n        now = datetime.now(timezone.utc)\n        return CancelTaskResult(\n            task_id=params.task_id,\n            status=\"cancelled\",\n            created_at=now,\n            last_updated_at=now,\n            ttl=60000,\n        )\n\n    server = Server(\"test\")\n    server.experimental.enable_tasks(on_cancel_task=handle_cancel_task)\n\n    async with Client(server) as client:\n        result = await client.session.experimental.cancel_task(\"test-task-123\")\n        assert result.task_id == \"test-task-123\"\n        assert result.status == \"cancelled\"\n\n\nasync def test_server_capabilities_include_tasks() -> None:\n    \"\"\"Test that server capabilities include tasks when handlers are registered.\"\"\"\n    server = Server(\"test\")\n\n    async def noop_list_tasks(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListTasksResult:\n        raise NotImplementedError\n\n    async def noop_cancel_task(ctx: ServerRequestContext, params: CancelTaskRequestParams) -> CancelTaskResult:\n        raise NotImplementedError\n\n    server.experimental.enable_tasks(on_list_tasks=noop_list_tasks, on_cancel_task=noop_cancel_task)\n\n    capabilities = server.get_capabilities(notification_options=NotificationOptions(), experimental_capabilities={})\n\n    assert capabilities.tasks is not None\n    assert capabilities.tasks.list is not None\n    assert capabilities.tasks.cancel is not None\n    assert capabilities.tasks.requests is not None\n    assert capabilities.tasks.requests.tools is not None\n\n\n@pytest.mark.skip(\n    reason=\"TODO(maxisbey): enable_tasks registers default handlers for all task methods, \"\n    \"so partial capabilities aren't possible yet. Low-level API should support \"\n    \"selectively enabling/disabling task capabilities.\"\n)\nasync def test_server_capabilities_partial_tasks() -> None:  # pragma: no cover\n    \"\"\"Test capabilities with only some task handlers registered.\"\"\"\n    server = Server(\"test\")\n\n    async def noop_list_tasks(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListTasksResult:\n        raise NotImplementedError\n\n    # Only list_tasks registered, not cancel_task\n    server.experimental.enable_tasks(on_list_tasks=noop_list_tasks)\n\n    capabilities = server.get_capabilities(notification_options=NotificationOptions(), experimental_capabilities={})\n\n    assert capabilities.tasks is not None\n    assert capabilities.tasks.list is not None\n    assert capabilities.tasks.cancel is None  # Not registered\n\n\nasync def test_tool_with_task_execution_metadata() -> None:\n    \"\"\"Test that tools can declare task execution mode.\"\"\"\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        return ListToolsResult(\n            tools=[\n                Tool(\n                    name=\"quick_tool\",\n                    description=\"Fast tool\",\n                    input_schema={\"type\": \"object\", \"properties\": {}},\n                    execution=ToolExecution(task_support=TASK_FORBIDDEN),\n                ),\n                Tool(\n                    name=\"long_tool\",\n                    description=\"Long running tool\",\n                    input_schema={\"type\": \"object\", \"properties\": {}},\n                    execution=ToolExecution(task_support=TASK_REQUIRED),\n                ),\n                Tool(\n                    name=\"flexible_tool\",\n                    description=\"Can be either\",\n                    input_schema={\"type\": \"object\", \"properties\": {}},\n                    execution=ToolExecution(task_support=TASK_OPTIONAL),\n                ),\n            ]\n        )\n\n    server = Server(\"test\", on_list_tools=handle_list_tools)\n\n    async with Client(server) as client:\n        result = await client.list_tools()\n        tools = result.tools\n\n        assert tools[0].execution is not None\n        assert tools[0].execution.task_support == TASK_FORBIDDEN\n        assert tools[1].execution is not None\n        assert tools[1].execution.task_support == TASK_REQUIRED\n        assert tools[2].execution is not None\n        assert tools[2].execution.task_support == TASK_OPTIONAL\n\n\nasync def test_task_metadata_in_call_tool_request() -> None:\n    \"\"\"Test that task metadata is accessible via ctx when calling a tool.\"\"\"\n    captured_task_metadata: TaskMetadata | None = None\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        raise NotImplementedError\n\n    async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:\n        nonlocal captured_task_metadata\n        captured_task_metadata = ctx.experimental.task_metadata\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"done\")])\n\n    server = Server(\"test\", on_list_tools=handle_list_tools, on_call_tool=handle_call_tool)\n\n    async with Client(server) as client:\n        # Call tool with task metadata\n        await client.session.send_request(\n            CallToolRequest(\n                params=CallToolRequestParams(\n                    name=\"long_task\",\n                    arguments={},\n                    task=TaskMetadata(ttl=60000),\n                ),\n            ),\n            CallToolResult,\n        )\n\n    assert captured_task_metadata is not None\n    assert captured_task_metadata.ttl == 60000\n\n\nasync def test_task_metadata_is_task_property() -> None:\n    \"\"\"Test that ctx.experimental.is_task works correctly.\"\"\"\n    is_task_values: list[bool] = []\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        raise NotImplementedError\n\n    async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:\n        is_task_values.append(ctx.experimental.is_task)\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"done\")])\n\n    server = Server(\"test\", on_list_tools=handle_list_tools, on_call_tool=handle_call_tool)\n\n    async with Client(server) as client:\n        # Call without task metadata\n        await client.session.send_request(\n            CallToolRequest(params=CallToolRequestParams(name=\"test_tool\", arguments={})),\n            CallToolResult,\n        )\n\n        # Call with task metadata\n        await client.session.send_request(\n            CallToolRequest(\n                params=CallToolRequestParams(name=\"test_tool\", arguments={}, task=TaskMetadata(ttl=60000)),\n            ),\n            CallToolResult,\n        )\n\n    assert len(is_task_values) == 2\n    assert is_task_values[0] is False  # First call without task\n    assert is_task_values[1] is True  # Second call with task\n\n\nasync def test_update_capabilities_no_handlers() -> None:\n    \"\"\"Test that update_capabilities returns early when no task handlers are registered.\"\"\"\n    server = Server(\"test-no-handlers\")\n    _ = server.experimental\n\n    caps = server.get_capabilities(NotificationOptions(), {})\n    assert caps.tasks is None\n\n\nasync def test_update_capabilities_partial_handlers() -> None:\n    \"\"\"Test that update_capabilities skips list/cancel when only tasks/get is registered.\"\"\"\n    server = Server(\"test-partial\")\n    # Access .experimental to create the ExperimentalHandlers instance\n    exp = server.experimental\n    # Second access returns the same cached instance\n    assert server.experimental is exp\n\n    async def noop_get(ctx: ServerRequestContext, params: GetTaskRequestParams) -> GetTaskResult:\n        raise NotImplementedError\n\n    server._add_request_handler(\"tasks/get\", noop_get)\n\n    caps = server.get_capabilities(NotificationOptions(), {})\n    assert caps.tasks is not None\n    assert caps.tasks.list is None\n    assert caps.tasks.cancel is None\n\n\nasync def test_default_task_handlers_via_enable_tasks() -> None:\n    \"\"\"Test that enable_tasks() auto-registers working default handlers.\"\"\"\n    server = Server(\"test-default-handlers\")\n    task_support = server.experimental.enable_tasks()\n    store = task_support.store\n\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)\n\n    async def message_handler(\n        message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception,\n    ) -> None: ...  # pragma: no branch\n\n    async def run_server() -> None:\n        async with task_support.run():\n            async with ServerSession(\n                client_to_server_receive,\n                server_to_client_send,\n                InitializationOptions(\n                    server_name=\"test-server\",\n                    server_version=\"1.0.0\",\n                    capabilities=server.get_capabilities(\n                        notification_options=NotificationOptions(),\n                        experimental_capabilities={},\n                    ),\n                ),\n            ) as server_session:\n                task_support.configure_session(server_session)\n                async for message in server_session.incoming_messages:\n                    await server._handle_message(message, server_session, {}, False)\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(run_server)\n\n        async with ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n            message_handler=message_handler,\n        ) as client_session:\n            await client_session.initialize()\n\n            # Create a task directly in the store for testing\n            task = await store.create_task(TaskMetadata(ttl=60000))\n\n            # Test list_tasks (default handler)\n            list_result = await client_session.experimental.list_tasks()\n            assert len(list_result.tasks) == 1\n            assert list_result.tasks[0].task_id == task.task_id\n\n            # Test get_task (default handler - found)\n            get_result = await client_session.experimental.get_task(task.task_id)\n            assert get_result.task_id == task.task_id\n            assert get_result.status == \"working\"\n\n            # Test get_task (default handler - not found path)\n            with pytest.raises(MCPError, match=\"not found\"):\n                await client_session.experimental.get_task(\"nonexistent-task\")\n\n            # Create a completed task to test get_task_result\n            completed_task = await store.create_task(TaskMetadata(ttl=60000))\n            await store.store_result(\n                completed_task.task_id, CallToolResult(content=[TextContent(type=\"text\", text=\"Test result\")])\n            )\n            await store.update_task(completed_task.task_id, status=\"completed\")\n\n            # Test get_task_result (default handler)\n            payload_result = await client_session.send_request(\n                GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=completed_task.task_id)),\n                GetTaskPayloadResult,\n            )\n            # The result should have the related-task metadata\n            assert payload_result.meta is not None\n            assert \"io.modelcontextprotocol/related-task\" in payload_result.meta\n\n            # Test cancel_task (default handler)\n            cancel_result = await client_session.experimental.cancel_task(task.task_id)\n            assert cancel_result.task_id == task.task_id\n            assert cancel_result.status == \"cancelled\"\n\n            tg.cancel_scope.cancel()\n\n\n@pytest.mark.anyio\nasync def test_build_elicit_form_request() -> None:\n    \"\"\"Test that _build_elicit_form_request builds a proper elicitation request.\"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)\n\n    try:\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(server_name=\"test-server\", server_version=\"1.0.0\", capabilities=ServerCapabilities()),\n        ) as server_session:\n            # Test without task_id\n            request = server_session._build_elicit_form_request(\n                message=\"Test message\",\n                requested_schema={\"type\": \"object\", \"properties\": {\"answer\": {\"type\": \"string\"}}},\n            )\n            assert request.method == \"elicitation/create\"\n            assert request.params is not None\n            assert request.params[\"message\"] == \"Test message\"\n\n            # Test with related_task_id (adds related-task metadata)\n            request_with_task = server_session._build_elicit_form_request(\n                message=\"Task message\",\n                requested_schema={\"type\": \"object\"},\n                related_task_id=\"test-task-123\",\n            )\n            assert request_with_task.method == \"elicitation/create\"\n            assert request_with_task.params is not None\n            assert \"_meta\" in request_with_task.params\n            assert \"io.modelcontextprotocol/related-task\" in request_with_task.params[\"_meta\"]\n            assert (\n                request_with_task.params[\"_meta\"][\"io.modelcontextprotocol/related-task\"][\"taskId\"] == \"test-task-123\"\n            )\n    finally:\n        await server_to_client_send.aclose()\n        await server_to_client_receive.aclose()\n        await client_to_server_send.aclose()\n        await client_to_server_receive.aclose()\n\n\n@pytest.mark.anyio\nasync def test_build_elicit_url_request() -> None:\n    \"\"\"Test that _build_elicit_url_request builds a proper URL mode elicitation request.\"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)\n\n    try:\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(server_name=\"test-server\", server_version=\"1.0.0\", capabilities=ServerCapabilities()),\n        ) as server_session:\n            # Test without related_task_id\n            request = server_session._build_elicit_url_request(\n                message=\"Please authorize with GitHub\",\n                url=\"https://github.com/login/oauth/authorize\",\n                elicitation_id=\"oauth-123\",\n            )\n            assert request.method == \"elicitation/create\"\n            assert request.params is not None\n            assert request.params[\"message\"] == \"Please authorize with GitHub\"\n            assert request.params[\"url\"] == \"https://github.com/login/oauth/authorize\"\n            assert request.params[\"elicitationId\"] == \"oauth-123\"\n            assert request.params[\"mode\"] == \"url\"\n\n            # Test with related_task_id (adds related-task metadata)\n            request_with_task = server_session._build_elicit_url_request(\n                message=\"OAuth required\",\n                url=\"https://example.com/oauth\",\n                elicitation_id=\"oauth-456\",\n                related_task_id=\"test-task-789\",\n            )\n            assert request_with_task.method == \"elicitation/create\"\n            assert request_with_task.params is not None\n            assert \"_meta\" in request_with_task.params\n            assert \"io.modelcontextprotocol/related-task\" in request_with_task.params[\"_meta\"]\n            assert (\n                request_with_task.params[\"_meta\"][\"io.modelcontextprotocol/related-task\"][\"taskId\"] == \"test-task-789\"\n            )\n    finally:\n        await server_to_client_send.aclose()\n        await server_to_client_receive.aclose()\n        await client_to_server_send.aclose()\n        await client_to_server_receive.aclose()\n\n\n@pytest.mark.anyio\nasync def test_build_create_message_request() -> None:\n    \"\"\"Test that _build_create_message_request builds a proper sampling request.\"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)\n\n    try:\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(\n                server_name=\"test-server\",\n                server_version=\"1.0.0\",\n                capabilities=ServerCapabilities(),\n            ),\n        ) as server_session:\n            messages = [\n                SamplingMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hello\")),\n            ]\n\n            # Test without task_id\n            request = server_session._build_create_message_request(\n                messages=messages,\n                max_tokens=100,\n                system_prompt=\"You are helpful\",\n            )\n            assert request.method == \"sampling/createMessage\"\n            assert request.params is not None\n            assert request.params[\"maxTokens\"] == 100\n\n            # Test with related_task_id (adds related-task metadata)\n            request_with_task = server_session._build_create_message_request(\n                messages=messages,\n                max_tokens=50,\n                related_task_id=\"sampling-task-456\",\n            )\n            assert request_with_task.method == \"sampling/createMessage\"\n            assert request_with_task.params is not None\n            assert \"_meta\" in request_with_task.params\n            assert \"io.modelcontextprotocol/related-task\" in request_with_task.params[\"_meta\"]\n            assert (\n                request_with_task.params[\"_meta\"][\"io.modelcontextprotocol/related-task\"][\"taskId\"]\n                == \"sampling-task-456\"\n            )\n    finally:\n        await server_to_client_send.aclose()\n        await server_to_client_receive.aclose()\n        await client_to_server_send.aclose()\n        await client_to_server_receive.aclose()\n\n\n@pytest.mark.anyio\nasync def test_send_message() -> None:\n    \"\"\"Test that send_message sends a raw session message.\"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)\n\n    try:\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(\n                server_name=\"test-server\",\n                server_version=\"1.0.0\",\n                capabilities=ServerCapabilities(),\n            ),\n        ) as server_session:\n            # Create a test message\n            notification = JSONRPCNotification(jsonrpc=\"2.0\", method=\"test/notification\")\n            message = SessionMessage(\n                message=notification,\n                metadata=ServerMessageMetadata(related_request_id=\"test-req-1\"),\n            )\n\n            # Send the message\n            await server_session.send_message(message)\n\n            # Verify it was sent to the stream\n            received = await server_to_client_receive.receive()\n            assert isinstance(received.message, JSONRPCNotification)\n            assert received.message.method == \"test/notification\"\n    finally:  # pragma: lax no cover\n        await server_to_client_send.aclose()\n        await server_to_client_receive.aclose()\n        await client_to_server_send.aclose()\n        await client_to_server_receive.aclose()\n\n\n@pytest.mark.anyio\nasync def test_response_routing_success() -> None:\n    \"\"\"Test that response routing works for success responses.\"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)\n\n    # Track routed responses with event for synchronization\n    routed_responses: list[dict[str, Any]] = []\n    response_received = anyio.Event()\n\n    class TestRouter(ResponseRouter):\n        def route_response(self, request_id: str | int, response: dict[str, Any]) -> bool:\n            routed_responses.append({\"id\": request_id, \"response\": response})\n            response_received.set()\n            return True  # Handled\n\n        def route_error(self, request_id: str | int, error: ErrorData) -> bool:\n            raise NotImplementedError\n\n    try:\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(\n                server_name=\"test-server\",\n                server_version=\"1.0.0\",\n                capabilities=ServerCapabilities(),\n            ),\n        ) as server_session:\n            router = TestRouter()\n            server_session.add_response_router(router)\n\n            # Simulate receiving a response from client\n            response = JSONRPCResponse(jsonrpc=\"2.0\", id=\"test-req-1\", result={\"status\": \"ok\"})\n            message = SessionMessage(message=response)\n\n            # Send from \"client\" side\n            await client_to_server_send.send(message)\n\n            # Wait for response to be routed\n            with anyio.fail_after(5):\n                await response_received.wait()\n\n            # Verify response was routed\n            assert len(routed_responses) == 1\n            assert routed_responses[0][\"id\"] == \"test-req-1\"\n            assert routed_responses[0][\"response\"][\"status\"] == \"ok\"\n    finally:  # pragma: lax no cover\n        await server_to_client_send.aclose()\n        await server_to_client_receive.aclose()\n        await client_to_server_send.aclose()\n        await client_to_server_receive.aclose()\n\n\n@pytest.mark.anyio\nasync def test_response_routing_error() -> None:\n    \"\"\"Test that error routing works for error responses.\"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)\n\n    # Track routed errors with event for synchronization\n    routed_errors: list[dict[str, Any]] = []\n    error_received = anyio.Event()\n\n    class TestRouter(ResponseRouter):\n        def route_response(self, request_id: str | int, response: dict[str, Any]) -> bool:\n            raise NotImplementedError\n\n        def route_error(self, request_id: str | int, error: ErrorData) -> bool:\n            routed_errors.append({\"id\": request_id, \"error\": error})\n            error_received.set()\n            return True  # Handled\n\n    try:\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(\n                server_name=\"test-server\",\n                server_version=\"1.0.0\",\n                capabilities=ServerCapabilities(),\n            ),\n        ) as server_session:\n            router = TestRouter()\n            server_session.add_response_router(router)\n\n            # Simulate receiving an error response from client\n            error_data = ErrorData(code=INVALID_REQUEST, message=\"Test error\")\n            error_response = JSONRPCError(jsonrpc=\"2.0\", id=\"test-req-2\", error=error_data)\n            message = SessionMessage(message=error_response)\n\n            # Send from \"client\" side\n            await client_to_server_send.send(message)\n\n            # Wait for error to be routed\n            with anyio.fail_after(5):\n                await error_received.wait()\n\n            # Verify error was routed\n            assert len(routed_errors) == 1\n            assert routed_errors[0][\"id\"] == \"test-req-2\"\n            assert routed_errors[0][\"error\"].message == \"Test error\"\n    finally:  # pragma: lax no cover\n        await server_to_client_send.aclose()\n        await server_to_client_receive.aclose()\n        await client_to_server_send.aclose()\n        await client_to_server_receive.aclose()\n\n\n@pytest.mark.anyio\nasync def test_response_routing_skips_non_matching_routers() -> None:\n    \"\"\"Test that routing continues to next router when first doesn't match.\"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)\n\n    # Track which routers were called\n    router_calls: list[str] = []\n    response_received = anyio.Event()\n\n    class NonMatchingRouter(ResponseRouter):\n        def route_response(self, request_id: str | int, response: dict[str, Any]) -> bool:\n            router_calls.append(\"non_matching_response\")\n            return False  # Doesn't handle it\n\n        def route_error(self, request_id: str | int, error: ErrorData) -> bool:\n            raise NotImplementedError\n\n    class MatchingRouter(ResponseRouter):\n        def route_response(self, request_id: str | int, response: dict[str, Any]) -> bool:\n            router_calls.append(\"matching_response\")\n            response_received.set()\n            return True  # Handles it\n\n        def route_error(self, request_id: str | int, error: ErrorData) -> bool:\n            raise NotImplementedError\n\n    try:\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(\n                server_name=\"test-server\",\n                server_version=\"1.0.0\",\n                capabilities=ServerCapabilities(),\n            ),\n        ) as server_session:\n            # Add non-matching router first, then matching router\n            server_session.add_response_router(NonMatchingRouter())\n            server_session.add_response_router(MatchingRouter())\n\n            # Send a response - should skip first router and be handled by second\n            response = JSONRPCResponse(jsonrpc=\"2.0\", id=\"test-req-1\", result={\"status\": \"ok\"})\n            message = SessionMessage(message=response)\n            await client_to_server_send.send(message)\n\n            with anyio.fail_after(5):\n                await response_received.wait()\n\n            # Verify both routers were called (first returned False, second returned True)\n            assert router_calls == [\"non_matching_response\", \"matching_response\"]\n    finally:  # pragma: lax no cover\n        await server_to_client_send.aclose()\n        await server_to_client_receive.aclose()\n        await client_to_server_send.aclose()\n        await client_to_server_receive.aclose()\n\n\n@pytest.mark.anyio\nasync def test_error_routing_skips_non_matching_routers() -> None:\n    \"\"\"Test that error routing continues to next router when first doesn't match.\"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)\n\n    # Track which routers were called\n    router_calls: list[str] = []\n    error_received = anyio.Event()\n\n    class NonMatchingRouter(ResponseRouter):\n        def route_response(self, request_id: str | int, response: dict[str, Any]) -> bool:\n            raise NotImplementedError\n\n        def route_error(self, request_id: str | int, error: ErrorData) -> bool:\n            router_calls.append(\"non_matching_error\")\n            return False  # Doesn't handle it\n\n    class MatchingRouter(ResponseRouter):\n        def route_response(self, request_id: str | int, response: dict[str, Any]) -> bool:\n            raise NotImplementedError\n\n        def route_error(self, request_id: str | int, error: ErrorData) -> bool:\n            router_calls.append(\"matching_error\")\n            error_received.set()\n            return True  # Handles it\n\n    try:\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(\n                server_name=\"test-server\",\n                server_version=\"1.0.0\",\n                capabilities=ServerCapabilities(),\n            ),\n        ) as server_session:\n            # Add non-matching router first, then matching router\n            server_session.add_response_router(NonMatchingRouter())\n            server_session.add_response_router(MatchingRouter())\n\n            # Send an error - should skip first router and be handled by second\n            error_data = ErrorData(code=INVALID_REQUEST, message=\"Test error\")\n            error_response = JSONRPCError(jsonrpc=\"2.0\", id=\"test-req-2\", error=error_data)\n            message = SessionMessage(message=error_response)\n            await client_to_server_send.send(message)\n\n            with anyio.fail_after(5):\n                await error_received.wait()\n\n            # Verify both routers were called (first returned False, second returned True)\n            assert router_calls == [\"non_matching_error\", \"matching_error\"]\n    finally:  # pragma: lax no cover\n        await server_to_client_send.aclose()\n        await server_to_client_receive.aclose()\n        await client_to_server_send.aclose()\n        await client_to_server_receive.aclose()\n"
  },
  {
    "path": "tests/experimental/tasks/server/test_server_task_context.py",
    "content": "\"\"\"Tests for ServerTaskContext.\"\"\"\n\nimport asyncio\nfrom unittest.mock import AsyncMock, Mock\n\nimport anyio\nimport pytest\n\nfrom mcp.server.experimental.task_context import ServerTaskContext\nfrom mcp.server.experimental.task_result_handler import TaskResultHandler\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore\nfrom mcp.shared.experimental.tasks.message_queue import InMemoryTaskMessageQueue\nfrom mcp.types import (\n    CallToolResult,\n    ClientCapabilities,\n    ClientTasksCapability,\n    ClientTasksRequestsCapability,\n    Implementation,\n    InitializeRequestParams,\n    JSONRPCRequest,\n    SamplingMessage,\n    TaskMetadata,\n    TasksCreateElicitationCapability,\n    TasksCreateMessageCapability,\n    TasksElicitationCapability,\n    TasksSamplingCapability,\n    TextContent,\n)\n\n\n@pytest.mark.anyio\nasync def test_server_task_context_properties() -> None:\n    \"\"\"Test ServerTaskContext property accessors.\"\"\"\n    store = InMemoryTaskStore()\n    mock_session = Mock()\n    queue = InMemoryTaskMessageQueue()\n    task = await store.create_task(TaskMetadata(ttl=60000), task_id=\"test-123\")\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n    )\n\n    assert ctx.task_id == \"test-123\"\n    assert ctx.task.task_id == \"test-123\"\n    assert ctx.is_cancelled is False\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_server_task_context_request_cancellation() -> None:\n    \"\"\"Test ServerTaskContext.request_cancellation().\"\"\"\n    store = InMemoryTaskStore()\n    mock_session = Mock()\n    queue = InMemoryTaskMessageQueue()\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n    )\n\n    assert ctx.is_cancelled is False\n    ctx.request_cancellation()\n    assert ctx.is_cancelled is True\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_server_task_context_update_status_with_notify() -> None:\n    \"\"\"Test update_status sends notification when notify=True.\"\"\"\n    store = InMemoryTaskStore()\n    mock_session = Mock()\n    mock_session.send_notification = AsyncMock()\n    queue = InMemoryTaskMessageQueue()\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n    )\n\n    await ctx.update_status(\"Working...\", notify=True)\n\n    mock_session.send_notification.assert_called_once()\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_server_task_context_update_status_without_notify() -> None:\n    \"\"\"Test update_status skips notification when notify=False.\"\"\"\n    store = InMemoryTaskStore()\n    mock_session = Mock()\n    mock_session.send_notification = AsyncMock()\n    queue = InMemoryTaskMessageQueue()\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n    )\n\n    await ctx.update_status(\"Working...\", notify=False)\n\n    mock_session.send_notification.assert_not_called()\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_server_task_context_complete_with_notify() -> None:\n    \"\"\"Test complete sends notification when notify=True.\"\"\"\n    store = InMemoryTaskStore()\n    mock_session = Mock()\n    mock_session.send_notification = AsyncMock()\n    queue = InMemoryTaskMessageQueue()\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n    )\n\n    result = CallToolResult(content=[TextContent(type=\"text\", text=\"Done\")])\n    await ctx.complete(result, notify=True)\n\n    mock_session.send_notification.assert_called_once()\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_server_task_context_fail_with_notify() -> None:\n    \"\"\"Test fail sends notification when notify=True.\"\"\"\n    store = InMemoryTaskStore()\n    mock_session = Mock()\n    mock_session.send_notification = AsyncMock()\n    queue = InMemoryTaskMessageQueue()\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n    )\n\n    await ctx.fail(\"Something went wrong\", notify=True)\n\n    mock_session.send_notification.assert_called_once()\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_elicit_raises_when_client_lacks_capability() -> None:\n    \"\"\"Test that elicit() raises MCPError when client doesn't support elicitation.\"\"\"\n    store = InMemoryTaskStore()\n    mock_session = Mock()\n    mock_session.check_client_capability = Mock(return_value=False)\n    queue = InMemoryTaskMessageQueue()\n    handler = TaskResultHandler(store, queue)\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n        handler=handler,\n    )\n\n    with pytest.raises(MCPError) as exc_info:\n        await ctx.elicit(message=\"Test?\", requested_schema={\"type\": \"object\"})\n\n    assert \"elicitation capability\" in exc_info.value.error.message\n    mock_session.check_client_capability.assert_called_once()\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_create_message_raises_when_client_lacks_capability() -> None:\n    \"\"\"Test that create_message() raises MCPError when client doesn't support sampling.\"\"\"\n    store = InMemoryTaskStore()\n    mock_session = Mock()\n    mock_session.check_client_capability = Mock(return_value=False)\n    queue = InMemoryTaskMessageQueue()\n    handler = TaskResultHandler(store, queue)\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n        handler=handler,\n    )\n\n    with pytest.raises(MCPError) as exc_info:\n        await ctx.create_message(messages=[], max_tokens=100)\n\n    assert \"sampling capability\" in exc_info.value.error.message\n    mock_session.check_client_capability.assert_called_once()\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_elicit_raises_without_handler() -> None:\n    \"\"\"Test that elicit() raises when handler is not provided.\"\"\"\n    store = InMemoryTaskStore()\n    mock_session = Mock()\n    mock_session.check_client_capability = Mock(return_value=True)\n    queue = InMemoryTaskMessageQueue()\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n        handler=None,\n    )\n\n    with pytest.raises(RuntimeError, match=\"handler is required\"):\n        await ctx.elicit(message=\"Test?\", requested_schema={\"type\": \"object\"})\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_elicit_url_raises_without_handler() -> None:\n    \"\"\"Test that elicit_url() raises when handler is not provided.\"\"\"\n    store = InMemoryTaskStore()\n    mock_session = Mock()\n    mock_session.check_client_capability = Mock(return_value=True)\n    queue = InMemoryTaskMessageQueue()\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n        handler=None,\n    )\n\n    with pytest.raises(RuntimeError, match=\"handler is required for elicit_url\"):\n        await ctx.elicit_url(\n            message=\"Please authorize\",\n            url=\"https://example.com/oauth\",\n            elicitation_id=\"oauth-123\",\n        )\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_create_message_raises_without_handler() -> None:\n    \"\"\"Test that create_message() raises when handler is not provided.\"\"\"\n    store = InMemoryTaskStore()\n    mock_session = Mock()\n    mock_session.check_client_capability = Mock(return_value=True)\n    queue = InMemoryTaskMessageQueue()\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n        handler=None,\n    )\n\n    with pytest.raises(RuntimeError, match=\"handler is required\"):\n        await ctx.create_message(messages=[], max_tokens=100)\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_elicit_queues_request_and_waits_for_response() -> None:\n    \"\"\"Test that elicit() queues request and waits for response.\"\"\"\n    store = InMemoryTaskStore()\n    queue = InMemoryTaskMessageQueue()\n    handler = TaskResultHandler(store, queue)\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    mock_session = Mock()\n    mock_session.check_client_capability = Mock(return_value=True)\n    mock_session._build_elicit_form_request = Mock(\n        return_value=JSONRPCRequest(\n            jsonrpc=\"2.0\",\n            id=\"test-req-1\",\n            method=\"elicitation/create\",\n            params={\"message\": \"Test?\", \"_meta\": {}},\n        )\n    )\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n        handler=handler,\n    )\n\n    elicit_result = None\n\n    async def run_elicit() -> None:\n        nonlocal elicit_result\n        elicit_result = await ctx.elicit(\n            message=\"Test?\",\n            requested_schema={\"type\": \"object\"},\n        )\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(run_elicit)\n\n        # Wait for request to be queued\n        await queue.wait_for_message(task.task_id)\n\n        # Verify task is in input_required status\n        updated_task = await store.get_task(task.task_id)\n        assert updated_task is not None\n        assert updated_task.status == \"input_required\"\n\n        # Dequeue and simulate response\n        msg = await queue.dequeue(task.task_id)\n        assert msg is not None\n        assert msg.resolver is not None\n\n        # Resolve with mock elicitation response\n        msg.resolver.set_result({\"action\": \"accept\", \"content\": {\"name\": \"Alice\"}})\n\n    # Verify result\n    assert elicit_result is not None\n    assert elicit_result.action == \"accept\"\n    assert elicit_result.content == {\"name\": \"Alice\"}\n\n    # Verify task is back to working\n    final_task = await store.get_task(task.task_id)\n    assert final_task is not None\n    assert final_task.status == \"working\"\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_elicit_url_queues_request_and_waits_for_response() -> None:\n    \"\"\"Test that elicit_url() queues request and waits for response.\"\"\"\n    store = InMemoryTaskStore()\n    queue = InMemoryTaskMessageQueue()\n    handler = TaskResultHandler(store, queue)\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    mock_session = Mock()\n    mock_session.check_client_capability = Mock(return_value=True)\n    mock_session._build_elicit_url_request = Mock(\n        return_value=JSONRPCRequest(\n            jsonrpc=\"2.0\",\n            id=\"test-url-req-1\",\n            method=\"elicitation/create\",\n            params={\"message\": \"Authorize\", \"url\": \"https://example.com\", \"elicitationId\": \"123\", \"mode\": \"url\"},\n        )\n    )\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n        handler=handler,\n    )\n\n    elicit_result = None\n\n    async def run_elicit_url() -> None:\n        nonlocal elicit_result\n        elicit_result = await ctx.elicit_url(\n            message=\"Authorize\",\n            url=\"https://example.com/oauth\",\n            elicitation_id=\"oauth-123\",\n        )\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(run_elicit_url)\n\n        # Wait for request to be queued\n        await queue.wait_for_message(task.task_id)\n\n        # Verify task is in input_required status\n        updated_task = await store.get_task(task.task_id)\n        assert updated_task is not None\n        assert updated_task.status == \"input_required\"\n\n        # Dequeue and simulate response\n        msg = await queue.dequeue(task.task_id)\n        assert msg is not None\n        assert msg.resolver is not None\n\n        # Resolve with mock elicitation response (URL mode just returns action)\n        msg.resolver.set_result({\"action\": \"accept\"})\n\n    # Verify result\n    assert elicit_result is not None\n    assert elicit_result.action == \"accept\"\n\n    # Verify task is back to working\n    final_task = await store.get_task(task.task_id)\n    assert final_task is not None\n    assert final_task.status == \"working\"\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_create_message_queues_request_and_waits_for_response() -> None:\n    \"\"\"Test that create_message() queues request and waits for response.\"\"\"\n    store = InMemoryTaskStore()\n    queue = InMemoryTaskMessageQueue()\n    handler = TaskResultHandler(store, queue)\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    mock_session = Mock()\n    mock_session.check_client_capability = Mock(return_value=True)\n    mock_session._build_create_message_request = Mock(\n        return_value=JSONRPCRequest(\n            jsonrpc=\"2.0\",\n            id=\"test-req-2\",\n            method=\"sampling/createMessage\",\n            params={\"messages\": [], \"maxTokens\": 100, \"_meta\": {}},\n        )\n    )\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n        handler=handler,\n    )\n\n    sampling_result = None\n\n    async def run_sampling() -> None:\n        nonlocal sampling_result\n        sampling_result = await ctx.create_message(\n            messages=[SamplingMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hello\"))],\n            max_tokens=100,\n        )\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(run_sampling)\n\n        # Wait for request to be queued\n        await queue.wait_for_message(task.task_id)\n\n        # Verify task is in input_required status\n        updated_task = await store.get_task(task.task_id)\n        assert updated_task is not None\n        assert updated_task.status == \"input_required\"\n\n        # Dequeue and simulate response\n        msg = await queue.dequeue(task.task_id)\n        assert msg is not None\n        assert msg.resolver is not None\n\n        # Resolve with mock sampling response\n        msg.resolver.set_result(\n            {\n                \"role\": \"assistant\",\n                \"content\": {\"type\": \"text\", \"text\": \"Hello back!\"},\n                \"model\": \"test-model\",\n                \"stopReason\": \"endTurn\",\n            }\n        )\n\n    # Verify result\n    assert sampling_result is not None\n    assert sampling_result.role == \"assistant\"\n    assert sampling_result.model == \"test-model\"\n\n    # Verify task is back to working\n    final_task = await store.get_task(task.task_id)\n    assert final_task is not None\n    assert final_task.status == \"working\"\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_elicit_restores_status_on_cancellation() -> None:\n    \"\"\"Test that elicit() restores task status to working when cancelled.\"\"\"\n    store = InMemoryTaskStore()\n    queue = InMemoryTaskMessageQueue()\n    handler = TaskResultHandler(store, queue)\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    mock_session = Mock()\n    mock_session.check_client_capability = Mock(return_value=True)\n    mock_session._build_elicit_form_request = Mock(\n        return_value=JSONRPCRequest(\n            jsonrpc=\"2.0\",\n            id=\"test-req-cancel\",\n            method=\"elicitation/create\",\n            params={\"message\": \"Test?\", \"_meta\": {}},\n        )\n    )\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n        handler=handler,\n    )\n\n    cancelled_error_raised = False\n\n    async with anyio.create_task_group() as tg:\n\n        async def do_elicit() -> None:\n            nonlocal cancelled_error_raised\n            try:\n                await ctx.elicit(\n                    message=\"Test?\",\n                    requested_schema={\"type\": \"object\"},\n                )\n            except anyio.get_cancelled_exc_class():\n                cancelled_error_raised = True\n                # Don't re-raise - let the test continue\n\n        tg.start_soon(do_elicit)\n\n        # Wait for request to be queued\n        await queue.wait_for_message(task.task_id)\n\n        # Verify task is in input_required status\n        updated_task = await store.get_task(task.task_id)\n        assert updated_task is not None\n        assert updated_task.status == \"input_required\"\n\n        # Get the queued message and set cancellation exception on its resolver\n        msg = await queue.dequeue(task.task_id)\n        assert msg is not None\n        assert msg.resolver is not None\n\n        # Trigger cancellation by setting exception (use asyncio.CancelledError directly)\n        msg.resolver.set_exception(asyncio.CancelledError())\n\n    # Verify task is back to working after cancellation\n    final_task = await store.get_task(task.task_id)\n    assert final_task is not None\n    assert final_task.status == \"working\"\n    assert cancelled_error_raised\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_create_message_restores_status_on_cancellation() -> None:\n    \"\"\"Test that create_message() restores task status to working when cancelled.\"\"\"\n    store = InMemoryTaskStore()\n    queue = InMemoryTaskMessageQueue()\n    handler = TaskResultHandler(store, queue)\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    mock_session = Mock()\n    mock_session.check_client_capability = Mock(return_value=True)\n    mock_session._build_create_message_request = Mock(\n        return_value=JSONRPCRequest(\n            jsonrpc=\"2.0\",\n            id=\"test-req-cancel-2\",\n            method=\"sampling/createMessage\",\n            params={\"messages\": [], \"maxTokens\": 100, \"_meta\": {}},\n        )\n    )\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n        handler=handler,\n    )\n\n    cancelled_error_raised = False\n\n    async with anyio.create_task_group() as tg:\n\n        async def do_sampling() -> None:\n            nonlocal cancelled_error_raised\n            try:\n                await ctx.create_message(\n                    messages=[SamplingMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hello\"))],\n                    max_tokens=100,\n                )\n            except anyio.get_cancelled_exc_class():\n                cancelled_error_raised = True\n                # Don't re-raise\n\n        tg.start_soon(do_sampling)\n\n        # Wait for request to be queued\n        await queue.wait_for_message(task.task_id)\n\n        # Verify task is in input_required status\n        updated_task = await store.get_task(task.task_id)\n        assert updated_task is not None\n        assert updated_task.status == \"input_required\"\n\n        # Get the queued message and set cancellation exception on its resolver\n        msg = await queue.dequeue(task.task_id)\n        assert msg is not None\n        assert msg.resolver is not None\n\n        # Trigger cancellation by setting exception (use asyncio.CancelledError directly)\n        msg.resolver.set_exception(asyncio.CancelledError())\n\n    # Verify task is back to working after cancellation\n    final_task = await store.get_task(task.task_id)\n    assert final_task is not None\n    assert final_task.status == \"working\"\n    assert cancelled_error_raised\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_elicit_as_task_raises_without_handler() -> None:\n    \"\"\"Test that elicit_as_task() raises when handler is not provided.\"\"\"\n    store = InMemoryTaskStore()\n    queue = InMemoryTaskMessageQueue()\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    # Create mock session with proper client capabilities\n    mock_session = Mock()\n    mock_session.client_params = InitializeRequestParams(\n        protocol_version=\"2025-01-01\",\n        capabilities=ClientCapabilities(\n            tasks=ClientTasksCapability(\n                requests=ClientTasksRequestsCapability(\n                    elicitation=TasksElicitationCapability(create=TasksCreateElicitationCapability())\n                )\n            )\n        ),\n        client_info=Implementation(name=\"test\", version=\"1.0\"),\n    )\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n        handler=None,\n    )\n\n    with pytest.raises(RuntimeError, match=\"handler is required for elicit_as_task\"):\n        await ctx.elicit_as_task(message=\"Test?\", requested_schema={\"type\": \"object\"})\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_create_message_as_task_raises_without_handler() -> None:\n    \"\"\"Test that create_message_as_task() raises when handler is not provided.\"\"\"\n    store = InMemoryTaskStore()\n    queue = InMemoryTaskMessageQueue()\n    task = await store.create_task(TaskMetadata(ttl=60000))\n\n    # Create mock session with proper client capabilities\n    mock_session = Mock()\n    mock_session.client_params = InitializeRequestParams(\n        protocol_version=\"2025-01-01\",\n        capabilities=ClientCapabilities(\n            tasks=ClientTasksCapability(\n                requests=ClientTasksRequestsCapability(\n                    sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability())\n                )\n            )\n        ),\n        client_info=Implementation(name=\"test\", version=\"1.0\"),\n    )\n\n    ctx = ServerTaskContext(\n        task=task,\n        store=store,\n        session=mock_session,\n        queue=queue,\n        handler=None,\n    )\n\n    with pytest.raises(RuntimeError, match=\"handler is required for create_message_as_task\"):\n        await ctx.create_message_as_task(\n            messages=[SamplingMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hello\"))],\n            max_tokens=100,\n        )\n\n    store.cleanup()\n"
  },
  {
    "path": "tests/experimental/tasks/server/test_store.py",
    "content": "\"\"\"Tests for InMemoryTaskStore.\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom datetime import datetime, timedelta, timezone\n\nimport pytest\n\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.experimental.tasks.helpers import cancel_task\nfrom mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore\nfrom mcp.types import INVALID_PARAMS, CallToolResult, TaskMetadata, TextContent\n\n\n@pytest.fixture\nasync def store() -> AsyncIterator[InMemoryTaskStore]:\n    \"\"\"Provide a clean InMemoryTaskStore for each test with automatic cleanup.\"\"\"\n    store = InMemoryTaskStore()\n    yield store\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_create_and_get(store: InMemoryTaskStore) -> None:\n    \"\"\"Test InMemoryTaskStore create and get operations.\"\"\"\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n\n    assert task.task_id is not None\n    assert task.status == \"working\"\n    assert task.ttl == 60000\n\n    retrieved = await store.get_task(task.task_id)\n    assert retrieved is not None\n    assert retrieved.task_id == task.task_id\n    assert retrieved.status == \"working\"\n\n\n@pytest.mark.anyio\nasync def test_create_with_custom_id(store: InMemoryTaskStore) -> None:\n    \"\"\"Test InMemoryTaskStore create with custom task ID.\"\"\"\n    task = await store.create_task(\n        metadata=TaskMetadata(ttl=60000),\n        task_id=\"my-custom-id\",\n    )\n\n    assert task.task_id == \"my-custom-id\"\n    assert task.status == \"working\"\n\n    retrieved = await store.get_task(\"my-custom-id\")\n    assert retrieved is not None\n    assert retrieved.task_id == \"my-custom-id\"\n\n\n@pytest.mark.anyio\nasync def test_create_duplicate_id_raises(store: InMemoryTaskStore) -> None:\n    \"\"\"Test that creating a task with duplicate ID raises.\"\"\"\n    await store.create_task(metadata=TaskMetadata(ttl=60000), task_id=\"duplicate\")\n\n    with pytest.raises(ValueError, match=\"already exists\"):\n        await store.create_task(metadata=TaskMetadata(ttl=60000), task_id=\"duplicate\")\n\n\n@pytest.mark.anyio\nasync def test_get_nonexistent_returns_none(store: InMemoryTaskStore) -> None:\n    \"\"\"Test that getting a nonexistent task returns None.\"\"\"\n    retrieved = await store.get_task(\"nonexistent\")\n    assert retrieved is None\n\n\n@pytest.mark.anyio\nasync def test_update_status(store: InMemoryTaskStore) -> None:\n    \"\"\"Test InMemoryTaskStore status updates.\"\"\"\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n\n    updated = await store.update_task(task.task_id, status=\"completed\", status_message=\"All done!\")\n\n    assert updated.status == \"completed\"\n    assert updated.status_message == \"All done!\"\n\n    retrieved = await store.get_task(task.task_id)\n    assert retrieved is not None\n    assert retrieved.status == \"completed\"\n    assert retrieved.status_message == \"All done!\"\n\n\n@pytest.mark.anyio\nasync def test_update_nonexistent_raises(store: InMemoryTaskStore) -> None:\n    \"\"\"Test that updating a nonexistent task raises.\"\"\"\n    with pytest.raises(ValueError, match=\"not found\"):\n        await store.update_task(\"nonexistent\", status=\"completed\")\n\n\n@pytest.mark.anyio\nasync def test_store_and_get_result(store: InMemoryTaskStore) -> None:\n    \"\"\"Test InMemoryTaskStore result storage and retrieval.\"\"\"\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n\n    # Store result\n    result = CallToolResult(content=[TextContent(type=\"text\", text=\"Result data\")])\n    await store.store_result(task.task_id, result)\n\n    # Retrieve result\n    retrieved_result = await store.get_result(task.task_id)\n    assert retrieved_result == result\n\n\n@pytest.mark.anyio\nasync def test_get_result_nonexistent_returns_none(store: InMemoryTaskStore) -> None:\n    \"\"\"Test that getting result for nonexistent task returns None.\"\"\"\n    result = await store.get_result(\"nonexistent\")\n    assert result is None\n\n\n@pytest.mark.anyio\nasync def test_get_result_no_result_returns_none(store: InMemoryTaskStore) -> None:\n    \"\"\"Test that getting result when none stored returns None.\"\"\"\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n    result = await store.get_result(task.task_id)\n    assert result is None\n\n\n@pytest.mark.anyio\nasync def test_list_tasks(store: InMemoryTaskStore) -> None:\n    \"\"\"Test InMemoryTaskStore list operation.\"\"\"\n    # Create multiple tasks\n    for _ in range(3):\n        await store.create_task(metadata=TaskMetadata(ttl=60000))\n\n    tasks, next_cursor = await store.list_tasks()\n    assert len(tasks) == 3\n    assert next_cursor is None  # Less than page size\n\n\n@pytest.mark.anyio\nasync def test_list_tasks_pagination() -> None:\n    \"\"\"Test InMemoryTaskStore pagination.\"\"\"\n    # Needs custom page_size, can't use fixture\n    store = InMemoryTaskStore(page_size=2)\n\n    # Create 5 tasks\n    for _ in range(5):\n        await store.create_task(metadata=TaskMetadata(ttl=60000))\n\n    # First page\n    tasks, next_cursor = await store.list_tasks()\n    assert len(tasks) == 2\n    assert next_cursor is not None\n\n    # Second page\n    tasks, next_cursor = await store.list_tasks(cursor=next_cursor)\n    assert len(tasks) == 2\n    assert next_cursor is not None\n\n    # Third page (last)\n    tasks, next_cursor = await store.list_tasks(cursor=next_cursor)\n    assert len(tasks) == 1\n    assert next_cursor is None\n\n    store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_list_tasks_invalid_cursor(store: InMemoryTaskStore) -> None:\n    \"\"\"Test that invalid cursor raises.\"\"\"\n    await store.create_task(metadata=TaskMetadata(ttl=60000))\n\n    with pytest.raises(ValueError, match=\"Invalid cursor\"):\n        await store.list_tasks(cursor=\"invalid-cursor\")\n\n\n@pytest.mark.anyio\nasync def test_delete_task(store: InMemoryTaskStore) -> None:\n    \"\"\"Test InMemoryTaskStore delete operation.\"\"\"\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n\n    deleted = await store.delete_task(task.task_id)\n    assert deleted is True\n\n    retrieved = await store.get_task(task.task_id)\n    assert retrieved is None\n\n    # Delete non-existent\n    deleted = await store.delete_task(task.task_id)\n    assert deleted is False\n\n\n@pytest.mark.anyio\nasync def test_get_all_tasks_helper(store: InMemoryTaskStore) -> None:\n    \"\"\"Test the get_all_tasks debugging helper.\"\"\"\n    await store.create_task(metadata=TaskMetadata(ttl=60000))\n    await store.create_task(metadata=TaskMetadata(ttl=60000))\n\n    all_tasks = store.get_all_tasks()\n    assert len(all_tasks) == 2\n\n\n@pytest.mark.anyio\nasync def test_store_result_nonexistent_raises(store: InMemoryTaskStore) -> None:\n    \"\"\"Test that storing result for nonexistent task raises ValueError.\"\"\"\n    result = CallToolResult(content=[TextContent(type=\"text\", text=\"Result\")])\n\n    with pytest.raises(ValueError, match=\"not found\"):\n        await store.store_result(\"nonexistent-id\", result)\n\n\n@pytest.mark.anyio\nasync def test_create_task_with_null_ttl(store: InMemoryTaskStore) -> None:\n    \"\"\"Test creating task with null TTL (never expires).\"\"\"\n    task = await store.create_task(metadata=TaskMetadata(ttl=None))\n\n    assert task.ttl is None\n\n    # Task should persist (not expire)\n    retrieved = await store.get_task(task.task_id)\n    assert retrieved is not None\n\n\n@pytest.mark.anyio\nasync def test_task_expiration_cleanup(store: InMemoryTaskStore) -> None:\n    \"\"\"Test that expired tasks are cleaned up lazily.\"\"\"\n    # Create a task with very short TTL\n    task = await store.create_task(metadata=TaskMetadata(ttl=1))  # 1ms TTL\n\n    # Manually force the expiry to be in the past\n    stored = store._tasks.get(task.task_id)\n    assert stored is not None\n    stored.expires_at = datetime.now(timezone.utc) - timedelta(seconds=10)\n\n    # Task should still exist in internal dict but be expired\n    assert task.task_id in store._tasks\n\n    # Any access operation should clean up expired tasks\n    # list_tasks triggers cleanup\n    tasks, _ = await store.list_tasks()\n\n    # Expired task should be cleaned up\n    assert task.task_id not in store._tasks\n    assert len(tasks) == 0\n\n\n@pytest.mark.anyio\nasync def test_task_with_null_ttl_never_expires(store: InMemoryTaskStore) -> None:\n    \"\"\"Test that tasks with null TTL never expire during cleanup.\"\"\"\n    # Create task with null TTL\n    task = await store.create_task(metadata=TaskMetadata(ttl=None))\n\n    # Verify internal storage has no expiry\n    stored = store._tasks.get(task.task_id)\n    assert stored is not None\n    assert stored.expires_at is None\n\n    # Access operations should NOT remove this task\n    await store.list_tasks()\n    await store.get_task(task.task_id)\n\n    # Task should still exist\n    assert task.task_id in store._tasks\n    retrieved = await store.get_task(task.task_id)\n    assert retrieved is not None\n\n\n@pytest.mark.anyio\nasync def test_terminal_task_ttl_reset(store: InMemoryTaskStore) -> None:\n    \"\"\"Test that TTL is reset when task enters terminal state.\"\"\"\n    # Create task with short TTL\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))  # 60s\n\n    # Get the initial expiry\n    stored = store._tasks.get(task.task_id)\n    assert stored is not None\n    initial_expiry = stored.expires_at\n    assert initial_expiry is not None\n\n    # Update to terminal state (completed)\n    await store.update_task(task.task_id, status=\"completed\")\n\n    # Expiry should be reset to a new time (from now + TTL)\n    new_expiry = stored.expires_at\n    assert new_expiry is not None\n    assert new_expiry >= initial_expiry\n\n\n@pytest.mark.anyio\nasync def test_terminal_status_transition_rejected(store: InMemoryTaskStore) -> None:\n    \"\"\"Test that transitions from terminal states are rejected.\n\n    Per spec: Terminal states (completed, failed, cancelled) MUST NOT\n    transition to any other status.\n    \"\"\"\n    # Test each terminal status\n    for terminal_status in (\"completed\", \"failed\", \"cancelled\"):\n        task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n\n        # Move to terminal state\n        await store.update_task(task.task_id, status=terminal_status)\n\n        # Attempting to transition to any other status should raise\n        with pytest.raises(ValueError, match=\"Cannot transition from terminal status\"):\n            await store.update_task(task.task_id, status=\"working\")\n\n        # Also test transitioning to another terminal state\n        other_terminal = \"failed\" if terminal_status != \"failed\" else \"completed\"\n        with pytest.raises(ValueError, match=\"Cannot transition from terminal status\"):\n            await store.update_task(task.task_id, status=other_terminal)\n\n\n@pytest.mark.anyio\nasync def test_terminal_status_allows_same_status(store: InMemoryTaskStore) -> None:\n    \"\"\"Test that setting the same terminal status doesn't raise.\n\n    This is not a transition, so it should be allowed (no-op).\n    \"\"\"\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n    await store.update_task(task.task_id, status=\"completed\")\n\n    # Setting the same status should not raise\n    updated = await store.update_task(task.task_id, status=\"completed\")\n    assert updated.status == \"completed\"\n\n    # Updating just the message should also work\n    updated = await store.update_task(task.task_id, status_message=\"Updated message\")\n    assert updated.status_message == \"Updated message\"\n\n\n@pytest.mark.anyio\nasync def test_wait_for_update_nonexistent_raises(store: InMemoryTaskStore) -> None:\n    \"\"\"Test that wait_for_update raises for nonexistent task.\"\"\"\n    with pytest.raises(ValueError, match=\"not found\"):\n        await store.wait_for_update(\"nonexistent-task-id\")\n\n\n@pytest.mark.anyio\nasync def test_cancel_task_succeeds_for_working_task(store: InMemoryTaskStore) -> None:\n    \"\"\"Test cancel_task helper succeeds for a working task.\"\"\"\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n    assert task.status == \"working\"\n\n    result = await cancel_task(store, task.task_id)\n\n    assert result.task_id == task.task_id\n    assert result.status == \"cancelled\"\n\n    # Verify store is updated\n    retrieved = await store.get_task(task.task_id)\n    assert retrieved is not None\n    assert retrieved.status == \"cancelled\"\n\n\n@pytest.mark.anyio\nasync def test_cancel_task_rejects_nonexistent_task(store: InMemoryTaskStore) -> None:\n    \"\"\"Test cancel_task raises MCPError with INVALID_PARAMS for nonexistent task.\"\"\"\n    with pytest.raises(MCPError) as exc_info:\n        await cancel_task(store, \"nonexistent-task-id\")\n\n    assert exc_info.value.error.code == INVALID_PARAMS\n    assert \"not found\" in exc_info.value.error.message\n\n\n@pytest.mark.anyio\nasync def test_cancel_task_rejects_completed_task(store: InMemoryTaskStore) -> None:\n    \"\"\"Test cancel_task raises MCPError with INVALID_PARAMS for completed task.\"\"\"\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n    await store.update_task(task.task_id, status=\"completed\")\n\n    with pytest.raises(MCPError) as exc_info:\n        await cancel_task(store, task.task_id)\n\n    assert exc_info.value.error.code == INVALID_PARAMS\n    assert \"terminal state 'completed'\" in exc_info.value.error.message\n\n\n@pytest.mark.anyio\nasync def test_cancel_task_rejects_failed_task(store: InMemoryTaskStore) -> None:\n    \"\"\"Test cancel_task raises MCPError with INVALID_PARAMS for failed task.\"\"\"\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n    await store.update_task(task.task_id, status=\"failed\")\n\n    with pytest.raises(MCPError) as exc_info:\n        await cancel_task(store, task.task_id)\n\n    assert exc_info.value.error.code == INVALID_PARAMS\n    assert \"terminal state 'failed'\" in exc_info.value.error.message\n\n\n@pytest.mark.anyio\nasync def test_cancel_task_rejects_already_cancelled_task(store: InMemoryTaskStore) -> None:\n    \"\"\"Test cancel_task raises MCPError with INVALID_PARAMS for already cancelled task.\"\"\"\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n    await store.update_task(task.task_id, status=\"cancelled\")\n\n    with pytest.raises(MCPError) as exc_info:\n        await cancel_task(store, task.task_id)\n\n    assert exc_info.value.error.code == INVALID_PARAMS\n    assert \"terminal state 'cancelled'\" in exc_info.value.error.message\n\n\n@pytest.mark.anyio\nasync def test_cancel_task_succeeds_for_input_required_task(store: InMemoryTaskStore) -> None:\n    \"\"\"Test cancel_task helper succeeds for a task in input_required status.\"\"\"\n    task = await store.create_task(metadata=TaskMetadata(ttl=60000))\n    await store.update_task(task.task_id, status=\"input_required\")\n\n    result = await cancel_task(store, task.task_id)\n\n    assert result.task_id == task.task_id\n    assert result.status == \"cancelled\"\n"
  },
  {
    "path": "tests/experimental/tasks/server/test_task_result_handler.py",
    "content": "\"\"\"Tests for TaskResultHandler.\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom typing import Any\nfrom unittest.mock import AsyncMock, Mock\n\nimport anyio\nimport pytest\n\nfrom mcp.server.experimental.task_result_handler import TaskResultHandler\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore\nfrom mcp.shared.experimental.tasks.message_queue import InMemoryTaskMessageQueue, QueuedMessage\nfrom mcp.shared.experimental.tasks.resolver import Resolver\nfrom mcp.shared.message import SessionMessage\nfrom mcp.types import (\n    INVALID_REQUEST,\n    CallToolResult,\n    ErrorData,\n    GetTaskPayloadRequest,\n    GetTaskPayloadRequestParams,\n    GetTaskPayloadResult,\n    JSONRPCRequest,\n    TaskMetadata,\n    TextContent,\n)\n\n\n@pytest.fixture\nasync def store() -> AsyncIterator[InMemoryTaskStore]:\n    \"\"\"Provide a clean store for each test.\"\"\"\n    s = InMemoryTaskStore()\n    yield s\n    s.cleanup()\n\n\n@pytest.fixture\ndef queue() -> InMemoryTaskMessageQueue:\n    \"\"\"Provide a clean queue for each test.\"\"\"\n    return InMemoryTaskMessageQueue()\n\n\n@pytest.fixture\ndef handler(store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue) -> TaskResultHandler:\n    \"\"\"Provide a handler for each test.\"\"\"\n    return TaskResultHandler(store, queue)\n\n\n@pytest.mark.anyio\nasync def test_handle_returns_result_for_completed_task(\n    store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler\n) -> None:\n    \"\"\"Test that handle() returns the stored result for a completed task.\"\"\"\n    task = await store.create_task(TaskMetadata(ttl=60000), task_id=\"test-task\")\n    result = CallToolResult(content=[TextContent(type=\"text\", text=\"Done!\")])\n    await store.store_result(task.task_id, result)\n    await store.update_task(task.task_id, status=\"completed\")\n\n    mock_session = Mock()\n    mock_session.send_message = AsyncMock()\n\n    request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=task.task_id))\n    response = await handler.handle(request, mock_session, \"req-1\")\n\n    assert response is not None\n    assert response.meta is not None\n    assert \"io.modelcontextprotocol/related-task\" in response.meta\n\n\n@pytest.mark.anyio\nasync def test_handle_raises_for_nonexistent_task(\n    store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler\n) -> None:\n    \"\"\"Test that handle() raises MCPError for nonexistent task.\"\"\"\n    mock_session = Mock()\n    request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=\"nonexistent\"))\n\n    with pytest.raises(MCPError) as exc_info:\n        await handler.handle(request, mock_session, \"req-1\")\n\n    assert \"not found\" in exc_info.value.error.message\n\n\n@pytest.mark.anyio\nasync def test_handle_returns_empty_result_when_no_result_stored(\n    store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler\n) -> None:\n    \"\"\"Test that handle() returns minimal result when task completed without stored result.\"\"\"\n    task = await store.create_task(TaskMetadata(ttl=60000), task_id=\"test-task\")\n    await store.update_task(task.task_id, status=\"completed\")\n\n    mock_session = Mock()\n    mock_session.send_message = AsyncMock()\n\n    request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=task.task_id))\n    response = await handler.handle(request, mock_session, \"req-1\")\n\n    assert response is not None\n    assert response.meta is not None\n    assert \"io.modelcontextprotocol/related-task\" in response.meta\n\n\n@pytest.mark.anyio\nasync def test_handle_delivers_queued_messages(\n    store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler\n) -> None:\n    \"\"\"Test that handle() delivers queued messages before returning.\"\"\"\n    task = await store.create_task(TaskMetadata(ttl=60000), task_id=\"test-task\")\n\n    queued_msg = QueuedMessage(\n        type=\"notification\",\n        message=JSONRPCRequest(\n            jsonrpc=\"2.0\",\n            id=\"notif-1\",\n            method=\"test/notification\",\n            params={},\n        ),\n    )\n    await queue.enqueue(task.task_id, queued_msg)\n    await store.update_task(task.task_id, status=\"completed\")\n\n    sent_messages: list[SessionMessage] = []\n\n    async def track_send(msg: SessionMessage) -> None:\n        sent_messages.append(msg)\n\n    mock_session = Mock()\n    mock_session.send_message = track_send\n\n    request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=task.task_id))\n    await handler.handle(request, mock_session, \"req-1\")\n\n    assert len(sent_messages) == 1\n\n\n@pytest.mark.anyio\nasync def test_handle_waits_for_task_completion(\n    store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler\n) -> None:\n    \"\"\"Test that handle() waits for task to complete before returning.\"\"\"\n    task = await store.create_task(TaskMetadata(ttl=60000), task_id=\"test-task\")\n\n    mock_session = Mock()\n    mock_session.send_message = AsyncMock()\n\n    request = GetTaskPayloadRequest(params=GetTaskPayloadRequestParams(task_id=task.task_id))\n    result_holder: list[GetTaskPayloadResult | None] = [None]\n\n    async def run_handle() -> None:\n        result_holder[0] = await handler.handle(request, mock_session, \"req-1\")\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(run_handle)\n\n        # Wait for handler to start waiting (event gets created when wait starts)\n        while task.task_id not in store._update_events:\n            await anyio.sleep(0)\n\n        await store.store_result(task.task_id, CallToolResult(content=[TextContent(type=\"text\", text=\"Done\")]))\n        await store.update_task(task.task_id, status=\"completed\")\n\n    assert result_holder[0] is not None\n\n\n@pytest.mark.anyio\nasync def test_route_response_resolves_pending_request(\n    store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler\n) -> None:\n    \"\"\"Test that route_response() resolves a pending request.\"\"\"\n    resolver: Resolver[dict[str, Any]] = Resolver()\n    handler._pending_requests[\"req-123\"] = resolver\n\n    result = handler.route_response(\"req-123\", {\"status\": \"ok\"})\n\n    assert result is True\n    assert resolver.done()\n    assert await resolver.wait() == {\"status\": \"ok\"}\n\n\n@pytest.mark.anyio\nasync def test_route_response_returns_false_for_unknown_request(\n    store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler\n) -> None:\n    \"\"\"Test that route_response() returns False for unknown request ID.\"\"\"\n    result = handler.route_response(\"unknown-req\", {\"status\": \"ok\"})\n    assert result is False\n\n\n@pytest.mark.anyio\nasync def test_route_response_returns_false_for_already_done_resolver(\n    store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler\n) -> None:\n    \"\"\"Test that route_response() returns False if resolver already completed.\"\"\"\n    resolver: Resolver[dict[str, Any]] = Resolver()\n    resolver.set_result({\"already\": \"done\"})\n    handler._pending_requests[\"req-123\"] = resolver\n\n    result = handler.route_response(\"req-123\", {\"new\": \"data\"})\n\n    assert result is False\n\n\n@pytest.mark.anyio\nasync def test_route_error_resolves_pending_request_with_exception(\n    store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler\n) -> None:\n    \"\"\"Test that route_error() sets exception on pending request.\"\"\"\n    resolver: Resolver[dict[str, Any]] = Resolver()\n    handler._pending_requests[\"req-123\"] = resolver\n\n    error = ErrorData(code=INVALID_REQUEST, message=\"Something went wrong\")\n    result = handler.route_error(\"req-123\", error)\n\n    assert result is True\n    assert resolver.done()\n\n    with pytest.raises(MCPError) as exc_info:\n        await resolver.wait()\n    assert exc_info.value.error.message == \"Something went wrong\"\n\n\n@pytest.mark.anyio\nasync def test_route_error_returns_false_for_unknown_request(\n    store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler\n) -> None:\n    \"\"\"Test that route_error() returns False for unknown request ID.\"\"\"\n    error = ErrorData(code=INVALID_REQUEST, message=\"Error\")\n    result = handler.route_error(\"unknown-req\", error)\n    assert result is False\n\n\n@pytest.mark.anyio\nasync def test_deliver_registers_resolver_for_request_messages(\n    store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler\n) -> None:\n    \"\"\"Test that _deliver_queued_messages registers resolvers for request messages.\"\"\"\n    task = await store.create_task(TaskMetadata(ttl=60000), task_id=\"test-task\")\n\n    resolver: Resolver[dict[str, Any]] = Resolver()\n    queued_msg = QueuedMessage(\n        type=\"request\",\n        message=JSONRPCRequest(\n            jsonrpc=\"2.0\",\n            id=\"inner-req-1\",\n            method=\"elicitation/create\",\n            params={},\n        ),\n        resolver=resolver,\n        original_request_id=\"inner-req-1\",\n    )\n    await queue.enqueue(task.task_id, queued_msg)\n\n    mock_session = Mock()\n    mock_session.send_message = AsyncMock()\n\n    await handler._deliver_queued_messages(task.task_id, mock_session, \"outer-req-1\")\n\n    assert \"inner-req-1\" in handler._pending_requests\n    assert handler._pending_requests[\"inner-req-1\"] is resolver\n\n\n@pytest.mark.anyio\nasync def test_deliver_skips_resolver_registration_when_no_original_id(\n    store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler\n) -> None:\n    \"\"\"Test that _deliver_queued_messages skips resolver registration when original_request_id is None.\"\"\"\n    task = await store.create_task(TaskMetadata(ttl=60000), task_id=\"test-task\")\n\n    resolver: Resolver[dict[str, Any]] = Resolver()\n    queued_msg = QueuedMessage(\n        type=\"request\",\n        message=JSONRPCRequest(\n            jsonrpc=\"2.0\",\n            id=\"inner-req-1\",\n            method=\"elicitation/create\",\n            params={},\n        ),\n        resolver=resolver,\n        original_request_id=None,  # No original request ID\n    )\n    await queue.enqueue(task.task_id, queued_msg)\n\n    mock_session = Mock()\n    mock_session.send_message = AsyncMock()\n\n    await handler._deliver_queued_messages(task.task_id, mock_session, \"outer-req-1\")\n\n    # Resolver should NOT be registered since original_request_id is None\n    assert len(handler._pending_requests) == 0\n    # But the message should still be sent\n    mock_session.send_message.assert_called_once()\n\n\n@pytest.mark.anyio\nasync def test_wait_for_task_update_handles_store_exception(\n    store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler\n) -> None:\n    \"\"\"Test that _wait_for_task_update handles store exception gracefully.\"\"\"\n    task = await store.create_task(TaskMetadata(ttl=60000), task_id=\"test-task\")\n\n    # Make wait_for_update raise an exception\n    async def failing_wait(task_id: str) -> None:\n        raise RuntimeError(\"Store error\")\n\n    store.wait_for_update = failing_wait  # type: ignore[method-assign]\n\n    # Queue a message to unblock the race via the queue path\n    async def enqueue_later() -> None:\n        # Wait for queue to start waiting (event gets created when wait starts)\n        while task.task_id not in queue._events:\n            await anyio.sleep(0)\n        await queue.enqueue(\n            task.task_id,\n            QueuedMessage(\n                type=\"notification\",\n                message=JSONRPCRequest(\n                    jsonrpc=\"2.0\",\n                    id=\"notif-1\",\n                    method=\"test/notification\",\n                    params={},\n                ),\n            ),\n        )\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(enqueue_later)\n        # This should complete via the queue path even though store raises\n        await handler._wait_for_task_update(task.task_id)\n\n\n@pytest.mark.anyio\nasync def test_wait_for_task_update_handles_queue_exception(\n    store: InMemoryTaskStore, queue: InMemoryTaskMessageQueue, handler: TaskResultHandler\n) -> None:\n    \"\"\"Test that _wait_for_task_update handles queue exception gracefully.\"\"\"\n    task = await store.create_task(TaskMetadata(ttl=60000), task_id=\"test-task\")\n\n    # Make wait_for_message raise an exception\n    async def failing_wait(task_id: str) -> None:\n        raise RuntimeError(\"Queue error\")\n\n    queue.wait_for_message = failing_wait  # type: ignore[method-assign]\n\n    # Update the store to unblock the race via the store path\n    async def update_later() -> None:\n        # Wait for store to start waiting (event gets created when wait starts)\n        while task.task_id not in store._update_events:\n            await anyio.sleep(0)\n        await store.update_task(task.task_id, status=\"completed\")\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(update_later)\n        # This should complete via the store path even though queue raises\n        await handler._wait_for_task_update(task.task_id)\n"
  },
  {
    "path": "tests/experimental/tasks/test_capabilities.py",
    "content": "\"\"\"Tests for tasks capability checking utilities.\"\"\"\n\nimport pytest\n\nfrom mcp import MCPError\nfrom mcp.shared.experimental.tasks.capabilities import (\n    check_tasks_capability,\n    has_task_augmented_elicitation,\n    has_task_augmented_sampling,\n    require_task_augmented_elicitation,\n    require_task_augmented_sampling,\n)\nfrom mcp.types import (\n    ClientCapabilities,\n    ClientTasksCapability,\n    ClientTasksRequestsCapability,\n    TasksCreateElicitationCapability,\n    TasksCreateMessageCapability,\n    TasksElicitationCapability,\n    TasksSamplingCapability,\n)\n\n\nclass TestCheckTasksCapability:\n    \"\"\"Tests for check_tasks_capability function.\"\"\"\n\n    def test_required_requests_none_returns_true(self) -> None:\n        \"\"\"When required.requests is None, should return True.\"\"\"\n        required = ClientTasksCapability()\n        client = ClientTasksCapability()\n        assert check_tasks_capability(required, client) is True\n\n    def test_client_requests_none_returns_false(self) -> None:\n        \"\"\"When client.requests is None but required.requests is set, should return False.\"\"\"\n        required = ClientTasksCapability(requests=ClientTasksRequestsCapability())\n        client = ClientTasksCapability()\n        assert check_tasks_capability(required, client) is False\n\n    def test_elicitation_required_but_client_missing(self) -> None:\n        \"\"\"When elicitation is required but client doesn't have it.\"\"\"\n        required = ClientTasksCapability(\n            requests=ClientTasksRequestsCapability(elicitation=TasksElicitationCapability())\n        )\n        client = ClientTasksCapability(requests=ClientTasksRequestsCapability())\n        assert check_tasks_capability(required, client) is False\n\n    def test_elicitation_create_required_but_client_missing(self) -> None:\n        \"\"\"When elicitation.create is required but client doesn't have it.\"\"\"\n        required = ClientTasksCapability(\n            requests=ClientTasksRequestsCapability(\n                elicitation=TasksElicitationCapability(create=TasksCreateElicitationCapability())\n            )\n        )\n        client = ClientTasksCapability(\n            requests=ClientTasksRequestsCapability(\n                elicitation=TasksElicitationCapability()  # No create\n            )\n        )\n        assert check_tasks_capability(required, client) is False\n\n    def test_elicitation_create_present(self) -> None:\n        \"\"\"When elicitation.create is required and client has it.\"\"\"\n        required = ClientTasksCapability(\n            requests=ClientTasksRequestsCapability(\n                elicitation=TasksElicitationCapability(create=TasksCreateElicitationCapability())\n            )\n        )\n        client = ClientTasksCapability(\n            requests=ClientTasksRequestsCapability(\n                elicitation=TasksElicitationCapability(create=TasksCreateElicitationCapability())\n            )\n        )\n        assert check_tasks_capability(required, client) is True\n\n    def test_sampling_required_but_client_missing(self) -> None:\n        \"\"\"When sampling is required but client doesn't have it.\"\"\"\n        required = ClientTasksCapability(requests=ClientTasksRequestsCapability(sampling=TasksSamplingCapability()))\n        client = ClientTasksCapability(requests=ClientTasksRequestsCapability())\n        assert check_tasks_capability(required, client) is False\n\n    def test_sampling_create_message_required_but_client_missing(self) -> None:\n        \"\"\"When sampling.createMessage is required but client doesn't have it.\"\"\"\n        required = ClientTasksCapability(\n            requests=ClientTasksRequestsCapability(\n                sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability())\n            )\n        )\n        client = ClientTasksCapability(\n            requests=ClientTasksRequestsCapability(\n                sampling=TasksSamplingCapability()  # No createMessage\n            )\n        )\n        assert check_tasks_capability(required, client) is False\n\n    def test_sampling_create_message_present(self) -> None:\n        \"\"\"When sampling.createMessage is required and client has it.\"\"\"\n        required = ClientTasksCapability(\n            requests=ClientTasksRequestsCapability(\n                sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability())\n            )\n        )\n        client = ClientTasksCapability(\n            requests=ClientTasksRequestsCapability(\n                sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability())\n            )\n        )\n        assert check_tasks_capability(required, client) is True\n\n    def test_both_elicitation_and_sampling_present(self) -> None:\n        \"\"\"When both elicitation.create and sampling.createMessage are required and client has both.\"\"\"\n        required = ClientTasksCapability(\n            requests=ClientTasksRequestsCapability(\n                elicitation=TasksElicitationCapability(create=TasksCreateElicitationCapability()),\n                sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability()),\n            )\n        )\n        client = ClientTasksCapability(\n            requests=ClientTasksRequestsCapability(\n                elicitation=TasksElicitationCapability(create=TasksCreateElicitationCapability()),\n                sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability()),\n            )\n        )\n        assert check_tasks_capability(required, client) is True\n\n    def test_elicitation_without_create_required(self) -> None:\n        \"\"\"When elicitation is required but not create specifically.\"\"\"\n        required = ClientTasksCapability(\n            requests=ClientTasksRequestsCapability(\n                elicitation=TasksElicitationCapability()  # No create\n            )\n        )\n        client = ClientTasksCapability(\n            requests=ClientTasksRequestsCapability(\n                elicitation=TasksElicitationCapability(create=TasksCreateElicitationCapability())\n            )\n        )\n        assert check_tasks_capability(required, client) is True\n\n    def test_sampling_without_create_message_required(self) -> None:\n        \"\"\"When sampling is required but not createMessage specifically.\"\"\"\n        required = ClientTasksCapability(\n            requests=ClientTasksRequestsCapability(\n                sampling=TasksSamplingCapability()  # No createMessage\n            )\n        )\n        client = ClientTasksCapability(\n            requests=ClientTasksRequestsCapability(\n                sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability())\n            )\n        )\n        assert check_tasks_capability(required, client) is True\n\n\nclass TestHasTaskAugmentedElicitation:\n    \"\"\"Tests for has_task_augmented_elicitation function.\"\"\"\n\n    def test_tasks_none(self) -> None:\n        \"\"\"Returns False when caps.tasks is None.\"\"\"\n        caps = ClientCapabilities()\n        assert has_task_augmented_elicitation(caps) is False\n\n    def test_requests_none(self) -> None:\n        \"\"\"Returns False when caps.tasks.requests is None.\"\"\"\n        caps = ClientCapabilities(tasks=ClientTasksCapability())\n        assert has_task_augmented_elicitation(caps) is False\n\n    def test_elicitation_none(self) -> None:\n        \"\"\"Returns False when caps.tasks.requests.elicitation is None.\"\"\"\n        caps = ClientCapabilities(tasks=ClientTasksCapability(requests=ClientTasksRequestsCapability()))\n        assert has_task_augmented_elicitation(caps) is False\n\n    def test_create_none(self) -> None:\n        \"\"\"Returns False when caps.tasks.requests.elicitation.create is None.\"\"\"\n        caps = ClientCapabilities(\n            tasks=ClientTasksCapability(\n                requests=ClientTasksRequestsCapability(elicitation=TasksElicitationCapability())\n            )\n        )\n        assert has_task_augmented_elicitation(caps) is False\n\n    def test_create_present(self) -> None:\n        \"\"\"Returns True when full capability path is present.\"\"\"\n        caps = ClientCapabilities(\n            tasks=ClientTasksCapability(\n                requests=ClientTasksRequestsCapability(\n                    elicitation=TasksElicitationCapability(create=TasksCreateElicitationCapability())\n                )\n            )\n        )\n        assert has_task_augmented_elicitation(caps) is True\n\n\nclass TestHasTaskAugmentedSampling:\n    \"\"\"Tests for has_task_augmented_sampling function.\"\"\"\n\n    def test_tasks_none(self) -> None:\n        \"\"\"Returns False when caps.tasks is None.\"\"\"\n        caps = ClientCapabilities()\n        assert has_task_augmented_sampling(caps) is False\n\n    def test_requests_none(self) -> None:\n        \"\"\"Returns False when caps.tasks.requests is None.\"\"\"\n        caps = ClientCapabilities(tasks=ClientTasksCapability())\n        assert has_task_augmented_sampling(caps) is False\n\n    def test_sampling_none(self) -> None:\n        \"\"\"Returns False when caps.tasks.requests.sampling is None.\"\"\"\n        caps = ClientCapabilities(tasks=ClientTasksCapability(requests=ClientTasksRequestsCapability()))\n        assert has_task_augmented_sampling(caps) is False\n\n    def test_create_message_none(self) -> None:\n        \"\"\"Returns False when caps.tasks.requests.sampling.createMessage is None.\"\"\"\n        caps = ClientCapabilities(\n            tasks=ClientTasksCapability(requests=ClientTasksRequestsCapability(sampling=TasksSamplingCapability()))\n        )\n        assert has_task_augmented_sampling(caps) is False\n\n    def test_create_message_present(self) -> None:\n        \"\"\"Returns True when full capability path is present.\"\"\"\n        caps = ClientCapabilities(\n            tasks=ClientTasksCapability(\n                requests=ClientTasksRequestsCapability(\n                    sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability())\n                )\n            )\n        )\n        assert has_task_augmented_sampling(caps) is True\n\n\nclass TestRequireTaskAugmentedElicitation:\n    \"\"\"Tests for require_task_augmented_elicitation function.\"\"\"\n\n    def test_raises_when_none(self) -> None:\n        \"\"\"Raises MCPError when client_caps is None.\"\"\"\n        with pytest.raises(MCPError) as exc_info:\n            require_task_augmented_elicitation(None)\n        assert \"task-augmented elicitation\" in str(exc_info.value)\n\n    def test_raises_when_missing(self) -> None:\n        \"\"\"Raises MCPError when capability is missing.\"\"\"\n        caps = ClientCapabilities()\n        with pytest.raises(MCPError) as exc_info:\n            require_task_augmented_elicitation(caps)\n        assert \"task-augmented elicitation\" in str(exc_info.value)\n\n    def test_passes_when_present(self) -> None:\n        \"\"\"Does not raise when capability is present.\"\"\"\n        caps = ClientCapabilities(\n            tasks=ClientTasksCapability(\n                requests=ClientTasksRequestsCapability(\n                    elicitation=TasksElicitationCapability(create=TasksCreateElicitationCapability())\n                )\n            )\n        )\n        require_task_augmented_elicitation(caps)\n\n\nclass TestRequireTaskAugmentedSampling:\n    \"\"\"Tests for require_task_augmented_sampling function.\"\"\"\n\n    def test_raises_when_none(self) -> None:\n        \"\"\"Raises MCPError when client_caps is None.\"\"\"\n        with pytest.raises(MCPError) as exc_info:\n            require_task_augmented_sampling(None)\n        assert \"task-augmented sampling\" in str(exc_info.value)\n\n    def test_raises_when_missing(self) -> None:\n        \"\"\"Raises MCPError when capability is missing.\"\"\"\n        caps = ClientCapabilities()\n        with pytest.raises(MCPError) as exc_info:\n            require_task_augmented_sampling(caps)\n        assert \"task-augmented sampling\" in str(exc_info.value)\n\n    def test_passes_when_present(self) -> None:\n        \"\"\"Does not raise when capability is present.\"\"\"\n        caps = ClientCapabilities(\n            tasks=ClientTasksCapability(\n                requests=ClientTasksRequestsCapability(\n                    sampling=TasksSamplingCapability(create_message=TasksCreateMessageCapability())\n                )\n            )\n        )\n        require_task_augmented_sampling(caps)\n"
  },
  {
    "path": "tests/experimental/tasks/test_elicitation_scenarios.py",
    "content": "\"\"\"Tests for the four elicitation scenarios with tasks.\n\nThis tests all combinations of tool call types and elicitation types:\n1. Normal tool call + Normal elicitation (session.elicit)\n2. Normal tool call + Task-augmented elicitation (session.experimental.elicit_as_task)\n3. Task-augmented tool call + Normal elicitation (task.elicit)\n4. Task-augmented tool call + Task-augmented elicitation (task.elicit_as_task)\n\nAnd the same for sampling (create_message).\n\"\"\"\n\nfrom typing import Any\n\nimport anyio\nimport pytest\nfrom anyio import Event\n\nfrom mcp.client.experimental.task_handlers import ExperimentalTaskHandlers\nfrom mcp.client.session import ClientSession\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.server.experimental.task_context import ServerTaskContext\nfrom mcp.server.lowlevel import NotificationOptions\nfrom mcp.shared._context import RequestContext\nfrom mcp.shared.experimental.tasks.helpers import is_terminal\nfrom mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore\nfrom mcp.shared.message import SessionMessage\nfrom mcp.types import (\n    TASK_REQUIRED,\n    CallToolRequestParams,\n    CallToolResult,\n    CreateMessageRequestParams,\n    CreateMessageResult,\n    CreateTaskResult,\n    ElicitRequestParams,\n    ElicitResult,\n    ErrorData,\n    GetTaskPayloadResult,\n    GetTaskResult,\n    ListToolsResult,\n    PaginatedRequestParams,\n    SamplingMessage,\n    TaskMetadata,\n    TextContent,\n    Tool,\n)\n\n\ndef create_client_task_handlers(\n    client_task_store: InMemoryTaskStore,\n    elicit_received: Event,\n) -> ExperimentalTaskHandlers:\n    \"\"\"Create task handlers for client to handle task-augmented elicitation from server.\"\"\"\n\n    elicit_response = ElicitResult(action=\"accept\", content={\"confirm\": True})\n    task_complete_events: dict[str, Event] = {}\n\n    async def handle_augmented_elicitation(\n        context: RequestContext[ClientSession],\n        params: ElicitRequestParams,\n        task_metadata: TaskMetadata,\n    ) -> CreateTaskResult:\n        \"\"\"Handle task-augmented elicitation by creating a client-side task.\"\"\"\n        elicit_received.set()\n        task = await client_task_store.create_task(task_metadata)\n        task_complete_events[task.task_id] = Event()\n\n        async def complete_task() -> None:\n            # Store result before updating status to avoid race condition\n            await client_task_store.store_result(task.task_id, elicit_response)\n            await client_task_store.update_task(task.task_id, status=\"completed\")\n            task_complete_events[task.task_id].set()\n\n        context.session._task_group.start_soon(complete_task)  # pyright: ignore[reportPrivateUsage]\n        return CreateTaskResult(task=task)\n\n    async def handle_get_task(\n        context: RequestContext[ClientSession],\n        params: Any,\n    ) -> GetTaskResult:\n        \"\"\"Handle tasks/get from server.\"\"\"\n        task = await client_task_store.get_task(params.task_id)\n        assert task is not None, f\"Task not found: {params.task_id}\"\n        return GetTaskResult(\n            task_id=task.task_id,\n            status=task.status,\n            status_message=task.status_message,\n            created_at=task.created_at,\n            last_updated_at=task.last_updated_at,\n            ttl=task.ttl,\n            poll_interval=100,\n        )\n\n    async def handle_get_task_result(\n        context: RequestContext[ClientSession],\n        params: Any,\n    ) -> GetTaskPayloadResult | ErrorData:\n        \"\"\"Handle tasks/result from server.\"\"\"\n        event = task_complete_events.get(params.task_id)\n        assert event is not None, f\"No completion event for task: {params.task_id}\"\n        await event.wait()\n        result = await client_task_store.get_result(params.task_id)\n        assert result is not None, f\"Result not found for task: {params.task_id}\"\n        return GetTaskPayloadResult.model_validate(result.model_dump(by_alias=True))\n\n    return ExperimentalTaskHandlers(\n        augmented_elicitation=handle_augmented_elicitation,\n        get_task=handle_get_task,\n        get_task_result=handle_get_task_result,\n    )\n\n\ndef create_sampling_task_handlers(\n    client_task_store: InMemoryTaskStore,\n    sampling_received: Event,\n) -> ExperimentalTaskHandlers:\n    \"\"\"Create task handlers for client to handle task-augmented sampling from server.\"\"\"\n\n    sampling_response = CreateMessageResult(\n        role=\"assistant\",\n        content=TextContent(type=\"text\", text=\"Hello from the model!\"),\n        model=\"test-model\",\n    )\n    task_complete_events: dict[str, Event] = {}\n\n    async def handle_augmented_sampling(\n        context: RequestContext[ClientSession],\n        params: CreateMessageRequestParams,\n        task_metadata: TaskMetadata,\n    ) -> CreateTaskResult:\n        \"\"\"Handle task-augmented sampling by creating a client-side task.\"\"\"\n        sampling_received.set()\n        task = await client_task_store.create_task(task_metadata)\n        task_complete_events[task.task_id] = Event()\n\n        async def complete_task() -> None:\n            # Store result before updating status to avoid race condition\n            await client_task_store.store_result(task.task_id, sampling_response)\n            await client_task_store.update_task(task.task_id, status=\"completed\")\n            task_complete_events[task.task_id].set()\n\n        context.session._task_group.start_soon(complete_task)  # pyright: ignore[reportPrivateUsage]\n        return CreateTaskResult(task=task)\n\n    async def handle_get_task(\n        context: RequestContext[ClientSession],\n        params: Any,\n    ) -> GetTaskResult:\n        \"\"\"Handle tasks/get from server.\"\"\"\n        task = await client_task_store.get_task(params.task_id)\n        assert task is not None, f\"Task not found: {params.task_id}\"\n        return GetTaskResult(\n            task_id=task.task_id,\n            status=task.status,\n            status_message=task.status_message,\n            created_at=task.created_at,\n            last_updated_at=task.last_updated_at,\n            ttl=task.ttl,\n            poll_interval=100,\n        )\n\n    async def handle_get_task_result(\n        context: RequestContext[ClientSession],\n        params: Any,\n    ) -> GetTaskPayloadResult | ErrorData:\n        \"\"\"Handle tasks/result from server.\"\"\"\n        event = task_complete_events.get(params.task_id)\n        assert event is not None, f\"No completion event for task: {params.task_id}\"\n        await event.wait()\n        result = await client_task_store.get_result(params.task_id)\n        assert result is not None, f\"Result not found for task: {params.task_id}\"\n        return GetTaskPayloadResult.model_validate(result.model_dump(by_alias=True))\n\n    return ExperimentalTaskHandlers(\n        augmented_sampling=handle_augmented_sampling,\n        get_task=handle_get_task,\n        get_task_result=handle_get_task_result,\n    )\n\n\n@pytest.mark.anyio\nasync def test_scenario1_normal_tool_normal_elicitation() -> None:\n    \"\"\"Scenario 1: Normal tool call with normal elicitation.\n\n    Server calls session.elicit() directly, client responds immediately.\n    \"\"\"\n    elicit_received = Event()\n    tool_result: list[str] = []\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        return ListToolsResult(\n            tools=[\n                Tool(\n                    name=\"confirm_action\",\n                    description=\"Confirm an action\",\n                    input_schema={\"type\": \"object\"},\n                )\n            ]\n        )\n\n    async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:\n        # Normal elicitation - expects immediate response\n        result = await ctx.session.elicit(\n            message=\"Please confirm the action\",\n            requested_schema={\"type\": \"object\", \"properties\": {\"confirm\": {\"type\": \"boolean\"}}},\n        )\n\n        confirmed = result.content.get(\"confirm\", False) if result.content else False\n        tool_result.append(\"confirmed\" if confirmed else \"cancelled\")\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"confirmed\" if confirmed else \"cancelled\")])\n\n    server = Server(\"test-scenario1\", on_list_tools=handle_list_tools, on_call_tool=handle_call_tool)\n\n    # Elicitation callback for client\n    async def elicitation_callback(\n        context: RequestContext[ClientSession],\n        params: ElicitRequestParams,\n    ) -> ElicitResult:\n        elicit_received.set()\n        return ElicitResult(action=\"accept\", content={\"confirm\": True})\n\n    # Set up streams\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)\n\n    async def run_server() -> None:\n        await server.run(\n            client_to_server_receive,\n            server_to_client_send,\n            server.create_initialization_options(\n                notification_options=NotificationOptions(),\n                experimental_capabilities={},\n            ),\n        )\n\n    async def run_client() -> None:\n        async with ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n            elicitation_callback=elicitation_callback,\n        ) as client_session:\n            await client_session.initialize()\n\n            # Call tool normally (not as task)\n            result = await client_session.call_tool(\"confirm_action\", {})\n\n            # Verify elicitation was received and tool completed\n            assert elicit_received.is_set()\n            assert len(result.content) > 0\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"confirmed\"\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(run_server)\n        tg.start_soon(run_client)\n\n    assert tool_result[0] == \"confirmed\"\n\n\n@pytest.mark.anyio\nasync def test_scenario2_normal_tool_task_augmented_elicitation() -> None:\n    \"\"\"Scenario 2: Normal tool call with task-augmented elicitation.\n\n    Server calls session.experimental.elicit_as_task(), client creates a task\n    for the elicitation and returns CreateTaskResult. Server polls client.\n    \"\"\"\n    elicit_received = Event()\n    tool_result: list[str] = []\n\n    # Client-side task store for handling task-augmented elicitation\n    client_task_store = InMemoryTaskStore()\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        return ListToolsResult(\n            tools=[\n                Tool(\n                    name=\"confirm_action\",\n                    description=\"Confirm an action\",\n                    input_schema={\"type\": \"object\"},\n                )\n            ]\n        )\n\n    async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:\n        # Task-augmented elicitation - server polls client\n        result = await ctx.session.experimental.elicit_as_task(\n            message=\"Please confirm the action\",\n            requested_schema={\"type\": \"object\", \"properties\": {\"confirm\": {\"type\": \"boolean\"}}},\n            ttl=60000,\n        )\n\n        confirmed = result.content.get(\"confirm\", False) if result.content else False\n        tool_result.append(\"confirmed\" if confirmed else \"cancelled\")\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"confirmed\" if confirmed else \"cancelled\")])\n\n    server = Server(\"test-scenario2\", on_list_tools=handle_list_tools, on_call_tool=handle_call_tool)\n    task_handlers = create_client_task_handlers(client_task_store, elicit_received)\n\n    # Set up streams\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)\n\n    async def run_server() -> None:\n        await server.run(\n            client_to_server_receive,\n            server_to_client_send,\n            server.create_initialization_options(\n                notification_options=NotificationOptions(),\n                experimental_capabilities={},\n            ),\n        )\n\n    async def run_client() -> None:\n        async with ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n            experimental_task_handlers=task_handlers,\n        ) as client_session:\n            await client_session.initialize()\n\n            # Call tool normally (not as task)\n            result = await client_session.call_tool(\"confirm_action\", {})\n\n            # Verify elicitation was received and tool completed\n            assert elicit_received.is_set()\n            assert len(result.content) > 0\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"confirmed\"\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(run_server)\n        tg.start_soon(run_client)\n\n    assert tool_result[0] == \"confirmed\"\n    client_task_store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_scenario3_task_augmented_tool_normal_elicitation() -> None:\n    \"\"\"Scenario 3: Task-augmented tool call with normal elicitation.\n\n    Client calls tool as task. Inside the task, server uses task.elicit()\n    which queues the request and delivers via tasks/result.\n    \"\"\"\n    elicit_received = Event()\n    work_completed = Event()\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        raise NotImplementedError\n\n    async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CreateTaskResult:\n        ctx.experimental.validate_task_mode(TASK_REQUIRED)\n\n        async def work(task: ServerTaskContext) -> CallToolResult:\n            # Normal elicitation within task - queued and delivered via tasks/result\n            result = await task.elicit(\n                message=\"Please confirm the action\",\n                requested_schema={\"type\": \"object\", \"properties\": {\"confirm\": {\"type\": \"boolean\"}}},\n            )\n\n            confirmed = result.content.get(\"confirm\", False) if result.content else False\n            work_completed.set()\n            return CallToolResult(content=[TextContent(type=\"text\", text=\"confirmed\" if confirmed else \"cancelled\")])\n\n        return await ctx.experimental.run_task(work)\n\n    server = Server(\"test-scenario3\", on_list_tools=handle_list_tools, on_call_tool=handle_call_tool)\n    server.experimental.enable_tasks()\n\n    # Elicitation callback for client\n    async def elicitation_callback(\n        context: RequestContext[ClientSession],\n        params: ElicitRequestParams,\n    ) -> ElicitResult:\n        elicit_received.set()\n        return ElicitResult(action=\"accept\", content={\"confirm\": True})\n\n    # Set up streams\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)\n\n    async def run_server() -> None:\n        await server.run(\n            client_to_server_receive,\n            server_to_client_send,\n            server.create_initialization_options(\n                notification_options=NotificationOptions(),\n                experimental_capabilities={},\n            ),\n        )\n\n    async def run_client() -> None:\n        async with ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n            elicitation_callback=elicitation_callback,\n        ) as client_session:\n            await client_session.initialize()\n\n            # Call tool as task\n            create_result = await client_session.experimental.call_tool_as_task(\"confirm_action\", {})\n            task_id = create_result.task.task_id\n            assert create_result.task.status == \"working\"\n\n            # Poll until input_required, then call tasks/result\n            found_input_required = False\n            async for status in client_session.experimental.poll_task(task_id):  # pragma: no branch\n                if status.status == \"input_required\":  # pragma: no branch\n                    found_input_required = True\n                    break\n            assert found_input_required, \"Expected to see input_required status\"\n\n            # This will deliver the elicitation and get the response\n            final_result = await client_session.experimental.get_task_result(task_id, CallToolResult)\n\n            # Verify\n            assert elicit_received.is_set()\n            assert len(final_result.content) > 0\n            assert isinstance(final_result.content[0], TextContent)\n            assert final_result.content[0].text == \"confirmed\"\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(run_server)\n        tg.start_soon(run_client)\n\n    assert work_completed.is_set()\n\n\n@pytest.mark.anyio\nasync def test_scenario4_task_augmented_tool_task_augmented_elicitation() -> None:\n    \"\"\"Scenario 4: Task-augmented tool call with task-augmented elicitation.\n\n    Client calls tool as task. Inside the task, server uses task.elicit_as_task()\n    which sends task-augmented elicitation. Client creates its own task for the\n    elicitation, and server polls the client.\n\n    This tests the full bidirectional flow where:\n    1. Client calls tasks/result on server (for tool task)\n    2. Server delivers task-augmented elicitation through that stream\n    3. Client creates its own task and returns CreateTaskResult\n    4. Server polls the client's task while the client's tasks/result is still open\n    5. Server gets the ElicitResult and completes the tool task\n    6. Client's tasks/result returns with the CallToolResult\n    \"\"\"\n    elicit_received = Event()\n    work_completed = Event()\n\n    # Client-side task store for handling task-augmented elicitation\n    client_task_store = InMemoryTaskStore()\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        raise NotImplementedError\n\n    async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CreateTaskResult:\n        ctx.experimental.validate_task_mode(TASK_REQUIRED)\n\n        async def work(task: ServerTaskContext) -> CallToolResult:\n            # Task-augmented elicitation within task - server polls client\n            result = await task.elicit_as_task(\n                message=\"Please confirm the action\",\n                requested_schema={\"type\": \"object\", \"properties\": {\"confirm\": {\"type\": \"boolean\"}}},\n                ttl=60000,\n            )\n\n            confirmed = result.content.get(\"confirm\", False) if result.content else False\n            work_completed.set()\n            return CallToolResult(content=[TextContent(type=\"text\", text=\"confirmed\" if confirmed else \"cancelled\")])\n\n        return await ctx.experimental.run_task(work)\n\n    server = Server(\"test-scenario4\", on_list_tools=handle_list_tools, on_call_tool=handle_call_tool)\n    server.experimental.enable_tasks()\n    task_handlers = create_client_task_handlers(client_task_store, elicit_received)\n\n    # Set up streams\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)\n\n    async def run_server() -> None:\n        await server.run(\n            client_to_server_receive,\n            server_to_client_send,\n            server.create_initialization_options(\n                notification_options=NotificationOptions(),\n                experimental_capabilities={},\n            ),\n        )\n\n    async def run_client() -> None:\n        async with ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n            experimental_task_handlers=task_handlers,\n        ) as client_session:\n            await client_session.initialize()\n\n            # Call tool as task\n            create_result = await client_session.experimental.call_tool_as_task(\"confirm_action\", {})\n            task_id = create_result.task.task_id\n            assert create_result.task.status == \"working\"\n\n            # Poll until input_required or terminal, then call tasks/result\n            found_expected_status = False\n            async for status in client_session.experimental.poll_task(task_id):  # pragma: no branch\n                if status.status == \"input_required\" or is_terminal(status.status):  # pragma: no branch\n                    found_expected_status = True\n                    break\n            assert found_expected_status, \"Expected to see input_required or terminal status\"\n\n            # This will deliver the task-augmented elicitation,\n            # server will poll client, and eventually return the tool result\n            final_result = await client_session.experimental.get_task_result(task_id, CallToolResult)\n\n            # Verify\n            assert elicit_received.is_set()\n            assert len(final_result.content) > 0\n            assert isinstance(final_result.content[0], TextContent)\n            assert final_result.content[0].text == \"confirmed\"\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(run_server)\n        tg.start_soon(run_client)\n\n    assert work_completed.is_set()\n    client_task_store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_scenario2_sampling_normal_tool_task_augmented_sampling() -> None:\n    \"\"\"Scenario 2 for sampling: Normal tool call with task-augmented sampling.\n\n    Server calls session.experimental.create_message_as_task(), client creates\n    a task for the sampling and returns CreateTaskResult. Server polls client.\n    \"\"\"\n    sampling_received = Event()\n    tool_result: list[str] = []\n\n    # Client-side task store for handling task-augmented sampling\n    client_task_store = InMemoryTaskStore()\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        return ListToolsResult(\n            tools=[\n                Tool(\n                    name=\"generate_text\",\n                    description=\"Generate text using sampling\",\n                    input_schema={\"type\": \"object\"},\n                )\n            ]\n        )\n\n    async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:\n        # Task-augmented sampling - server polls client\n        result = await ctx.session.experimental.create_message_as_task(\n            messages=[SamplingMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hello\"))],\n            max_tokens=100,\n            ttl=60000,\n        )\n\n        assert isinstance(result.content, TextContent), \"Expected TextContent response\"\n        response_text = result.content.text\n\n        tool_result.append(response_text)\n        return CallToolResult(content=[TextContent(type=\"text\", text=response_text)])\n\n    server = Server(\"test-scenario2-sampling\", on_list_tools=handle_list_tools, on_call_tool=handle_call_tool)\n    task_handlers = create_sampling_task_handlers(client_task_store, sampling_received)\n\n    # Set up streams\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)\n\n    async def run_server() -> None:\n        await server.run(\n            client_to_server_receive,\n            server_to_client_send,\n            server.create_initialization_options(\n                notification_options=NotificationOptions(),\n                experimental_capabilities={},\n            ),\n        )\n\n    async def run_client() -> None:\n        async with ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n            experimental_task_handlers=task_handlers,\n        ) as client_session:\n            await client_session.initialize()\n\n            # Call tool normally (not as task)\n            result = await client_session.call_tool(\"generate_text\", {})\n\n            # Verify sampling was received and tool completed\n            assert sampling_received.is_set()\n            assert len(result.content) > 0\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"Hello from the model!\"\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(run_server)\n        tg.start_soon(run_client)\n\n    assert tool_result[0] == \"Hello from the model!\"\n    client_task_store.cleanup()\n\n\n@pytest.mark.anyio\nasync def test_scenario4_sampling_task_augmented_tool_task_augmented_sampling() -> None:\n    \"\"\"Scenario 4 for sampling: Task-augmented tool call with task-augmented sampling.\n\n    Client calls tool as task. Inside the task, server uses task.create_message_as_task()\n    which sends task-augmented sampling. Client creates its own task for the sampling,\n    and server polls the client.\n    \"\"\"\n    sampling_received = Event()\n    work_completed = Event()\n\n    # Client-side task store for handling task-augmented sampling\n    client_task_store = InMemoryTaskStore()\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        raise NotImplementedError\n\n    async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CreateTaskResult:\n        ctx.experimental.validate_task_mode(TASK_REQUIRED)\n\n        async def work(task: ServerTaskContext) -> CallToolResult:\n            # Task-augmented sampling within task - server polls client\n            result = await task.create_message_as_task(\n                messages=[SamplingMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hello\"))],\n                max_tokens=100,\n                ttl=60000,\n            )\n\n            assert isinstance(result.content, TextContent), \"Expected TextContent response\"\n            response_text = result.content.text\n\n            work_completed.set()\n            return CallToolResult(content=[TextContent(type=\"text\", text=response_text)])\n\n        return await ctx.experimental.run_task(work)\n\n    server = Server(\"test-scenario4-sampling\", on_list_tools=handle_list_tools, on_call_tool=handle_call_tool)\n    server.experimental.enable_tasks()\n    task_handlers = create_sampling_task_handlers(client_task_store, sampling_received)\n\n    # Set up streams\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](10)\n\n    async def run_server() -> None:\n        await server.run(\n            client_to_server_receive,\n            server_to_client_send,\n            server.create_initialization_options(\n                notification_options=NotificationOptions(),\n                experimental_capabilities={},\n            ),\n        )\n\n    async def run_client() -> None:\n        async with ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n            experimental_task_handlers=task_handlers,\n        ) as client_session:\n            await client_session.initialize()\n\n            # Call tool as task\n            create_result = await client_session.experimental.call_tool_as_task(\"generate_text\", {})\n            task_id = create_result.task.task_id\n            assert create_result.task.status == \"working\"\n\n            # Poll until input_required or terminal\n            found_expected_status = False\n            async for status in client_session.experimental.poll_task(task_id):  # pragma: no branch\n                if status.status == \"input_required\" or is_terminal(status.status):  # pragma: no branch\n                    found_expected_status = True\n                    break\n            assert found_expected_status, \"Expected to see input_required or terminal status\"\n\n            final_result = await client_session.experimental.get_task_result(task_id, CallToolResult)\n\n            # Verify\n            assert sampling_received.is_set()\n            assert len(final_result.content) > 0\n            assert isinstance(final_result.content[0], TextContent)\n            assert final_result.content[0].text == \"Hello from the model!\"\n\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(run_server)\n        tg.start_soon(run_client)\n\n    assert work_completed.is_set()\n    client_task_store.cleanup()\n"
  },
  {
    "path": "tests/experimental/tasks/test_message_queue.py",
    "content": "\"\"\"Tests for TaskMessageQueue and InMemoryTaskMessageQueue.\"\"\"\n\nfrom collections import deque\nfrom datetime import datetime, timezone\n\nimport anyio\nimport pytest\n\nfrom mcp.shared.experimental.tasks.message_queue import InMemoryTaskMessageQueue, QueuedMessage\nfrom mcp.shared.experimental.tasks.resolver import Resolver\nfrom mcp.types import JSONRPCNotification, JSONRPCRequest\n\n\n@pytest.fixture\ndef queue() -> InMemoryTaskMessageQueue:\n    return InMemoryTaskMessageQueue()\n\n\ndef make_request(id: int = 1, method: str = \"test/method\") -> JSONRPCRequest:\n    return JSONRPCRequest(jsonrpc=\"2.0\", id=id, method=method)\n\n\ndef make_notification(method: str = \"test/notify\") -> JSONRPCNotification:\n    return JSONRPCNotification(jsonrpc=\"2.0\", method=method)\n\n\nclass TestInMemoryTaskMessageQueue:\n    @pytest.mark.anyio\n    async def test_enqueue_and_dequeue(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"Test basic enqueue and dequeue operations.\"\"\"\n        task_id = \"task-1\"\n        msg = QueuedMessage(type=\"request\", message=make_request())\n\n        await queue.enqueue(task_id, msg)\n        result = await queue.dequeue(task_id)\n\n        assert result is not None\n        assert result.type == \"request\"\n        assert result.message.method == \"test/method\"\n\n    @pytest.mark.anyio\n    async def test_dequeue_empty_returns_none(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"Dequeue from empty queue returns None.\"\"\"\n        result = await queue.dequeue(\"nonexistent-task\")\n        assert result is None\n\n    @pytest.mark.anyio\n    async def test_fifo_ordering(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"Messages are dequeued in FIFO order.\"\"\"\n        task_id = \"task-1\"\n\n        await queue.enqueue(task_id, QueuedMessage(type=\"request\", message=make_request(1, \"first\")))\n        await queue.enqueue(task_id, QueuedMessage(type=\"request\", message=make_request(2, \"second\")))\n        await queue.enqueue(task_id, QueuedMessage(type=\"request\", message=make_request(3, \"third\")))\n\n        msg1 = await queue.dequeue(task_id)\n        msg2 = await queue.dequeue(task_id)\n        msg3 = await queue.dequeue(task_id)\n\n        assert msg1 is not None and msg1.message.method == \"first\"\n        assert msg2 is not None and msg2.message.method == \"second\"\n        assert msg3 is not None and msg3.message.method == \"third\"\n\n    @pytest.mark.anyio\n    async def test_separate_queues_per_task(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"Each task has its own queue.\"\"\"\n        await queue.enqueue(\"task-1\", QueuedMessage(type=\"request\", message=make_request(1, \"task1-msg\")))\n        await queue.enqueue(\"task-2\", QueuedMessage(type=\"request\", message=make_request(2, \"task2-msg\")))\n\n        msg1 = await queue.dequeue(\"task-1\")\n        msg2 = await queue.dequeue(\"task-2\")\n\n        assert msg1 is not None and msg1.message.method == \"task1-msg\"\n        assert msg2 is not None and msg2.message.method == \"task2-msg\"\n\n    @pytest.mark.anyio\n    async def test_peek_does_not_remove(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"Peek returns message without removing it.\"\"\"\n        task_id = \"task-1\"\n        await queue.enqueue(task_id, QueuedMessage(type=\"request\", message=make_request()))\n\n        peeked = await queue.peek(task_id)\n        dequeued = await queue.dequeue(task_id)\n\n        assert peeked is not None\n        assert dequeued is not None\n        assert isinstance(peeked.message, JSONRPCRequest)\n        assert isinstance(dequeued.message, JSONRPCRequest)\n        assert peeked.message.id == dequeued.message.id\n\n    @pytest.mark.anyio\n    async def test_is_empty(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"Test is_empty method.\"\"\"\n        task_id = \"task-1\"\n\n        assert await queue.is_empty(task_id) is True\n\n        await queue.enqueue(task_id, QueuedMessage(type=\"notification\", message=make_notification()))\n        assert await queue.is_empty(task_id) is False\n\n        await queue.dequeue(task_id)\n        assert await queue.is_empty(task_id) is True\n\n    @pytest.mark.anyio\n    async def test_clear_returns_all_messages(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"Clear removes and returns all messages.\"\"\"\n        task_id = \"task-1\"\n\n        await queue.enqueue(task_id, QueuedMessage(type=\"request\", message=make_request(1)))\n        await queue.enqueue(task_id, QueuedMessage(type=\"request\", message=make_request(2)))\n        await queue.enqueue(task_id, QueuedMessage(type=\"request\", message=make_request(3)))\n\n        messages = await queue.clear(task_id)\n\n        assert len(messages) == 3\n        assert await queue.is_empty(task_id) is True\n\n    @pytest.mark.anyio\n    async def test_clear_empty_queue(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"Clear on empty queue returns empty list.\"\"\"\n        messages = await queue.clear(\"nonexistent\")\n        assert messages == []\n\n    @pytest.mark.anyio\n    async def test_notification_messages(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"Test queuing notification messages.\"\"\"\n        task_id = \"task-1\"\n        msg = QueuedMessage(type=\"notification\", message=make_notification(\"log/message\"))\n\n        await queue.enqueue(task_id, msg)\n        result = await queue.dequeue(task_id)\n\n        assert result is not None\n        assert result.type == \"notification\"\n        assert result.message.method == \"log/message\"\n\n    @pytest.mark.anyio\n    async def test_message_timestamp(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"Messages have timestamps.\"\"\"\n        before = datetime.now(timezone.utc)\n        msg = QueuedMessage(type=\"request\", message=make_request())\n        after = datetime.now(timezone.utc)\n\n        assert before <= msg.timestamp <= after\n\n    @pytest.mark.anyio\n    async def test_message_with_resolver(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"Messages can have resolvers.\"\"\"\n        task_id = \"task-1\"\n        resolver: Resolver[dict[str, str]] = Resolver()\n\n        msg = QueuedMessage(\n            type=\"request\",\n            message=make_request(),\n            resolver=resolver,\n            original_request_id=42,\n        )\n\n        await queue.enqueue(task_id, msg)\n        result = await queue.dequeue(task_id)\n\n        assert result is not None\n        assert result.resolver is resolver\n        assert result.original_request_id == 42\n\n    @pytest.mark.anyio\n    async def test_cleanup_specific_task(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"Cleanup removes specific task's data.\"\"\"\n        await queue.enqueue(\"task-1\", QueuedMessage(type=\"request\", message=make_request(1)))\n        await queue.enqueue(\"task-2\", QueuedMessage(type=\"request\", message=make_request(2)))\n\n        queue.cleanup(\"task-1\")\n\n        assert await queue.is_empty(\"task-1\") is True\n        assert await queue.is_empty(\"task-2\") is False\n\n    @pytest.mark.anyio\n    async def test_cleanup_all(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"Cleanup without task_id removes all data.\"\"\"\n        await queue.enqueue(\"task-1\", QueuedMessage(type=\"request\", message=make_request(1)))\n        await queue.enqueue(\"task-2\", QueuedMessage(type=\"request\", message=make_request(2)))\n\n        queue.cleanup()\n\n        assert await queue.is_empty(\"task-1\") is True\n        assert await queue.is_empty(\"task-2\") is True\n\n    @pytest.mark.anyio\n    async def test_wait_for_message_returns_immediately_if_message_exists(\n        self, queue: InMemoryTaskMessageQueue\n    ) -> None:\n        \"\"\"wait_for_message returns immediately if queue not empty.\"\"\"\n        task_id = \"task-1\"\n        await queue.enqueue(task_id, QueuedMessage(type=\"request\", message=make_request()))\n\n        # Should return immediately, not block\n        with anyio.fail_after(1):\n            await queue.wait_for_message(task_id)\n\n    @pytest.mark.anyio\n    async def test_wait_for_message_blocks_until_message(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"wait_for_message blocks until a message is enqueued.\"\"\"\n        task_id = \"task-1\"\n        received = False\n        waiter_started = anyio.Event()\n\n        async def enqueue_when_ready() -> None:\n            # Wait until the waiter has started before enqueueing\n            await waiter_started.wait()\n            await queue.enqueue(task_id, QueuedMessage(type=\"request\", message=make_request()))\n\n        async def wait_for_msg() -> None:\n            nonlocal received\n            # Signal that we're about to start waiting\n            waiter_started.set()\n            await queue.wait_for_message(task_id)\n            received = True\n\n        async with anyio.create_task_group() as tg:\n            tg.start_soon(wait_for_msg)\n            tg.start_soon(enqueue_when_ready)\n\n        assert received is True\n\n    @pytest.mark.anyio\n    async def test_notify_message_available_wakes_waiter(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"notify_message_available wakes up waiting coroutines.\"\"\"\n        task_id = \"task-1\"\n        notified = False\n        waiter_started = anyio.Event()\n\n        async def notify_when_ready() -> None:\n            # Wait until the waiter has started before notifying\n            await waiter_started.wait()\n            await queue.notify_message_available(task_id)\n\n        async def wait_for_notification() -> None:\n            nonlocal notified\n            # Signal that we're about to start waiting\n            waiter_started.set()\n            await queue.wait_for_message(task_id)\n            notified = True\n\n        async with anyio.create_task_group() as tg:\n            tg.start_soon(wait_for_notification)\n            tg.start_soon(notify_when_ready)\n\n        assert notified is True\n\n    @pytest.mark.anyio\n    async def test_peek_empty_queue_returns_none(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"Peek on empty queue returns None.\"\"\"\n        result = await queue.peek(\"nonexistent-task\")\n        assert result is None\n\n    @pytest.mark.anyio\n    async def test_wait_for_message_double_check_race_condition(self, queue: InMemoryTaskMessageQueue) -> None:\n        \"\"\"wait_for_message returns early if message arrives after event creation but before wait.\"\"\"\n        task_id = \"task-1\"\n\n        # To test the double-check path (lines 223-225), we need a message to arrive\n        # after the event is created (line 220) but before event.wait() (line 228).\n        # We simulate this by injecting a message before is_empty is called the second time.\n\n        original_is_empty = queue.is_empty\n        call_count = 0\n\n        async def is_empty_with_injection(tid: str) -> bool:\n            nonlocal call_count\n            call_count += 1\n            if call_count == 2 and tid == task_id:\n                # Before second check, inject a message - this simulates a message\n                # arriving between event creation and the double-check\n                queue._queues[task_id] = deque([QueuedMessage(type=\"request\", message=make_request())])\n            return await original_is_empty(tid)\n\n        queue.is_empty = is_empty_with_injection  # type: ignore[method-assign]\n\n        # Should return immediately due to double-check finding the message\n        with anyio.fail_after(1):\n            await queue.wait_for_message(task_id)\n\n\nclass TestResolver:\n    @pytest.mark.anyio\n    async def test_set_result_and_wait(self) -> None:\n        \"\"\"Test basic set_result and wait flow.\"\"\"\n        resolver: Resolver[str] = Resolver()\n\n        resolver.set_result(\"hello\")\n        result = await resolver.wait()\n\n        assert result == \"hello\"\n        assert resolver.done()\n\n    @pytest.mark.anyio\n    async def test_set_exception_and_wait(self) -> None:\n        \"\"\"Test set_exception raises on wait.\"\"\"\n        resolver: Resolver[str] = Resolver()\n\n        resolver.set_exception(ValueError(\"test error\"))\n\n        with pytest.raises(ValueError, match=\"test error\"):\n            await resolver.wait()\n\n        assert resolver.done()\n\n    @pytest.mark.anyio\n    async def test_set_result_when_already_completed_raises(self) -> None:\n        \"\"\"Test that set_result raises if resolver already completed.\"\"\"\n        resolver: Resolver[str] = Resolver()\n        resolver.set_result(\"first\")\n\n        with pytest.raises(RuntimeError, match=\"already completed\"):\n            resolver.set_result(\"second\")\n\n    @pytest.mark.anyio\n    async def test_set_exception_when_already_completed_raises(self) -> None:\n        \"\"\"Test that set_exception raises if resolver already completed.\"\"\"\n        resolver: Resolver[str] = Resolver()\n        resolver.set_result(\"done\")\n\n        with pytest.raises(RuntimeError, match=\"already completed\"):\n            resolver.set_exception(ValueError(\"too late\"))\n\n    @pytest.mark.anyio\n    async def test_done_returns_false_before_completion(self) -> None:\n        \"\"\"Test done() returns False before any result is set.\"\"\"\n        resolver: Resolver[str] = Resolver()\n        assert resolver.done() is False\n"
  },
  {
    "path": "tests/experimental/tasks/test_request_context.py",
    "content": "\"\"\"Tests for the RequestContext.experimental (Experimental class) task validation helpers.\"\"\"\n\nimport pytest\n\nfrom mcp.server.experimental.request_context import Experimental\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.types import (\n    METHOD_NOT_FOUND,\n    TASK_FORBIDDEN,\n    TASK_OPTIONAL,\n    TASK_REQUIRED,\n    ClientCapabilities,\n    ClientTasksCapability,\n    TaskMetadata,\n    Tool,\n    ToolExecution,\n)\n\n\ndef test_is_task_true_when_metadata_present() -> None:\n    exp = Experimental(task_metadata=TaskMetadata(ttl=60000))\n    assert exp.is_task is True\n\n\ndef test_is_task_false_when_no_metadata() -> None:\n    exp = Experimental(task_metadata=None)\n    assert exp.is_task is False\n\n\ndef test_client_supports_tasks_true() -> None:\n    exp = Experimental(_client_capabilities=ClientCapabilities(tasks=ClientTasksCapability()))\n    assert exp.client_supports_tasks is True\n\n\ndef test_client_supports_tasks_false_no_tasks() -> None:\n    exp = Experimental(_client_capabilities=ClientCapabilities())\n    assert exp.client_supports_tasks is False\n\n\ndef test_client_supports_tasks_false_no_capabilities() -> None:\n    exp = Experimental(_client_capabilities=None)\n    assert exp.client_supports_tasks is False\n\n\ndef test_validate_task_mode_required_with_task_is_valid() -> None:\n    exp = Experimental(task_metadata=TaskMetadata(ttl=60000))\n    error = exp.validate_task_mode(TASK_REQUIRED, raise_error=False)\n    assert error is None\n\n\ndef test_validate_task_mode_required_without_task_returns_error() -> None:\n    exp = Experimental(task_metadata=None)\n    error = exp.validate_task_mode(TASK_REQUIRED, raise_error=False)\n    assert error is not None\n    assert error.code == METHOD_NOT_FOUND\n    assert \"requires task-augmented\" in error.message\n\n\ndef test_validate_task_mode_required_without_task_raises_by_default() -> None:\n    exp = Experimental(task_metadata=None)\n    with pytest.raises(MCPError) as exc_info:\n        exp.validate_task_mode(TASK_REQUIRED)\n    assert exc_info.value.error.code == METHOD_NOT_FOUND\n\n\ndef test_validate_task_mode_forbidden_without_task_is_valid() -> None:\n    exp = Experimental(task_metadata=None)\n    error = exp.validate_task_mode(TASK_FORBIDDEN, raise_error=False)\n    assert error is None\n\n\ndef test_validate_task_mode_forbidden_with_task_returns_error() -> None:\n    exp = Experimental(task_metadata=TaskMetadata(ttl=60000))\n    error = exp.validate_task_mode(TASK_FORBIDDEN, raise_error=False)\n    assert error is not None\n    assert error.code == METHOD_NOT_FOUND\n    assert \"does not support task-augmented\" in error.message\n\n\ndef test_validate_task_mode_forbidden_with_task_raises_by_default() -> None:\n    exp = Experimental(task_metadata=TaskMetadata(ttl=60000))\n    with pytest.raises(MCPError) as exc_info:\n        exp.validate_task_mode(TASK_FORBIDDEN)\n    assert exc_info.value.error.code == METHOD_NOT_FOUND\n\n\ndef test_validate_task_mode_none_treated_as_forbidden() -> None:\n    exp = Experimental(task_metadata=TaskMetadata(ttl=60000))\n    error = exp.validate_task_mode(None, raise_error=False)\n    assert error is not None\n    assert \"does not support task-augmented\" in error.message\n\n\ndef test_validate_task_mode_optional_with_task_is_valid() -> None:\n    exp = Experimental(task_metadata=TaskMetadata(ttl=60000))\n    error = exp.validate_task_mode(TASK_OPTIONAL, raise_error=False)\n    assert error is None\n\n\ndef test_validate_task_mode_optional_without_task_is_valid() -> None:\n    exp = Experimental(task_metadata=None)\n    error = exp.validate_task_mode(TASK_OPTIONAL, raise_error=False)\n    assert error is None\n\n\ndef test_validate_for_tool_with_execution_required() -> None:\n    exp = Experimental(task_metadata=None)\n    tool = Tool(\n        name=\"test\",\n        description=\"test\",\n        input_schema={\"type\": \"object\"},\n        execution=ToolExecution(task_support=TASK_REQUIRED),\n    )\n    error = exp.validate_for_tool(tool, raise_error=False)\n    assert error is not None\n    assert \"requires task-augmented\" in error.message\n\n\ndef test_validate_for_tool_without_execution() -> None:\n    exp = Experimental(task_metadata=TaskMetadata(ttl=60000))\n    tool = Tool(\n        name=\"test\",\n        description=\"test\",\n        input_schema={\"type\": \"object\"},\n        execution=None,\n    )\n    error = exp.validate_for_tool(tool, raise_error=False)\n    assert error is not None\n    assert \"does not support task-augmented\" in error.message\n\n\ndef test_validate_for_tool_optional_with_task() -> None:\n    exp = Experimental(task_metadata=TaskMetadata(ttl=60000))\n    tool = Tool(\n        name=\"test\",\n        description=\"test\",\n        input_schema={\"type\": \"object\"},\n        execution=ToolExecution(task_support=TASK_OPTIONAL),\n    )\n    error = exp.validate_for_tool(tool, raise_error=False)\n    assert error is None\n\n\ndef test_can_use_tool_required_with_task_support() -> None:\n    exp = Experimental(_client_capabilities=ClientCapabilities(tasks=ClientTasksCapability()))\n    assert exp.can_use_tool(TASK_REQUIRED) is True\n\n\ndef test_can_use_tool_required_without_task_support() -> None:\n    exp = Experimental(_client_capabilities=ClientCapabilities())\n    assert exp.can_use_tool(TASK_REQUIRED) is False\n\n\ndef test_can_use_tool_optional_without_task_support() -> None:\n    exp = Experimental(_client_capabilities=ClientCapabilities())\n    assert exp.can_use_tool(TASK_OPTIONAL) is True\n\n\ndef test_can_use_tool_forbidden_without_task_support() -> None:\n    exp = Experimental(_client_capabilities=ClientCapabilities())\n    assert exp.can_use_tool(TASK_FORBIDDEN) is True\n\n\ndef test_can_use_tool_none_without_task_support() -> None:\n    exp = Experimental(_client_capabilities=ClientCapabilities())\n    assert exp.can_use_tool(None) is True\n"
  },
  {
    "path": "tests/experimental/tasks/test_spec_compliance.py",
    "content": "\"\"\"Tasks Spec Compliance Tests\n===========================\n\nTest structure mirrors: https://modelcontextprotocol.io/specification/draft/basic/utilities/tasks.md\n\nEach section contains tests for normative requirements (MUST/SHOULD/MAY).\n\"\"\"\n\nfrom datetime import datetime, timezone\n\nimport pytest\n\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.server.lowlevel import NotificationOptions\nfrom mcp.shared.experimental.tasks.helpers import MODEL_IMMEDIATE_RESPONSE_KEY\nfrom mcp.types import (\n    CancelTaskRequestParams,\n    CancelTaskResult,\n    CreateTaskResult,\n    GetTaskRequestParams,\n    GetTaskResult,\n    ListTasksResult,\n    PaginatedRequestParams,\n    ServerCapabilities,\n    Task,\n)\n\n# Shared test datetime\nTEST_DATETIME = datetime(2025, 1, 1, tzinfo=timezone.utc)\n\n\ndef _get_capabilities(server: Server) -> ServerCapabilities:\n    \"\"\"Helper to get capabilities from a server.\"\"\"\n    return server.get_capabilities(\n        notification_options=NotificationOptions(),\n        experimental_capabilities={},\n    )\n\n\ndef test_server_without_task_handlers_has_no_tasks_capability() -> None:\n    \"\"\"Server without any task handlers has no tasks capability.\"\"\"\n    server: Server = Server(\"test\")\n    caps = _get_capabilities(server)\n    assert caps.tasks is None\n\n\nasync def _noop_get_task(ctx: ServerRequestContext, params: GetTaskRequestParams) -> GetTaskResult:\n    raise NotImplementedError\n\n\nasync def _noop_list_tasks(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListTasksResult:\n    raise NotImplementedError\n\n\nasync def _noop_cancel_task(ctx: ServerRequestContext, params: CancelTaskRequestParams) -> CancelTaskResult:\n    raise NotImplementedError\n\n\ndef test_server_with_list_tasks_handler_declares_list_capability() -> None:\n    \"\"\"Server with list_tasks handler declares tasks.list capability.\"\"\"\n    server: Server = Server(\"test\")\n    server.experimental.enable_tasks(on_list_tasks=_noop_list_tasks)\n\n    caps = _get_capabilities(server)\n    assert caps.tasks is not None\n    assert caps.tasks.list is not None\n\n\ndef test_server_with_cancel_task_handler_declares_cancel_capability() -> None:\n    \"\"\"Server with cancel_task handler declares tasks.cancel capability.\"\"\"\n    server: Server = Server(\"test\")\n    server.experimental.enable_tasks(on_cancel_task=_noop_cancel_task)\n\n    caps = _get_capabilities(server)\n    assert caps.tasks is not None\n    assert caps.tasks.cancel is not None\n\n\ndef test_server_with_get_task_handler_declares_requests_tools_call_capability() -> None:\n    \"\"\"Server with get_task handler declares tasks.requests.tools.call capability.\n    (get_task is required for task-augmented tools/call support)\n    \"\"\"\n    server: Server = Server(\"test\")\n    server.experimental.enable_tasks(on_get_task=_noop_get_task)\n\n    caps = _get_capabilities(server)\n    assert caps.tasks is not None\n    assert caps.tasks.requests is not None\n    assert caps.tasks.requests.tools is not None\n\n\n@pytest.mark.skip(\n    reason=\"TODO(maxisbey): enable_tasks registers default handlers for all task methods, \"\n    \"so partial capabilities aren't possible yet. Low-level API should support \"\n    \"selectively enabling/disabling task capabilities.\"\n)\ndef test_server_without_list_handler_has_no_list_capability() -> None:  # pragma: no cover\n    \"\"\"Server without list_tasks handler has no tasks.list capability.\"\"\"\n    server: Server = Server(\"test\")\n    server.experimental.enable_tasks(on_get_task=_noop_get_task)\n\n    caps = _get_capabilities(server)\n    assert caps.tasks is not None\n    assert caps.tasks.list is None\n\n\n@pytest.mark.skip(\n    reason=\"TODO(maxisbey): enable_tasks registers default handlers for all task methods, \"\n    \"so partial capabilities aren't possible yet. Low-level API should support \"\n    \"selectively enabling/disabling task capabilities.\"\n)\ndef test_server_without_cancel_handler_has_no_cancel_capability() -> None:  # pragma: no cover\n    \"\"\"Server without cancel_task handler has no tasks.cancel capability.\"\"\"\n    server: Server = Server(\"test\")\n    server.experimental.enable_tasks(on_get_task=_noop_get_task)\n\n    caps = _get_capabilities(server)\n    assert caps.tasks is not None\n    assert caps.tasks.cancel is None\n\n\ndef test_server_with_all_task_handlers_has_full_capability() -> None:\n    \"\"\"Server with all task handlers declares complete tasks capability.\"\"\"\n    server: Server = Server(\"test\")\n    server.experimental.enable_tasks(\n        on_list_tasks=_noop_list_tasks,\n        on_cancel_task=_noop_cancel_task,\n        on_get_task=_noop_get_task,\n    )\n\n    caps = _get_capabilities(server)\n    assert caps.tasks is not None\n    assert caps.tasks.list is not None\n    assert caps.tasks.cancel is not None\n    assert caps.tasks.requests is not None\n    assert caps.tasks.requests.tools is not None\n\n\nclass TestClientCapabilities:\n    \"\"\"Clients declare:\n    - tasks.list — supports listing operations\n    - tasks.cancel — supports cancellation\n    - tasks.requests.sampling.createMessage — task-augmented sampling\n    - tasks.requests.elicitation.create — task-augmented elicitation\n    \"\"\"\n\n    def test_client_declares_tasks_capability(self) -> None:\n        \"\"\"Client can declare tasks capability.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestToolLevelNegotiation:\n    \"\"\"Tools in tools/list responses include execution.taskSupport with values:\n    - Not present or \"forbidden\": No task augmentation allowed\n    - \"optional\": Task augmentation allowed at requestor discretion\n    - \"required\": Task augmentation is mandatory\n    \"\"\"\n\n    def test_tool_execution_task_forbidden_rejects_task_augmented_call(self) -> None:\n        \"\"\"Tool with execution.taskSupport=\"forbidden\" MUST reject task-augmented calls (-32601).\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tool_execution_task_absent_rejects_task_augmented_call(self) -> None:\n        \"\"\"Tool without execution.taskSupport MUST reject task-augmented calls (-32601).\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tool_execution_task_optional_accepts_normal_call(self) -> None:\n        \"\"\"Tool with execution.taskSupport=\"optional\" accepts normal calls.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tool_execution_task_optional_accepts_task_augmented_call(self) -> None:\n        \"\"\"Tool with execution.taskSupport=\"optional\" accepts task-augmented calls.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tool_execution_task_required_rejects_normal_call(self) -> None:\n        \"\"\"Tool with execution.taskSupport=\"required\" MUST reject non-task calls (-32601).\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tool_execution_task_required_accepts_task_augmented_call(self) -> None:\n        \"\"\"Tool with execution.taskSupport=\"required\" accepts task-augmented calls.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestCapabilityNegotiation:\n    \"\"\"Requestors SHOULD only augment requests with a task if the corresponding\n    capability has been declared by the receiver.\n\n    Receivers that do not declare the task capability for a request type\n    MUST process requests of that type normally, ignoring any task-augmentation\n    metadata if present.\n    \"\"\"\n\n    def test_receiver_without_capability_ignores_task_metadata(self) -> None:\n        \"\"\"Receiver without task capability MUST process request normally,\n        ignoring task-augmentation metadata.\n        \"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_receiver_with_capability_may_require_task_augmentation(self) -> None:\n        \"\"\"Receivers that declare task capability MAY return error (-32600)\n        for non-task-augmented requests, requiring task augmentation.\n        \"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestTaskStatusLifecycle:\n    \"\"\"Tasks begin in working status and follow valid transitions:\n      working → input_required → working → terminal\n      working → terminal (directly)\n      input_required → terminal (directly)\n\n    Terminal states (no further transitions allowed):\n      - completed\n      - failed\n      - cancelled\n    \"\"\"\n\n    def test_task_begins_in_working_status(self) -> None:\n        \"\"\"Tasks MUST begin in working status.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_working_to_completed_transition(self) -> None:\n        \"\"\"working → completed is valid.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_working_to_failed_transition(self) -> None:\n        \"\"\"working → failed is valid.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_working_to_cancelled_transition(self) -> None:\n        \"\"\"working → cancelled is valid.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_working_to_input_required_transition(self) -> None:\n        \"\"\"working → input_required is valid.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_input_required_to_working_transition(self) -> None:\n        \"\"\"input_required → working is valid.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_input_required_to_terminal_transition(self) -> None:\n        \"\"\"input_required → terminal is valid.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_terminal_state_no_further_transitions(self) -> None:\n        \"\"\"Terminal states allow no further transitions.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_completed_is_terminal(self) -> None:\n        \"\"\"completed is a terminal state.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_failed_is_terminal(self) -> None:\n        \"\"\"failed is a terminal state.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_cancelled_is_terminal(self) -> None:\n        \"\"\"cancelled is a terminal state.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestInputRequiredStatus:\n    \"\"\"When a receiver needs information to proceed, it moves the task to input_required.\n    The requestor should call tasks/result to retrieve input requests.\n    The task must include io.modelcontextprotocol/related-task metadata in associated requests.\n    \"\"\"\n\n    def test_input_required_status_retrievable_via_tasks_get(self) -> None:\n        \"\"\"Task in input_required status is retrievable via tasks/get.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_input_required_related_task_metadata_in_requests(self) -> None:\n        \"\"\"Task MUST include io.modelcontextprotocol/related-task metadata\n        in associated requests.\n        \"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestCreatingTask:\n    \"\"\"Request structure:\n      {\"method\": \"tools/call\", \"params\": {\"name\": \"...\", \"arguments\": {...}, \"task\": {\"ttl\": 60000}}}\n\n    Response (CreateTaskResult):\n      {\"result\": {\"task\": {\"taskId\": \"...\", \"status\": \"working\", ...}}}\n\n    Receivers may include io.modelcontextprotocol/model-immediate-response in _meta.\n    \"\"\"\n\n    def test_task_augmented_request_returns_create_task_result(self) -> None:\n        \"\"\"Task-augmented request MUST return CreateTaskResult immediately.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_create_task_result_contains_task_id(self) -> None:\n        \"\"\"CreateTaskResult MUST contain taskId.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_create_task_result_contains_status_working(self) -> None:\n        \"\"\"CreateTaskResult MUST have status=working initially.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_create_task_result_contains_created_at(self) -> None:\n        \"\"\"CreateTaskResult MUST contain createdAt timestamp.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_create_task_result_created_at_is_iso8601(self) -> None:\n        \"\"\"createdAt MUST be ISO 8601 formatted.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_create_task_result_may_contain_ttl(self) -> None:\n        \"\"\"CreateTaskResult MAY contain ttl.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_create_task_result_may_contain_poll_interval(self) -> None:\n        \"\"\"CreateTaskResult MAY contain pollInterval.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_create_task_result_may_contain_status_message(self) -> None:\n        \"\"\"CreateTaskResult MAY contain statusMessage.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_receiver_may_override_requested_ttl(self) -> None:\n        \"\"\"Receiver MAY override requested ttl but MUST return actual value.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_model_immediate_response_in_meta(self) -> None:\n        \"\"\"Receiver MAY include io.modelcontextprotocol/model-immediate-response\n        in _meta to provide immediate response while task executes.\n        \"\"\"\n        # Verify the constant has the correct value per spec\n        assert MODEL_IMMEDIATE_RESPONSE_KEY == \"io.modelcontextprotocol/model-immediate-response\"\n\n        # CreateTaskResult can include model-immediate-response in _meta\n        task = Task(\n            task_id=\"test-123\",\n            status=\"working\",\n            created_at=TEST_DATETIME,\n            last_updated_at=TEST_DATETIME,\n            ttl=60000,\n        )\n        immediate_msg = \"Task started, processing your request...\"\n        # Note: Must use _meta= (alias) not meta= due to Pydantic alias handling\n        result = CreateTaskResult(\n            task=task,\n            **{\"_meta\": {MODEL_IMMEDIATE_RESPONSE_KEY: immediate_msg}},\n        )\n\n        # Verify the metadata is present and correct\n        assert result.meta is not None\n        assert MODEL_IMMEDIATE_RESPONSE_KEY in result.meta\n        assert result.meta[MODEL_IMMEDIATE_RESPONSE_KEY] == immediate_msg\n\n        # Verify it serializes correctly with _meta alias\n        serialized = result.model_dump(by_alias=True)\n        assert \"_meta\" in serialized\n        assert MODEL_IMMEDIATE_RESPONSE_KEY in serialized[\"_meta\"]\n        assert serialized[\"_meta\"][MODEL_IMMEDIATE_RESPONSE_KEY] == immediate_msg\n\n\nclass TestGettingTaskStatus:\n    \"\"\"Request: {\"method\": \"tasks/get\", \"params\": {\"taskId\": \"...\"}}\n    Response: Returns full Task object with current status and pollInterval.\n    \"\"\"\n\n    def test_tasks_get_returns_task_object(self) -> None:\n        \"\"\"tasks/get MUST return full Task object.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_get_returns_current_status(self) -> None:\n        \"\"\"tasks/get MUST return current status.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_get_may_return_poll_interval(self) -> None:\n        \"\"\"tasks/get MAY return pollInterval.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_get_invalid_task_id_returns_error(self) -> None:\n        \"\"\"tasks/get with invalid taskId MUST return -32602.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_get_nonexistent_task_id_returns_error(self) -> None:\n        \"\"\"tasks/get with nonexistent taskId MUST return -32602.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestRetrievingResults:\n    \"\"\"Request: {\"method\": \"tasks/result\", \"params\": {\"taskId\": \"...\"}}\n    Response: The actual operation result structure (e.g., CallToolResult).\n\n    This call blocks until terminal status.\n    \"\"\"\n\n    def test_tasks_result_returns_underlying_result(self) -> None:\n        \"\"\"tasks/result MUST return exactly what underlying request would return.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_result_blocks_until_terminal(self) -> None:\n        \"\"\"tasks/result MUST block for non-terminal tasks.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_result_unblocks_on_terminal(self) -> None:\n        \"\"\"tasks/result MUST unblock upon reaching terminal status.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_result_includes_related_task_metadata(self) -> None:\n        \"\"\"tasks/result MUST include io.modelcontextprotocol/related-task in _meta.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_result_returns_error_for_failed_task(self) -> None:\n        \"\"\"tasks/result returns the same error the underlying request\n        would have produced for failed tasks.\n        \"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_result_invalid_task_id_returns_error(self) -> None:\n        \"\"\"tasks/result with invalid taskId MUST return -32602.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestListingTasks:\n    \"\"\"Request: {\"method\": \"tasks/list\", \"params\": {\"cursor\": \"optional\"}}\n    Response: Array of tasks with pagination support via nextCursor.\n    \"\"\"\n\n    def test_tasks_list_returns_array_of_tasks(self) -> None:\n        \"\"\"tasks/list MUST return array of tasks.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_list_pagination_with_cursor(self) -> None:\n        \"\"\"tasks/list supports pagination via cursor.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_list_returns_next_cursor_when_more_results(self) -> None:\n        \"\"\"tasks/list MUST return nextCursor when more results available.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_list_cursors_are_opaque(self) -> None:\n        \"\"\"Implementers MUST treat cursors as opaque tokens.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_list_invalid_cursor_returns_error(self) -> None:\n        \"\"\"tasks/list with invalid cursor MUST return -32602.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestCancellingTasks:\n    \"\"\"Request: {\"method\": \"tasks/cancel\", \"params\": {\"taskId\": \"...\"}}\n    Response: Returns the task object with status: \"cancelled\".\n    \"\"\"\n\n    def test_tasks_cancel_returns_cancelled_task(self) -> None:\n        \"\"\"tasks/cancel MUST return task with status=cancelled.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_cancel_terminal_task_returns_error(self) -> None:\n        \"\"\"Cancelling already-terminal task MUST return -32602.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_cancel_completed_task_returns_error(self) -> None:\n        \"\"\"Cancelling completed task MUST return -32602.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_cancel_failed_task_returns_error(self) -> None:\n        \"\"\"Cancelling failed task MUST return -32602.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_cancel_already_cancelled_task_returns_error(self) -> None:\n        \"\"\"Cancelling already-cancelled task MUST return -32602.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_cancel_invalid_task_id_returns_error(self) -> None:\n        \"\"\"tasks/cancel with invalid taskId MUST return -32602.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestStatusNotifications:\n    \"\"\"Receivers MAY send: {\"method\": \"notifications/tasks/status\", \"params\": {...}}\n    These are optional; requestors MUST NOT rely on them and SHOULD continue polling.\n    \"\"\"\n\n    def test_receiver_may_send_status_notification(self) -> None:\n        \"\"\"Receiver MAY send notifications/tasks/status.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_status_notification_contains_task_id(self) -> None:\n        \"\"\"Status notification MUST contain taskId.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_status_notification_contains_status(self) -> None:\n        \"\"\"Status notification MUST contain status.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestTaskManagement:\n    \"\"\"- Receivers generate unique task IDs as strings\n    - Tasks must begin in working status\n    - createdAt timestamps must be ISO 8601 formatted\n    - Receivers may override requested ttl but must return actual value\n    - Receivers may delete tasks after TTL expires\n    - All task-related messages must include io.modelcontextprotocol/related-task\n      in _meta except for tasks/get, tasks/list, tasks/cancel operations\n    \"\"\"\n\n    def test_task_ids_are_unique_strings(self) -> None:\n        \"\"\"Receivers MUST generate unique task IDs as strings.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_multiple_tasks_have_unique_ids(self) -> None:\n        \"\"\"Multiple tasks MUST have unique IDs.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_receiver_may_delete_tasks_after_ttl(self) -> None:\n        \"\"\"Receivers MAY delete tasks after TTL expires.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_related_task_metadata_in_task_messages(self) -> None:\n        \"\"\"All task-related messages MUST include io.modelcontextprotocol/related-task\n        in _meta.\n        \"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_get_does_not_require_related_task_metadata(self) -> None:\n        \"\"\"tasks/get does not require related-task metadata.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_list_does_not_require_related_task_metadata(self) -> None:\n        \"\"\"tasks/list does not require related-task metadata.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_cancel_does_not_require_related_task_metadata(self) -> None:\n        \"\"\"tasks/cancel does not require related-task metadata.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestResultHandling:\n    \"\"\"- Receivers must return CreateTaskResult immediately upon accepting task-augmented requests\n    - tasks/result must return exactly what the underlying request would return\n    - tasks/result blocks for non-terminal tasks; must unblock upon reaching terminal status\n    \"\"\"\n\n    def test_create_task_result_returned_immediately(self) -> None:\n        \"\"\"Receiver MUST return CreateTaskResult immediately (not after work completes).\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_result_matches_underlying_result_structure(self) -> None:\n        \"\"\"tasks/result MUST return same structure as underlying request.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_result_for_tool_call_returns_call_tool_result(self) -> None:\n        \"\"\"tasks/result for tools/call returns CallToolResult.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestProgressTracking:\n    \"\"\"Task-augmented requests support progress notifications using the progressToken\n    mechanism, which remains valid throughout the task lifetime.\n    \"\"\"\n\n    def test_progress_token_valid_throughout_task_lifetime(self) -> None:\n        \"\"\"progressToken remains valid throughout task lifetime.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_progress_notifications_sent_during_task_execution(self) -> None:\n        \"\"\"Progress notifications can be sent during task execution.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestProtocolErrors:\n    \"\"\"Protocol Errors (JSON-RPC standard codes):\n    - -32600 (Invalid request): Non-task requests to endpoint requiring task augmentation\n    - -32602 (Invalid params): Invalid/nonexistent taskId, invalid cursor, cancel terminal task\n    - -32603 (Internal error): Server-side execution failures\n    \"\"\"\n\n    def test_invalid_request_for_required_task_augmentation(self) -> None:\n        \"\"\"Non-task request to task-required endpoint returns -32600.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_invalid_params_for_invalid_task_id(self) -> None:\n        \"\"\"Invalid taskId returns -32602.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_invalid_params_for_nonexistent_task_id(self) -> None:\n        \"\"\"Nonexistent taskId returns -32602.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_invalid_params_for_invalid_cursor(self) -> None:\n        \"\"\"Invalid cursor in tasks/list returns -32602.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_invalid_params_for_cancel_terminal_task(self) -> None:\n        \"\"\"Attempt to cancel terminal task returns -32602.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_internal_error_for_server_failure(self) -> None:\n        \"\"\"Server-side execution failure returns -32603.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestTaskExecutionErrors:\n    \"\"\"When underlying requests fail, the task moves to failed status.\n    - tasks/get response should include statusMessage explaining failure\n    - tasks/result returns same error the underlying request would have produced\n    - For tool calls, isError: true moves task to failed status\n    \"\"\"\n\n    def test_underlying_failure_moves_task_to_failed(self) -> None:\n        \"\"\"Underlying request failure moves task to failed status.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_failed_task_has_status_message(self) -> None:\n        \"\"\"Failed task SHOULD include statusMessage explaining failure.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tasks_result_returns_underlying_error(self) -> None:\n        \"\"\"tasks/result returns same error underlying request would produce.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_tool_call_is_error_true_moves_to_failed(self) -> None:\n        \"\"\"Tool call with isError: true moves task to failed status.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestTaskObject:\n    \"\"\"Task Object fields:\n    - taskId: String identifier\n    - status: Current execution state\n    - statusMessage: Optional human-readable description\n    - createdAt: ISO 8601 timestamp of creation\n    - ttl: Milliseconds before potential deletion\n    - pollInterval: Suggested milliseconds between polls\n    \"\"\"\n\n    def test_task_has_task_id_string(self) -> None:\n        \"\"\"Task MUST have taskId as string.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_task_has_status(self) -> None:\n        \"\"\"Task MUST have status.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_task_status_message_is_optional(self) -> None:\n        \"\"\"Task statusMessage is optional.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_task_has_created_at(self) -> None:\n        \"\"\"Task MUST have createdAt.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_task_ttl_is_optional(self) -> None:\n        \"\"\"Task ttl is optional.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_task_poll_interval_is_optional(self) -> None:\n        \"\"\"Task pollInterval is optional.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestRelatedTaskMetadata:\n    \"\"\"Related Task Metadata structure:\n    {\"_meta\": {\"io.modelcontextprotocol/related-task\": {\"taskId\": \"...\"}}}\n    \"\"\"\n\n    def test_related_task_metadata_structure(self) -> None:\n        \"\"\"Related task metadata has correct structure.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_related_task_metadata_contains_task_id(self) -> None:\n        \"\"\"Related task metadata contains taskId.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestAccessAndIsolation:\n    \"\"\"- Task IDs enable access to sensitive results\n    - Authorization context binding is essential where available\n    - For non-authorized environments: strong entropy IDs, strict TTL limits\n    \"\"\"\n\n    def test_task_bound_to_authorization_context(self) -> None:\n        \"\"\"Receivers receiving authorization context MUST bind tasks to that context.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_reject_task_operations_outside_authorization_context(self) -> None:\n        \"\"\"Receivers MUST reject task operations for tasks outside\n        requestor's authorization context.\n        \"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_non_authorized_environments_use_secure_ids(self) -> None:\n        \"\"\"For non-authorized environments, receivers SHOULD use\n        cryptographically secure IDs.\n        \"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_non_authorized_environments_use_shorter_ttls(self) -> None:\n        \"\"\"For non-authorized environments, receivers SHOULD use shorter TTLs.\"\"\"\n        pytest.skip(\"TODO\")\n\n\nclass TestResourceLimits:\n    \"\"\"Receivers should:\n    - Enforce concurrent task limits per requestor\n    - Implement maximum TTL constraints\n    - Clean up expired tasks promptly\n    \"\"\"\n\n    def test_concurrent_task_limit_enforced(self) -> None:\n        \"\"\"Receiver SHOULD enforce concurrent task limits per requestor.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_maximum_ttl_constraint_enforced(self) -> None:\n        \"\"\"Receiver SHOULD implement maximum TTL constraints.\"\"\"\n        pytest.skip(\"TODO\")\n\n    def test_expired_tasks_cleaned_up(self) -> None:\n        \"\"\"Receiver SHOULD clean up expired tasks promptly.\"\"\"\n        pytest.skip(\"TODO\")\n"
  },
  {
    "path": "tests/issues/test_100_tool_listing.py",
    "content": "import pytest\n\nfrom mcp.server.mcpserver import MCPServer\n\npytestmark = pytest.mark.anyio\n\n\nasync def test_list_tools_returns_all_tools():\n    mcp = MCPServer(\"TestTools\")\n\n    # Create 100 tools with unique names\n    num_tools = 100\n    for i in range(num_tools):\n\n        @mcp.tool(name=f\"tool_{i}\")\n        def dummy_tool_func():  # pragma: no cover\n            f\"\"\"Tool number {i}\"\"\"\n            return i\n\n        globals()[f\"dummy_tool_{i}\"] = dummy_tool_func  # Keep reference to avoid garbage collection\n\n    # Get all tools\n    tools = await mcp.list_tools()\n\n    # Verify we get all tools\n    assert len(tools) == num_tools, f\"Expected {num_tools} tools, but got {len(tools)}\"\n\n    # Verify each tool is unique and has the correct name\n    tool_names = [tool.name for tool in tools]\n    expected_names = [f\"tool_{i}\" for i in range(num_tools)]\n    assert sorted(tool_names) == sorted(expected_names), \"Tool names don't match expected names\"\n"
  },
  {
    "path": "tests/issues/test_1027_win_unreachable_cleanup.py",
    "content": "\"\"\"Regression test for issue #1027: Ensure cleanup procedures run properly during shutdown\n\nIssue #1027 reported that cleanup code after \"yield\" in lifespan was unreachable when\nprocesses were terminated. This has been fixed by implementing the MCP spec-compliant\nstdio shutdown sequence that closes stdin first, allowing graceful exit.\n\nThese tests verify the fix continues to work correctly across all platforms.\n\"\"\"\n\nimport sys\nimport tempfile\nimport textwrap\nfrom pathlib import Path\n\nimport anyio\nimport pytest\n\nfrom mcp import ClientSession, StdioServerParameters\nfrom mcp.client.stdio import _create_platform_compatible_process, stdio_client\nfrom tests.shared.test_win32_utils import escape_path_for_python\n\n\n@pytest.mark.anyio\nasync def test_lifespan_cleanup_executed():\n    \"\"\"Regression test ensuring MCP server cleanup code runs during shutdown.\n\n    This test verifies that the fix for issue #1027 works correctly by:\n    1. Starting an MCP server that writes a marker file on startup\n    2. Shutting down the server normally via stdio_client\n    3. Verifying the cleanup code (after yield) executed and wrote its marker file\n\n    The fix implements proper stdin closure before termination, giving servers\n    time to run their cleanup handlers.\n    \"\"\"\n\n    # Create marker files to track server lifecycle\n    with tempfile.NamedTemporaryFile(mode=\"w\", delete=False, suffix=\".txt\") as f:\n        startup_marker = f.name\n    with tempfile.NamedTemporaryFile(mode=\"w\", delete=False, suffix=\".txt\") as f:\n        cleanup_marker = f.name\n\n    # Remove the files so we can detect when they're created\n    Path(startup_marker).unlink()\n    Path(cleanup_marker).unlink()\n\n    # Create a minimal MCP server using MCPServer that tracks lifecycle\n    server_code = textwrap.dedent(f\"\"\"\n        import asyncio\n        import sys\n        from pathlib import Path\n        from contextlib import asynccontextmanager\n        from mcp.server.mcpserver import MCPServer\n\n        STARTUP_MARKER = {escape_path_for_python(startup_marker)}\n        CLEANUP_MARKER = {escape_path_for_python(cleanup_marker)}\n\n        @asynccontextmanager\n        async def lifespan(server):\n            # Write startup marker\n            Path(STARTUP_MARKER).write_text(\"started\")\n            try:\n                yield {{\"started\": True}}\n            finally:\n                # This cleanup code now runs properly during shutdown\n                Path(CLEANUP_MARKER).write_text(\"cleaned up\")\n\n        mcp = MCPServer(\"test-server\", lifespan=lifespan)\n\n        @mcp.tool()\n        def echo(text: str) -> str:\n            return text\n\n        if __name__ == \"__main__\":\n            mcp.run()\n    \"\"\")\n\n    # Write the server script to a temporary file\n    with tempfile.NamedTemporaryFile(mode=\"w\", delete=False, suffix=\".py\") as f:\n        server_script = f.name\n        f.write(server_code)\n\n    try:\n        # Launch the MCP server\n        params = StdioServerParameters(command=sys.executable, args=[server_script])\n\n        async with stdio_client(params) as (read, write):\n            async with ClientSession(read, write) as session:\n                # Initialize the session\n                result = await session.initialize()\n                assert result.protocol_version in [\"2024-11-05\", \"2025-06-18\", \"2025-11-25\"]\n\n                # Verify startup marker was created\n                assert Path(startup_marker).exists(), \"Server startup marker not created\"\n                assert Path(startup_marker).read_text() == \"started\"\n\n                # Make a test request to ensure server is working\n                response = await session.call_tool(\"echo\", {\"text\": \"hello\"})\n                assert response.content[0].type == \"text\"\n                assert getattr(response.content[0], \"text\") == \"hello\"\n\n                # Session will be closed when exiting the context manager\n\n        # Give server a moment to complete cleanup\n        with anyio.move_on_after(5.0):\n            while not Path(cleanup_marker).exists():  # pragma: lax no cover\n                await anyio.sleep(0.1)\n\n        # Verify cleanup marker was created - this works now that stdio_client\n        # properly closes stdin before termination, allowing graceful shutdown\n        assert Path(cleanup_marker).exists(), \"Server cleanup marker not created - regression in issue #1027 fix\"\n        assert Path(cleanup_marker).read_text() == \"cleaned up\"\n\n    finally:\n        # Clean up files\n        for path in [server_script, startup_marker, cleanup_marker]:\n            try:  # pragma: lax no cover\n                Path(path).unlink()\n            except FileNotFoundError:  # pragma: lax no cover\n                pass\n\n\n@pytest.mark.anyio\n@pytest.mark.filterwarnings(\"ignore::ResourceWarning\" if sys.platform == \"win32\" else \"default\")\nasync def test_stdin_close_triggers_cleanup():\n    \"\"\"Regression test verifying the stdin-based graceful shutdown mechanism.\n\n    This test ensures the core fix for issue #1027 continues to work by:\n    1. Manually managing a server process\n    2. Closing stdin to trigger graceful shutdown\n    3. Verifying cleanup handlers run before the process exits\n\n    This mimics the behavior now implemented in stdio_client's shutdown sequence.\n\n    Note on Windows ResourceWarning:\n    On Windows, we may see ResourceWarning about unclosed file descriptors.\n    This is expected behavior because:\n    - We're manually managing the process lifecycle\n    - Windows file handle cleanup works differently than Unix\n    - The warning doesn't indicate a real issue - cleanup still works\n    We filter this warning on Windows only to avoid test noise.\n    \"\"\"\n\n    # Create marker files to track server lifecycle\n    with tempfile.NamedTemporaryFile(mode=\"w\", delete=False, suffix=\".txt\") as f:\n        startup_marker = f.name\n    with tempfile.NamedTemporaryFile(mode=\"w\", delete=False, suffix=\".txt\") as f:\n        cleanup_marker = f.name\n\n    # Remove the files so we can detect when they're created\n    Path(startup_marker).unlink()\n    Path(cleanup_marker).unlink()\n\n    # Create an MCP server that handles stdin closure gracefully\n    server_code = textwrap.dedent(f\"\"\"\n        import asyncio\n        import sys\n        from pathlib import Path\n        from contextlib import asynccontextmanager\n        from mcp.server.mcpserver import MCPServer\n\n        STARTUP_MARKER = {escape_path_for_python(startup_marker)}\n        CLEANUP_MARKER = {escape_path_for_python(cleanup_marker)}\n\n        @asynccontextmanager\n        async def lifespan(server):\n            # Write startup marker\n            Path(STARTUP_MARKER).write_text(\"started\")\n            try:\n                yield {{\"started\": True}}\n            finally:\n                # This cleanup code runs when stdin closes, enabling graceful shutdown\n                Path(CLEANUP_MARKER).write_text(\"cleaned up\")\n\n        mcp = MCPServer(\"test-server\", lifespan=lifespan)\n\n        @mcp.tool()\n        def echo(text: str) -> str:\n            return text\n\n        if __name__ == \"__main__\":\n            # The server should exit gracefully when stdin closes\n            try:\n                mcp.run()\n            except Exception:\n                # Server might get EOF or other errors when stdin closes\n                pass\n    \"\"\")\n\n    # Write the server script to a temporary file\n    with tempfile.NamedTemporaryFile(mode=\"w\", delete=False, suffix=\".py\") as f:\n        server_script = f.name\n        f.write(server_code)\n\n    try:\n        # This test manually manages the process to verify stdin-based shutdown\n        # Start the server process\n        process = await _create_platform_compatible_process(\n            command=sys.executable, args=[server_script], env=None, errlog=sys.stderr, cwd=None\n        )\n\n        # Wait for server to start\n        with anyio.move_on_after(10.0):\n            while not Path(startup_marker).exists():\n                await anyio.sleep(0.1)\n\n        # Check if process is still running\n        if hasattr(process, \"returncode\") and process.returncode is not None:  # pragma: lax no cover\n            pytest.fail(f\"Server process exited with code {process.returncode}\")\n\n        assert Path(startup_marker).exists(), \"Server startup marker not created\"\n\n        # Close stdin to signal shutdown\n        if process.stdin:  # pragma: no branch\n            await process.stdin.aclose()\n\n        # Wait for process to exit gracefully\n        try:\n            with anyio.fail_after(5.0):  # Increased from 2.0 to 5.0\n                await process.wait()\n        except TimeoutError:  # pragma: lax no cover\n            # If it doesn't exit after stdin close, terminate it\n            process.terminate()\n            await process.wait()\n\n        # Check if cleanup ran\n        with anyio.move_on_after(5.0):\n            while not Path(cleanup_marker).exists():  # pragma: lax no cover\n                await anyio.sleep(0.1)\n\n        # Verify the cleanup ran - stdin closure enables graceful shutdown\n        assert Path(cleanup_marker).exists(), \"Server cleanup marker not created - stdin-based shutdown failed\"\n        assert Path(cleanup_marker).read_text() == \"cleaned up\"\n\n    finally:\n        # Clean up files\n        for path in [server_script, startup_marker, cleanup_marker]:\n            try:  # pragma: lax no cover\n                Path(path).unlink()\n            except FileNotFoundError:  # pragma: lax no cover\n                pass\n"
  },
  {
    "path": "tests/issues/test_129_resource_templates.py",
    "content": "import pytest\n\nfrom mcp import Client\nfrom mcp.server.mcpserver import MCPServer\n\n\n@pytest.mark.anyio\nasync def test_resource_templates():\n    mcp = MCPServer(\"Demo\")\n\n    @mcp.resource(\"greeting://{name}\")\n    def get_greeting(name: str) -> str:  # pragma: no cover\n        \"\"\"Get a personalized greeting\"\"\"\n        return f\"Hello, {name}!\"\n\n    @mcp.resource(\"users://{user_id}/profile\")\n    def get_user_profile(user_id: str) -> str:  # pragma: no cover\n        \"\"\"Dynamic user data\"\"\"\n        return f\"Profile data for user {user_id}\"\n\n    async with Client(mcp) as client:\n        result = await client.list_resource_templates()\n        templates = result.resource_templates\n\n        assert len(templates) == 2\n\n        greeting_template = next(t for t in templates if t.name == \"get_greeting\")\n        assert greeting_template.uri_template == \"greeting://{name}\"\n        assert greeting_template.description == \"Get a personalized greeting\"\n\n        profile_template = next(t for t in templates if t.name == \"get_user_profile\")\n        assert profile_template.uri_template == \"users://{user_id}/profile\"\n        assert profile_template.description == \"Dynamic user data\"\n"
  },
  {
    "path": "tests/issues/test_1338_icons_and_metadata.py",
    "content": "\"\"\"Test icon and metadata support (SEP-973).\"\"\"\n\nimport pytest\n\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.types import Icon\n\npytestmark = pytest.mark.anyio\n\n\nasync def test_icons_and_website_url():\n    \"\"\"Test that icons and websiteUrl are properly returned in API calls.\"\"\"\n\n    # Create test icon\n    test_icon = Icon(\n        src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==\",\n        mime_type=\"image/png\",\n        sizes=[\"1x1\"],\n    )\n\n    # Create server with website URL and icon\n    mcp = MCPServer(\"TestServer\", website_url=\"https://example.com\", icons=[test_icon])\n\n    # Create tool with icon\n    @mcp.tool(icons=[test_icon])\n    def test_tool(message: str) -> str:  # pragma: no cover\n        \"\"\"A test tool with an icon.\"\"\"\n        return message\n\n    # Create resource with icon\n    @mcp.resource(\"test://resource\", icons=[test_icon])\n    def test_resource() -> str:  # pragma: no cover\n        \"\"\"A test resource with an icon.\"\"\"\n        return \"test content\"\n\n    # Create prompt with icon\n    @mcp.prompt(\"test_prompt\", icons=[test_icon])\n    def test_prompt(text: str) -> str:  # pragma: no cover\n        \"\"\"A test prompt with an icon.\"\"\"\n        return text\n\n    # Create resource template with icon\n    @mcp.resource(\"test://weather/{city}\", icons=[test_icon])\n    def test_resource_template(city: str) -> str:  # pragma: no cover\n        \"\"\"Get weather for a city.\"\"\"\n        return f\"Weather for {city}\"\n\n    # Test server metadata includes websiteUrl and icons\n    assert mcp.name == \"TestServer\"\n    assert mcp.website_url == \"https://example.com\"\n    assert mcp.icons is not None\n    assert len(mcp.icons) == 1\n    assert mcp.icons[0].src == test_icon.src\n    assert mcp.icons[0].mime_type == test_icon.mime_type\n    assert mcp.icons[0].sizes == test_icon.sizes\n\n    # Test tool includes icon\n    tools = await mcp.list_tools()\n    assert len(tools) == 1\n    tool = tools[0]\n    assert tool.name == \"test_tool\"\n    assert tool.icons is not None\n    assert len(tool.icons) == 1\n    assert tool.icons[0].src == test_icon.src\n\n    # Test resource includes icon\n    resources = await mcp.list_resources()\n    assert len(resources) == 1\n    resource = resources[0]\n    assert str(resource.uri) == \"test://resource\"\n    assert resource.icons is not None\n    assert len(resource.icons) == 1\n    assert resource.icons[0].src == test_icon.src\n\n    # Test prompt includes icon\n    prompts = await mcp.list_prompts()\n    assert len(prompts) == 1\n    prompt = prompts[0]\n    assert prompt.name == \"test_prompt\"\n    assert prompt.icons is not None\n    assert len(prompt.icons) == 1\n    assert prompt.icons[0].src == test_icon.src\n\n    # Test resource template includes icon\n    templates = await mcp.list_resource_templates()\n    assert len(templates) == 1\n    template = templates[0]\n    assert template.name == \"test_resource_template\"\n    assert template.uri_template == \"test://weather/{city}\"\n    assert template.icons is not None\n    assert len(template.icons) == 1\n    assert template.icons[0].src == test_icon.src\n\n\nasync def test_multiple_icons():\n    \"\"\"Test that multiple icons can be added to tools, resources, and prompts.\"\"\"\n\n    # Create multiple test icons\n    icon1 = Icon(src=\"data:image/png;base64,icon1\", mime_type=\"image/png\", sizes=[\"16x16\"])\n    icon2 = Icon(src=\"data:image/png;base64,icon2\", mime_type=\"image/png\", sizes=[\"32x32\"])\n    icon3 = Icon(src=\"data:image/png;base64,icon3\", mime_type=\"image/png\", sizes=[\"64x64\"])\n\n    mcp = MCPServer(\"MultiIconServer\")\n\n    # Create tool with multiple icons\n    @mcp.tool(icons=[icon1, icon2, icon3])\n    def multi_icon_tool() -> str:  # pragma: no cover\n        \"\"\"A tool with multiple icons.\"\"\"\n        return \"success\"\n\n    # Test tool has all icons\n    tools = await mcp.list_tools()\n    assert len(tools) == 1\n    tool = tools[0]\n    assert tool.icons is not None\n    assert len(tool.icons) == 3\n    assert tool.icons[0].sizes == [\"16x16\"]\n    assert tool.icons[1].sizes == [\"32x32\"]\n    assert tool.icons[2].sizes == [\"64x64\"]\n\n\nasync def test_no_icons_or_website():\n    \"\"\"Test that server works without icons or websiteUrl.\"\"\"\n\n    mcp = MCPServer(\"BasicServer\")\n\n    @mcp.tool()\n    def basic_tool() -> str:  # pragma: no cover\n        \"\"\"A basic tool without icons.\"\"\"\n        return \"success\"\n\n    # Test server metadata has no websiteUrl or icons\n    assert mcp.name == \"BasicServer\"\n    assert mcp.website_url is None\n    assert mcp.icons is None\n\n    # Test tool has no icons\n    tools = await mcp.list_tools()\n    assert len(tools) == 1\n    tool = tools[0]\n    assert tool.name == \"basic_tool\"\n    assert tool.icons is None\n"
  },
  {
    "path": "tests/issues/test_1363_race_condition_streamable_http.py",
    "content": "\"\"\"Test for issue #1363 - Race condition in StreamableHTTP transport causes ClosedResourceError.\n\nThis test reproduces the race condition described in issue #1363 where MCP servers\nin HTTP Streamable mode experience ClosedResourceError exceptions when requests\nfail validation early (e.g., due to incorrect Accept headers).\n\nThe race condition occurs because:\n1. Transport setup creates a message_router task\n2. Message router enters async for write_stream_reader loop\n3. write_stream_reader calls checkpoint() in receive(), yielding control\n4. Request handling processes HTTP request\n5. If validation fails early, request returns immediately\n6. Transport termination closes all streams including write_stream_reader\n7. Message router may still be in checkpoint() yield and hasn't returned to check stream state\n8. When message router resumes, it encounters a closed stream, raising ClosedResourceError\n\"\"\"\n\nimport logging\nimport threading\nfrom collections.abc import AsyncGenerator\nfrom contextlib import asynccontextmanager\n\nimport anyio\nimport httpx\nimport pytest\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\n\nfrom mcp.server import Server\nfrom mcp.server.streamable_http_manager import StreamableHTTPSessionManager\n\nSERVER_NAME = \"test_race_condition_server\"\n\n\nclass RaceConditionTestServer(Server):\n    def __init__(self):\n        super().__init__(SERVER_NAME)\n\n\ndef create_app(json_response: bool = False) -> Starlette:\n    \"\"\"Create a Starlette application for testing.\"\"\"\n    app = RaceConditionTestServer()\n\n    # Create session manager\n    session_manager = StreamableHTTPSessionManager(\n        app=app,\n        json_response=json_response,\n        stateless=True,  # Use stateless mode to trigger the race condition\n    )\n\n    # Create Starlette app with lifespan\n    @asynccontextmanager\n    async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:\n        async with session_manager.run():\n            yield\n\n    routes = [\n        Mount(\"/\", app=session_manager.handle_request),\n    ]\n\n    return Starlette(routes=routes, lifespan=lifespan)\n\n\nclass ServerThread(threading.Thread):\n    \"\"\"Thread that runs the ASGI application lifespan in a separate event loop.\"\"\"\n\n    def __init__(self, app: Starlette):\n        super().__init__(daemon=True)\n        self.app = app\n        self._stop_event = threading.Event()\n\n    def run(self) -> None:\n        \"\"\"Run the lifespan in a new event loop.\"\"\"\n\n        # Create a new event loop for this thread\n        async def run_lifespan():\n            # Use the lifespan context (always present in our tests)\n            lifespan_context = getattr(self.app.router, \"lifespan_context\", None)\n            assert lifespan_context is not None  # Tests always create apps with lifespan\n            async with lifespan_context(self.app):\n                # Wait until stop is requested\n                while not self._stop_event.is_set():\n                    await anyio.sleep(0.1)\n\n        anyio.run(run_lifespan)\n\n    def stop(self) -> None:\n        \"\"\"Signal the thread to stop.\"\"\"\n        self._stop_event.set()\n\n\ndef check_logs_for_race_condition_errors(caplog: pytest.LogCaptureFixture, test_name: str) -> None:\n    \"\"\"Check logs for ClosedResourceError and other race condition errors.\n\n    Args:\n        caplog: pytest log capture fixture\n        test_name: Name of the test for better error messages\n    \"\"\"\n    # Check for specific race condition errors in logs\n    errors_found: list[str] = []\n\n    for record in caplog.records:  # pragma: lax no cover\n        message = record.getMessage()\n        if \"ClosedResourceError\" in message:\n            errors_found.append(\"ClosedResourceError\")\n        if \"Error in message router\" in message:\n            errors_found.append(\"Error in message router\")\n        if \"anyio.ClosedResourceError\" in message:\n            errors_found.append(\"anyio.ClosedResourceError\")\n\n    # Assert no race condition errors occurred\n    if errors_found:  # pragma: no cover\n        error_msg = f\"Test '{test_name}' found race condition errors in logs: {', '.join(set(errors_found))}\\n\"\n        error_msg += \"Log records:\\n\"\n        for record in caplog.records:\n            if any(err in record.getMessage() for err in [\"ClosedResourceError\", \"Error in message router\"]):\n                error_msg += f\"  {record.levelname}: {record.getMessage()}\\n\"\n        pytest.fail(error_msg)\n\n\n@pytest.mark.anyio\nasync def test_race_condition_invalid_accept_headers(caplog: pytest.LogCaptureFixture):\n    \"\"\"Test the race condition with invalid Accept headers.\n\n    This test reproduces the exact scenario described in issue #1363:\n    - Send POST request with incorrect Accept headers (missing either application/json or text/event-stream)\n    - Request fails validation early and returns quickly\n    - This should trigger the race condition where message_router encounters ClosedResourceError\n    \"\"\"\n    app = create_app()\n    server_thread = ServerThread(app)\n    server_thread.start()\n\n    try:\n        # Give the server thread a moment to start\n        await anyio.sleep(0.1)\n\n        # Suppress WARNING logs (expected validation errors) and capture ERROR logs\n        with caplog.at_level(logging.ERROR):\n            # Test with missing text/event-stream in Accept header\n            async with httpx.AsyncClient(\n                transport=httpx.ASGITransport(app=app), base_url=\"http://testserver\", timeout=5.0\n            ) as client:\n                response = await client.post(\n                    \"/\",\n                    json={\"jsonrpc\": \"2.0\", \"method\": \"initialize\", \"id\": 1, \"params\": {}},\n                    headers={\n                        \"Accept\": \"application/json\",  # Missing text/event-stream\n                        \"Content-Type\": \"application/json\",\n                    },\n                )\n                # Should get 406 Not Acceptable due to missing text/event-stream\n                assert response.status_code == 406\n\n            # Test with missing application/json in Accept header\n            async with httpx.AsyncClient(\n                transport=httpx.ASGITransport(app=app), base_url=\"http://testserver\", timeout=5.0\n            ) as client:\n                response = await client.post(\n                    \"/\",\n                    json={\"jsonrpc\": \"2.0\", \"method\": \"initialize\", \"id\": 1, \"params\": {}},\n                    headers={\n                        \"Accept\": \"text/event-stream\",  # Missing application/json\n                        \"Content-Type\": \"application/json\",\n                    },\n                )\n                # Should get 406 Not Acceptable due to missing application/json\n                assert response.status_code == 406\n\n            # Test with completely invalid Accept header\n            async with httpx.AsyncClient(\n                transport=httpx.ASGITransport(app=app), base_url=\"http://testserver\", timeout=5.0\n            ) as client:\n                response = await client.post(\n                    \"/\",\n                    json={\"jsonrpc\": \"2.0\", \"method\": \"initialize\", \"id\": 1, \"params\": {}},\n                    headers={\n                        \"Accept\": \"text/plain\",  # Invalid Accept header\n                        \"Content-Type\": \"application/json\",\n                    },\n                )\n                # Should get 406 Not Acceptable\n                assert response.status_code == 406\n\n            # Give background tasks time to complete\n            await anyio.sleep(0.2)\n\n    finally:\n        server_thread.stop()\n        server_thread.join(timeout=5.0)\n        # Check logs for race condition errors\n        check_logs_for_race_condition_errors(caplog, \"test_race_condition_invalid_accept_headers\")\n\n\n@pytest.mark.anyio\nasync def test_race_condition_invalid_content_type(caplog: pytest.LogCaptureFixture):\n    \"\"\"Test the race condition with invalid Content-Type headers.\n\n    This test reproduces the race condition scenario with Content-Type validation failure.\n    \"\"\"\n    app = create_app()\n    server_thread = ServerThread(app)\n    server_thread.start()\n\n    try:\n        # Give the server thread a moment to start\n        await anyio.sleep(0.1)\n\n        # Suppress WARNING logs (expected validation errors) and capture ERROR logs\n        with caplog.at_level(logging.ERROR):\n            # Test with invalid Content-Type\n            async with httpx.AsyncClient(\n                transport=httpx.ASGITransport(app=app), base_url=\"http://testserver\", timeout=5.0\n            ) as client:\n                response = await client.post(\n                    \"/\",\n                    json={\"jsonrpc\": \"2.0\", \"method\": \"initialize\", \"id\": 1, \"params\": {}},\n                    headers={\n                        \"Accept\": \"application/json, text/event-stream\",\n                        \"Content-Type\": \"text/plain\",  # Invalid Content-Type\n                    },\n                )\n                assert response.status_code == 400\n\n            # Give background tasks time to complete\n            await anyio.sleep(0.2)\n\n    finally:\n        server_thread.stop()\n        server_thread.join(timeout=5.0)\n        # Check logs for race condition errors\n        check_logs_for_race_condition_errors(caplog, \"test_race_condition_invalid_content_type\")\n\n\n@pytest.mark.anyio\nasync def test_race_condition_message_router_async_for(caplog: pytest.LogCaptureFixture):\n    \"\"\"Uses json_response=True to trigger the `if self.is_json_response_enabled` branch,\n    which reproduces the ClosedResourceError when message_router is suspended\n    in async for loop while transport cleanup closes streams concurrently.\n    \"\"\"\n    app = create_app(json_response=True)\n    server_thread = ServerThread(app)\n    server_thread.start()\n\n    try:\n        # Give the server thread a moment to start\n        await anyio.sleep(0.1)\n\n        # Suppress WARNING logs (expected validation errors) and capture ERROR logs\n        with caplog.at_level(logging.ERROR):\n            # Use httpx.ASGITransport to test the ASGI app directly\n            async with httpx.AsyncClient(\n                transport=httpx.ASGITransport(app=app), base_url=\"http://testserver\", timeout=5.0\n            ) as client:\n                # Send a valid initialize request\n                response = await client.post(\n                    \"/\",\n                    json={\"jsonrpc\": \"2.0\", \"method\": \"initialize\", \"id\": 1, \"params\": {}},\n                    headers={\n                        \"Accept\": \"application/json, text/event-stream\",\n                        \"Content-Type\": \"application/json\",\n                    },\n                )\n                # Should get a successful response\n                assert response.status_code in (200, 201)\n\n            # Give background tasks time to complete\n            await anyio.sleep(0.2)\n\n    finally:\n        server_thread.stop()\n        server_thread.join(timeout=5.0)\n        # Check logs for race condition errors in message router\n        check_logs_for_race_condition_errors(caplog, \"test_race_condition_message_router_async_for\")\n"
  },
  {
    "path": "tests/issues/test_141_resource_templates.py",
    "content": "import pytest\n\nfrom mcp import Client\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.server.mcpserver.exceptions import ResourceError\nfrom mcp.types import (\n    ListResourceTemplatesResult,\n    TextResourceContents,\n)\n\n\n@pytest.mark.anyio\nasync def test_resource_template_edge_cases():\n    \"\"\"Test server-side resource template validation\"\"\"\n    mcp = MCPServer(\"Demo\")\n\n    # Test case 1: Template with multiple parameters\n    @mcp.resource(\"resource://users/{user_id}/posts/{post_id}\")\n    def get_user_post(user_id: str, post_id: str) -> str:\n        return f\"Post {post_id} by user {user_id}\"\n\n    # Test case 2: Template with optional parameter (should fail)\n    with pytest.raises(ValueError, match=\"Mismatch between URI parameters\"):\n\n        @mcp.resource(\"resource://users/{user_id}/profile\")\n        def get_user_profile(user_id: str, optional_param: str | None = None) -> str:  # pragma: no cover\n            return f\"Profile for user {user_id}\"\n\n    # Test case 3: Template with mismatched parameters\n    with pytest.raises(ValueError, match=\"Mismatch between URI parameters\"):\n\n        @mcp.resource(\"resource://users/{user_id}/profile\")\n        def get_user_profile_mismatch(different_param: str) -> str:  # pragma: no cover\n            return f\"Profile for user {different_param}\"\n\n    # Test case 4: Template with extra function parameters\n    with pytest.raises(ValueError, match=\"Mismatch between URI parameters\"):\n\n        @mcp.resource(\"resource://users/{user_id}/profile\")\n        def get_user_profile_extra(user_id: str, extra_param: str) -> str:  # pragma: no cover\n            return f\"Profile for user {user_id}\"\n\n    # Test case 5: Template with missing function parameters\n    with pytest.raises(ValueError, match=\"Mismatch between URI parameters\"):\n\n        @mcp.resource(\"resource://users/{user_id}/profile/{section}\")\n        def get_user_profile_missing(user_id: str) -> str:  # pragma: no cover\n            return f\"Profile for user {user_id}\"\n\n    # Verify valid template works\n    result = await mcp.read_resource(\"resource://users/123/posts/456\")\n    result_list = list(result)\n    assert len(result_list) == 1\n    assert result_list[0].content == \"Post 456 by user 123\"\n    assert result_list[0].mime_type == \"text/plain\"\n\n    # Verify invalid parameters raise error\n    with pytest.raises(ResourceError, match=\"Unknown resource\"):\n        await mcp.read_resource(\"resource://users/123/posts\")  # Missing post_id\n\n    with pytest.raises(ResourceError, match=\"Unknown resource\"):\n        await mcp.read_resource(\"resource://users/123/posts/456/extra\")  # Extra path component\n\n\n@pytest.mark.anyio\nasync def test_resource_template_client_interaction():\n    \"\"\"Test client-side resource template interaction\"\"\"\n    mcp = MCPServer(\"Demo\")\n\n    # Register some templated resources\n    @mcp.resource(\"resource://users/{user_id}/posts/{post_id}\")\n    def get_user_post(user_id: str, post_id: str) -> str:\n        return f\"Post {post_id} by user {user_id}\"\n\n    @mcp.resource(\"resource://users/{user_id}/profile\")\n    def get_user_profile(user_id: str) -> str:\n        return f\"Profile for user {user_id}\"\n\n    async with Client(mcp) as session:\n        # List available resources\n        resources = await session.list_resource_templates()\n        assert isinstance(resources, ListResourceTemplatesResult)\n        assert len(resources.resource_templates) == 2\n\n        # Verify resource templates are listed correctly\n        templates = [r.uri_template for r in resources.resource_templates]\n        assert \"resource://users/{user_id}/posts/{post_id}\" in templates\n        assert \"resource://users/{user_id}/profile\" in templates\n\n        # Read a resource with valid parameters\n        result = await session.read_resource(\"resource://users/123/posts/456\")\n        contents = result.contents[0]\n        assert isinstance(contents, TextResourceContents)\n        assert contents.text == \"Post 456 by user 123\"\n        assert contents.mime_type == \"text/plain\"\n\n        # Read another resource with valid parameters\n        result = await session.read_resource(\"resource://users/789/profile\")\n        contents = result.contents[0]\n        assert isinstance(contents, TextResourceContents)\n        assert contents.text == \"Profile for user 789\"\n        assert contents.mime_type == \"text/plain\"\n\n        # Verify invalid resource URIs raise appropriate errors\n        with pytest.raises(Exception):  # Specific exception type may vary\n            await session.read_resource(\"resource://users/123/posts\")  # Missing post_id\n\n        with pytest.raises(Exception):  # Specific exception type may vary\n            await session.read_resource(\"resource://users/123/invalid\")  # Invalid template\n"
  },
  {
    "path": "tests/issues/test_152_resource_mime_type.py",
    "content": "import base64\n\nimport pytest\n\nfrom mcp import Client, types\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.types import (\n    BlobResourceContents,\n    ListResourcesResult,\n    PaginatedRequestParams,\n    ReadResourceRequestParams,\n    ReadResourceResult,\n    TextResourceContents,\n)\n\npytestmark = pytest.mark.anyio\n\n\nasync def test_mcpserver_resource_mime_type():\n    \"\"\"Test that mime_type parameter is respected for resources.\"\"\"\n    mcp = MCPServer(\"test\")\n\n    # Create a small test image as bytes\n    image_bytes = b\"fake_image_data\"\n    base64_string = base64.b64encode(image_bytes).decode(\"utf-8\")\n\n    @mcp.resource(\"test://image\", mime_type=\"image/png\")\n    def get_image_as_string() -> str:\n        \"\"\"Return a test image as base64 string.\"\"\"\n        return base64_string\n\n    @mcp.resource(\"test://image_bytes\", mime_type=\"image/png\")\n    def get_image_as_bytes() -> bytes:\n        \"\"\"Return a test image as bytes.\"\"\"\n        return image_bytes\n\n    # Test that resources are listed with correct mime type\n    async with Client(mcp) as client:\n        # List resources and verify mime types\n        resources = await client.list_resources()\n        assert resources.resources is not None\n\n        mapping = {str(r.uri): r for r in resources.resources}\n\n        # Find our resources\n        string_resource = mapping[\"test://image\"]\n        bytes_resource = mapping[\"test://image_bytes\"]\n\n        # Verify mime types\n        assert string_resource.mime_type == \"image/png\", \"String resource mime type not respected\"\n        assert bytes_resource.mime_type == \"image/png\", \"Bytes resource mime type not respected\"\n\n        # Also verify the content can be read correctly\n        string_result = await client.read_resource(\"test://image\")\n        assert len(string_result.contents) == 1\n        assert getattr(string_result.contents[0], \"text\") == base64_string, \"Base64 string mismatch\"\n        assert string_result.contents[0].mime_type == \"image/png\", \"String content mime type not preserved\"\n\n        bytes_result = await client.read_resource(\"test://image_bytes\")\n        assert len(bytes_result.contents) == 1\n        assert base64.b64decode(getattr(bytes_result.contents[0], \"blob\")) == image_bytes, \"Bytes mismatch\"\n        assert bytes_result.contents[0].mime_type == \"image/png\", \"Bytes content mime type not preserved\"\n\n\nasync def test_lowlevel_resource_mime_type():\n    \"\"\"Test that mime_type parameter is respected for resources.\"\"\"\n\n    # Create a small test image as bytes\n    image_bytes = b\"fake_image_data\"\n    base64_string = base64.b64encode(image_bytes).decode(\"utf-8\")\n\n    # Create test resources with specific mime types\n    test_resources = [\n        types.Resource(uri=\"test://image\", name=\"test image\", mime_type=\"image/png\"),\n        types.Resource(\n            uri=\"test://image_bytes\",\n            name=\"test image bytes\",\n            mime_type=\"image/png\",\n        ),\n    ]\n\n    async def handle_list_resources(\n        ctx: ServerRequestContext, params: PaginatedRequestParams | None\n    ) -> ListResourcesResult:\n        return ListResourcesResult(resources=test_resources)\n\n    resource_contents: dict[str, list[TextResourceContents | BlobResourceContents]] = {\n        \"test://image\": [TextResourceContents(uri=\"test://image\", text=base64_string, mime_type=\"image/png\")],\n        \"test://image_bytes\": [\n            BlobResourceContents(\n                uri=\"test://image_bytes\", blob=base64.b64encode(image_bytes).decode(\"utf-8\"), mime_type=\"image/png\"\n            )\n        ],\n    }\n\n    async def handle_read_resource(ctx: ServerRequestContext, params: ReadResourceRequestParams) -> ReadResourceResult:\n        return ReadResourceResult(contents=resource_contents[str(params.uri)])\n\n    server = Server(\"test\", on_list_resources=handle_list_resources, on_read_resource=handle_read_resource)\n\n    # Test that resources are listed with correct mime type\n    async with Client(server) as client:\n        # List resources and verify mime types\n        resources = await client.list_resources()\n        assert resources.resources is not None\n\n        mapping = {str(r.uri): r for r in resources.resources}\n\n        # Find our resources\n        string_resource = mapping[\"test://image\"]\n        bytes_resource = mapping[\"test://image_bytes\"]\n\n        # Verify mime types\n        assert string_resource.mime_type == \"image/png\", \"String resource mime type not respected\"\n        assert bytes_resource.mime_type == \"image/png\", \"Bytes resource mime type not respected\"\n\n        # Also verify the content can be read correctly\n        string_result = await client.read_resource(\"test://image\")\n        assert len(string_result.contents) == 1\n        assert getattr(string_result.contents[0], \"text\") == base64_string, \"Base64 string mismatch\"\n        assert string_result.contents[0].mime_type == \"image/png\", \"String content mime type not preserved\"\n\n        bytes_result = await client.read_resource(\"test://image_bytes\")\n        assert len(bytes_result.contents) == 1\n        assert base64.b64decode(getattr(bytes_result.contents[0], \"blob\")) == image_bytes, \"Bytes mismatch\"\n        assert bytes_result.contents[0].mime_type == \"image/png\", \"Bytes content mime type not preserved\"\n"
  },
  {
    "path": "tests/issues/test_1574_resource_uri_validation.py",
    "content": "\"\"\"Tests for issue #1574: Python SDK incorrectly validates Resource URIs.\n\nThe Python SDK previously used Pydantic's AnyUrl for URI fields, which rejected\nrelative paths like 'users/me' that are valid according to the MCP spec and\naccepted by the TypeScript SDK.\n\nThe fix changed URI fields to plain strings to match the spec, which defines\nuri fields as strings with no JSON Schema format validation.\n\nThese tests verify the fix works end-to-end through the JSON-RPC protocol.\n\"\"\"\n\nimport pytest\n\nfrom mcp import Client, types\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.types import (\n    ListResourcesResult,\n    PaginatedRequestParams,\n    ReadResourceRequestParams,\n    ReadResourceResult,\n    TextResourceContents,\n)\n\npytestmark = pytest.mark.anyio\n\n\nasync def test_relative_uri_roundtrip():\n    \"\"\"Relative URIs survive the full server-client JSON-RPC roundtrip.\n\n    This is the critical regression test - if someone reintroduces AnyUrl,\n    the server would fail to serialize resources with relative URIs,\n    or the URI would be transformed during the roundtrip.\n    \"\"\"\n\n    async def handle_list_resources(\n        ctx: ServerRequestContext, params: PaginatedRequestParams | None\n    ) -> ListResourcesResult:\n        return ListResourcesResult(\n            resources=[\n                types.Resource(name=\"user\", uri=\"users/me\"),\n                types.Resource(name=\"config\", uri=\"./config\"),\n                types.Resource(name=\"parent\", uri=\"../parent/resource\"),\n            ]\n        )\n\n    async def handle_read_resource(ctx: ServerRequestContext, params: ReadResourceRequestParams) -> ReadResourceResult:\n        return ReadResourceResult(\n            contents=[TextResourceContents(uri=str(params.uri), text=f\"data for {params.uri}\", mime_type=\"text/plain\")]\n        )\n\n    server = Server(\"test\", on_list_resources=handle_list_resources, on_read_resource=handle_read_resource)\n\n    async with Client(server) as client:\n        # List should return the exact URIs we specified\n        resources = await client.list_resources()\n        uri_map = {r.uri: r for r in resources.resources}\n\n        assert \"users/me\" in uri_map, f\"Expected 'users/me' in {list(uri_map.keys())}\"\n        assert \"./config\" in uri_map, f\"Expected './config' in {list(uri_map.keys())}\"\n        assert \"../parent/resource\" in uri_map, f\"Expected '../parent/resource' in {list(uri_map.keys())}\"\n\n        # Read should work with each relative URI and preserve it in the response\n        for uri_str in [\"users/me\", \"./config\", \"../parent/resource\"]:\n            result = await client.read_resource(uri_str)\n            assert len(result.contents) == 1\n            assert result.contents[0].uri == uri_str\n\n\nasync def test_custom_scheme_uri_roundtrip():\n    \"\"\"Custom scheme URIs work through the protocol.\n\n    Some MCP servers use custom schemes like \"custom://resource\".\n    These should work end-to-end.\n    \"\"\"\n\n    async def handle_list_resources(\n        ctx: ServerRequestContext, params: PaginatedRequestParams | None\n    ) -> ListResourcesResult:\n        return ListResourcesResult(\n            resources=[\n                types.Resource(name=\"custom\", uri=\"custom://my-resource\"),\n                types.Resource(name=\"file\", uri=\"file:///path/to/file\"),\n            ]\n        )\n\n    async def handle_read_resource(ctx: ServerRequestContext, params: ReadResourceRequestParams) -> ReadResourceResult:\n        return ReadResourceResult(\n            contents=[TextResourceContents(uri=str(params.uri), text=\"data\", mime_type=\"text/plain\")]\n        )\n\n    server = Server(\"test\", on_list_resources=handle_list_resources, on_read_resource=handle_read_resource)\n\n    async with Client(server) as client:\n        resources = await client.list_resources()\n        uri_map = {r.uri: r for r in resources.resources}\n\n        assert \"custom://my-resource\" in uri_map\n        assert \"file:///path/to/file\" in uri_map\n\n        # Read with custom scheme\n        result = await client.read_resource(\"custom://my-resource\")\n        assert len(result.contents) == 1\n\n\ndef test_uri_json_roundtrip_preserves_value():\n    \"\"\"URI is preserved exactly through JSON serialization.\n\n    This catches any Pydantic validation or normalization that would\n    alter the URI during the JSON-RPC message flow.\n    \"\"\"\n    test_uris = [\n        \"users/me\",\n        \"custom://resource\",\n        \"./relative\",\n        \"../parent\",\n        \"file:///absolute/path\",\n        \"https://example.com/path\",\n    ]\n\n    for uri_str in test_uris:\n        resource = types.Resource(name=\"test\", uri=uri_str)\n        json_data = resource.model_dump(mode=\"json\")\n        restored = types.Resource.model_validate(json_data)\n        assert restored.uri == uri_str, f\"URI mutated: {uri_str} -> {restored.uri}\"\n\n\ndef test_resource_contents_uri_json_roundtrip():\n    \"\"\"TextResourceContents URI is preserved through JSON serialization.\"\"\"\n    test_uris = [\"users/me\", \"./relative\", \"custom://resource\"]\n\n    for uri_str in test_uris:\n        contents = types.TextResourceContents(\n            uri=uri_str,\n            text=\"data\",\n            mime_type=\"text/plain\",\n        )\n        json_data = contents.model_dump(mode=\"json\")\n        restored = types.TextResourceContents.model_validate(json_data)\n        assert restored.uri == uri_str, f\"URI mutated: {uri_str} -> {restored.uri}\"\n"
  },
  {
    "path": "tests/issues/test_1754_mime_type_parameters.py",
    "content": "\"\"\"Test for GitHub issue #1754: MIME type validation rejects valid RFC 2045 parameters.\n\nThe MIME type validation regex was too restrictive and rejected valid MIME types\nwith parameters like 'text/html;profile=mcp-app' which are valid per RFC 2045.\n\"\"\"\n\nimport pytest\n\nfrom mcp import Client\nfrom mcp.server.mcpserver import MCPServer\n\npytestmark = pytest.mark.anyio\n\n\nasync def test_mime_type_with_parameters():\n    \"\"\"Test that MIME types with parameters are accepted (RFC 2045).\"\"\"\n    mcp = MCPServer(\"test\")\n\n    # This should NOT raise a validation error\n    @mcp.resource(\"ui://widget\", mime_type=\"text/html;profile=mcp-app\")\n    def widget() -> str:\n        raise NotImplementedError()\n\n    resources = await mcp.list_resources()\n    assert len(resources) == 1\n    assert resources[0].mime_type == \"text/html;profile=mcp-app\"\n\n\nasync def test_mime_type_with_parameters_and_space():\n    \"\"\"Test MIME type with space after semicolon.\"\"\"\n    mcp = MCPServer(\"test\")\n\n    @mcp.resource(\"data://json\", mime_type=\"application/json; charset=utf-8\")\n    def data() -> str:\n        raise NotImplementedError()\n\n    resources = await mcp.list_resources()\n    assert len(resources) == 1\n    assert resources[0].mime_type == \"application/json; charset=utf-8\"\n\n\nasync def test_mime_type_with_multiple_parameters():\n    \"\"\"Test MIME type with multiple parameters.\"\"\"\n    mcp = MCPServer(\"test\")\n\n    @mcp.resource(\"data://multi\", mime_type=\"text/plain; charset=utf-8; format=fixed\")\n    def data() -> str:\n        raise NotImplementedError()\n\n    resources = await mcp.list_resources()\n    assert len(resources) == 1\n    assert resources[0].mime_type == \"text/plain; charset=utf-8; format=fixed\"\n\n\nasync def test_mime_type_preserved_in_read_resource():\n    \"\"\"Test that MIME type with parameters is preserved when reading resource.\"\"\"\n    mcp = MCPServer(\"test\")\n\n    @mcp.resource(\"ui://my-widget\", mime_type=\"text/html;profile=mcp-app\")\n    def my_widget() -> str:\n        return \"<html><body>Hello MCP-UI</body></html>\"\n\n    async with Client(mcp) as client:\n        # Read the resource\n        result = await client.read_resource(\"ui://my-widget\")\n        assert len(result.contents) == 1\n        assert result.contents[0].mime_type == \"text/html;profile=mcp-app\"\n"
  },
  {
    "path": "tests/issues/test_176_progress_token.py",
    "content": "from unittest.mock import AsyncMock, MagicMock\n\nimport pytest\n\nfrom mcp.server.context import ServerRequestContext\nfrom mcp.server.experimental.request_context import Experimental\nfrom mcp.server.mcpserver import Context\n\npytestmark = pytest.mark.anyio\n\n\nasync def test_progress_token_zero_first_call():\n    \"\"\"Test that progress notifications work when progress_token is 0 on first call.\"\"\"\n\n    # Create mock session with progress notification tracking\n    mock_session = AsyncMock()\n    mock_session.send_progress_notification = AsyncMock()\n\n    # Create request context with progress token 0\n    request_context = ServerRequestContext(\n        request_id=\"test-request\",\n        session=mock_session,\n        meta={\"progress_token\": 0},\n        lifespan_context=None,\n        experimental=Experimental(),\n    )\n\n    # Create context with our mocks\n    ctx = Context(request_context=request_context, mcp_server=MagicMock())\n\n    # Test progress reporting\n    await ctx.report_progress(0, 10)  # First call with 0\n    await ctx.report_progress(5, 10)  # Middle progress\n    await ctx.report_progress(10, 10)  # Complete\n\n    # Verify progress notifications\n    assert mock_session.send_progress_notification.call_count == 3, \"All progress notifications should be sent\"\n    mock_session.send_progress_notification.assert_any_call(\n        progress_token=0, progress=0.0, total=10.0, message=None, related_request_id=\"test-request\"\n    )\n    mock_session.send_progress_notification.assert_any_call(\n        progress_token=0, progress=5.0, total=10.0, message=None, related_request_id=\"test-request\"\n    )\n    mock_session.send_progress_notification.assert_any_call(\n        progress_token=0, progress=10.0, total=10.0, message=None, related_request_id=\"test-request\"\n    )\n"
  },
  {
    "path": "tests/issues/test_188_concurrency.py",
    "content": "import anyio\nimport pytest\n\nfrom mcp import Client\nfrom mcp.server.mcpserver import MCPServer\n\n\n@pytest.mark.anyio\nasync def test_messages_are_executed_concurrently_tools():\n    server = MCPServer(\"test\")\n    event = anyio.Event()\n    tool_started = anyio.Event()\n    call_order: list[str] = []\n\n    @server.tool(\"sleep\")\n    async def sleep_tool():\n        call_order.append(\"waiting_for_event\")\n        tool_started.set()\n        await event.wait()\n        call_order.append(\"tool_end\")\n        return \"done\"\n\n    @server.tool(\"trigger\")\n    async def trigger():\n        # Wait for tool to start before setting the event\n        await tool_started.wait()\n        call_order.append(\"trigger_started\")\n        event.set()\n        call_order.append(\"trigger_end\")\n        return \"slow\"\n\n    async with Client(server) as client_session:\n        # First tool will wait on event, second will set it\n        async with anyio.create_task_group() as tg:\n            # Start the tool first (it will wait on event)\n            tg.start_soon(client_session.call_tool, \"sleep\")\n            # Then the trigger tool will set the event to allow the first tool to continue\n            await client_session.call_tool(\"trigger\")\n\n        # Verify that both ran concurrently\n        assert call_order == [\n            \"waiting_for_event\",\n            \"trigger_started\",\n            \"trigger_end\",\n            \"tool_end\",\n        ], f\"Expected concurrent execution, but got: {call_order}\"\n\n\n@pytest.mark.anyio\nasync def test_messages_are_executed_concurrently_tools_and_resources():\n    server = MCPServer(\"test\")\n    event = anyio.Event()\n    tool_started = anyio.Event()\n    call_order: list[str] = []\n\n    @server.tool(\"sleep\")\n    async def sleep_tool():\n        call_order.append(\"waiting_for_event\")\n        tool_started.set()\n        await event.wait()\n        call_order.append(\"tool_end\")\n        return \"done\"\n\n    @server.resource(\"slow://slow_resource\")\n    async def slow_resource():\n        # Wait for tool to start before setting the event\n        await tool_started.wait()\n        event.set()\n        call_order.append(\"resource_end\")\n        return \"slow\"\n\n    async with Client(server) as client_session:\n        # First tool will wait on event, second will set it\n        async with anyio.create_task_group() as tg:\n            # Start the tool first (it will wait on event)\n            tg.start_soon(client_session.call_tool, \"sleep\")\n            # Then the resource (it will set the event)\n            tg.start_soon(client_session.read_resource, \"slow://slow_resource\")\n\n        # Verify that both ran concurrently\n        assert call_order == [\n            \"waiting_for_event\",\n            \"resource_end\",\n            \"tool_end\",\n        ], f\"Expected concurrent execution, but got: {call_order}\"\n"
  },
  {
    "path": "tests/issues/test_192_request_id.py",
    "content": "import anyio\nimport pytest\n\nfrom mcp.server.lowlevel import NotificationOptions, Server\nfrom mcp.server.models import InitializationOptions\nfrom mcp.shared.message import SessionMessage\nfrom mcp.types import (\n    LATEST_PROTOCOL_VERSION,\n    ClientCapabilities,\n    Implementation,\n    InitializeRequestParams,\n    JSONRPCMessage,\n    JSONRPCNotification,\n    JSONRPCRequest,\n    JSONRPCResponse,\n    NotificationParams,\n)\n\n\n@pytest.mark.anyio\nasync def test_request_id_match() -> None:\n    \"\"\"Test that the server preserves request IDs in responses.\"\"\"\n    server = Server(\"test\")\n    custom_request_id = \"test-123\"\n\n    # Create memory streams for communication\n    client_writer, client_reader = anyio.create_memory_object_stream[SessionMessage | Exception](1)\n    server_writer, server_reader = anyio.create_memory_object_stream[SessionMessage | Exception](1)\n\n    # Server task to process the request\n    async def run_server():\n        async with client_reader, server_writer:\n            await server.run(\n                client_reader,\n                server_writer,\n                InitializationOptions(\n                    server_name=\"test\",\n                    server_version=\"1.0.0\",\n                    capabilities=server.get_capabilities(\n                        notification_options=NotificationOptions(),\n                        experimental_capabilities={},\n                    ),\n                ),\n                raise_exceptions=True,\n            )\n\n    # Start server task\n    async with (\n        anyio.create_task_group() as tg,\n        client_writer,\n        client_reader,\n        server_writer,\n        server_reader,\n    ):\n        tg.start_soon(run_server)\n\n        # Send initialize request\n        init_req = JSONRPCRequest(\n            id=\"init-1\",\n            method=\"initialize\",\n            params=InitializeRequestParams(\n                protocol_version=LATEST_PROTOCOL_VERSION,\n                capabilities=ClientCapabilities(),\n                client_info=Implementation(name=\"test-client\", version=\"1.0.0\"),\n            ).model_dump(by_alias=True, exclude_none=True),\n            jsonrpc=\"2.0\",\n        )\n\n        await client_writer.send(SessionMessage(init_req))\n        response = await server_reader.receive()  # Get init response but don't need to check it\n\n        # Send initialized notification\n        initialized_notification = JSONRPCNotification(\n            method=\"notifications/initialized\",\n            params=NotificationParams().model_dump(by_alias=True, exclude_none=True),\n            jsonrpc=\"2.0\",\n        )\n        await client_writer.send(SessionMessage(initialized_notification))\n\n        # Send ping request with custom ID\n        ping_request = JSONRPCRequest(id=custom_request_id, method=\"ping\", params={}, jsonrpc=\"2.0\")\n\n        await client_writer.send(SessionMessage(ping_request))\n\n        # Read response\n        response = await server_reader.receive()\n\n        # Verify response ID matches request ID\n        assert isinstance(response, SessionMessage)\n        assert isinstance(response.message, JSONRPCMessage)\n        assert isinstance(response.message, JSONRPCResponse)\n        assert response.message.id == custom_request_id, \"Response ID should match request ID\"\n\n        # Cancel server task\n        tg.cancel_scope.cancel()\n"
  },
  {
    "path": "tests/issues/test_342_base64_encoding.py",
    "content": "\"\"\"Test for base64 encoding issue in MCP server.\n\nThis test verifies that binary resource data is encoded with standard base64\n(not urlsafe_b64encode), so BlobResourceContents validation succeeds.\n\"\"\"\n\nimport base64\n\nimport pytest\n\nfrom mcp import Client\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.types import BlobResourceContents\n\npytestmark = pytest.mark.anyio\n\n\nasync def test_server_base64_encoding():\n    \"\"\"Tests that binary resource data round-trips correctly through base64 encoding.\n\n    The test uses binary data that produces different results with urlsafe vs standard\n    base64, ensuring the server uses standard encoding.\n    \"\"\"\n    mcp = MCPServer(\"test\")\n\n    # Create binary data that will definitely result in + and / characters\n    # when encoded with standard base64\n    binary_data = bytes(list(range(255)) * 4)\n\n    # Sanity check: our test data produces different encodings\n    urlsafe_b64 = base64.urlsafe_b64encode(binary_data).decode()\n    standard_b64 = base64.b64encode(binary_data).decode()\n    assert urlsafe_b64 != standard_b64, \"Test data doesn't demonstrate encoding difference\"\n\n    @mcp.resource(\"test://binary\", mime_type=\"application/octet-stream\")\n    def get_binary() -> bytes:\n        \"\"\"Return binary test data.\"\"\"\n        return binary_data\n\n    async with Client(mcp) as client:\n        result = await client.read_resource(\"test://binary\")\n        assert len(result.contents) == 1\n\n        blob_content = result.contents[0]\n        assert isinstance(blob_content, BlobResourceContents)\n\n        # Verify standard base64 was used (not urlsafe)\n        assert blob_content.blob == standard_b64\n\n        # Verify we can decode the data back correctly\n        decoded = base64.b64decode(blob_content.blob)\n        assert decoded == binary_data\n"
  },
  {
    "path": "tests/issues/test_355_type_error.py",
    "content": "from collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\nfrom dataclasses import dataclass\n\nfrom mcp.server.mcpserver import Context, MCPServer\n\n\nclass Database:  # Replace with your actual DB type\n    @classmethod\n    async def connect(cls):  # pragma: no cover\n        return cls()\n\n    async def disconnect(self):  # pragma: no cover\n        pass\n\n    def query(self):  # pragma: no cover\n        return \"Hello, World!\"\n\n\n# Create a named server\nmcp = MCPServer(\"My App\")\n\n\n@dataclass\nclass AppContext:\n    db: Database\n\n\n@asynccontextmanager\nasync def app_lifespan(server: MCPServer) -> AsyncIterator[AppContext]:  # pragma: no cover\n    \"\"\"Manage application lifecycle with type-safe context\"\"\"\n    # Initialize on startup\n    db = await Database.connect()\n    try:\n        yield AppContext(db=db)\n    finally:\n        # Cleanup on shutdown\n        await db.disconnect()\n\n\n# Pass lifespan to server\nmcp = MCPServer(\"My App\", lifespan=app_lifespan)\n\n\n# Access type-safe lifespan context in tools\n@mcp.tool()\ndef query_db(ctx: Context[AppContext]) -> str:  # pragma: no cover\n    \"\"\"Tool that uses initialized resources\"\"\"\n    db = ctx.request_context.lifespan_context.db\n    return db.query()\n"
  },
  {
    "path": "tests/issues/test_552_windows_hang.py",
    "content": "\"\"\"Test for issue #552: stdio_client hangs on Windows.\"\"\"\n\nimport sys\nfrom textwrap import dedent\n\nimport anyio\nimport pytest\n\nfrom mcp import ClientSession, StdioServerParameters\nfrom mcp.client.stdio import stdio_client\n\n\n@pytest.mark.skipif(sys.platform != \"win32\", reason=\"Windows-specific test\")  # pragma: no cover\n@pytest.mark.anyio\nasync def test_windows_stdio_client_with_session():\n    \"\"\"Test the exact scenario from issue #552: Using ClientSession with stdio_client.\n\n    This reproduces the original bug report where stdio_client hangs on Windows 11\n    when used with ClientSession.\n    \"\"\"\n    # Create a minimal MCP server that responds to initialization\n    server_script = dedent(\"\"\"\n        import json\n        import sys\n\n        # Read initialization request\n        line = sys.stdin.readline()\n\n        # Send initialization response\n        response = {\n            \"jsonrpc\": \"2.0\",\n            \"id\": 1,\n            \"result\": {\n                \"protocolVersion\": \"1.0\",\n                \"capabilities\": {},\n                \"serverInfo\": {\"name\": \"test-server\", \"version\": \"1.0\"}\n            }\n        }\n        print(json.dumps(response))\n        sys.stdout.flush()\n\n        # Exit after a short delay\n        import time\n        time.sleep(0.1)\n        sys.exit(0)\n    \"\"\").strip()\n\n    params = StdioServerParameters(\n        command=sys.executable,\n        args=[\"-c\", server_script],\n    )\n\n    # This is the exact pattern from the bug report\n    with anyio.fail_after(10):\n        try:\n            async with stdio_client(params) as (read, write):\n                async with ClientSession(read, write) as session:\n                    await session.initialize()\n                # Should exit ClientSession without hanging\n            # Should exit stdio_client without hanging\n        except Exception:\n            # Connection errors are expected when process exits\n            pass\n"
  },
  {
    "path": "tests/issues/test_88_random_error.py",
    "content": "\"\"\"Test to reproduce issue #88: Random error thrown on response.\"\"\"\n\nfrom pathlib import Path\n\nimport anyio\nimport pytest\nfrom anyio.abc import TaskStatus\nfrom anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream\n\nfrom mcp import types\nfrom mcp.client.session import ClientSession\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.message import SessionMessage\nfrom mcp.types import CallToolRequestParams, CallToolResult, ListToolsResult, PaginatedRequestParams, TextContent\n\n\n@pytest.mark.anyio\nasync def test_notification_validation_error(tmp_path: Path):\n    \"\"\"Test that timeouts are handled gracefully and don't break the server.\n\n    This test verifies that when a client request times out:\n    1. The server task stays alive\n    2. The server can still handle new requests\n    3. The client can make new requests\n    4. No resources are leaked\n\n    Uses per-request timeouts to avoid race conditions:\n    - Fast operations use no timeout (reliable in any environment)\n    - Slow operations use minimal timeout (10ms) for quick test execution\n    \"\"\"\n\n    request_count = 0\n    slow_request_lock = anyio.Event()\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        return ListToolsResult(\n            tools=[\n                types.Tool(\n                    name=\"slow\",\n                    description=\"A slow tool\",\n                    input_schema={\"type\": \"object\"},\n                ),\n                types.Tool(\n                    name=\"fast\",\n                    description=\"A fast tool\",\n                    input_schema={\"type\": \"object\"},\n                ),\n            ]\n        )\n\n    async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:\n        nonlocal request_count\n        request_count += 1\n        assert params.name in (\"slow\", \"fast\"), f\"Unknown tool: {params.name}\"\n\n        if params.name == \"slow\":\n            await slow_request_lock.wait()  # it should timeout here\n            text = f\"slow {request_count}\"\n        else:\n            text = f\"fast {request_count}\"\n        return CallToolResult(content=[TextContent(type=\"text\", text=text)])\n\n    server = Server(name=\"test\", on_list_tools=handle_list_tools, on_call_tool=handle_call_tool)\n\n    async def server_handler(\n        read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],\n        write_stream: MemoryObjectSendStream[SessionMessage],\n        task_status: TaskStatus[str] = anyio.TASK_STATUS_IGNORED,\n    ):\n        with anyio.CancelScope() as scope:\n            task_status.started(scope)  # type: ignore\n            await server.run(\n                read_stream,\n                write_stream,\n                server.create_initialization_options(),\n                raise_exceptions=True,\n            )\n\n    async def client(\n        read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],\n        write_stream: MemoryObjectSendStream[SessionMessage],\n        scope: anyio.CancelScope,\n    ):\n        # No session-level timeout to avoid race conditions with fast operations\n        async with ClientSession(read_stream, write_stream) as session:\n            await session.initialize()\n\n            # First call should work (fast operation, no timeout)\n            result = await session.call_tool(\"fast\", read_timeout_seconds=None)\n            assert result.content == [TextContent(type=\"text\", text=\"fast 1\")]\n            assert not slow_request_lock.is_set()\n\n            # Second call should timeout (slow operation with minimal timeout)\n            # Use very small timeout to trigger quickly without waiting\n            with pytest.raises(MCPError) as exc_info:\n                await session.call_tool(\"slow\", read_timeout_seconds=0.000001)  # artificial timeout that always fails\n            assert \"Timed out while waiting\" in str(exc_info.value)\n\n            # release the slow request not to have hanging process\n            slow_request_lock.set()\n\n            # Third call should work (fast operation, no timeout),\n            # proving server is still responsive\n            result = await session.call_tool(\"fast\", read_timeout_seconds=None)\n            assert result.content == [TextContent(type=\"text\", text=\"fast 3\")]\n        scope.cancel()  # pragma: lax no cover\n\n    # Run server and client in separate task groups to avoid cancellation\n    server_writer, server_reader = anyio.create_memory_object_stream[SessionMessage](1)\n    client_writer, client_reader = anyio.create_memory_object_stream[SessionMessage](1)\n\n    async with anyio.create_task_group() as tg:\n        scope = await tg.start(server_handler, server_reader, client_writer)\n        # Run client in a separate task to avoid cancellation\n        tg.start_soon(client, client_reader, server_writer, scope)\n"
  },
  {
    "path": "tests/issues/test_973_url_decoding.py",
    "content": "\"\"\"Test that URL-encoded parameters are decoded in resource templates.\n\nRegression test for https://github.com/modelcontextprotocol/python-sdk/issues/973\n\"\"\"\n\nfrom mcp.server.mcpserver.resources import ResourceTemplate\n\n\ndef test_template_matches_decodes_space():\n    \"\"\"Test that %20 is decoded to space.\"\"\"\n\n    def search(query: str) -> str:  # pragma: no cover\n        return f\"Results for: {query}\"\n\n    template = ResourceTemplate.from_function(\n        fn=search,\n        uri_template=\"search://{query}\",\n        name=\"search\",\n    )\n\n    params = template.matches(\"search://hello%20world\")\n    assert params is not None\n    assert params[\"query\"] == \"hello world\"\n\n\ndef test_template_matches_decodes_accented_characters():\n    \"\"\"Test that %C3%A9 is decoded to e with accent.\"\"\"\n\n    def search(query: str) -> str:  # pragma: no cover\n        return f\"Results for: {query}\"\n\n    template = ResourceTemplate.from_function(\n        fn=search,\n        uri_template=\"search://{query}\",\n        name=\"search\",\n    )\n\n    params = template.matches(\"search://caf%C3%A9\")\n    assert params is not None\n    assert params[\"query\"] == \"café\"\n\n\ndef test_template_matches_decodes_complex_phrase():\n    \"\"\"Test complex French phrase from the original issue.\"\"\"\n\n    def search(query: str) -> str:  # pragma: no cover\n        return f\"Results for: {query}\"\n\n    template = ResourceTemplate.from_function(\n        fn=search,\n        uri_template=\"search://{query}\",\n        name=\"search\",\n    )\n\n    params = template.matches(\"search://stick%20correcteur%20teint%C3%A9%20anti-imperfections\")\n    assert params is not None\n    assert params[\"query\"] == \"stick correcteur teinté anti-imperfections\"\n\n\ndef test_template_matches_preserves_plus_sign():\n    \"\"\"Test that plus sign remains as plus (not converted to space).\n\n    In URI encoding, %20 is space. Plus-as-space is only for\n    application/x-www-form-urlencoded (HTML forms).\n    \"\"\"\n\n    def search(query: str) -> str:  # pragma: no cover\n        return f\"Results for: {query}\"\n\n    template = ResourceTemplate.from_function(\n        fn=search,\n        uri_template=\"search://{query}\",\n        name=\"search\",\n    )\n\n    params = template.matches(\"search://hello+world\")\n    assert params is not None\n    assert params[\"query\"] == \"hello+world\"\n"
  },
  {
    "path": "tests/issues/test_malformed_input.py",
    "content": "# Claude Debug\n\"\"\"Test for HackerOne vulnerability report #3156202 - malformed input DOS.\"\"\"\n\nimport anyio\nimport pytest\n\nfrom mcp.server.models import InitializationOptions\nfrom mcp.server.session import ServerSession\nfrom mcp.shared.message import SessionMessage\nfrom mcp.types import INVALID_PARAMS, JSONRPCError, JSONRPCMessage, JSONRPCRequest, ServerCapabilities\n\n\n@pytest.mark.anyio\nasync def test_malformed_initialize_request_does_not_crash_server():\n    \"\"\"Test that malformed initialize requests return proper error responses\n    instead of crashing the server (HackerOne #3156202).\n    \"\"\"\n    # Create in-memory streams for testing\n    read_send_stream, read_receive_stream = anyio.create_memory_object_stream[SessionMessage | Exception](10)\n    write_send_stream, write_receive_stream = anyio.create_memory_object_stream[SessionMessage](10)\n\n    try:\n        # Create a malformed initialize request (missing required params field)\n        malformed_request = JSONRPCRequest(\n            jsonrpc=\"2.0\",\n            id=\"f20fe86132ed4cd197f89a7134de5685\",\n            method=\"initialize\",\n            # params=None  # Missing required params field\n        )\n\n        # Wrap in session message\n        request_message = SessionMessage(message=malformed_request)\n\n        # Start a server session\n        async with ServerSession(\n            read_stream=read_receive_stream,\n            write_stream=write_send_stream,\n            init_options=InitializationOptions(\n                server_name=\"test_server\",\n                server_version=\"1.0.0\",\n                capabilities=ServerCapabilities(),\n            ),\n        ):\n            # Send the malformed request\n            await read_send_stream.send(request_message)\n\n            # Give the session time to process the request\n            await anyio.sleep(0.1)\n\n            # Check that we received an error response instead of a crash\n            try:\n                response_message = write_receive_stream.receive_nowait()\n                response = response_message.message\n\n                # Verify it's a proper JSON-RPC error response\n                assert isinstance(response, JSONRPCError)\n                assert response.jsonrpc == \"2.0\"\n                assert response.id == \"f20fe86132ed4cd197f89a7134de5685\"\n                assert response.error.code == INVALID_PARAMS\n                assert \"Invalid request parameters\" in response.error.message\n\n                # Verify the session is still alive and can handle more requests\n                # Send another malformed request to confirm server stability\n                another_malformed_request = JSONRPCRequest(\n                    jsonrpc=\"2.0\",\n                    id=\"test_id_2\",\n                    method=\"tools/call\",\n                    # params=None  # Missing required params\n                )\n                another_request_message = SessionMessage(message=another_malformed_request)\n\n                await read_send_stream.send(another_request_message)\n                await anyio.sleep(0.1)\n\n                # Should get another error response, not a crash\n                second_response_message = write_receive_stream.receive_nowait()\n                second_response = second_response_message.message\n\n                assert isinstance(second_response, JSONRPCError)\n                assert second_response.id == \"test_id_2\"\n                assert second_response.error.code == INVALID_PARAMS\n\n            except anyio.WouldBlock:  # pragma: no cover\n                pytest.fail(\"No response received - server likely crashed\")\n    finally:  # pragma: lax no cover\n        # Close all streams to ensure proper cleanup\n        await read_send_stream.aclose()\n        await write_send_stream.aclose()\n        await read_receive_stream.aclose()\n        await write_receive_stream.aclose()\n\n\n@pytest.mark.anyio\nasync def test_multiple_concurrent_malformed_requests():\n    \"\"\"Test that multiple concurrent malformed requests don't crash the server.\"\"\"\n    # Create in-memory streams for testing\n    read_send_stream, read_receive_stream = anyio.create_memory_object_stream[SessionMessage | Exception](100)\n    write_send_stream, write_receive_stream = anyio.create_memory_object_stream[SessionMessage](100)\n\n    try:\n        # Start a server session\n        async with ServerSession(\n            read_stream=read_receive_stream,\n            write_stream=write_send_stream,\n            init_options=InitializationOptions(\n                server_name=\"test_server\",\n                server_version=\"1.0.0\",\n                capabilities=ServerCapabilities(),\n            ),\n        ):\n            # Send multiple malformed requests concurrently\n            malformed_requests: list[SessionMessage] = []\n            for i in range(10):\n                malformed_request = JSONRPCRequest(\n                    jsonrpc=\"2.0\",\n                    id=f\"malformed_{i}\",\n                    method=\"initialize\",\n                    # params=None  # Missing required params\n                )\n                request_message = SessionMessage(message=malformed_request)\n                malformed_requests.append(request_message)\n\n            # Send all requests\n            for request in malformed_requests:\n                await read_send_stream.send(request)\n\n            # Give time to process\n            await anyio.sleep(0.2)\n\n            # Verify we get error responses for all requests\n            error_responses: list[JSONRPCMessage] = []\n            try:\n                while True:\n                    response_message = write_receive_stream.receive_nowait()\n                    error_responses.append(response_message.message)\n            except anyio.WouldBlock:\n                pass  # No more messages\n\n            # Should have received 10 error responses\n            assert len(error_responses) == 10\n\n            for i, response in enumerate(error_responses):\n                assert isinstance(response, JSONRPCError)\n                assert response.id == f\"malformed_{i}\"\n                assert response.error.code == INVALID_PARAMS\n    finally:  # pragma: lax no cover\n        # Close all streams to ensure proper cleanup\n        await read_send_stream.aclose()\n        await write_send_stream.aclose()\n        await read_receive_stream.aclose()\n        await write_receive_stream.aclose()\n"
  },
  {
    "path": "tests/server/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/auth/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/auth/middleware/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/auth/middleware/test_auth_context.py",
    "content": "\"\"\"Tests for the AuthContext middleware components.\"\"\"\n\nimport time\n\nimport pytest\nfrom starlette.types import Message, Receive, Scope, Send\n\nfrom mcp.server.auth.middleware.auth_context import (\n    AuthContextMiddleware,\n    auth_context_var,\n    get_access_token,\n)\nfrom mcp.server.auth.middleware.bearer_auth import AuthenticatedUser\nfrom mcp.server.auth.provider import AccessToken\n\n\nclass MockApp:\n    \"\"\"Mock ASGI app for testing.\"\"\"\n\n    def __init__(self):\n        self.called = False\n        self.scope: Scope | None = None\n        self.receive: Receive | None = None\n        self.send: Send | None = None\n        self.access_token_during_call: AccessToken | None = None\n\n    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:\n        self.called = True\n        self.scope = scope\n        self.receive = receive\n        self.send = send\n        # Check the context during the call\n        self.access_token_during_call = get_access_token()\n\n\n@pytest.fixture\ndef valid_access_token() -> AccessToken:\n    \"\"\"Create a valid access token.\"\"\"\n    return AccessToken(\n        token=\"valid_token\",\n        client_id=\"test_client\",\n        scopes=[\"read\", \"write\"],\n        expires_at=int(time.time()) + 3600,  # 1 hour from now\n    )\n\n\n@pytest.mark.anyio\nasync def test_auth_context_middleware_with_authenticated_user(valid_access_token: AccessToken):\n    \"\"\"Test middleware with an authenticated user in scope.\"\"\"\n    app = MockApp()\n    middleware = AuthContextMiddleware(app)\n\n    # Create an authenticated user\n    user = AuthenticatedUser(valid_access_token)\n\n    scope: Scope = {\"type\": \"http\", \"user\": user}\n\n    # Create dummy async functions for receive and send\n    async def receive() -> Message:  # pragma: no cover\n        return {\"type\": \"http.request\"}\n\n    async def send(message: Message) -> None:  # pragma: no cover\n        pass\n\n    # Verify context is empty before middleware\n    assert auth_context_var.get() is None\n    assert get_access_token() is None\n\n    # Run the middleware\n    await middleware(scope, receive, send)\n\n    # Verify the app was called\n    assert app.called\n    assert app.scope == scope\n    assert app.receive == receive\n    assert app.send == send\n\n    # Verify the access token was available during the call\n    assert app.access_token_during_call == valid_access_token\n\n    # Verify context is reset after middleware\n    assert auth_context_var.get() is None\n    assert get_access_token() is None\n\n\n@pytest.mark.anyio\nasync def test_auth_context_middleware_with_no_user():\n    \"\"\"Test middleware with no user in scope.\"\"\"\n    app = MockApp()\n    middleware = AuthContextMiddleware(app)\n\n    scope: Scope = {\"type\": \"http\"}  # No user\n\n    # Create dummy async functions for receive and send\n    async def receive() -> Message:  # pragma: no cover\n        return {\"type\": \"http.request\"}\n\n    async def send(message: Message) -> None:  # pragma: no cover\n        pass\n\n    # Verify context is empty before middleware\n    assert auth_context_var.get() is None\n    assert get_access_token() is None\n\n    # Run the middleware\n    await middleware(scope, receive, send)\n\n    # Verify the app was called\n    assert app.called\n    assert app.scope == scope\n    assert app.receive == receive\n    assert app.send == send\n\n    # Verify the access token was not available during the call\n    assert app.access_token_during_call is None\n\n    # Verify context is still empty after middleware\n    assert auth_context_var.get() is None\n    assert get_access_token() is None\n"
  },
  {
    "path": "tests/server/auth/middleware/test_bearer_auth.py",
    "content": "\"\"\"Tests for the BearerAuth middleware components.\"\"\"\n\nimport time\nfrom typing import Any, cast\n\nimport pytest\nfrom starlette.authentication import AuthCredentials\nfrom starlette.datastructures import Headers\nfrom starlette.requests import Request\nfrom starlette.types import Message, Receive, Scope, Send\n\nfrom mcp.server.auth.middleware.bearer_auth import AuthenticatedUser, BearerAuthBackend, RequireAuthMiddleware\nfrom mcp.server.auth.provider import AccessToken, OAuthAuthorizationServerProvider, ProviderTokenVerifier\n\n\nclass MockOAuthProvider:\n    \"\"\"Mock OAuth provider for testing.\n\n    This is a simplified version that only implements the methods needed for testing\n    the BearerAuthMiddleware components.\n    \"\"\"\n\n    def __init__(self):\n        self.tokens: dict[str, AccessToken] = {}  # token -> AccessToken\n\n    def add_token(self, token: str, access_token: AccessToken) -> None:\n        \"\"\"Add a token to the provider.\"\"\"\n        self.tokens[token] = access_token\n\n    async def load_access_token(self, token: str) -> AccessToken | None:\n        \"\"\"Load an access token.\"\"\"\n        return self.tokens.get(token)\n\n\ndef add_token_to_provider(\n    provider: OAuthAuthorizationServerProvider[Any, Any, Any],\n    token: str,\n    access_token: AccessToken,\n) -> None:\n    \"\"\"Helper function to add a token to a provider.\n\n    This is used to work around type checking issues with our mock provider.\n    \"\"\"\n    # We know this is actually a MockOAuthProvider\n    mock_provider = cast(MockOAuthProvider, provider)\n    mock_provider.add_token(token, access_token)\n\n\nclass MockApp:\n    \"\"\"Mock ASGI app for testing.\"\"\"\n\n    def __init__(self):\n        self.called = False\n        self.scope: Scope | None = None\n        self.receive: Receive | None = None\n        self.send: Send | None = None\n\n    async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:\n        self.called = True\n        self.scope = scope\n        self.receive = receive\n        self.send = send\n\n\n@pytest.fixture\ndef mock_oauth_provider() -> OAuthAuthorizationServerProvider[Any, Any, Any]:\n    \"\"\"Create a mock OAuth provider.\"\"\"\n    # Use type casting to satisfy the type checker\n    return cast(OAuthAuthorizationServerProvider[Any, Any, Any], MockOAuthProvider())\n\n\n@pytest.fixture\ndef valid_access_token() -> AccessToken:\n    \"\"\"Create a valid access token.\"\"\"\n    return AccessToken(\n        token=\"valid_token\",\n        client_id=\"test_client\",\n        scopes=[\"read\", \"write\"],\n        expires_at=int(time.time()) + 3600,  # 1 hour from now\n    )\n\n\n@pytest.fixture\ndef expired_access_token() -> AccessToken:\n    \"\"\"Create an expired access token.\"\"\"\n    return AccessToken(\n        token=\"expired_token\",\n        client_id=\"test_client\",\n        scopes=[\"read\"],\n        expires_at=int(time.time()) - 3600,  # 1 hour ago\n    )\n\n\n@pytest.fixture\ndef no_expiry_access_token() -> AccessToken:\n    \"\"\"Create an access token with no expiry.\"\"\"\n    return AccessToken(\n        token=\"no_expiry_token\",\n        client_id=\"test_client\",\n        scopes=[\"read\", \"write\"],\n        expires_at=None,\n    )\n\n\n@pytest.mark.anyio\nclass TestBearerAuthBackend:\n    \"\"\"Tests for the BearerAuthBackend class.\"\"\"\n\n    async def test_no_auth_header(self, mock_oauth_provider: OAuthAuthorizationServerProvider[Any, Any, Any]):\n        \"\"\"Test authentication with no Authorization header.\"\"\"\n        backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))\n        request = Request({\"type\": \"http\", \"headers\": []})\n        result = await backend.authenticate(request)\n        assert result is None\n\n    async def test_non_bearer_auth_header(self, mock_oauth_provider: OAuthAuthorizationServerProvider[Any, Any, Any]):\n        \"\"\"Test authentication with non-Bearer Authorization header.\"\"\"\n        backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))\n        request = Request(\n            {\n                \"type\": \"http\",\n                \"headers\": [(b\"authorization\", b\"Basic dXNlcjpwYXNz\")],\n            }\n        )\n        result = await backend.authenticate(request)\n        assert result is None\n\n    async def test_invalid_token(self, mock_oauth_provider: OAuthAuthorizationServerProvider[Any, Any, Any]):\n        \"\"\"Test authentication with invalid token.\"\"\"\n        backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))\n        request = Request(\n            {\n                \"type\": \"http\",\n                \"headers\": [(b\"authorization\", b\"Bearer invalid_token\")],\n            }\n        )\n        result = await backend.authenticate(request)\n        assert result is None\n\n    async def test_expired_token(\n        self,\n        mock_oauth_provider: OAuthAuthorizationServerProvider[Any, Any, Any],\n        expired_access_token: AccessToken,\n    ):\n        \"\"\"Test authentication with expired token.\"\"\"\n        backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))\n        add_token_to_provider(mock_oauth_provider, \"expired_token\", expired_access_token)\n        request = Request(\n            {\n                \"type\": \"http\",\n                \"headers\": [(b\"authorization\", b\"Bearer expired_token\")],\n            }\n        )\n        result = await backend.authenticate(request)\n        assert result is None\n\n    async def test_valid_token(\n        self,\n        mock_oauth_provider: OAuthAuthorizationServerProvider[Any, Any, Any],\n        valid_access_token: AccessToken,\n    ):\n        \"\"\"Test authentication with valid token.\"\"\"\n        backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))\n        add_token_to_provider(mock_oauth_provider, \"valid_token\", valid_access_token)\n        request = Request(\n            {\n                \"type\": \"http\",\n                \"headers\": [(b\"authorization\", b\"Bearer valid_token\")],\n            }\n        )\n        result = await backend.authenticate(request)\n        assert result is not None\n        credentials, user = result\n        assert isinstance(credentials, AuthCredentials)\n        assert isinstance(user, AuthenticatedUser)\n        assert credentials.scopes == [\"read\", \"write\"]\n        assert user.display_name == \"test_client\"\n        assert user.access_token == valid_access_token\n        assert user.scopes == [\"read\", \"write\"]\n\n    async def test_token_without_expiry(\n        self,\n        mock_oauth_provider: OAuthAuthorizationServerProvider[Any, Any, Any],\n        no_expiry_access_token: AccessToken,\n    ):\n        \"\"\"Test authentication with token that has no expiry.\"\"\"\n        backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))\n        add_token_to_provider(mock_oauth_provider, \"no_expiry_token\", no_expiry_access_token)\n        request = Request(\n            {\n                \"type\": \"http\",\n                \"headers\": [(b\"authorization\", b\"Bearer no_expiry_token\")],\n            }\n        )\n        result = await backend.authenticate(request)\n        assert result is not None\n        credentials, user = result\n        assert isinstance(credentials, AuthCredentials)\n        assert isinstance(user, AuthenticatedUser)\n        assert credentials.scopes == [\"read\", \"write\"]\n        assert user.display_name == \"test_client\"\n        assert user.access_token == no_expiry_access_token\n        assert user.scopes == [\"read\", \"write\"]\n\n    async def test_lowercase_bearer_prefix(\n        self,\n        mock_oauth_provider: OAuthAuthorizationServerProvider[Any, Any, Any],\n        valid_access_token: AccessToken,\n    ):\n        \"\"\"Test with lowercase 'bearer' prefix in Authorization header\"\"\"\n        backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))\n        add_token_to_provider(mock_oauth_provider, \"valid_token\", valid_access_token)\n        headers = Headers({\"Authorization\": \"bearer valid_token\"})\n        scope = {\"type\": \"http\", \"headers\": headers.raw}\n        request = Request(scope)\n        result = await backend.authenticate(request)\n        assert result is not None\n        credentials, user = result\n        assert isinstance(credentials, AuthCredentials)\n        assert isinstance(user, AuthenticatedUser)\n        assert credentials.scopes == [\"read\", \"write\"]\n        assert user.display_name == \"test_client\"\n        assert user.access_token == valid_access_token\n\n    async def test_mixed_case_bearer_prefix(\n        self,\n        mock_oauth_provider: OAuthAuthorizationServerProvider[Any, Any, Any],\n        valid_access_token: AccessToken,\n    ):\n        \"\"\"Test with mixed 'BeArEr' prefix in Authorization header\"\"\"\n        backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))\n        add_token_to_provider(mock_oauth_provider, \"valid_token\", valid_access_token)\n        headers = Headers({\"authorization\": \"BeArEr valid_token\"})\n        scope = {\"type\": \"http\", \"headers\": headers.raw}\n        request = Request(scope)\n        result = await backend.authenticate(request)\n        assert result is not None\n        credentials, user = result\n        assert isinstance(credentials, AuthCredentials)\n        assert isinstance(user, AuthenticatedUser)\n        assert credentials.scopes == [\"read\", \"write\"]\n        assert user.display_name == \"test_client\"\n        assert user.access_token == valid_access_token\n\n    async def test_mixed_case_authorization_header(\n        self,\n        mock_oauth_provider: OAuthAuthorizationServerProvider[Any, Any, Any],\n        valid_access_token: AccessToken,\n    ):\n        \"\"\"Test authentication with mixed 'Authorization' header.\"\"\"\n        backend = BearerAuthBackend(token_verifier=ProviderTokenVerifier(mock_oauth_provider))\n        add_token_to_provider(mock_oauth_provider, \"valid_token\", valid_access_token)\n        headers = Headers({\"AuThOrIzAtIoN\": \"BeArEr valid_token\"})\n        scope = {\"type\": \"http\", \"headers\": headers.raw}\n        request = Request(scope)\n        result = await backend.authenticate(request)\n        assert result is not None\n        credentials, user = result\n        assert isinstance(credentials, AuthCredentials)\n        assert isinstance(user, AuthenticatedUser)\n        assert credentials.scopes == [\"read\", \"write\"]\n        assert user.display_name == \"test_client\"\n        assert user.access_token == valid_access_token\n\n\n@pytest.mark.anyio\nclass TestRequireAuthMiddleware:\n    \"\"\"Tests for the RequireAuthMiddleware class.\"\"\"\n\n    async def test_no_user(self):\n        \"\"\"Test middleware with no user in scope.\"\"\"\n        app = MockApp()\n        middleware = RequireAuthMiddleware(app, required_scopes=[\"read\"])\n        scope: Scope = {\"type\": \"http\"}\n\n        # Create dummy async functions for receive and send\n        async def receive() -> Message:  # pragma: no cover\n            return {\"type\": \"http.request\"}\n\n        sent_messages: list[Message] = []\n\n        async def send(message: Message) -> None:\n            sent_messages.append(message)\n\n        await middleware(scope, receive, send)\n\n        # Check that a 401 response was sent\n        assert len(sent_messages) == 2\n        assert sent_messages[0][\"type\"] == \"http.response.start\"\n        assert sent_messages[0][\"status\"] == 401\n        assert any(h[0] == b\"www-authenticate\" for h in sent_messages[0][\"headers\"])\n        assert not app.called\n\n    async def test_non_authenticated_user(self):\n        \"\"\"Test middleware with non-authenticated user in scope.\"\"\"\n        app = MockApp()\n        middleware = RequireAuthMiddleware(app, required_scopes=[\"read\"])\n        scope: Scope = {\"type\": \"http\", \"user\": object()}\n\n        # Create dummy async functions for receive and send\n        async def receive() -> Message:  # pragma: no cover\n            return {\"type\": \"http.request\"}\n\n        sent_messages: list[Message] = []\n\n        async def send(message: Message) -> None:\n            sent_messages.append(message)\n\n        await middleware(scope, receive, send)\n\n        # Check that a 401 response was sent\n        assert len(sent_messages) == 2\n        assert sent_messages[0][\"type\"] == \"http.response.start\"\n        assert sent_messages[0][\"status\"] == 401\n        assert any(h[0] == b\"www-authenticate\" for h in sent_messages[0][\"headers\"])\n        assert not app.called\n\n    async def test_missing_required_scope(self, valid_access_token: AccessToken):\n        \"\"\"Test middleware with user missing required scope.\"\"\"\n        app = MockApp()\n        middleware = RequireAuthMiddleware(app, required_scopes=[\"admin\"])\n\n        # Create a user with read/write scopes but not admin\n        user = AuthenticatedUser(valid_access_token)\n        auth = AuthCredentials([\"read\", \"write\"])\n\n        scope: Scope = {\"type\": \"http\", \"user\": user, \"auth\": auth}\n\n        # Create dummy async functions for receive and send\n        async def receive() -> Message:  # pragma: no cover\n            return {\"type\": \"http.request\"}\n\n        sent_messages: list[Message] = []\n\n        async def send(message: Message) -> None:\n            sent_messages.append(message)\n\n        await middleware(scope, receive, send)\n\n        # Check that a 403 response was sent\n        assert len(sent_messages) == 2\n        assert sent_messages[0][\"type\"] == \"http.response.start\"\n        assert sent_messages[0][\"status\"] == 403\n        assert any(h[0] == b\"www-authenticate\" for h in sent_messages[0][\"headers\"])\n        assert not app.called\n\n    async def test_no_auth_credentials(self, valid_access_token: AccessToken):\n        \"\"\"Test middleware with no auth credentials in scope.\"\"\"\n        app = MockApp()\n        middleware = RequireAuthMiddleware(app, required_scopes=[\"read\"])\n\n        # Create a user with read/write scopes\n        user = AuthenticatedUser(valid_access_token)\n\n        scope: Scope = {\"type\": \"http\", \"user\": user}  # No auth credentials\n\n        # Create dummy async functions for receive and send\n        async def receive() -> Message:  # pragma: no cover\n            return {\"type\": \"http.request\"}\n\n        sent_messages: list[Message] = []\n\n        async def send(message: Message) -> None:\n            sent_messages.append(message)\n\n        await middleware(scope, receive, send)\n\n        # Check that a 403 response was sent\n        assert len(sent_messages) == 2\n        assert sent_messages[0][\"type\"] == \"http.response.start\"\n        assert sent_messages[0][\"status\"] == 403\n        assert any(h[0] == b\"www-authenticate\" for h in sent_messages[0][\"headers\"])\n        assert not app.called\n\n    async def test_has_required_scopes(self, valid_access_token: AccessToken):\n        \"\"\"Test middleware with user having all required scopes.\"\"\"\n        app = MockApp()\n        middleware = RequireAuthMiddleware(app, required_scopes=[\"read\"])\n\n        # Create a user with read/write scopes\n        user = AuthenticatedUser(valid_access_token)\n        auth = AuthCredentials([\"read\", \"write\"])\n\n        scope: Scope = {\"type\": \"http\", \"user\": user, \"auth\": auth}\n\n        # Create dummy async functions for receive and send\n        async def receive() -> Message:  # pragma: no cover\n            return {\"type\": \"http.request\"}\n\n        async def send(message: Message) -> None:  # pragma: no cover\n            pass\n\n        await middleware(scope, receive, send)\n\n        assert app.called\n        assert app.scope == scope\n        assert app.receive == receive\n        assert app.send == send\n\n    async def test_multiple_required_scopes(self, valid_access_token: AccessToken):\n        \"\"\"Test middleware with multiple required scopes.\"\"\"\n        app = MockApp()\n        middleware = RequireAuthMiddleware(app, required_scopes=[\"read\", \"write\"])\n\n        # Create a user with read/write scopes\n        user = AuthenticatedUser(valid_access_token)\n        auth = AuthCredentials([\"read\", \"write\"])\n\n        scope: Scope = {\"type\": \"http\", \"user\": user, \"auth\": auth}\n\n        # Create dummy async functions for receive and send\n        async def receive() -> Message:  # pragma: no cover\n            return {\"type\": \"http.request\"}\n\n        async def send(message: Message) -> None:  # pragma: no cover\n            pass\n\n        await middleware(scope, receive, send)\n\n        assert app.called\n        assert app.scope == scope\n        assert app.receive == receive\n        assert app.send == send\n\n    async def test_no_required_scopes(self, valid_access_token: AccessToken):\n        \"\"\"Test middleware with no required scopes.\"\"\"\n        app = MockApp()\n        middleware = RequireAuthMiddleware(app, required_scopes=[])\n\n        # Create a user with read/write scopes\n        user = AuthenticatedUser(valid_access_token)\n        auth = AuthCredentials([\"read\", \"write\"])\n\n        scope: Scope = {\"type\": \"http\", \"user\": user, \"auth\": auth}\n\n        # Create dummy async functions for receive and send\n        async def receive() -> Message:  # pragma: no cover\n            return {\"type\": \"http.request\"}\n\n        async def send(message: Message) -> None:  # pragma: no cover\n            pass\n\n        await middleware(scope, receive, send)\n\n        assert app.called\n        assert app.scope == scope\n        assert app.receive == receive\n        assert app.send == send\n"
  },
  {
    "path": "tests/server/auth/test_error_handling.py",
    "content": "\"\"\"Tests for OAuth error handling in the auth handlers.\"\"\"\n\nimport base64\nimport hashlib\nimport secrets\nimport unittest.mock\nfrom typing import Any\nfrom urllib.parse import parse_qs, urlparse\n\nimport httpx\nimport pytest\nfrom httpx import ASGITransport\nfrom pydantic import AnyHttpUrl\nfrom starlette.applications import Starlette\n\nfrom mcp.server.auth.provider import AuthorizeError, RegistrationError, TokenError\nfrom mcp.server.auth.routes import create_auth_routes\nfrom mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions\nfrom tests.server.mcpserver.auth.test_auth_integration import MockOAuthProvider\n\n\n@pytest.fixture\ndef oauth_provider():\n    \"\"\"Return a MockOAuthProvider instance that can be configured to raise errors.\"\"\"\n    return MockOAuthProvider()\n\n\n@pytest.fixture\ndef app(oauth_provider: MockOAuthProvider):\n    # Enable client registration\n    client_registration_options = ClientRegistrationOptions(enabled=True)\n    revocation_options = RevocationOptions(enabled=True)\n\n    # Create auth routes\n    auth_routes = create_auth_routes(\n        oauth_provider,\n        issuer_url=AnyHttpUrl(\"http://localhost\"),\n        client_registration_options=client_registration_options,\n        revocation_options=revocation_options,\n    )\n\n    # Create Starlette app with routes directly\n    return Starlette(routes=auth_routes)\n\n\n@pytest.fixture\ndef client(app: Starlette):\n    transport = ASGITransport(app=app)\n    # Use base_url without a path since routes are directly on the app\n    return httpx.AsyncClient(transport=transport, base_url=\"http://localhost\")\n\n\n@pytest.fixture\ndef pkce_challenge():\n    \"\"\"Create a PKCE challenge with code_verifier and code_challenge.\"\"\"\n    # Generate a code verifier\n    code_verifier = secrets.token_urlsafe(64)[:128]\n\n    # Create code challenge using S256 method\n    code_verifier_bytes = code_verifier.encode(\"ascii\")\n    sha256 = hashlib.sha256(code_verifier_bytes).digest()\n    code_challenge = base64.urlsafe_b64encode(sha256).decode().rstrip(\"=\")\n\n    return {\"code_verifier\": code_verifier, \"code_challenge\": code_challenge}\n\n\n@pytest.fixture\nasync def registered_client(client: httpx.AsyncClient) -> dict[str, Any]:\n    \"\"\"Create and register a test client.\"\"\"\n    # Default client metadata\n    client_metadata = {\n        \"redirect_uris\": [\"https://client.example.com/callback\"],\n        \"token_endpoint_auth_method\": \"client_secret_post\",\n        \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n        \"response_types\": [\"code\"],\n        \"client_name\": \"Test Client\",\n    }\n\n    response = await client.post(\"/register\", json=client_metadata)\n    assert response.status_code == 201, f\"Failed to register client: {response.content}\"\n\n    client_info = response.json()\n    return client_info\n\n\n@pytest.mark.anyio\nasync def test_registration_error_handling(client: httpx.AsyncClient, oauth_provider: MockOAuthProvider):\n    # Mock the register_client method to raise a registration error\n    with unittest.mock.patch.object(\n        oauth_provider,\n        \"register_client\",\n        side_effect=RegistrationError(\n            error=\"invalid_redirect_uri\",\n            error_description=\"The redirect URI is invalid\",\n        ),\n    ):\n        # Prepare a client registration request\n        client_data = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"token_endpoint_auth_method\": \"client_secret_post\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n            \"response_types\": [\"code\"],\n            \"client_name\": \"Test Client\",\n        }\n\n        # Send the registration request\n        response = await client.post(\n            \"/register\",\n            json=client_data,\n        )\n\n        # Verify the response\n        assert response.status_code == 400, response.content\n        data = response.json()\n        assert data[\"error\"] == \"invalid_redirect_uri\"\n        assert data[\"error_description\"] == \"The redirect URI is invalid\"\n\n\n@pytest.mark.anyio\nasync def test_authorize_error_handling(\n    client: httpx.AsyncClient,\n    oauth_provider: MockOAuthProvider,\n    registered_client: dict[str, Any],\n    pkce_challenge: dict[str, str],\n):\n    # Mock the authorize method to raise an authorize error\n    with unittest.mock.patch.object(\n        oauth_provider,\n        \"authorize\",\n        side_effect=AuthorizeError(error=\"access_denied\", error_description=\"The user denied the request\"),\n    ):\n        # Register the client\n        client_id = registered_client[\"client_id\"]\n        redirect_uri = registered_client[\"redirect_uris\"][0]\n\n        # Prepare an authorization request\n        params = {\n            \"client_id\": client_id,\n            \"redirect_uri\": redirect_uri,\n            \"response_type\": \"code\",\n            \"code_challenge\": pkce_challenge[\"code_challenge\"],\n            \"code_challenge_method\": \"S256\",\n            \"state\": \"test_state\",\n        }\n\n        # Send the authorization request\n        response = await client.get(\"/authorize\", params=params)\n\n        # Verify the response is a redirect with error parameters\n        assert response.status_code == 302\n        redirect_url = response.headers[\"location\"]\n        parsed_url = urlparse(redirect_url)\n        query_params = parse_qs(parsed_url.query)\n\n        assert query_params[\"error\"][0] == \"access_denied\"\n        assert \"error_description\" in query_params\n        assert query_params[\"state\"][0] == \"test_state\"\n\n\n@pytest.mark.anyio\nasync def test_token_error_handling_auth_code(\n    client: httpx.AsyncClient,\n    oauth_provider: MockOAuthProvider,\n    registered_client: dict[str, Any],\n    pkce_challenge: dict[str, str],\n):\n    # Register the client and get an auth code\n    client_id = registered_client[\"client_id\"]\n    client_secret = registered_client[\"client_secret\"]\n    redirect_uri = registered_client[\"redirect_uris\"][0]\n\n    # First get an authorization code\n    auth_response = await client.get(\n        \"/authorize\",\n        params={\n            \"client_id\": client_id,\n            \"redirect_uri\": redirect_uri,\n            \"response_type\": \"code\",\n            \"code_challenge\": pkce_challenge[\"code_challenge\"],\n            \"code_challenge_method\": \"S256\",\n            \"state\": \"test_state\",\n        },\n    )\n\n    redirect_url = auth_response.headers[\"location\"]\n    parsed_url = urlparse(redirect_url)\n    query_params = parse_qs(parsed_url.query)\n    code = query_params[\"code\"][0]\n\n    # Mock the exchange_authorization_code method to raise a token error\n    with unittest.mock.patch.object(\n        oauth_provider,\n        \"exchange_authorization_code\",\n        side_effect=TokenError(\n            error=\"invalid_grant\",\n            error_description=\"The authorization code is invalid\",\n        ),\n    ):\n        # Try to exchange the code for tokens\n        token_response = await client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"code\": code,\n                \"redirect_uri\": redirect_uri,\n                \"client_id\": client_id,\n                \"client_secret\": client_secret,\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n            },\n        )\n\n        # Verify the response\n        assert token_response.status_code == 400\n        data = token_response.json()\n        assert data[\"error\"] == \"invalid_grant\"\n        assert data[\"error_description\"] == \"The authorization code is invalid\"\n\n\n@pytest.mark.anyio\nasync def test_token_error_handling_refresh_token(\n    client: httpx.AsyncClient,\n    oauth_provider: MockOAuthProvider,\n    registered_client: dict[str, Any],\n    pkce_challenge: dict[str, str],\n):\n    # Register the client and get tokens\n    client_id = registered_client[\"client_id\"]\n    client_secret = registered_client[\"client_secret\"]\n    redirect_uri = registered_client[\"redirect_uris\"][0]\n\n    # First get an authorization code\n    auth_response = await client.get(\n        \"/authorize\",\n        params={\n            \"client_id\": client_id,\n            \"redirect_uri\": redirect_uri,\n            \"response_type\": \"code\",\n            \"code_challenge\": pkce_challenge[\"code_challenge\"],\n            \"code_challenge_method\": \"S256\",\n            \"state\": \"test_state\",\n        },\n    )\n    assert auth_response.status_code == 302, auth_response.content\n\n    redirect_url = auth_response.headers[\"location\"]\n    parsed_url = urlparse(redirect_url)\n    query_params = parse_qs(parsed_url.query)\n    code = query_params[\"code\"][0]\n\n    # Exchange the code for tokens\n    token_response = await client.post(\n        \"/token\",\n        data={\n            \"grant_type\": \"authorization_code\",\n            \"code\": code,\n            \"redirect_uri\": redirect_uri,\n            \"client_id\": client_id,\n            \"client_secret\": client_secret,\n            \"code_verifier\": pkce_challenge[\"code_verifier\"],\n        },\n    )\n\n    tokens = token_response.json()\n    refresh_token = tokens[\"refresh_token\"]\n\n    # Mock the exchange_refresh_token method to raise a token error\n    with unittest.mock.patch.object(\n        oauth_provider,\n        \"exchange_refresh_token\",\n        side_effect=TokenError(\n            error=\"invalid_scope\",\n            error_description=\"The requested scope is invalid\",\n        ),\n    ):\n        # Try to use the refresh token\n        refresh_response = await client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"refresh_token\",\n                \"refresh_token\": refresh_token,\n                \"client_id\": client_id,\n                \"client_secret\": client_secret,\n            },\n        )\n\n        # Verify the response\n        assert refresh_response.status_code == 400\n        data = refresh_response.json()\n        assert data[\"error\"] == \"invalid_scope\"\n        assert data[\"error_description\"] == \"The requested scope is invalid\"\n"
  },
  {
    "path": "tests/server/auth/test_protected_resource.py",
    "content": "\"\"\"Integration tests for MCP Oauth Protected Resource.\"\"\"\n\nfrom urllib.parse import urlparse\n\nimport httpx\nimport pytest\nfrom inline_snapshot import snapshot\nfrom pydantic import AnyHttpUrl\nfrom starlette.applications import Starlette\n\nfrom mcp.server.auth.routes import build_resource_metadata_url, create_protected_resource_routes\n\n\n@pytest.fixture\ndef test_app():\n    \"\"\"Fixture to create protected resource routes for testing.\"\"\"\n\n    # Create the protected resource routes\n    protected_resource_routes = create_protected_resource_routes(\n        resource_url=AnyHttpUrl(\"https://example.com/resource\"),\n        authorization_servers=[AnyHttpUrl(\"https://auth.example.com/authorization\")],\n        scopes_supported=[\"read\", \"write\"],\n        resource_name=\"Example Resource\",\n        resource_documentation=AnyHttpUrl(\"https://docs.example.com/resource\"),\n    )\n\n    app = Starlette(routes=protected_resource_routes)\n    return app\n\n\n@pytest.fixture\nasync def test_client(test_app: Starlette):\n    \"\"\"Fixture to create an HTTP client for the protected resource app.\"\"\"\n    async with httpx.AsyncClient(transport=httpx.ASGITransport(app=test_app), base_url=\"https://mcptest.com\") as client:\n        yield client\n\n\n@pytest.mark.anyio\nasync def test_metadata_endpoint_with_path(test_client: httpx.AsyncClient):\n    \"\"\"Test the OAuth 2.0 Protected Resource metadata endpoint for path-based resource.\"\"\"\n\n    # For resource with path \"/resource\", metadata should be accessible at the path-aware location\n    response = await test_client.get(\"/.well-known/oauth-protected-resource/resource\")\n    assert response.json() == snapshot(\n        {\n            \"resource\": \"https://example.com/resource\",\n            \"authorization_servers\": [\"https://auth.example.com/authorization\"],\n            \"scopes_supported\": [\"read\", \"write\"],\n            \"resource_name\": \"Example Resource\",\n            \"resource_documentation\": \"https://docs.example.com/resource\",\n            \"bearer_methods_supported\": [\"header\"],\n        }\n    )\n\n\n@pytest.mark.anyio\nasync def test_metadata_endpoint_root_path_returns_404(test_client: httpx.AsyncClient):\n    \"\"\"Test that root path returns 404 for path-based resource.\"\"\"\n\n    # Root path should return 404 for path-based resources\n    response = await test_client.get(\"/.well-known/oauth-protected-resource\")\n    assert response.status_code == 404\n\n\n@pytest.fixture\ndef root_resource_app():\n    \"\"\"Fixture to create protected resource routes for root-level resource.\"\"\"\n\n    # Create routes for a resource without path component\n    protected_resource_routes = create_protected_resource_routes(\n        resource_url=AnyHttpUrl(\"https://example.com\"),\n        authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n        scopes_supported=[\"read\"],\n        resource_name=\"Root Resource\",\n    )\n\n    app = Starlette(routes=protected_resource_routes)\n    return app\n\n\n@pytest.fixture\nasync def root_resource_client(root_resource_app: Starlette):\n    \"\"\"Fixture to create an HTTP client for the root resource app.\"\"\"\n    async with httpx.AsyncClient(\n        transport=httpx.ASGITransport(app=root_resource_app), base_url=\"https://mcptest.com\"\n    ) as client:\n        yield client\n\n\n@pytest.mark.anyio\nasync def test_metadata_endpoint_without_path(root_resource_client: httpx.AsyncClient):\n    \"\"\"Test metadata endpoint for root-level resource.\"\"\"\n\n    # For root resource, metadata should be at standard location\n    response = await root_resource_client.get(\"/.well-known/oauth-protected-resource\")\n    assert response.status_code == 200\n    assert response.json() == snapshot(\n        {\n            \"resource\": \"https://example.com/\",\n            \"authorization_servers\": [\"https://auth.example.com/\"],\n            \"scopes_supported\": [\"read\"],\n            \"resource_name\": \"Root Resource\",\n            \"bearer_methods_supported\": [\"header\"],\n        }\n    )\n\n\n# Tests for URL construction utility function\n\n\ndef test_metadata_url_construction_url_without_path():\n    \"\"\"Test URL construction for resource without path component.\"\"\"\n    resource_url = AnyHttpUrl(\"https://example.com\")\n    result = build_resource_metadata_url(resource_url)\n    assert str(result) == \"https://example.com/.well-known/oauth-protected-resource\"\n\n\ndef test_metadata_url_construction_url_with_path_component():\n    \"\"\"Test URL construction for resource with path component.\"\"\"\n    resource_url = AnyHttpUrl(\"https://example.com/mcp\")\n    result = build_resource_metadata_url(resource_url)\n    assert str(result) == \"https://example.com/.well-known/oauth-protected-resource/mcp\"\n\n\ndef test_metadata_url_construction_url_with_trailing_slash_only():\n    \"\"\"Test URL construction for resource with trailing slash only.\"\"\"\n    resource_url = AnyHttpUrl(\"https://example.com/\")\n    result = build_resource_metadata_url(resource_url)\n    # Trailing slash should be treated as empty path\n    assert str(result) == \"https://example.com/.well-known/oauth-protected-resource\"\n\n\n@pytest.mark.parametrize(\n    \"resource_url,expected_url\",\n    [\n        (\"https://example.com\", \"https://example.com/.well-known/oauth-protected-resource\"),\n        (\"https://example.com/\", \"https://example.com/.well-known/oauth-protected-resource\"),\n        (\"https://example.com/mcp\", \"https://example.com/.well-known/oauth-protected-resource/mcp\"),\n        (\"http://localhost:8001/mcp\", \"http://localhost:8001/.well-known/oauth-protected-resource/mcp\"),\n    ],\n)\ndef test_metadata_url_construction_various_resource_configurations(resource_url: str, expected_url: str):\n    \"\"\"Test URL construction with various resource configurations.\"\"\"\n    result = build_resource_metadata_url(AnyHttpUrl(resource_url))\n    assert str(result) == expected_url\n\n\n# Tests for consistency between URL generation and route registration\n\n\ndef test_route_consistency_route_path_matches_metadata_url():\n    \"\"\"Test that route path matches the generated metadata URL.\"\"\"\n    resource_url = AnyHttpUrl(\"https://example.com/mcp\")\n\n    # Generate metadata URL\n    metadata_url = build_resource_metadata_url(resource_url)\n\n    # Create routes\n    routes = create_protected_resource_routes(\n        resource_url=resource_url,\n        authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n    )\n\n    # Extract path from metadata URL\n    metadata_path = urlparse(str(metadata_url)).path\n\n    # Verify consistency\n    assert len(routes) == 1\n    assert routes[0].path == metadata_path\n\n\n@pytest.mark.parametrize(\n    \"resource_url,expected_path\",\n    [\n        (\"https://example.com\", \"/.well-known/oauth-protected-resource\"),\n        (\"https://example.com/\", \"/.well-known/oauth-protected-resource\"),\n        (\"https://example.com/mcp\", \"/.well-known/oauth-protected-resource/mcp\"),\n    ],\n)\ndef test_route_consistency_consistent_paths_for_various_resources(resource_url: str, expected_path: str):\n    \"\"\"Test that URL generation and route creation are consistent.\"\"\"\n    resource_url_obj = AnyHttpUrl(resource_url)\n\n    # Test URL generation\n    metadata_url = build_resource_metadata_url(resource_url_obj)\n    url_path = urlparse(str(metadata_url)).path\n\n    # Test route creation\n    routes = create_protected_resource_routes(\n        resource_url=resource_url_obj,\n        authorization_servers=[AnyHttpUrl(\"https://auth.example.com\")],\n    )\n    route_path = routes[0].path\n\n    # Both should match expected path\n    assert url_path == expected_path\n    assert route_path == expected_path\n    assert url_path == route_path\n"
  },
  {
    "path": "tests/server/auth/test_provider.py",
    "content": "\"\"\"Tests for mcp.server.auth.provider module.\"\"\"\n\nfrom mcp.server.auth.provider import construct_redirect_uri\n\n\ndef test_construct_redirect_uri_no_existing_params():\n    \"\"\"Test construct_redirect_uri with no existing query parameters.\"\"\"\n    base_uri = \"http://localhost:8000/callback\"\n    result = construct_redirect_uri(base_uri, code=\"auth_code\", state=\"test_state\")\n\n    assert \"http://localhost:8000/callback?code=auth_code&state=test_state\" == result\n\n\ndef test_construct_redirect_uri_with_existing_params():\n    \"\"\"Test construct_redirect_uri with existing query parameters (regression test for #1279).\"\"\"\n    base_uri = \"http://localhost:8000/callback?session_id=1234\"\n    result = construct_redirect_uri(base_uri, code=\"auth_code\", state=\"test_state\")\n\n    # Should preserve existing params and add new ones\n    assert \"session_id=1234\" in result\n    assert \"code=auth_code\" in result\n    assert \"state=test_state\" in result\n    assert result.startswith(\"http://localhost:8000/callback?\")\n\n\ndef test_construct_redirect_uri_multiple_existing_params():\n    \"\"\"Test construct_redirect_uri with multiple existing query parameters.\"\"\"\n    base_uri = \"http://localhost:8000/callback?session_id=1234&user=test\"\n    result = construct_redirect_uri(base_uri, code=\"auth_code\")\n\n    assert \"session_id=1234\" in result\n    assert \"user=test\" in result\n    assert \"code=auth_code\" in result\n\n\ndef test_construct_redirect_uri_with_none_values():\n    \"\"\"Test construct_redirect_uri filters out None values.\"\"\"\n    base_uri = \"http://localhost:8000/callback\"\n    result = construct_redirect_uri(base_uri, code=\"auth_code\", state=None)\n\n    assert result == \"http://localhost:8000/callback?code=auth_code\"\n    assert \"state\" not in result\n\n\ndef test_construct_redirect_uri_empty_params():\n    \"\"\"Test construct_redirect_uri with no additional parameters.\"\"\"\n    base_uri = \"http://localhost:8000/callback?existing=param\"\n    result = construct_redirect_uri(base_uri)\n\n    assert result == \"http://localhost:8000/callback?existing=param\"\n\n\ndef test_construct_redirect_uri_duplicate_param_names():\n    \"\"\"Test construct_redirect_uri when adding param that already exists.\"\"\"\n    base_uri = \"http://localhost:8000/callback?code=existing\"\n    result = construct_redirect_uri(base_uri, code=\"new_code\")\n\n    # Should contain both values (this is expected behavior of parse_qs/urlencode)\n    assert \"code=existing\" in result\n    assert \"code=new_code\" in result\n\n\ndef test_construct_redirect_uri_multivalued_existing_params():\n    \"\"\"Test construct_redirect_uri with existing multi-valued parameters.\"\"\"\n    base_uri = \"http://localhost:8000/callback?scope=read&scope=write\"\n    result = construct_redirect_uri(base_uri, code=\"auth_code\")\n\n    assert \"scope=read\" in result\n    assert \"scope=write\" in result\n    assert \"code=auth_code\" in result\n\n\ndef test_construct_redirect_uri_encoded_values():\n    \"\"\"Test construct_redirect_uri handles URL encoding properly.\"\"\"\n    base_uri = \"http://localhost:8000/callback\"\n    result = construct_redirect_uri(base_uri, state=\"test state with spaces\")\n\n    # urlencode uses + for spaces by default\n    assert \"state=test+state+with+spaces\" in result\n"
  },
  {
    "path": "tests/server/auth/test_routes.py",
    "content": "import pytest\nfrom pydantic import AnyHttpUrl\n\nfrom mcp.server.auth.routes import validate_issuer_url\n\n\ndef test_validate_issuer_url_https_allowed():\n    validate_issuer_url(AnyHttpUrl(\"https://example.com/path\"))\n\n\ndef test_validate_issuer_url_http_localhost_allowed():\n    validate_issuer_url(AnyHttpUrl(\"http://localhost:8080/path\"))\n\n\ndef test_validate_issuer_url_http_127_0_0_1_allowed():\n    validate_issuer_url(AnyHttpUrl(\"http://127.0.0.1:8080/path\"))\n\n\ndef test_validate_issuer_url_http_ipv6_loopback_allowed():\n    validate_issuer_url(AnyHttpUrl(\"http://[::1]:8080/path\"))\n\n\ndef test_validate_issuer_url_http_non_loopback_rejected():\n    with pytest.raises(ValueError, match=\"Issuer URL must be HTTPS\"):\n        validate_issuer_url(AnyHttpUrl(\"http://evil.com/path\"))\n\n\ndef test_validate_issuer_url_http_127_prefix_domain_rejected():\n    \"\"\"A domain like 127.0.0.1.evil.com is not loopback.\"\"\"\n    with pytest.raises(ValueError, match=\"Issuer URL must be HTTPS\"):\n        validate_issuer_url(AnyHttpUrl(\"http://127.0.0.1.evil.com/path\"))\n\n\ndef test_validate_issuer_url_http_127_prefix_subdomain_rejected():\n    \"\"\"A domain like 127.0.0.1something.example.com is not loopback.\"\"\"\n    with pytest.raises(ValueError, match=\"Issuer URL must be HTTPS\"):\n        validate_issuer_url(AnyHttpUrl(\"http://127.0.0.1something.example.com/path\"))\n\n\ndef test_validate_issuer_url_fragment_rejected():\n    with pytest.raises(ValueError, match=\"fragment\"):\n        validate_issuer_url(AnyHttpUrl(\"https://example.com/path#frag\"))\n\n\ndef test_validate_issuer_url_query_rejected():\n    with pytest.raises(ValueError, match=\"query\"):\n        validate_issuer_url(AnyHttpUrl(\"https://example.com/path?q=1\"))\n"
  },
  {
    "path": "tests/server/lowlevel/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/lowlevel/test_helper_types.py",
    "content": "\"\"\"Test helper_types.py meta field.\n\nThese tests verify the changes made to helper_types.py:11 where we added:\n    meta: dict[str, Any] | None = field(default=None)\n\nReadResourceContents is the return type for resource read handlers. It's used internally\nby the low-level server to package resource content before sending it over the MCP protocol.\n\"\"\"\n\nfrom mcp.server.lowlevel.helper_types import ReadResourceContents\n\n\ndef test_read_resource_contents_with_metadata():\n    \"\"\"Test that ReadResourceContents accepts meta parameter.\n\n    ReadResourceContents is an internal helper type used by the low-level MCP server.\n    When a resource is read, the server creates a ReadResourceContents instance that\n    contains the content, mime type, and now metadata. The low-level server then\n    extracts the meta field and includes it in the protocol response as _meta.\n    \"\"\"\n    # Bridge between Resource.meta and MCP protocol _meta field (helper_types.py:11)\n    metadata = {\"version\": \"1.0\", \"cached\": True}\n\n    contents = ReadResourceContents(\n        content=\"test content\",\n        mime_type=\"text/plain\",\n        meta=metadata,\n    )\n\n    assert contents.meta is not None\n    assert contents.meta == metadata\n    assert contents.meta[\"version\"] == \"1.0\"\n    assert contents.meta[\"cached\"] is True\n\n\ndef test_read_resource_contents_without_metadata():\n    \"\"\"Test that ReadResourceContents meta defaults to None.\"\"\"\n    # Ensures backward compatibility - meta defaults to None, _meta omitted from protocol (helper_types.py:11)\n    contents = ReadResourceContents(\n        content=\"test content\",\n        mime_type=\"text/plain\",\n    )\n\n    assert contents.meta is None\n\n\ndef test_read_resource_contents_with_bytes():\n    \"\"\"Test that ReadResourceContents works with bytes content and meta.\"\"\"\n    # Verifies meta works with both str and bytes content (binary resources like images, PDFs)\n    metadata = {\"encoding\": \"utf-8\"}\n\n    contents = ReadResourceContents(\n        content=b\"binary content\",\n        mime_type=\"application/octet-stream\",\n        meta=metadata,\n    )\n\n    assert contents.content == b\"binary content\"\n    assert contents.meta == metadata\n"
  },
  {
    "path": "tests/server/lowlevel/test_server_listing.py",
    "content": "\"\"\"Basic tests for list_prompts, list_resources, and list_tools handlers without pagination.\"\"\"\n\nimport pytest\n\nfrom mcp import Client\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.types import (\n    ListPromptsResult,\n    ListResourcesResult,\n    ListToolsResult,\n    PaginatedRequestParams,\n    Prompt,\n    Resource,\n    Tool,\n)\n\n\n@pytest.mark.anyio\nasync def test_list_prompts_basic() -> None:\n    \"\"\"Test basic prompt listing without pagination.\"\"\"\n    test_prompts = [\n        Prompt(name=\"prompt1\", description=\"First prompt\"),\n        Prompt(name=\"prompt2\", description=\"Second prompt\"),\n    ]\n\n    async def handle_list_prompts(\n        ctx: ServerRequestContext, params: PaginatedRequestParams | None\n    ) -> ListPromptsResult:\n        return ListPromptsResult(prompts=test_prompts)\n\n    server = Server(\"test\", on_list_prompts=handle_list_prompts)\n    async with Client(server) as client:\n        result = await client.list_prompts()\n        assert result.prompts == test_prompts\n\n\n@pytest.mark.anyio\nasync def test_list_resources_basic() -> None:\n    \"\"\"Test basic resource listing without pagination.\"\"\"\n    test_resources = [\n        Resource(uri=\"file:///test1.txt\", name=\"Test 1\"),\n        Resource(uri=\"file:///test2.txt\", name=\"Test 2\"),\n    ]\n\n    async def handle_list_resources(\n        ctx: ServerRequestContext, params: PaginatedRequestParams | None\n    ) -> ListResourcesResult:\n        return ListResourcesResult(resources=test_resources)\n\n    server = Server(\"test\", on_list_resources=handle_list_resources)\n    async with Client(server) as client:\n        result = await client.list_resources()\n        assert result.resources == test_resources\n\n\n@pytest.mark.anyio\nasync def test_list_tools_basic() -> None:\n    \"\"\"Test basic tool listing without pagination.\"\"\"\n    test_tools = [\n        Tool(\n            name=\"tool1\",\n            description=\"First tool\",\n            input_schema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"message\": {\"type\": \"string\"},\n                },\n                \"required\": [\"message\"],\n            },\n        ),\n        Tool(\n            name=\"tool2\",\n            description=\"Second tool\",\n            input_schema={\n                \"type\": \"object\",\n                \"properties\": {\n                    \"count\": {\"type\": \"number\"},\n                    \"enabled\": {\"type\": \"boolean\"},\n                },\n                \"required\": [\"count\"],\n            },\n        ),\n    ]\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        return ListToolsResult(tools=test_tools)\n\n    server = Server(\"test\", on_list_tools=handle_list_tools)\n    async with Client(server) as client:\n        result = await client.list_tools()\n        assert result.tools == test_tools\n\n\n@pytest.mark.anyio\nasync def test_list_prompts_empty() -> None:\n    \"\"\"Test listing with empty results.\"\"\"\n\n    async def handle_list_prompts(\n        ctx: ServerRequestContext, params: PaginatedRequestParams | None\n    ) -> ListPromptsResult:\n        return ListPromptsResult(prompts=[])\n\n    server = Server(\"test\", on_list_prompts=handle_list_prompts)\n    async with Client(server) as client:\n        result = await client.list_prompts()\n        assert result.prompts == []\n\n\n@pytest.mark.anyio\nasync def test_list_resources_empty() -> None:\n    \"\"\"Test listing with empty results.\"\"\"\n\n    async def handle_list_resources(\n        ctx: ServerRequestContext, params: PaginatedRequestParams | None\n    ) -> ListResourcesResult:\n        return ListResourcesResult(resources=[])\n\n    server = Server(\"test\", on_list_resources=handle_list_resources)\n    async with Client(server) as client:\n        result = await client.list_resources()\n        assert result.resources == []\n\n\n@pytest.mark.anyio\nasync def test_list_tools_empty() -> None:\n    \"\"\"Test listing with empty results.\"\"\"\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        return ListToolsResult(tools=[])\n\n    server = Server(\"test\", on_list_tools=handle_list_tools)\n    async with Client(server) as client:\n        result = await client.list_tools()\n        assert result.tools == []\n"
  },
  {
    "path": "tests/server/lowlevel/test_server_pagination.py",
    "content": "import pytest\n\nfrom mcp import Client\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.types import (\n    ListPromptsResult,\n    ListResourcesResult,\n    ListToolsResult,\n    PaginatedRequestParams,\n)\n\n\n@pytest.mark.anyio\nasync def test_list_prompts_pagination() -> None:\n    test_cursor = \"test-cursor-123\"\n    received_params: PaginatedRequestParams | None = None\n\n    async def handle_list_prompts(\n        ctx: ServerRequestContext, params: PaginatedRequestParams | None\n    ) -> ListPromptsResult:\n        nonlocal received_params\n        received_params = params\n        return ListPromptsResult(prompts=[], next_cursor=\"next\")\n\n    server = Server(\"test\", on_list_prompts=handle_list_prompts)\n    async with Client(server) as client:\n        # No cursor provided\n        await client.list_prompts()\n        assert received_params is not None\n        assert received_params.cursor is None\n\n        # Cursor provided\n        await client.list_prompts(cursor=test_cursor)\n        assert received_params is not None\n        assert received_params.cursor == test_cursor\n\n\n@pytest.mark.anyio\nasync def test_list_resources_pagination() -> None:\n    test_cursor = \"resource-cursor-456\"\n    received_params: PaginatedRequestParams | None = None\n\n    async def handle_list_resources(\n        ctx: ServerRequestContext, params: PaginatedRequestParams | None\n    ) -> ListResourcesResult:\n        nonlocal received_params\n        received_params = params\n        return ListResourcesResult(resources=[], next_cursor=\"next\")\n\n    server = Server(\"test\", on_list_resources=handle_list_resources)\n    async with Client(server) as client:\n        # No cursor provided\n        await client.list_resources()\n        assert received_params is not None\n        assert received_params.cursor is None\n\n        # Cursor provided\n        await client.list_resources(cursor=test_cursor)\n        assert received_params is not None\n        assert received_params.cursor == test_cursor\n\n\n@pytest.mark.anyio\nasync def test_list_tools_pagination() -> None:\n    test_cursor = \"tools-cursor-789\"\n    received_params: PaginatedRequestParams | None = None\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        nonlocal received_params\n        received_params = params\n        return ListToolsResult(tools=[], next_cursor=\"next\")\n\n    server = Server(\"test\", on_list_tools=handle_list_tools)\n    async with Client(server) as client:\n        # No cursor provided\n        await client.list_tools()\n        assert received_params is not None\n        assert received_params.cursor is None\n\n        # Cursor provided\n        await client.list_tools(cursor=test_cursor)\n        assert received_params is not None\n        assert received_params.cursor == test_cursor\n"
  },
  {
    "path": "tests/server/mcpserver/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/mcpserver/auth/__init__.py",
    "content": "\"\"\"Tests for the MCP server auth components.\"\"\"\n"
  },
  {
    "path": "tests/server/mcpserver/auth/test_auth_integration.py",
    "content": "\"\"\"Integration tests for MCP authorization components.\"\"\"\n\nimport base64\nimport hashlib\nimport secrets\nimport time\nimport unittest.mock\nfrom typing import Any\nfrom urllib.parse import parse_qs, urlparse\n\nimport httpx\nimport pytest\nfrom pydantic import AnyHttpUrl, AnyUrl\nfrom starlette.applications import Starlette\n\nfrom mcp.server.auth.provider import (\n    AccessToken,\n    AuthorizationCode,\n    AuthorizationParams,\n    OAuthAuthorizationServerProvider,\n    RefreshToken,\n    construct_redirect_uri,\n)\nfrom mcp.server.auth.routes import create_auth_routes\nfrom mcp.server.auth.settings import ClientRegistrationOptions, RevocationOptions\nfrom mcp.shared.auth import OAuthClientInformationFull, OAuthToken\n\n\n# Mock OAuth provider for testing\nclass MockOAuthProvider(OAuthAuthorizationServerProvider[AuthorizationCode, RefreshToken, AccessToken]):\n    def __init__(self):\n        self.clients: dict[str, OAuthClientInformationFull] = {}\n        self.auth_codes: dict[str, AuthorizationCode] = {}  # code -> {client_id, code_challenge, redirect_uri}\n        self.tokens: dict[str, AccessToken] = {}  # token -> {client_id, scopes, expires_at}\n        self.refresh_tokens: dict[str, str] = {}  # refresh_token -> access_token\n\n    async def get_client(self, client_id: str) -> OAuthClientInformationFull | None:\n        return self.clients.get(client_id)\n\n    async def register_client(self, client_info: OAuthClientInformationFull):\n        assert client_info.client_id is not None\n        self.clients[client_info.client_id] = client_info\n\n    async def authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str:\n        # toy authorize implementation which just immediately generates an authorization\n        # code and completes the redirect\n        assert client.client_id is not None\n        code = AuthorizationCode(\n            code=f\"code_{int(time.time())}\",\n            client_id=client.client_id,\n            code_challenge=params.code_challenge,\n            redirect_uri=params.redirect_uri,\n            redirect_uri_provided_explicitly=params.redirect_uri_provided_explicitly,\n            expires_at=time.time() + 300,\n            scopes=params.scopes or [\"read\", \"write\"],\n        )\n        self.auth_codes[code.code] = code\n\n        return construct_redirect_uri(str(params.redirect_uri), code=code.code, state=params.state)\n\n    async def load_authorization_code(\n        self, client: OAuthClientInformationFull, authorization_code: str\n    ) -> AuthorizationCode | None:\n        return self.auth_codes.get(authorization_code)\n\n    async def exchange_authorization_code(\n        self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode\n    ) -> OAuthToken:\n        assert authorization_code.code in self.auth_codes\n\n        # Generate an access token and refresh token\n        access_token = f\"access_{secrets.token_hex(32)}\"\n        refresh_token = f\"refresh_{secrets.token_hex(32)}\"\n\n        # Store the tokens\n        assert client.client_id is not None\n        self.tokens[access_token] = AccessToken(\n            token=access_token,\n            client_id=client.client_id,\n            scopes=authorization_code.scopes,\n            expires_at=int(time.time()) + 3600,\n        )\n\n        self.refresh_tokens[refresh_token] = access_token\n\n        # Remove the used code\n        del self.auth_codes[authorization_code.code]\n\n        return OAuthToken(\n            access_token=access_token,\n            token_type=\"Bearer\",\n            expires_in=3600,\n            scope=\"read write\",\n            refresh_token=refresh_token,\n        )\n\n    async def load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None:\n        old_access_token = self.refresh_tokens.get(refresh_token)\n        if old_access_token is None:\n            return None\n        token_info = self.tokens.get(old_access_token)\n        if token_info is None:  # pragma: no cover\n            return None\n\n        # Create a RefreshToken object that matches what is expected in later code\n        refresh_obj = RefreshToken(\n            token=refresh_token,\n            client_id=token_info.client_id,\n            scopes=token_info.scopes,\n            expires_at=token_info.expires_at,\n        )\n\n        return refresh_obj\n\n    async def exchange_refresh_token(\n        self,\n        client: OAuthClientInformationFull,\n        refresh_token: RefreshToken,\n        scopes: list[str],\n    ) -> OAuthToken:\n        # Check if refresh token exists\n        assert refresh_token.token in self.refresh_tokens\n\n        old_access_token = self.refresh_tokens[refresh_token.token]\n\n        # Check if the access token exists\n        assert old_access_token in self.tokens\n\n        # Check if the token was issued to this client\n        token_info = self.tokens[old_access_token]\n        assert token_info.client_id == client.client_id\n\n        # Generate a new access token and refresh token\n        new_access_token = f\"access_{secrets.token_hex(32)}\"\n        new_refresh_token = f\"refresh_{secrets.token_hex(32)}\"\n\n        # Store the new tokens\n        assert client.client_id is not None\n        self.tokens[new_access_token] = AccessToken(\n            token=new_access_token,\n            client_id=client.client_id,\n            scopes=scopes or token_info.scopes,\n            expires_at=int(time.time()) + 3600,\n        )\n\n        self.refresh_tokens[new_refresh_token] = new_access_token\n\n        # Remove the old tokens\n        del self.refresh_tokens[refresh_token.token]\n        del self.tokens[old_access_token]\n\n        return OAuthToken(\n            access_token=new_access_token,\n            token_type=\"Bearer\",\n            expires_in=3600,\n            scope=\" \".join(scopes) if scopes else \" \".join(token_info.scopes),\n            refresh_token=new_refresh_token,\n        )\n\n    async def load_access_token(self, token: str) -> AccessToken | None:\n        token_info = self.tokens.get(token)\n\n        # Check if token is expired\n        # if token_info.expires_at < int(time.time()):\n        #     raise InvalidTokenError(\"Access token has expired\")\n\n        return token_info and AccessToken(\n            token=token,\n            client_id=token_info.client_id,\n            scopes=token_info.scopes,\n            expires_at=token_info.expires_at,\n        )\n\n    async def revoke_token(self, token: AccessToken | RefreshToken) -> None:\n        match token:\n            case RefreshToken():  # pragma: lax no cover\n                # Remove the refresh token\n                del self.refresh_tokens[token.token]\n\n            case AccessToken():  # pragma: no branch\n                # Remove the access token\n                del self.tokens[token.token]\n\n                # Also remove any refresh tokens that point to this access token\n                for refresh_token, access_token in list(self.refresh_tokens.items()):\n                    if access_token == token.token:  # pragma: no branch\n                        del self.refresh_tokens[refresh_token]\n\n\n@pytest.fixture\ndef mock_oauth_provider():\n    return MockOAuthProvider()\n\n\n@pytest.fixture\ndef auth_app(mock_oauth_provider: MockOAuthProvider):\n    # Create auth router\n    auth_routes = create_auth_routes(\n        mock_oauth_provider,\n        AnyHttpUrl(\"https://auth.example.com\"),\n        AnyHttpUrl(\"https://docs.example.com\"),\n        client_registration_options=ClientRegistrationOptions(\n            enabled=True,\n            valid_scopes=[\"read\", \"write\", \"profile\"],\n            default_scopes=[\"read\", \"write\"],\n        ),\n        revocation_options=RevocationOptions(enabled=True),\n    )\n\n    # Create Starlette app\n    app = Starlette(routes=auth_routes)\n\n    return app\n\n\n@pytest.fixture\nasync def test_client(auth_app: Starlette):\n    async with httpx.AsyncClient(transport=httpx.ASGITransport(app=auth_app), base_url=\"https://mcptest.com\") as client:\n        yield client\n\n\n@pytest.fixture\nasync def registered_client(\n    test_client: httpx.AsyncClient, request: pytest.FixtureRequest\n) -> OAuthClientInformationFull:\n    \"\"\"Create and register a test client.\n\n    Parameters can be customized via indirect parameterization:\n    @pytest.mark.parametrize(\"registered_client\",\n                            [{\"grant_types\": [\"authorization_code\"]}],\n                            indirect=True)\n    \"\"\"\n    # Default client metadata\n    client_metadata = {\n        \"redirect_uris\": [\"https://client.example.com/callback\"],\n        \"client_name\": \"Test Client\",\n        \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n    }\n\n    # Override with any parameters from the test\n    if hasattr(request, \"param\") and request.param:\n        client_metadata.update(request.param)\n\n    response = await test_client.post(\"/register\", json=client_metadata)\n    assert response.status_code == 201, f\"Failed to register client: {response.content}\"\n\n    client_info = response.json()\n    return client_info\n\n\n@pytest.fixture\ndef pkce_challenge():\n    \"\"\"Create a PKCE challenge with code_verifier and code_challenge.\"\"\"\n    code_verifier = \"some_random_verifier_string\"\n    code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()).decode().rstrip(\"=\")\n\n    return {\"code_verifier\": code_verifier, \"code_challenge\": code_challenge}\n\n\n@pytest.fixture\nasync def auth_code(\n    test_client: httpx.AsyncClient,\n    registered_client: dict[str, Any],\n    pkce_challenge: dict[str, str],\n    request: pytest.FixtureRequest,\n):\n    \"\"\"Get an authorization code.\n\n    Parameters can be customized via indirect parameterization:\n    @pytest.mark.parametrize(\"auth_code\",\n                            [{\"redirect_uri\": \"https://client.example.com/other-callback\"}],\n                            indirect=True)\n    \"\"\"\n    # Default authorize params\n    auth_params = {\n        \"response_type\": \"code\",\n        \"client_id\": registered_client[\"client_id\"],\n        \"redirect_uri\": \"https://client.example.com/callback\",\n        \"code_challenge\": pkce_challenge[\"code_challenge\"],\n        \"code_challenge_method\": \"S256\",\n        \"state\": \"test_state\",\n    }\n\n    # Override with any parameters from the test\n    if hasattr(request, \"param\") and request.param:  # pragma: no cover\n        auth_params.update(request.param)\n\n    response = await test_client.get(\"/authorize\", params=auth_params)\n    assert response.status_code == 302, f\"Failed to get auth code: {response.content}\"\n\n    # Extract the authorization code\n    redirect_url = response.headers[\"location\"]\n    parsed_url = urlparse(redirect_url)\n    query_params = parse_qs(parsed_url.query)\n\n    assert \"code\" in query_params, f\"No code in response: {query_params}\"\n    auth_code = query_params[\"code\"][0]\n\n    return {\n        \"code\": auth_code,\n        \"redirect_uri\": auth_params[\"redirect_uri\"],\n        \"state\": query_params.get(\"state\", [None])[0],\n    }\n\n\nclass TestAuthEndpoints:\n    @pytest.mark.anyio\n    async def test_metadata_endpoint(self, test_client: httpx.AsyncClient):\n        \"\"\"Test the OAuth 2.0 metadata endpoint.\"\"\"\n\n        response = await test_client.get(\"/.well-known/oauth-authorization-server\")\n        assert response.status_code == 200\n\n        metadata = response.json()\n        assert metadata[\"issuer\"] == \"https://auth.example.com/\"\n        assert metadata[\"authorization_endpoint\"] == \"https://auth.example.com/authorize\"\n        assert metadata[\"token_endpoint\"] == \"https://auth.example.com/token\"\n        assert metadata[\"registration_endpoint\"] == \"https://auth.example.com/register\"\n        assert metadata[\"revocation_endpoint\"] == \"https://auth.example.com/revoke\"\n        assert metadata[\"response_types_supported\"] == [\"code\"]\n        assert metadata[\"code_challenge_methods_supported\"] == [\"S256\"]\n        assert metadata[\"token_endpoint_auth_methods_supported\"] == [\"client_secret_post\", \"client_secret_basic\"]\n        assert metadata[\"grant_types_supported\"] == [\n            \"authorization_code\",\n            \"refresh_token\",\n        ]\n        assert metadata[\"service_documentation\"] == \"https://docs.example.com/\"\n\n    @pytest.mark.anyio\n    async def test_token_validation_error(self, test_client: httpx.AsyncClient):\n        \"\"\"Test token endpoint error - validation error.\"\"\"\n        # Missing required fields\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                # Missing code, code_verifier, client_id, etc.\n            },\n        )\n        error_response = response.json()\n        # Per RFC 6749 Section 5.2, authentication failures (missing client_id)\n        # must return \"invalid_client\", not \"unauthorized_client\"\n        assert error_response[\"error\"] == \"invalid_client\"\n        assert \"error_description\" in error_response  # Contains error message\n\n    @pytest.mark.anyio\n    async def test_token_invalid_client_secret_returns_invalid_client(\n        self,\n        test_client: httpx.AsyncClient,\n        registered_client: dict[str, Any],\n        pkce_challenge: dict[str, str],\n        mock_oauth_provider: MockOAuthProvider,\n    ):\n        \"\"\"Test token endpoint returns 'invalid_client' for wrong client_secret per RFC 6749.\n\n        RFC 6749 Section 5.2 defines:\n        - invalid_client: Client authentication failed (wrong credentials, unknown client)\n        - unauthorized_client: Authenticated client not authorized for grant type\n\n        When client_secret is wrong, this is an authentication failure, so the\n        error code MUST be 'invalid_client'.\n        \"\"\"\n        # Create an auth code for the registered client\n        auth_code = f\"code_{int(time.time())}\"\n        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(\n            code=auth_code,\n            client_id=registered_client[\"client_id\"],\n            code_challenge=pkce_challenge[\"code_challenge\"],\n            redirect_uri=AnyUrl(\"https://client.example.com/callback\"),\n            redirect_uri_provided_explicitly=True,\n            scopes=[\"read\", \"write\"],\n            expires_at=time.time() + 600,\n        )\n\n        # Try to exchange the auth code with a WRONG client_secret\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": \"wrong_secret_that_does_not_match\",\n                \"code\": auth_code,\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n            },\n        )\n\n        assert response.status_code == 401\n        error_response = response.json()\n        # RFC 6749 Section 5.2: authentication failures MUST return \"invalid_client\"\n        assert error_response[\"error\"] == \"invalid_client\"\n        assert \"Invalid client_secret\" in error_response[\"error_description\"]\n\n    @pytest.mark.anyio\n    async def test_token_invalid_auth_code(\n        self,\n        test_client: httpx.AsyncClient,\n        registered_client: dict[str, Any],\n        pkce_challenge: dict[str, str],\n    ):\n        \"\"\"Test token endpoint error - authorization code does not exist.\"\"\"\n        # Try to use a non-existent authorization code\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"code\": \"non_existent_auth_code\",\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n            },\n        )\n\n        assert response.status_code == 400\n        error_response = response.json()\n        assert error_response[\"error\"] == \"invalid_grant\"\n        assert \"authorization code does not exist\" in error_response[\"error_description\"]\n\n    @pytest.mark.anyio\n    async def test_token_expired_auth_code(\n        self,\n        test_client: httpx.AsyncClient,\n        registered_client: dict[str, Any],\n        auth_code: dict[str, str],\n        pkce_challenge: dict[str, str],\n        mock_oauth_provider: MockOAuthProvider,\n    ):\n        \"\"\"Test token endpoint error - authorization code has expired.\"\"\"\n        # Get the current time for our time mocking\n        current_time = time.time()\n\n        # Find the auth code object\n        code_value = auth_code[\"code\"]\n        found_code = None\n        for code_obj in mock_oauth_provider.auth_codes.values():  # pragma: no branch\n            if code_obj.code == code_value:  # pragma: no branch\n                found_code = code_obj\n                break\n\n        assert found_code is not None\n\n        # Authorization codes are typically short-lived (5 minutes = 300 seconds)\n        # So we'll mock time to be 10 minutes (600 seconds) in the future\n        with unittest.mock.patch(\"time.time\", return_value=current_time + 600):\n            # Try to use the expired authorization code\n            response = await test_client.post(\n                \"/token\",\n                data={\n                    \"grant_type\": \"authorization_code\",\n                    \"client_id\": registered_client[\"client_id\"],\n                    \"client_secret\": registered_client[\"client_secret\"],\n                    \"code\": code_value,\n                    \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                    \"redirect_uri\": auth_code[\"redirect_uri\"],\n                },\n            )\n            assert response.status_code == 400\n            error_response = response.json()\n            assert error_response[\"error\"] == \"invalid_grant\"\n            assert \"authorization code has expired\" in error_response[\"error_description\"]\n\n    @pytest.mark.anyio\n    @pytest.mark.parametrize(\n        \"registered_client\",\n        [\n            {\n                \"redirect_uris\": [\n                    \"https://client.example.com/callback\",\n                    \"https://client.example.com/other-callback\",\n                ]\n            }\n        ],\n        indirect=True,\n    )\n    async def test_token_redirect_uri_mismatch(\n        self,\n        test_client: httpx.AsyncClient,\n        registered_client: dict[str, Any],\n        auth_code: dict[str, str],\n        pkce_challenge: dict[str, str],\n    ):\n        \"\"\"Test token endpoint error - redirect URI mismatch.\"\"\"\n        # Try to use the code with a different redirect URI\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"code\": auth_code[\"code\"],\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                # Different from the one used in /authorize\n                \"redirect_uri\": \"https://client.example.com/other-callback\",\n            },\n        )\n        assert response.status_code == 400\n        error_response = response.json()\n        assert error_response[\"error\"] == \"invalid_request\"\n        assert \"redirect_uri did not match\" in error_response[\"error_description\"]\n\n    @pytest.mark.anyio\n    async def test_token_code_verifier_mismatch(\n        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], auth_code: dict[str, str]\n    ):\n        \"\"\"Test token endpoint error - PKCE code verifier mismatch.\"\"\"\n        # Try to use the code with an incorrect code verifier\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"code\": auth_code[\"code\"],\n                # Different from the one used to create challenge\n                \"code_verifier\": \"incorrect_code_verifier\",\n                \"redirect_uri\": auth_code[\"redirect_uri\"],\n            },\n        )\n        assert response.status_code == 400\n        error_response = response.json()\n        assert error_response[\"error\"] == \"invalid_grant\"\n        assert \"incorrect code_verifier\" in error_response[\"error_description\"]\n\n    @pytest.mark.anyio\n    async def test_token_invalid_refresh_token(self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]):\n        \"\"\"Test token endpoint error - refresh token does not exist.\"\"\"\n        # Try to use a non-existent refresh token\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"refresh_token\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"refresh_token\": \"non_existent_refresh_token\",\n            },\n        )\n        assert response.status_code == 400\n        error_response = response.json()\n        assert error_response[\"error\"] == \"invalid_grant\"\n        assert \"refresh token does not exist\" in error_response[\"error_description\"]\n\n    @pytest.mark.anyio\n    async def test_token_expired_refresh_token(\n        self,\n        test_client: httpx.AsyncClient,\n        registered_client: dict[str, Any],\n        auth_code: dict[str, str],\n        pkce_challenge: dict[str, str],\n    ):\n        \"\"\"Test token endpoint error - refresh token has expired.\"\"\"\n        # Step 1: First, let's create a token and refresh token at the current time\n        current_time = time.time()\n\n        # Exchange authorization code for tokens normally\n        token_response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"code\": auth_code[\"code\"],\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": auth_code[\"redirect_uri\"],\n            },\n        )\n        assert token_response.status_code == 200\n        tokens = token_response.json()\n        refresh_token = tokens[\"refresh_token\"]\n\n        # Step 2: Time travel forward 4 hours (tokens expire in 1 hour by default)\n        # Mock the time.time() function to return a value 4 hours in the future\n        with unittest.mock.patch(\"time.time\", return_value=current_time + 14400):  # 4 hours = 14400 seconds\n            # Try to use the refresh token which should now be considered expired\n            response = await test_client.post(\n                \"/token\",\n                data={\n                    \"grant_type\": \"refresh_token\",\n                    \"client_id\": registered_client[\"client_id\"],\n                    \"client_secret\": registered_client[\"client_secret\"],\n                    \"refresh_token\": refresh_token,\n                },\n            )\n\n            # In the \"future\", the token should be considered expired\n            assert response.status_code == 400\n            error_response = response.json()\n            assert error_response[\"error\"] == \"invalid_grant\"\n            assert \"refresh token has expired\" in error_response[\"error_description\"]\n\n    @pytest.mark.anyio\n    async def test_token_invalid_scope(\n        self,\n        test_client: httpx.AsyncClient,\n        registered_client: dict[str, Any],\n        auth_code: dict[str, str],\n        pkce_challenge: dict[str, str],\n    ):\n        \"\"\"Test token endpoint error - invalid scope in refresh token request.\"\"\"\n        # Exchange authorization code for tokens\n        token_response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"code\": auth_code[\"code\"],\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": auth_code[\"redirect_uri\"],\n            },\n        )\n        assert token_response.status_code == 200\n\n        tokens = token_response.json()\n        refresh_token = tokens[\"refresh_token\"]\n\n        # Try to use refresh token with an invalid scope\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"refresh_token\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"refresh_token\": refresh_token,\n                \"scope\": \"read write invalid_scope\",  # Adding an invalid scope\n            },\n        )\n        assert response.status_code == 400\n        error_response = response.json()\n        assert error_response[\"error\"] == \"invalid_scope\"\n        assert \"cannot request scope\" in error_response[\"error_description\"]\n\n    @pytest.mark.anyio\n    async def test_client_registration(self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider):\n        \"\"\"Test client registration.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            \"client_uri\": \"https://client.example.com\",\n        }\n\n        response = await test_client.post(\n            \"/register\",\n            json=client_metadata,\n        )\n        assert response.status_code == 201, response.content\n\n        client_info = response.json()\n        assert \"client_id\" in client_info\n        assert \"client_secret\" in client_info\n        assert client_info[\"client_name\"] == \"Test Client\"\n        assert client_info[\"redirect_uris\"] == [\"https://client.example.com/callback\"]\n\n        # Verify that the client was registered\n        # assert await mock_oauth_provider.clients_store.get_client(\n        #     client_info[\"client_id\"]\n        # ) is not None\n\n    @pytest.mark.anyio\n    async def test_client_registration_missing_required_fields(self, test_client: httpx.AsyncClient):\n        \"\"\"Test client registration with missing required fields.\"\"\"\n        # Missing redirect_uris which is a required field\n        client_metadata = {\n            \"client_name\": \"Test Client\",\n            \"client_uri\": \"https://client.example.com\",\n        }\n\n        response = await test_client.post(\n            \"/register\",\n            json=client_metadata,\n        )\n        assert response.status_code == 400\n        error_data = response.json()\n        assert \"error\" in error_data\n        assert error_data[\"error\"] == \"invalid_client_metadata\"\n        assert error_data[\"error_description\"] == \"redirect_uris: Field required\"\n\n    @pytest.mark.anyio\n    async def test_client_registration_invalid_uri(self, test_client: httpx.AsyncClient):\n        \"\"\"Test client registration with invalid URIs.\"\"\"\n        # Invalid redirect_uri format\n        client_metadata = {\n            \"redirect_uris\": [\"not-a-valid-uri\"],\n            \"client_name\": \"Test Client\",\n        }\n\n        response = await test_client.post(\n            \"/register\",\n            json=client_metadata,\n        )\n        assert response.status_code == 400\n        error_data = response.json()\n        assert \"error\" in error_data\n        assert error_data[\"error\"] == \"invalid_client_metadata\"\n        assert error_data[\"error_description\"] == (\n            \"redirect_uris.0: Input should be a valid URL, relative URL without a base\"\n        )\n\n    @pytest.mark.anyio\n    async def test_client_registration_empty_redirect_uris(self, test_client: httpx.AsyncClient):\n        \"\"\"Test client registration with empty redirect_uris array.\"\"\"\n        redirect_uris: list[str] = []\n        client_metadata = {\n            \"redirect_uris\": redirect_uris,  # Empty array\n            \"client_name\": \"Test Client\",\n        }\n\n        response = await test_client.post(\n            \"/register\",\n            json=client_metadata,\n        )\n        assert response.status_code == 400\n        error_data = response.json()\n        assert \"error\" in error_data\n        assert error_data[\"error\"] == \"invalid_client_metadata\"\n        assert (\n            error_data[\"error_description\"] == \"redirect_uris: List should have at least 1 item after validation, not 0\"\n        )\n\n    @pytest.mark.anyio\n    async def test_authorize_form_post(self, test_client: httpx.AsyncClient, pkce_challenge: dict[str, str]):\n        \"\"\"Test the authorization endpoint using POST with form-encoded data.\"\"\"\n        # Register a client\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n        }\n\n        response = await test_client.post(\n            \"/register\",\n            json=client_metadata,\n        )\n        assert response.status_code == 201\n        client_info = response.json()\n\n        # Use POST with form-encoded data for authorization\n        response = await test_client.post(\n            \"/authorize\",\n            data={\n                \"response_type\": \"code\",\n                \"client_id\": client_info[\"client_id\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"state\": \"test_form_state\",\n            },\n        )\n        assert response.status_code == 302\n\n        # Extract the authorization code from the redirect URL\n        redirect_url = response.headers[\"location\"]\n        parsed_url = urlparse(redirect_url)\n        query_params = parse_qs(parsed_url.query)\n\n        assert \"code\" in query_params\n        assert query_params[\"state\"][0] == \"test_form_state\"\n\n    @pytest.mark.anyio\n    async def test_authorization_get(\n        self,\n        test_client: httpx.AsyncClient,\n        mock_oauth_provider: MockOAuthProvider,\n        pkce_challenge: dict[str, str],\n    ):\n        \"\"\"Test the full authorization flow.\"\"\"\n        # 1. Register a client\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n        }\n\n        response = await test_client.post(\n            \"/register\",\n            json=client_metadata,\n        )\n        assert response.status_code == 201\n        client_info = response.json()\n\n        # 2. Request authorization using GET with query params\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                \"client_id\": client_info[\"client_id\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"state\": \"test_state\",\n            },\n        )\n        assert response.status_code == 302\n\n        # 3. Extract the authorization code from the redirect URL\n        redirect_url = response.headers[\"location\"]\n        parsed_url = urlparse(redirect_url)\n        query_params = parse_qs(parsed_url.query)\n\n        assert \"code\" in query_params\n        assert query_params[\"state\"][0] == \"test_state\"\n        auth_code = query_params[\"code\"][0]\n\n        # 4. Exchange the authorization code for tokens\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": client_info[\"client_id\"],\n                \"client_secret\": client_info[\"client_secret\"],\n                \"code\": auth_code,\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n            },\n        )\n        assert response.status_code == 200\n\n        token_response = response.json()\n        assert \"access_token\" in token_response\n        assert \"token_type\" in token_response\n        assert \"refresh_token\" in token_response\n        assert \"expires_in\" in token_response\n        assert token_response[\"token_type\"] == \"Bearer\"\n\n        # 5. Verify the access token\n        access_token = token_response[\"access_token\"]\n        refresh_token = token_response[\"refresh_token\"]\n\n        # Create a test client with the token\n        auth_info = await mock_oauth_provider.load_access_token(access_token)\n        assert auth_info\n        assert auth_info.client_id == client_info[\"client_id\"]\n        assert \"read\" in auth_info.scopes\n        assert \"write\" in auth_info.scopes\n\n        # 6. Refresh the token\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"refresh_token\",\n                \"client_id\": client_info[\"client_id\"],\n                \"client_secret\": client_info[\"client_secret\"],\n                \"refresh_token\": refresh_token,\n                \"redirect_uri\": \"https://client.example.com/callback\",\n            },\n        )\n        assert response.status_code == 200\n\n        new_token_response = response.json()\n        assert \"access_token\" in new_token_response\n        assert \"refresh_token\" in new_token_response\n        assert new_token_response[\"access_token\"] != access_token\n        assert new_token_response[\"refresh_token\"] != refresh_token\n\n        # 7. Revoke the token\n        response = await test_client.post(\n            \"/revoke\",\n            data={\n                \"client_id\": client_info[\"client_id\"],\n                \"client_secret\": client_info[\"client_secret\"],\n                \"token\": new_token_response[\"access_token\"],\n            },\n        )\n        assert response.status_code == 200\n\n        # Verify that the token was revoked\n        assert await mock_oauth_provider.load_access_token(new_token_response[\"access_token\"]) is None\n\n    @pytest.mark.anyio\n    async def test_revoke_invalid_token(self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]):\n        \"\"\"Test revoking an invalid token.\"\"\"\n        response = await test_client.post(\n            \"/revoke\",\n            data={\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"token\": \"invalid_token\",\n            },\n        )\n        # per RFC, this should return 200 even if the token is invalid\n        assert response.status_code == 200\n\n    @pytest.mark.anyio\n    async def test_revoke_with_malformed_token(self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]):\n        response = await test_client.post(\n            \"/revoke\",\n            data={\n                \"client_id\": registered_client[\"client_id\"],\n                \"client_secret\": registered_client[\"client_secret\"],\n                \"token\": 123,\n                \"token_type_hint\": \"asdf\",\n            },\n        )\n        assert response.status_code == 400\n        error_response = response.json()\n        assert error_response[\"error\"] == \"invalid_request\"\n        assert \"token_type_hint\" in error_response[\"error_description\"]\n\n    @pytest.mark.anyio\n    async def test_client_registration_disallowed_scopes(self, test_client: httpx.AsyncClient):\n        \"\"\"Test client registration with scopes that are not allowed.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            \"scope\": \"read write profile admin\",  # 'admin' is not in valid_scopes\n        }\n\n        response = await test_client.post(\"/register\", json=client_metadata)\n        assert response.status_code == 400\n        error_data = response.json()\n        assert \"error\" in error_data\n        assert error_data[\"error\"] == \"invalid_client_metadata\"\n        assert \"scope\" in error_data[\"error_description\"]\n        assert \"admin\" in error_data[\"error_description\"]\n\n    @pytest.mark.anyio\n    async def test_client_registration_default_scopes(\n        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider\n    ):\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            # No scope specified\n        }\n\n        response = await test_client.post(\"/register\", json=client_metadata)\n        assert response.status_code == 201\n        client_info = response.json()\n\n        # Verify client was registered successfully\n        assert client_info[\"scope\"] == \"read write\"\n\n        # Retrieve the client from the store to verify default scopes\n        registered_client = await mock_oauth_provider.get_client(client_info[\"client_id\"])\n        assert registered_client is not None\n\n        # Check that default scopes were applied\n        assert registered_client.scope == \"read write\"\n\n    @pytest.mark.anyio\n    async def test_client_registration_with_authorization_code_only(self, test_client: httpx.AsyncClient):\n        \"\"\"Test that registration succeeds with only authorization_code (refresh_token is optional per RFC 7591).\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            \"grant_types\": [\"authorization_code\"],\n        }\n\n        response = await test_client.post(\"/register\", json=client_metadata)\n        assert response.status_code == 201\n        client_info = response.json()\n        assert \"client_id\" in client_info\n        assert client_info[\"grant_types\"] == [\"authorization_code\"]\n\n    @pytest.mark.anyio\n    async def test_client_registration_missing_authorization_code(self, test_client: httpx.AsyncClient):\n        \"\"\"Test that registration fails when authorization_code grant type is missing.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            \"grant_types\": [\"refresh_token\"],\n        }\n\n        response = await test_client.post(\"/register\", json=client_metadata)\n        assert response.status_code == 400\n        error_data = response.json()\n        assert \"error\" in error_data\n        assert error_data[\"error\"] == \"invalid_client_metadata\"\n        assert error_data[\"error_description\"] == \"grant_types must include 'authorization_code'\"\n\n    @pytest.mark.anyio\n    async def test_client_registration_with_additional_grant_type(self, test_client: httpx.AsyncClient):\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\", \"urn:ietf:params:oauth:grant-type:device_code\"],\n        }\n\n        response = await test_client.post(\"/register\", json=client_metadata)\n        assert response.status_code == 201\n        client_info = response.json()\n\n        # Verify client was registered successfully\n        assert \"client_id\" in client_info\n        assert \"client_secret\" in client_info\n        assert client_info[\"client_name\"] == \"Test Client\"\n\n    @pytest.mark.anyio\n    async def test_client_registration_with_additional_response_types(\n        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider\n    ):\n        \"\"\"Test that registration accepts additional response_types values alongside 'code'.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n            \"response_types\": [\"code\", \"none\"],  # Keycloak-style response with additional value\n        }\n\n        response = await test_client.post(\"/register\", json=client_metadata)\n        assert response.status_code == 201\n        data = response.json()\n\n        client = await mock_oauth_provider.get_client(data[\"client_id\"])\n        assert client is not None\n        assert \"code\" in client.response_types\n\n    @pytest.mark.anyio\n    async def test_client_registration_response_types_without_code(self, test_client: httpx.AsyncClient):\n        \"\"\"Test that registration rejects response_types that don't include 'code'.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n            \"response_types\": [\"token\", \"none\", \"nonsense-string\"],\n        }\n\n        response = await test_client.post(\"/register\", json=client_metadata)\n        assert response.status_code == 400\n        error_data = response.json()\n        assert \"error\" in error_data\n        assert error_data[\"error\"] == \"invalid_client_metadata\"\n        assert \"response_types must include 'code'\" in error_data[\"error_description\"]\n\n    @pytest.mark.anyio\n    async def test_client_registration_default_response_types(\n        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider\n    ):\n        \"\"\"Test that registration uses default response_types of ['code'] when not specified.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Test Client\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n            # response_types not specified, should default to [\"code\"]\n        }\n\n        response = await test_client.post(\"/register\", json=client_metadata)\n        assert response.status_code == 201\n        data = response.json()\n\n        assert \"response_types\" in data\n        assert data[\"response_types\"] == [\"code\"]\n\n    @pytest.mark.anyio\n    async def test_client_secret_basic_authentication(\n        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]\n    ):\n        \"\"\"Test that client_secret_basic authentication works correctly.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Basic Auth Client\",\n            \"token_endpoint_auth_method\": \"client_secret_basic\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n        }\n\n        response = await test_client.post(\"/register\", json=client_metadata)\n        assert response.status_code == 201\n        client_info = response.json()\n        assert client_info[\"token_endpoint_auth_method\"] == \"client_secret_basic\"\n\n        auth_code = f\"code_{int(time.time())}\"\n        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(\n            code=auth_code,\n            client_id=client_info[\"client_id\"],\n            code_challenge=pkce_challenge[\"code_challenge\"],\n            redirect_uri=AnyUrl(\"https://client.example.com/callback\"),\n            redirect_uri_provided_explicitly=True,\n            scopes=[\"read\", \"write\"],\n            expires_at=time.time() + 600,\n        )\n\n        credentials = f\"{client_info['client_id']}:{client_info['client_secret']}\"\n        encoded_credentials = base64.b64encode(credentials.encode()).decode()\n\n        response = await test_client.post(\n            \"/token\",\n            headers={\"Authorization\": f\"Basic {encoded_credentials}\"},\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": client_info[\"client_id\"],\n                \"code\": auth_code,\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n            },\n        )\n        assert response.status_code == 200\n        token_response = response.json()\n        assert \"access_token\" in token_response\n\n    @pytest.mark.anyio\n    async def test_wrong_auth_method_without_valid_credentials_fails(\n        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]\n    ):\n        \"\"\"Test that using the wrong authentication method fails when credentials are missing.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Post Auth Client\",\n            \"token_endpoint_auth_method\": \"client_secret_post\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n        }\n\n        response = await test_client.post(\"/register\", json=client_metadata)\n        assert response.status_code == 201\n        client_info = response.json()\n        assert client_info[\"token_endpoint_auth_method\"] == \"client_secret_post\"\n\n        auth_code = f\"code_{int(time.time())}\"\n        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(\n            code=auth_code,\n            client_id=client_info[\"client_id\"],\n            code_challenge=pkce_challenge[\"code_challenge\"],\n            redirect_uri=AnyUrl(\"https://client.example.com/callback\"),\n            redirect_uri_provided_explicitly=True,\n            scopes=[\"read\", \"write\"],\n            expires_at=time.time() + 600,\n        )\n\n        # Try to use Basic auth when client_secret_post is registered (without secret in body)\n        # This should fail because the secret is missing from the expected location\n\n        credentials = f\"{client_info['client_id']}:{client_info['client_secret']}\"\n        encoded_credentials = base64.b64encode(credentials.encode()).decode()\n\n        response = await test_client.post(\n            \"/token\",\n            headers={\"Authorization\": f\"Basic {encoded_credentials}\"},\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": client_info[\"client_id\"],\n                # client_secret NOT in body where it should be\n                \"code\": auth_code,\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n            },\n        )\n        assert response.status_code == 401\n        error_response = response.json()\n        # RFC 6749: authentication failures return \"invalid_client\"\n        assert error_response[\"error\"] == \"invalid_client\"\n        assert \"Client secret is required\" in error_response[\"error_description\"]\n\n    @pytest.mark.anyio\n    async def test_basic_auth_without_header_fails(\n        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]\n    ):\n        \"\"\"Test that omitting Basic auth when client_secret_basic is registered fails.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Basic Auth Client\",\n            \"token_endpoint_auth_method\": \"client_secret_basic\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n        }\n\n        response = await test_client.post(\"/register\", json=client_metadata)\n        assert response.status_code == 201\n        client_info = response.json()\n        assert client_info[\"token_endpoint_auth_method\"] == \"client_secret_basic\"\n\n        auth_code = f\"code_{int(time.time())}\"\n        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(\n            code=auth_code,\n            client_id=client_info[\"client_id\"],\n            code_challenge=pkce_challenge[\"code_challenge\"],\n            redirect_uri=AnyUrl(\"https://client.example.com/callback\"),\n            redirect_uri_provided_explicitly=True,\n            scopes=[\"read\", \"write\"],\n            expires_at=time.time() + 600,\n        )\n\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": client_info[\"client_id\"],\n                \"client_secret\": client_info[\"client_secret\"],  # Secret in body (ignored)\n                \"code\": auth_code,\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n            },\n        )\n        assert response.status_code == 401\n        error_response = response.json()\n        # RFC 6749: authentication failures return \"invalid_client\"\n        assert error_response[\"error\"] == \"invalid_client\"\n        assert \"Missing or invalid Basic authentication\" in error_response[\"error_description\"]\n\n    @pytest.mark.anyio\n    async def test_basic_auth_invalid_base64_fails(\n        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]\n    ):\n        \"\"\"Test that invalid base64 in Basic auth header fails.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Basic Auth Client\",\n            \"token_endpoint_auth_method\": \"client_secret_basic\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n        }\n\n        response = await test_client.post(\"/register\", json=client_metadata)\n        assert response.status_code == 201\n        client_info = response.json()\n\n        auth_code = f\"code_{int(time.time())}\"\n        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(\n            code=auth_code,\n            client_id=client_info[\"client_id\"],\n            code_challenge=pkce_challenge[\"code_challenge\"],\n            redirect_uri=AnyUrl(\"https://client.example.com/callback\"),\n            redirect_uri_provided_explicitly=True,\n            scopes=[\"read\", \"write\"],\n            expires_at=time.time() + 600,\n        )\n\n        # Send invalid base64\n        response = await test_client.post(\n            \"/token\",\n            headers={\"Authorization\": \"Basic !!!invalid-base64!!!\"},\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": client_info[\"client_id\"],\n                \"code\": auth_code,\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n            },\n        )\n        assert response.status_code == 401\n        error_response = response.json()\n        # RFC 6749: authentication failures return \"invalid_client\"\n        assert error_response[\"error\"] == \"invalid_client\"\n        assert \"Invalid Basic authentication header\" in error_response[\"error_description\"]\n\n    @pytest.mark.anyio\n    async def test_basic_auth_no_colon_fails(\n        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]\n    ):\n        \"\"\"Test that Basic auth without colon separator fails.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Basic Auth Client\",\n            \"token_endpoint_auth_method\": \"client_secret_basic\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n        }\n\n        response = await test_client.post(\"/register\", json=client_metadata)\n        assert response.status_code == 201\n        client_info = response.json()\n\n        auth_code = f\"code_{int(time.time())}\"\n        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(\n            code=auth_code,\n            client_id=client_info[\"client_id\"],\n            code_challenge=pkce_challenge[\"code_challenge\"],\n            redirect_uri=AnyUrl(\"https://client.example.com/callback\"),\n            redirect_uri_provided_explicitly=True,\n            scopes=[\"read\", \"write\"],\n            expires_at=time.time() + 600,\n        )\n\n        # Send base64 without colon (invalid format)\n        invalid_creds = base64.b64encode(b\"no-colon-here\").decode()\n        response = await test_client.post(\n            \"/token\",\n            headers={\"Authorization\": f\"Basic {invalid_creds}\"},\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": client_info[\"client_id\"],\n                \"code\": auth_code,\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n            },\n        )\n        assert response.status_code == 401\n        error_response = response.json()\n        # RFC 6749: authentication failures return \"invalid_client\"\n        assert error_response[\"error\"] == \"invalid_client\"\n        assert \"Invalid Basic authentication header\" in error_response[\"error_description\"]\n\n    @pytest.mark.anyio\n    async def test_basic_auth_client_id_mismatch_fails(\n        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]\n    ):\n        \"\"\"Test that client_id mismatch between body and Basic auth fails.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Basic Auth Client\",\n            \"token_endpoint_auth_method\": \"client_secret_basic\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n        }\n\n        response = await test_client.post(\"/register\", json=client_metadata)\n        assert response.status_code == 201\n        client_info = response.json()\n\n        auth_code = f\"code_{int(time.time())}\"\n        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(\n            code=auth_code,\n            client_id=client_info[\"client_id\"],\n            code_challenge=pkce_challenge[\"code_challenge\"],\n            redirect_uri=AnyUrl(\"https://client.example.com/callback\"),\n            redirect_uri_provided_explicitly=True,\n            scopes=[\"read\", \"write\"],\n            expires_at=time.time() + 600,\n        )\n\n        # Send different client_id in Basic auth header\n        wrong_creds = base64.b64encode(f\"wrong-client-id:{client_info['client_secret']}\".encode()).decode()\n        response = await test_client.post(\n            \"/token\",\n            headers={\"Authorization\": f\"Basic {wrong_creds}\"},\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": client_info[\"client_id\"],  # Correct client_id in body\n                \"code\": auth_code,\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n            },\n        )\n        assert response.status_code == 401\n        error_response = response.json()\n        # RFC 6749: authentication failures return \"invalid_client\"\n        assert error_response[\"error\"] == \"invalid_client\"\n        assert \"Client ID mismatch\" in error_response[\"error_description\"]\n\n    @pytest.mark.anyio\n    async def test_none_auth_method_public_client(\n        self, test_client: httpx.AsyncClient, mock_oauth_provider: MockOAuthProvider, pkce_challenge: dict[str, str]\n    ):\n        \"\"\"Test that 'none' authentication method works for public clients.\"\"\"\n        client_metadata = {\n            \"redirect_uris\": [\"https://client.example.com/callback\"],\n            \"client_name\": \"Public Client\",\n            \"token_endpoint_auth_method\": \"none\",\n            \"grant_types\": [\"authorization_code\", \"refresh_token\"],\n        }\n\n        response = await test_client.post(\"/register\", json=client_metadata)\n        assert response.status_code == 201\n        client_info = response.json()\n        assert client_info[\"token_endpoint_auth_method\"] == \"none\"\n        # Public clients should not have a client_secret\n        assert \"client_secret\" not in client_info or client_info.get(\"client_secret\") is None\n\n        auth_code = f\"code_{int(time.time())}\"\n        mock_oauth_provider.auth_codes[auth_code] = AuthorizationCode(\n            code=auth_code,\n            client_id=client_info[\"client_id\"],\n            code_challenge=pkce_challenge[\"code_challenge\"],\n            redirect_uri=AnyUrl(\"https://client.example.com/callback\"),\n            redirect_uri_provided_explicitly=True,\n            scopes=[\"read\", \"write\"],\n            expires_at=time.time() + 600,\n        )\n\n        # Token request without any client secret\n        response = await test_client.post(\n            \"/token\",\n            data={\n                \"grant_type\": \"authorization_code\",\n                \"client_id\": client_info[\"client_id\"],\n                \"code\": auth_code,\n                \"code_verifier\": pkce_challenge[\"code_verifier\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n            },\n        )\n        assert response.status_code == 200\n        token_response = response.json()\n        assert \"access_token\" in token_response\n\n\nclass TestAuthorizeEndpointErrors:\n    \"\"\"Test error handling in the OAuth authorization endpoint.\"\"\"\n\n    @pytest.mark.anyio\n    async def test_authorize_missing_client_id(self, test_client: httpx.AsyncClient, pkce_challenge: dict[str, str]):\n        \"\"\"Test authorization endpoint with missing client_id.\n\n        According to the OAuth2.0 spec, if client_id is missing, the server should\n        inform the resource owner and NOT redirect.\n        \"\"\"\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                # Missing client_id\n                \"redirect_uri\": \"https://client.example.com/callback\",\n                \"state\": \"test_state\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n            },\n        )\n\n        # Should NOT redirect, should show an error page\n        assert response.status_code == 400\n        # The response should include an error message about missing client_id\n        assert \"client_id\" in response.text.lower()\n\n    @pytest.mark.anyio\n    async def test_authorize_invalid_client_id(self, test_client: httpx.AsyncClient, pkce_challenge: dict[str, str]):\n        \"\"\"Test authorization endpoint with invalid client_id.\n\n        According to the OAuth2.0 spec, if client_id is invalid, the server should\n        inform the resource owner and NOT redirect.\n        \"\"\"\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                \"client_id\": \"invalid_client_id_that_does_not_exist\",\n                \"redirect_uri\": \"https://client.example.com/callback\",\n                \"state\": \"test_state\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n            },\n        )\n\n        # Should NOT redirect, should show an error page\n        assert response.status_code == 400\n        # The response should include an error message about invalid client_id\n        assert \"client\" in response.text.lower()\n\n    @pytest.mark.anyio\n    async def test_authorize_missing_redirect_uri(\n        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]\n    ):\n        \"\"\"Test authorization endpoint with missing redirect_uri.\n\n        If client has only one registered redirect_uri, it can be omitted.\n        \"\"\"\n\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                \"client_id\": registered_client[\"client_id\"],\n                # Missing redirect_uri\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"state\": \"test_state\",\n            },\n        )\n\n        # Should redirect to the registered redirect_uri\n        assert response.status_code == 302, response.content\n        redirect_url = response.headers[\"location\"]\n        assert redirect_url.startswith(\"https://client.example.com/callback\")\n\n    @pytest.mark.anyio\n    async def test_authorize_invalid_redirect_uri(\n        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]\n    ):\n        \"\"\"Test authorization endpoint with invalid redirect_uri.\n\n        According to the OAuth2.0 spec, if redirect_uri is invalid or doesn't match,\n        the server should inform the resource owner and NOT redirect.\n        \"\"\"\n\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                \"client_id\": registered_client[\"client_id\"],\n                # Non-matching URI\n                \"redirect_uri\": \"https://attacker.example.com/callback\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"state\": \"test_state\",\n            },\n        )\n\n        # Should NOT redirect, should show an error page\n        assert response.status_code == 400, response.content\n        # The response should include an error message about redirect_uri mismatch\n        assert \"redirect\" in response.text.lower()\n\n    @pytest.mark.anyio\n    @pytest.mark.parametrize(\n        \"registered_client\",\n        [\n            {\n                \"redirect_uris\": [\n                    \"https://client.example.com/callback\",\n                    \"https://client.example.com/other-callback\",\n                ]\n            }\n        ],\n        indirect=True,\n    )\n    async def test_authorize_missing_redirect_uri_multiple_registered(\n        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]\n    ):\n        \"\"\"Test endpoint with missing redirect_uri with multiple registered URIs.\n\n        If client has multiple registered redirect_uris, redirect_uri must be provided.\n        \"\"\"\n\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                \"client_id\": registered_client[\"client_id\"],\n                # Missing redirect_uri\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"state\": \"test_state\",\n            },\n        )\n\n        # Should NOT redirect, should return a 400 error\n        assert response.status_code == 400\n        # The response should include an error message about missing redirect_uri\n        assert \"redirect_uri\" in response.text.lower()\n\n    @pytest.mark.anyio\n    async def test_authorize_unsupported_response_type(\n        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]\n    ):\n        \"\"\"Test authorization endpoint with unsupported response_type.\n\n        According to the OAuth2.0 spec, for other errors like unsupported_response_type,\n        the server should redirect with error parameters.\n        \"\"\"\n\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"token\",  # Unsupported (we only support \"code\")\n                \"client_id\": registered_client[\"client_id\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"state\": \"test_state\",\n            },\n        )\n\n        # Should redirect with error parameters\n        assert response.status_code == 302\n        redirect_url = response.headers[\"location\"]\n        parsed_url = urlparse(redirect_url)\n        query_params = parse_qs(parsed_url.query)\n\n        assert \"error\" in query_params\n        assert query_params[\"error\"][0] == \"unsupported_response_type\"\n        # State should be preserved\n        assert \"state\" in query_params\n        assert query_params[\"state\"][0] == \"test_state\"\n\n    @pytest.mark.anyio\n    async def test_authorize_missing_response_type(\n        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]\n    ):\n        \"\"\"Test authorization endpoint with missing response_type.\n\n        Missing required parameter should result in invalid_request error.\n        \"\"\"\n\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                # Missing response_type\n                \"client_id\": registered_client[\"client_id\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"state\": \"test_state\",\n            },\n        )\n\n        # Should redirect with error parameters\n        assert response.status_code == 302\n        redirect_url = response.headers[\"location\"]\n        parsed_url = urlparse(redirect_url)\n        query_params = parse_qs(parsed_url.query)\n\n        assert \"error\" in query_params\n        assert query_params[\"error\"][0] == \"invalid_request\"\n        # State should be preserved\n        assert \"state\" in query_params\n        assert query_params[\"state\"][0] == \"test_state\"\n\n    @pytest.mark.anyio\n    async def test_authorize_missing_pkce_challenge(\n        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any]\n    ):\n        \"\"\"Test authorization endpoint with missing PKCE code_challenge.\n\n        Missing PKCE parameters should result in invalid_request error.\n        \"\"\"\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                \"client_id\": registered_client[\"client_id\"],\n                # Missing code_challenge\n                \"state\": \"test_state\",\n                # using default URL\n            },\n        )\n\n        # Should redirect with error parameters\n        assert response.status_code == 302\n        redirect_url = response.headers[\"location\"]\n        parsed_url = urlparse(redirect_url)\n        query_params = parse_qs(parsed_url.query)\n\n        assert \"error\" in query_params\n        assert query_params[\"error\"][0] == \"invalid_request\"\n        # State should be preserved\n        assert \"state\" in query_params\n        assert query_params[\"state\"][0] == \"test_state\"\n\n    @pytest.mark.anyio\n    async def test_authorize_invalid_scope(\n        self, test_client: httpx.AsyncClient, registered_client: dict[str, Any], pkce_challenge: dict[str, str]\n    ):\n        \"\"\"Test authorization endpoint with invalid scope.\n\n        Invalid scope should redirect with invalid_scope error.\n        \"\"\"\n\n        response = await test_client.get(\n            \"/authorize\",\n            params={\n                \"response_type\": \"code\",\n                \"client_id\": registered_client[\"client_id\"],\n                \"redirect_uri\": \"https://client.example.com/callback\",\n                \"code_challenge\": pkce_challenge[\"code_challenge\"],\n                \"code_challenge_method\": \"S256\",\n                \"scope\": \"invalid_scope_that_does_not_exist\",\n                \"state\": \"test_state\",\n            },\n        )\n\n        # Should redirect with error parameters\n        assert response.status_code == 302\n        redirect_url = response.headers[\"location\"]\n        parsed_url = urlparse(redirect_url)\n        query_params = parse_qs(parsed_url.query)\n\n        assert \"error\" in query_params\n        assert query_params[\"error\"][0] == \"invalid_scope\"\n        # State should be preserved\n        assert \"state\" in query_params\n        assert query_params[\"state\"][0] == \"test_state\"\n"
  },
  {
    "path": "tests/server/mcpserver/prompts/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/mcpserver/prompts/test_base.py",
    "content": "from typing import Any\n\nimport pytest\n\nfrom mcp.server.mcpserver import Context\nfrom mcp.server.mcpserver.prompts.base import AssistantMessage, Message, Prompt, UserMessage\nfrom mcp.types import EmbeddedResource, TextContent, TextResourceContents\n\n\nclass TestRenderPrompt:\n    @pytest.mark.anyio\n    async def test_basic_fn(self):\n        def fn() -> str:\n            return \"Hello, world!\"\n\n        prompt = Prompt.from_function(fn)\n        assert await prompt.render(None, Context()) == [\n            UserMessage(content=TextContent(type=\"text\", text=\"Hello, world!\"))\n        ]\n\n    @pytest.mark.anyio\n    async def test_async_fn(self):\n        async def fn() -> str:\n            return \"Hello, world!\"\n\n        prompt = Prompt.from_function(fn)\n        assert await prompt.render(None, Context()) == [\n            UserMessage(content=TextContent(type=\"text\", text=\"Hello, world!\"))\n        ]\n\n    @pytest.mark.anyio\n    async def test_fn_with_args(self):\n        async def fn(name: str, age: int = 30) -> str:\n            return f\"Hello, {name}! You're {age} years old.\"\n\n        prompt = Prompt.from_function(fn)\n        assert await prompt.render({\"name\": \"World\"}, Context()) == [\n            UserMessage(content=TextContent(type=\"text\", text=\"Hello, World! You're 30 years old.\"))\n        ]\n\n    @pytest.mark.anyio\n    async def test_fn_with_invalid_kwargs(self):\n        async def fn(name: str, age: int = 30) -> str:  # pragma: no cover\n            return f\"Hello, {name}! You're {age} years old.\"\n\n        prompt = Prompt.from_function(fn)\n        with pytest.raises(ValueError):\n            await prompt.render({\"age\": 40}, Context())\n\n    @pytest.mark.anyio\n    async def test_fn_returns_message(self):\n        async def fn() -> UserMessage:\n            return UserMessage(content=\"Hello, world!\")\n\n        prompt = Prompt.from_function(fn)\n        assert await prompt.render(None, Context()) == [\n            UserMessage(content=TextContent(type=\"text\", text=\"Hello, world!\"))\n        ]\n\n    @pytest.mark.anyio\n    async def test_fn_returns_assistant_message(self):\n        async def fn() -> AssistantMessage:\n            return AssistantMessage(content=TextContent(type=\"text\", text=\"Hello, world!\"))\n\n        prompt = Prompt.from_function(fn)\n        assert await prompt.render(None, Context()) == [\n            AssistantMessage(content=TextContent(type=\"text\", text=\"Hello, world!\"))\n        ]\n\n    @pytest.mark.anyio\n    async def test_fn_returns_multiple_messages(self):\n        expected: list[Message] = [\n            UserMessage(\"Hello, world!\"),\n            AssistantMessage(\"How can I help you today?\"),\n            UserMessage(\"I'm looking for a restaurant in the center of town.\"),\n        ]\n\n        async def fn() -> list[Message]:\n            return expected\n\n        prompt = Prompt.from_function(fn)\n        assert await prompt.render(None, Context()) == expected\n\n    @pytest.mark.anyio\n    async def test_fn_returns_list_of_strings(self):\n        expected = [\n            \"Hello, world!\",\n            \"I'm looking for a restaurant in the center of town.\",\n        ]\n\n        async def fn() -> list[str]:\n            return expected\n\n        prompt = Prompt.from_function(fn)\n        assert await prompt.render(None, Context()) == [UserMessage(t) for t in expected]\n\n    @pytest.mark.anyio\n    async def test_fn_returns_resource_content(self):\n        \"\"\"Test returning a message with resource content.\"\"\"\n\n        async def fn() -> UserMessage:\n            return UserMessage(\n                content=EmbeddedResource(\n                    type=\"resource\",\n                    resource=TextResourceContents(\n                        uri=\"file://file.txt\",\n                        text=\"File contents\",\n                        mime_type=\"text/plain\",\n                    ),\n                )\n            )\n\n        prompt = Prompt.from_function(fn)\n        assert await prompt.render(None, Context()) == [\n            UserMessage(\n                content=EmbeddedResource(\n                    type=\"resource\",\n                    resource=TextResourceContents(\n                        uri=\"file://file.txt\",\n                        text=\"File contents\",\n                        mime_type=\"text/plain\",\n                    ),\n                )\n            )\n        ]\n\n    @pytest.mark.anyio\n    async def test_fn_returns_mixed_content(self):\n        \"\"\"Test returning messages with mixed content types.\"\"\"\n\n        async def fn() -> list[Message]:\n            return [\n                UserMessage(content=\"Please analyze this file:\"),\n                UserMessage(\n                    content=EmbeddedResource(\n                        type=\"resource\",\n                        resource=TextResourceContents(\n                            uri=\"file://file.txt\",\n                            text=\"File contents\",\n                            mime_type=\"text/plain\",\n                        ),\n                    )\n                ),\n                AssistantMessage(content=\"I'll help analyze that file.\"),\n            ]\n\n        prompt = Prompt.from_function(fn)\n        assert await prompt.render(None, Context()) == [\n            UserMessage(content=TextContent(type=\"text\", text=\"Please analyze this file:\")),\n            UserMessage(\n                content=EmbeddedResource(\n                    type=\"resource\",\n                    resource=TextResourceContents(\n                        uri=\"file://file.txt\",\n                        text=\"File contents\",\n                        mime_type=\"text/plain\",\n                    ),\n                )\n            ),\n            AssistantMessage(content=TextContent(type=\"text\", text=\"I'll help analyze that file.\")),\n        ]\n\n    @pytest.mark.anyio\n    async def test_fn_returns_dict_with_resource(self):\n        \"\"\"Test returning a dict with resource content.\"\"\"\n\n        async def fn() -> dict[str, Any]:\n            return {\n                \"role\": \"user\",\n                \"content\": {\n                    \"type\": \"resource\",\n                    \"resource\": {\n                        \"uri\": \"file://file.txt\",\n                        \"text\": \"File contents\",\n                        \"mimeType\": \"text/plain\",\n                    },\n                },\n            }\n\n        prompt = Prompt.from_function(fn)\n        assert await prompt.render(None, Context()) == [\n            UserMessage(\n                content=EmbeddedResource(\n                    type=\"resource\",\n                    resource=TextResourceContents(\n                        uri=\"file://file.txt\",\n                        text=\"File contents\",\n                        mime_type=\"text/plain\",\n                    ),\n                )\n            )\n        ]\n"
  },
  {
    "path": "tests/server/mcpserver/prompts/test_manager.py",
    "content": "import pytest\n\nfrom mcp.server.mcpserver import Context\nfrom mcp.server.mcpserver.prompts.base import Prompt, UserMessage\nfrom mcp.server.mcpserver.prompts.manager import PromptManager\nfrom mcp.types import TextContent\n\n\nclass TestPromptManager:\n    def test_add_prompt(self):\n        \"\"\"Test adding a prompt to the manager.\"\"\"\n\n        def fn() -> str:  # pragma: no cover\n            return \"Hello, world!\"\n\n        manager = PromptManager()\n        prompt = Prompt.from_function(fn)\n        added = manager.add_prompt(prompt)\n        assert added == prompt\n        assert manager.get_prompt(\"fn\") == prompt\n\n    def test_add_duplicate_prompt(self, caplog: pytest.LogCaptureFixture):\n        \"\"\"Test adding the same prompt twice.\"\"\"\n\n        def fn() -> str:  # pragma: no cover\n            return \"Hello, world!\"\n\n        manager = PromptManager()\n        prompt = Prompt.from_function(fn)\n        first = manager.add_prompt(prompt)\n        second = manager.add_prompt(prompt)\n        assert first == second\n        assert \"Prompt already exists\" in caplog.text\n\n    def test_disable_warn_on_duplicate_prompts(self, caplog: pytest.LogCaptureFixture):\n        \"\"\"Test disabling warning on duplicate prompts.\"\"\"\n\n        def fn() -> str:  # pragma: no cover\n            return \"Hello, world!\"\n\n        manager = PromptManager(warn_on_duplicate_prompts=False)\n        prompt = Prompt.from_function(fn)\n        first = manager.add_prompt(prompt)\n        second = manager.add_prompt(prompt)\n        assert first == second\n        assert \"Prompt already exists\" not in caplog.text\n\n    def test_list_prompts(self):\n        \"\"\"Test listing all prompts.\"\"\"\n\n        def fn1() -> str:  # pragma: no cover\n            return \"Hello, world!\"\n\n        def fn2() -> str:  # pragma: no cover\n            return \"Goodbye, world!\"\n\n        manager = PromptManager()\n        prompt1 = Prompt.from_function(fn1)\n        prompt2 = Prompt.from_function(fn2)\n        manager.add_prompt(prompt1)\n        manager.add_prompt(prompt2)\n        prompts = manager.list_prompts()\n        assert len(prompts) == 2\n        assert prompts == [prompt1, prompt2]\n\n    @pytest.mark.anyio\n    async def test_render_prompt(self):\n        \"\"\"Test rendering a prompt.\"\"\"\n\n        def fn() -> str:\n            return \"Hello, world!\"\n\n        manager = PromptManager()\n        prompt = Prompt.from_function(fn)\n        manager.add_prompt(prompt)\n        messages = await manager.render_prompt(\"fn\", None, Context())\n        assert messages == [UserMessage(content=TextContent(type=\"text\", text=\"Hello, world!\"))]\n\n    @pytest.mark.anyio\n    async def test_render_prompt_with_args(self):\n        \"\"\"Test rendering a prompt with arguments.\"\"\"\n\n        def fn(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        manager = PromptManager()\n        prompt = Prompt.from_function(fn)\n        manager.add_prompt(prompt)\n        messages = await manager.render_prompt(\"fn\", {\"name\": \"World\"}, Context())\n        assert messages == [UserMessage(content=TextContent(type=\"text\", text=\"Hello, World!\"))]\n\n    @pytest.mark.anyio\n    async def test_render_unknown_prompt(self):\n        \"\"\"Test rendering a non-existent prompt.\"\"\"\n        manager = PromptManager()\n        with pytest.raises(ValueError, match=\"Unknown prompt: unknown\"):\n            await manager.render_prompt(\"unknown\", None, Context())\n\n    @pytest.mark.anyio\n    async def test_render_prompt_with_missing_args(self):\n        \"\"\"Test rendering a prompt with missing required arguments.\"\"\"\n\n        def fn(name: str) -> str:  # pragma: no cover\n            return f\"Hello, {name}!\"\n\n        manager = PromptManager()\n        prompt = Prompt.from_function(fn)\n        manager.add_prompt(prompt)\n        with pytest.raises(ValueError, match=\"Missing required arguments\"):\n            await manager.render_prompt(\"fn\", None, Context())\n"
  },
  {
    "path": "tests/server/mcpserver/resources/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/mcpserver/resources/test_file_resources.py",
    "content": "import os\nfrom pathlib import Path\nfrom tempfile import NamedTemporaryFile\n\nimport pytest\n\nfrom mcp.server.mcpserver.resources import FileResource\n\n\n@pytest.fixture\ndef temp_file():\n    \"\"\"Create a temporary file for testing.\n\n    File is automatically cleaned up after the test if it still exists.\n    \"\"\"\n    content = \"test content\"\n    with NamedTemporaryFile(mode=\"w\", delete=False) as f:\n        f.write(content)\n        path = Path(f.name).resolve()\n    yield path\n    try:  # pragma: lax no cover\n        path.unlink()\n    except FileNotFoundError:  # pragma: lax no cover\n        pass  # File was already deleted by the test\n\n\nclass TestFileResource:\n    \"\"\"Test FileResource functionality.\"\"\"\n\n    def test_file_resource_creation(self, temp_file: Path):\n        \"\"\"Test creating a FileResource.\"\"\"\n        resource = FileResource(\n            uri=temp_file.as_uri(),\n            name=\"test\",\n            description=\"test file\",\n            path=temp_file,\n        )\n        assert str(resource.uri) == temp_file.as_uri()\n        assert resource.name == \"test\"\n        assert resource.description == \"test file\"\n        assert resource.mime_type == \"text/plain\"  # default\n        assert resource.path == temp_file\n        assert resource.is_binary is False  # default\n\n    def test_file_resource_str_path_conversion(self, temp_file: Path):\n        \"\"\"Test FileResource handles string paths.\"\"\"\n        resource = FileResource(\n            uri=f\"file://{temp_file}\",\n            name=\"test\",\n            path=Path(str(temp_file)),\n        )\n        assert isinstance(resource.path, Path)\n        assert resource.path.is_absolute()\n\n    @pytest.mark.anyio\n    async def test_read_text_file(self, temp_file: Path):\n        \"\"\"Test reading a text file.\"\"\"\n        resource = FileResource(\n            uri=f\"file://{temp_file}\",\n            name=\"test\",\n            path=temp_file,\n        )\n        content = await resource.read()\n        assert content == \"test content\"\n        assert resource.mime_type == \"text/plain\"\n\n    @pytest.mark.anyio\n    async def test_read_binary_file(self, temp_file: Path):\n        \"\"\"Test reading a file as binary.\"\"\"\n        resource = FileResource(\n            uri=f\"file://{temp_file}\",\n            name=\"test\",\n            path=temp_file,\n            is_binary=True,\n        )\n        content = await resource.read()\n        assert isinstance(content, bytes)\n        assert content == b\"test content\"\n\n    def test_relative_path_error(self):\n        \"\"\"Test error on relative path.\"\"\"\n        with pytest.raises(ValueError, match=\"Path must be absolute\"):\n            FileResource(\n                uri=\"file:///test.txt\",\n                name=\"test\",\n                path=Path(\"test.txt\"),\n            )\n\n    @pytest.mark.anyio\n    async def test_missing_file_error(self, temp_file: Path):\n        \"\"\"Test error when file doesn't exist.\"\"\"\n        # Create path to non-existent file\n        missing = temp_file.parent / \"missing.txt\"\n        resource = FileResource(\n            uri=\"file:///missing.txt\",\n            name=\"test\",\n            path=missing,\n        )\n        with pytest.raises(ValueError, match=\"Error reading file\"):\n            await resource.read()\n\n    @pytest.mark.skipif(os.name == \"nt\", reason=\"File permissions behave differently on Windows\")\n    @pytest.mark.anyio\n    async def test_permission_error(self, temp_file: Path):  # pragma: lax no cover\n        \"\"\"Test reading a file without permissions.\"\"\"\n        temp_file.chmod(0o000)  # Remove all permissions\n        try:\n            resource = FileResource(\n                uri=temp_file.as_uri(),\n                name=\"test\",\n                path=temp_file,\n            )\n            with pytest.raises(ValueError, match=\"Error reading file\"):\n                await resource.read()\n        finally:\n            temp_file.chmod(0o644)  # Restore permissions\n"
  },
  {
    "path": "tests/server/mcpserver/resources/test_function_resources.py",
    "content": "import pytest\nfrom pydantic import BaseModel\n\nfrom mcp.server.mcpserver.resources import FunctionResource\n\n\nclass TestFunctionResource:\n    \"\"\"Test FunctionResource functionality.\"\"\"\n\n    def test_function_resource_creation(self):\n        \"\"\"Test creating a FunctionResource.\"\"\"\n\n        def my_func() -> str:  # pragma: no cover\n            return \"test content\"\n\n        resource = FunctionResource(\n            uri=\"fn://test\",\n            name=\"test\",\n            description=\"test function\",\n            fn=my_func,\n        )\n        assert str(resource.uri) == \"fn://test\"\n        assert resource.name == \"test\"\n        assert resource.description == \"test function\"\n        assert resource.mime_type == \"text/plain\"  # default\n        assert resource.fn == my_func\n\n    @pytest.mark.anyio\n    async def test_read_text(self):\n        \"\"\"Test reading text from a FunctionResource.\"\"\"\n\n        def get_data() -> str:\n            return \"Hello, world!\"\n\n        resource = FunctionResource(\n            uri=\"function://test\",\n            name=\"test\",\n            fn=get_data,\n        )\n        content = await resource.read()\n        assert content == \"Hello, world!\"\n        assert resource.mime_type == \"text/plain\"\n\n    @pytest.mark.anyio\n    async def test_read_binary(self):\n        \"\"\"Test reading binary data from a FunctionResource.\"\"\"\n\n        def get_data() -> bytes:\n            return b\"Hello, world!\"\n\n        resource = FunctionResource(\n            uri=\"function://test\",\n            name=\"test\",\n            fn=get_data,\n        )\n        content = await resource.read()\n        assert content == b\"Hello, world!\"\n\n    @pytest.mark.anyio\n    async def test_json_conversion(self):\n        \"\"\"Test automatic JSON conversion of non-string results.\"\"\"\n\n        def get_data() -> dict[str, str]:\n            return {\"key\": \"value\"}\n\n        resource = FunctionResource(\n            uri=\"function://test\",\n            name=\"test\",\n            fn=get_data,\n        )\n        content = await resource.read()\n        assert isinstance(content, str)\n        assert '\"key\": \"value\"' in content\n\n    @pytest.mark.anyio\n    async def test_error_handling(self):\n        \"\"\"Test error handling in FunctionResource.\"\"\"\n\n        def failing_func() -> str:\n            raise ValueError(\"Test error\")\n\n        resource = FunctionResource(\n            uri=\"function://test\",\n            name=\"test\",\n            fn=failing_func,\n        )\n        with pytest.raises(ValueError, match=\"Error reading resource function://test\"):\n            await resource.read()\n\n    @pytest.mark.anyio\n    async def test_basemodel_conversion(self):\n        \"\"\"Test handling of BaseModel types.\"\"\"\n\n        class MyModel(BaseModel):\n            name: str\n\n        resource = FunctionResource(\n            uri=\"function://test\",\n            name=\"test\",\n            fn=lambda: MyModel(name=\"test\"),\n        )\n        content = await resource.read()\n        assert content == '{\\n  \"name\": \"test\"\\n}'\n\n    @pytest.mark.anyio\n    async def test_custom_type_conversion(self):\n        \"\"\"Test handling of custom types.\"\"\"\n\n        class CustomData:\n            def __str__(self) -> str:\n                return \"custom data\"\n\n        def get_data() -> CustomData:\n            return CustomData()\n\n        resource = FunctionResource(\n            uri=\"function://test\",\n            name=\"test\",\n            fn=get_data,\n        )\n        content = await resource.read()\n        assert isinstance(content, str)\n\n    @pytest.mark.anyio\n    async def test_async_read_text(self):\n        \"\"\"Test reading text from async FunctionResource.\"\"\"\n\n        async def get_data() -> str:\n            return \"Hello, world!\"\n\n        resource = FunctionResource(\n            uri=\"function://test\",\n            name=\"test\",\n            fn=get_data,\n        )\n        content = await resource.read()\n        assert content == \"Hello, world!\"\n        assert resource.mime_type == \"text/plain\"\n\n    @pytest.mark.anyio\n    async def test_from_function(self):\n        \"\"\"Test creating a FunctionResource from a function.\"\"\"\n\n        async def get_data() -> str:  # pragma: no cover\n            \"\"\"get_data returns a string\"\"\"\n            return \"Hello, world!\"\n\n        resource = FunctionResource.from_function(\n            fn=get_data,\n            uri=\"function://test\",\n            name=\"test\",\n        )\n\n        assert resource.description == \"get_data returns a string\"\n        assert resource.mime_type == \"text/plain\"\n        assert resource.name == \"test\"\n        assert resource.uri == \"function://test\"\n\n\nclass TestFunctionResourceMetadata:\n    def test_from_function_with_metadata(self):\n        # from_function() accepts meta dict and stores it on the resource for static resources\n\n        def get_data() -> str:  # pragma: no cover\n            return \"test data\"\n\n        metadata = {\"cache_ttl\": 300, \"tags\": [\"data\", \"readonly\"]}\n\n        resource = FunctionResource.from_function(\n            fn=get_data,\n            uri=\"resource://data\",\n            meta=metadata,\n        )\n\n        assert resource.meta is not None\n        assert resource.meta == metadata\n        assert resource.meta[\"cache_ttl\"] == 300\n        assert \"data\" in resource.meta[\"tags\"]\n        assert \"readonly\" in resource.meta[\"tags\"]\n\n    def test_from_function_without_metadata(self):\n        # meta parameter is optional and defaults to None for backward compatibility\n\n        def get_data() -> str:  # pragma: no cover\n            return \"test data\"\n\n        resource = FunctionResource.from_function(\n            fn=get_data,\n            uri=\"resource://data\",\n        )\n\n        assert resource.meta is None\n"
  },
  {
    "path": "tests/server/mcpserver/resources/test_resource_manager.py",
    "content": "from pathlib import Path\nfrom tempfile import NamedTemporaryFile\n\nimport pytest\nfrom pydantic import AnyUrl\n\nfrom mcp.server.mcpserver import Context\nfrom mcp.server.mcpserver.resources import FileResource, FunctionResource, ResourceManager, ResourceTemplate\n\n\n@pytest.fixture\ndef temp_file():\n    \"\"\"Create a temporary file for testing.\n\n    File is automatically cleaned up after the test if it still exists.\n    \"\"\"\n    content = \"test content\"\n    with NamedTemporaryFile(mode=\"w\", delete=False) as f:\n        f.write(content)\n        path = Path(f.name).resolve()\n    yield path\n    try:  # pragma: lax no cover\n        path.unlink()\n    except FileNotFoundError:  # pragma: lax no cover\n        pass  # File was already deleted by the test\n\n\nclass TestResourceManager:\n    \"\"\"Test ResourceManager functionality.\"\"\"\n\n    def test_add_resource(self, temp_file: Path):\n        \"\"\"Test adding a resource.\"\"\"\n        manager = ResourceManager()\n        resource = FileResource(\n            uri=f\"file://{temp_file}\",\n            name=\"test\",\n            path=temp_file,\n        )\n        added = manager.add_resource(resource)\n        assert added == resource\n        assert manager.list_resources() == [resource]\n\n    def test_add_duplicate_resource(self, temp_file: Path):\n        \"\"\"Test adding the same resource twice.\"\"\"\n        manager = ResourceManager()\n        resource = FileResource(\n            uri=f\"file://{temp_file}\",\n            name=\"test\",\n            path=temp_file,\n        )\n        first = manager.add_resource(resource)\n        second = manager.add_resource(resource)\n        assert first == second\n        assert manager.list_resources() == [resource]\n\n    def test_warn_on_duplicate_resources(self, temp_file: Path, caplog: pytest.LogCaptureFixture):\n        \"\"\"Test warning on duplicate resources.\"\"\"\n        manager = ResourceManager()\n        resource = FileResource(\n            uri=f\"file://{temp_file}\",\n            name=\"test\",\n            path=temp_file,\n        )\n        manager.add_resource(resource)\n        manager.add_resource(resource)\n        assert \"Resource already exists\" in caplog.text\n\n    def test_disable_warn_on_duplicate_resources(self, temp_file: Path, caplog: pytest.LogCaptureFixture):\n        \"\"\"Test disabling warning on duplicate resources.\"\"\"\n        manager = ResourceManager(warn_on_duplicate_resources=False)\n        resource = FileResource(\n            uri=f\"file://{temp_file}\",\n            name=\"test\",\n            path=temp_file,\n        )\n        manager.add_resource(resource)\n        manager.add_resource(resource)\n        assert \"Resource already exists\" not in caplog.text\n\n    @pytest.mark.anyio\n    async def test_get_resource(self, temp_file: Path):\n        \"\"\"Test getting a resource by URI.\"\"\"\n        manager = ResourceManager()\n        resource = FileResource(\n            uri=f\"file://{temp_file}\",\n            name=\"test\",\n            path=temp_file,\n        )\n        manager.add_resource(resource)\n        retrieved = await manager.get_resource(resource.uri, Context())\n        assert retrieved == resource\n\n    @pytest.mark.anyio\n    async def test_get_resource_from_template(self):\n        \"\"\"Test getting a resource through a template.\"\"\"\n        manager = ResourceManager()\n\n        def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        template = ResourceTemplate.from_function(\n            fn=greet,\n            uri_template=\"greet://{name}\",\n            name=\"greeter\",\n        )\n        manager._templates[template.uri_template] = template\n\n        resource = await manager.get_resource(AnyUrl(\"greet://world\"), Context())\n        assert isinstance(resource, FunctionResource)\n        content = await resource.read()\n        assert content == \"Hello, world!\"\n\n    @pytest.mark.anyio\n    async def test_get_unknown_resource(self):\n        \"\"\"Test getting a non-existent resource.\"\"\"\n        manager = ResourceManager()\n        with pytest.raises(ValueError, match=\"Unknown resource\"):\n            await manager.get_resource(AnyUrl(\"unknown://test\"), Context())\n\n    def test_list_resources(self, temp_file: Path):\n        \"\"\"Test listing all resources.\"\"\"\n        manager = ResourceManager()\n        resource1 = FileResource(\n            uri=f\"file://{temp_file}\",\n            name=\"test1\",\n            path=temp_file,\n        )\n        resource2 = FileResource(\n            uri=f\"file://{temp_file}2\",\n            name=\"test2\",\n            path=temp_file,\n        )\n        manager.add_resource(resource1)\n        manager.add_resource(resource2)\n        resources = manager.list_resources()\n        assert len(resources) == 2\n        assert resources == [resource1, resource2]\n\n\nclass TestResourceManagerMetadata:\n    \"\"\"Test ResourceManager Metadata\"\"\"\n\n    def test_add_template_with_metadata(self):\n        \"\"\"Test that ResourceManager.add_template() accepts and passes meta parameter.\"\"\"\n\n        manager = ResourceManager()\n\n        def get_item(id: str) -> str:  # pragma: no cover\n            return f\"Item {id}\"\n\n        metadata = {\"source\": \"database\", \"cached\": True}\n\n        template = manager.add_template(\n            fn=get_item,\n            uri_template=\"resource://items/{id}\",\n            meta=metadata,\n        )\n\n        assert template.meta is not None\n        assert template.meta == metadata\n        assert template.meta[\"source\"] == \"database\"\n        assert template.meta[\"cached\"] is True\n\n    def test_add_template_without_metadata(self):\n        \"\"\"Test that ResourceManager.add_template() works without meta parameter.\"\"\"\n\n        manager = ResourceManager()\n\n        def get_item(id: str) -> str:  # pragma: no cover\n            return f\"Item {id}\"\n\n        template = manager.add_template(\n            fn=get_item,\n            uri_template=\"resource://items/{id}\",\n        )\n\n        assert template.meta is None\n"
  },
  {
    "path": "tests/server/mcpserver/resources/test_resource_template.py",
    "content": "import json\nfrom typing import Any\n\nimport pytest\nfrom pydantic import BaseModel\n\nfrom mcp.server.mcpserver import Context, MCPServer\nfrom mcp.server.mcpserver.resources import FunctionResource, ResourceTemplate\nfrom mcp.types import Annotations\n\n\nclass TestResourceTemplate:\n    \"\"\"Test ResourceTemplate functionality.\"\"\"\n\n    def test_template_creation(self):\n        \"\"\"Test creating a template from a function.\"\"\"\n\n        def my_func(key: str, value: int) -> dict[str, Any]:\n            return {\"key\": key, \"value\": value}\n\n        template = ResourceTemplate.from_function(\n            fn=my_func,\n            uri_template=\"test://{key}/{value}\",\n            name=\"test\",\n        )\n        assert template.uri_template == \"test://{key}/{value}\"\n        assert template.name == \"test\"\n        assert template.mime_type == \"text/plain\"  # default\n        assert template.fn(key=\"test\", value=42) == my_func(key=\"test\", value=42)\n\n    def test_template_matches(self):\n        \"\"\"Test matching URIs against a template.\"\"\"\n\n        def my_func(key: str, value: int) -> dict[str, Any]:  # pragma: no cover\n            return {\"key\": key, \"value\": value}\n\n        template = ResourceTemplate.from_function(\n            fn=my_func,\n            uri_template=\"test://{key}/{value}\",\n            name=\"test\",\n        )\n\n        # Valid match\n        params = template.matches(\"test://foo/123\")\n        assert params == {\"key\": \"foo\", \"value\": \"123\"}\n\n        # No match\n        assert template.matches(\"test://foo\") is None\n        assert template.matches(\"other://foo/123\") is None\n\n    @pytest.mark.anyio\n    async def test_create_resource(self):\n        \"\"\"Test creating a resource from a template.\"\"\"\n\n        def my_func(key: str, value: int) -> dict[str, Any]:\n            return {\"key\": key, \"value\": value}\n\n        template = ResourceTemplate.from_function(\n            fn=my_func,\n            uri_template=\"test://{key}/{value}\",\n            name=\"test\",\n        )\n\n        resource = await template.create_resource(\n            \"test://foo/123\",\n            {\"key\": \"foo\", \"value\": 123},\n            Context(),\n        )\n\n        assert isinstance(resource, FunctionResource)\n        content = await resource.read()\n        assert isinstance(content, str)\n        data = json.loads(content)\n        assert data == {\"key\": \"foo\", \"value\": 123}\n\n    @pytest.mark.anyio\n    async def test_template_error(self):\n        \"\"\"Test error handling in template resource creation.\"\"\"\n\n        def failing_func(x: str) -> str:\n            raise ValueError(\"Test error\")\n\n        template = ResourceTemplate.from_function(\n            fn=failing_func,\n            uri_template=\"fail://{x}\",\n            name=\"fail\",\n        )\n\n        with pytest.raises(ValueError, match=\"Error creating resource from template\"):\n            await template.create_resource(\"fail://test\", {\"x\": \"test\"}, Context())\n\n    @pytest.mark.anyio\n    async def test_async_text_resource(self):\n        \"\"\"Test creating a text resource from async function.\"\"\"\n\n        async def greet(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        template = ResourceTemplate.from_function(\n            fn=greet,\n            uri_template=\"greet://{name}\",\n            name=\"greeter\",\n        )\n\n        resource = await template.create_resource(\n            \"greet://world\",\n            {\"name\": \"world\"},\n            Context(),\n        )\n\n        assert isinstance(resource, FunctionResource)\n        content = await resource.read()\n        assert content == \"Hello, world!\"\n\n    @pytest.mark.anyio\n    async def test_async_binary_resource(self):\n        \"\"\"Test creating a binary resource from async function.\"\"\"\n\n        async def get_bytes(value: str) -> bytes:\n            return value.encode()\n\n        template = ResourceTemplate.from_function(\n            fn=get_bytes,\n            uri_template=\"bytes://{value}\",\n            name=\"bytes\",\n        )\n\n        resource = await template.create_resource(\n            \"bytes://test\",\n            {\"value\": \"test\"},\n            Context(),\n        )\n\n        assert isinstance(resource, FunctionResource)\n        content = await resource.read()\n        assert content == b\"test\"\n\n    @pytest.mark.anyio\n    async def test_basemodel_conversion(self):\n        \"\"\"Test handling of BaseModel types.\"\"\"\n\n        class MyModel(BaseModel):\n            key: str\n            value: int\n\n        def get_data(key: str, value: int) -> MyModel:\n            return MyModel(key=key, value=value)\n\n        template = ResourceTemplate.from_function(\n            fn=get_data,\n            uri_template=\"test://{key}/{value}\",\n            name=\"test\",\n        )\n\n        resource = await template.create_resource(\n            \"test://foo/123\",\n            {\"key\": \"foo\", \"value\": 123},\n            Context(),\n        )\n\n        assert isinstance(resource, FunctionResource)\n        content = await resource.read()\n        assert isinstance(content, str)\n        data = json.loads(content)\n        assert data == {\"key\": \"foo\", \"value\": 123}\n\n    @pytest.mark.anyio\n    async def test_custom_type_conversion(self):\n        \"\"\"Test handling of custom types.\"\"\"\n\n        class CustomData:\n            def __init__(self, value: str):\n                self.value = value\n\n            def __str__(self) -> str:\n                return self.value\n\n        def get_data(value: str) -> CustomData:\n            return CustomData(value)\n\n        template = ResourceTemplate.from_function(\n            fn=get_data,\n            uri_template=\"test://{value}\",\n            name=\"test\",\n        )\n\n        resource = await template.create_resource(\n            \"test://hello\",\n            {\"value\": \"hello\"},\n            Context(),\n        )\n\n        assert isinstance(resource, FunctionResource)\n        content = await resource.read()\n        assert content == '\"hello\"'\n\n\nclass TestResourceTemplateAnnotations:\n    \"\"\"Test annotations on resource templates.\"\"\"\n\n    def test_template_with_annotations(self):\n        \"\"\"Test creating a template with annotations.\"\"\"\n\n        def get_user_data(user_id: str) -> str:  # pragma: no cover\n            return f\"User {user_id}\"\n\n        annotations = Annotations(priority=0.9)\n\n        template = ResourceTemplate.from_function(\n            fn=get_user_data, uri_template=\"resource://users/{user_id}\", annotations=annotations\n        )\n\n        assert template.annotations is not None\n        assert template.annotations.priority == 0.9\n\n    def test_template_without_annotations(self):\n        \"\"\"Test that annotations are optional for templates.\"\"\"\n\n        def get_user_data(user_id: str) -> str:  # pragma: no cover\n            return f\"User {user_id}\"\n\n        template = ResourceTemplate.from_function(fn=get_user_data, uri_template=\"resource://users/{user_id}\")\n\n        assert template.annotations is None\n\n    @pytest.mark.anyio\n    async def test_template_annotations_in_mcpserver(self):\n        \"\"\"Test template annotations via an MCPServer decorator.\"\"\"\n\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://dynamic/{id}\", annotations=Annotations(audience=[\"user\"], priority=0.7))\n        def get_dynamic(id: str) -> str:  # pragma: no cover\n            \"\"\"A dynamic annotated resource.\"\"\"\n            return f\"Data for {id}\"\n\n        templates = await mcp.list_resource_templates()\n        assert len(templates) == 1\n        assert templates[0].annotations is not None\n        assert templates[0].annotations.audience == [\"user\"]\n        assert templates[0].annotations.priority == 0.7\n\n    @pytest.mark.anyio\n    async def test_template_created_resources_inherit_annotations(self):\n        \"\"\"Test that resources created from templates inherit annotations.\"\"\"\n\n        def get_item(item_id: str) -> str:\n            return f\"Item {item_id}\"\n\n        annotations = Annotations(priority=0.6)\n\n        template = ResourceTemplate.from_function(\n            fn=get_item, uri_template=\"resource://items/{item_id}\", annotations=annotations\n        )\n\n        # Create a resource from the template\n        resource = await template.create_resource(\"resource://items/123\", {\"item_id\": \"123\"}, Context())\n\n        # The resource should inherit the template's annotations\n        assert resource.annotations is not None\n        assert resource.annotations.priority == 0.6\n\n        # Verify the resource works correctly\n        content = await resource.read()\n        assert content == \"Item 123\"\n\n\nclass TestResourceTemplateMetadata:\n    \"\"\"Test ResourceTemplate meta handling.\"\"\"\n\n    def test_template_from_function_with_metadata(self):\n        \"\"\"Test that ResourceTemplate.from_function() accepts and stores meta parameter.\"\"\"\n\n        def get_user(user_id: str) -> str:  # pragma: no cover\n            return f\"User {user_id}\"\n\n        metadata = {\"requires_auth\": True, \"rate_limit\": 100}\n\n        template = ResourceTemplate.from_function(\n            fn=get_user,\n            uri_template=\"resource://users/{user_id}\",\n            meta=metadata,\n        )\n\n        assert template.meta is not None\n        assert template.meta == metadata\n        assert template.meta[\"requires_auth\"] is True\n        assert template.meta[\"rate_limit\"] == 100\n\n    @pytest.mark.anyio\n    async def test_template_created_resources_inherit_metadata(self):\n        \"\"\"Test that resources created from templates inherit meta from template.\"\"\"\n\n        def get_item(item_id: str) -> str:\n            return f\"Item {item_id}\"\n\n        metadata = {\"category\": \"inventory\", \"cacheable\": True}\n\n        template = ResourceTemplate.from_function(\n            fn=get_item,\n            uri_template=\"resource://items/{item_id}\",\n            meta=metadata,\n        )\n\n        # Create a resource from the template\n        resource = await template.create_resource(\"resource://items/123\", {\"item_id\": \"123\"}, Context())\n\n        # The resource should inherit the template's metadata\n        assert resource.meta is not None\n        assert resource.meta == metadata\n        assert resource.meta[\"category\"] == \"inventory\"\n        assert resource.meta[\"cacheable\"] is True\n"
  },
  {
    "path": "tests/server/mcpserver/resources/test_resources.py",
    "content": "import pytest\n\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.server.mcpserver.resources import FunctionResource, Resource\nfrom mcp.types import Annotations\n\n\nclass TestResourceValidation:\n    \"\"\"Test base Resource validation.\"\"\"\n\n    def test_resource_uri_accepts_any_string(self):\n        \"\"\"Test that URI field accepts any string per MCP spec.\"\"\"\n\n        def dummy_func() -> str:  # pragma: no cover\n            return \"data\"\n\n        # Valid URI\n        resource = FunctionResource(\n            uri=\"http://example.com/data\",\n            name=\"test\",\n            fn=dummy_func,\n        )\n        assert resource.uri == \"http://example.com/data\"\n\n        # Relative path - now accepted per MCP spec\n        resource = FunctionResource(\n            uri=\"users/me\",\n            name=\"test\",\n            fn=dummy_func,\n        )\n        assert resource.uri == \"users/me\"\n\n        # Custom scheme\n        resource = FunctionResource(\n            uri=\"custom://resource\",\n            name=\"test\",\n            fn=dummy_func,\n        )\n        assert resource.uri == \"custom://resource\"\n\n    def test_resource_name_from_uri(self):\n        \"\"\"Test name is extracted from URI if not provided.\"\"\"\n\n        def dummy_func() -> str:  # pragma: no cover\n            return \"data\"\n\n        resource = FunctionResource(\n            uri=\"resource://my-resource\",\n            fn=dummy_func,\n        )\n        assert resource.name == \"resource://my-resource\"\n\n    def test_resource_name_validation(self):\n        \"\"\"Test name validation.\"\"\"\n\n        def dummy_func() -> str:  # pragma: no cover\n            return \"data\"\n\n        # Must provide either name or URI\n        with pytest.raises(ValueError, match=\"Either name or uri must be provided\"):\n            FunctionResource(\n                fn=dummy_func,\n            )\n\n        # Explicit name takes precedence over URI\n        resource = FunctionResource(\n            uri=\"resource://uri-name\",\n            name=\"explicit-name\",\n            fn=dummy_func,\n        )\n        assert resource.name == \"explicit-name\"\n\n    def test_resource_mime_type(self):\n        \"\"\"Test mime type handling.\"\"\"\n\n        def dummy_func() -> str:  # pragma: no cover\n            return \"data\"\n\n        # Default mime type\n        resource = FunctionResource(\n            uri=\"resource://test\",\n            fn=dummy_func,\n        )\n        assert resource.mime_type == \"text/plain\"\n\n        # Custom mime type\n        resource = FunctionResource(\n            uri=\"resource://test\",\n            fn=dummy_func,\n            mime_type=\"application/json\",\n        )\n        assert resource.mime_type == \"application/json\"\n\n        # RFC 2045 quoted parameter value (gh-1756)\n        resource = FunctionResource(\n            uri=\"resource://test\",\n            fn=dummy_func,\n            mime_type='text/plain; charset=\"utf-8\"',\n        )\n        assert resource.mime_type == 'text/plain; charset=\"utf-8\"'\n\n    @pytest.mark.anyio\n    async def test_resource_read_abstract(self):\n        \"\"\"Test that Resource.read() is abstract.\"\"\"\n\n        class ConcreteResource(Resource):\n            pass\n\n        with pytest.raises(TypeError, match=\"abstract method\"):\n            ConcreteResource(uri=\"test://test\", name=\"test\")  # type: ignore\n\n\nclass TestResourceAnnotations:\n    \"\"\"Test annotations on resources.\"\"\"\n\n    def test_resource_with_annotations(self):\n        \"\"\"Test creating a resource with annotations.\"\"\"\n\n        def get_data() -> str:  # pragma: no cover\n            return \"data\"\n\n        annotations = Annotations(audience=[\"user\"], priority=0.8)\n\n        resource = FunctionResource.from_function(fn=get_data, uri=\"resource://test\", annotations=annotations)\n\n        assert resource.annotations is not None\n        assert resource.annotations.audience == [\"user\"]\n        assert resource.annotations.priority == 0.8\n\n    def test_resource_without_annotations(self):\n        \"\"\"Test that annotations are optional.\"\"\"\n\n        def get_data() -> str:  # pragma: no cover\n            return \"data\"\n\n        resource = FunctionResource.from_function(fn=get_data, uri=\"resource://test\")\n\n        assert resource.annotations is None\n\n    @pytest.mark.anyio\n    async def test_resource_annotations_in_mcpserver(self):\n        \"\"\"Test resource annotations via MCPServer decorator.\"\"\"\n\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://annotated\", annotations=Annotations(audience=[\"assistant\"], priority=0.5))\n        def get_annotated() -> str:  # pragma: no cover\n            \"\"\"An annotated resource.\"\"\"\n            return \"annotated data\"\n\n        resources = await mcp.list_resources()\n        assert len(resources) == 1\n        assert resources[0].annotations is not None\n        assert resources[0].annotations.audience == [\"assistant\"]\n        assert resources[0].annotations.priority == 0.5\n\n    @pytest.mark.anyio\n    async def test_resource_annotations_with_both_audiences(self):\n        \"\"\"Test resource with both user and assistant audience.\"\"\"\n\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://both\", annotations=Annotations(audience=[\"user\", \"assistant\"], priority=1.0))\n        def get_both() -> str:  # pragma: no cover\n            return \"for everyone\"\n\n        resources = await mcp.list_resources()\n        assert resources[0].annotations is not None\n        assert resources[0].annotations.audience == [\"user\", \"assistant\"]\n        assert resources[0].annotations.priority == 1.0\n\n\nclass TestAnnotationsValidation:\n    \"\"\"Test validation of annotation values.\"\"\"\n\n    def test_priority_validation(self):\n        \"\"\"Test that priority is validated to be between 0.0 and 1.0.\"\"\"\n\n        # Valid priorities\n        Annotations(priority=0.0)\n        Annotations(priority=0.5)\n        Annotations(priority=1.0)\n\n        # Invalid priorities should raise validation error\n        with pytest.raises(Exception):  # Pydantic validation error\n            Annotations(priority=-0.1)\n\n        with pytest.raises(Exception):\n            Annotations(priority=1.1)\n\n    def test_audience_validation(self):\n        \"\"\"Test that audience only accepts valid roles.\"\"\"\n\n        # Valid audiences\n        Annotations(audience=[\"user\"])\n        Annotations(audience=[\"assistant\"])\n        Annotations(audience=[\"user\", \"assistant\"])\n        Annotations(audience=[])\n\n        # Invalid roles should raise validation error\n        with pytest.raises(Exception):  # Pydantic validation error\n            Annotations(audience=[\"invalid_role\"])  # type: ignore\n\n\nclass TestResourceMetadata:\n    \"\"\"Test metadata field on base Resource class.\"\"\"\n\n    def test_resource_with_metadata(self):\n        \"\"\"Test that Resource base class accepts meta parameter.\"\"\"\n\n        def dummy_func() -> str:  # pragma: no cover\n            return \"data\"\n\n        metadata = {\"version\": \"1.0\", \"category\": \"test\"}\n\n        resource = FunctionResource(\n            uri=\"resource://test\",\n            name=\"test\",\n            fn=dummy_func,\n            meta=metadata,\n        )\n\n        assert resource.meta is not None\n        assert resource.meta == metadata\n        assert resource.meta[\"version\"] == \"1.0\"\n        assert resource.meta[\"category\"] == \"test\"\n\n    def test_resource_without_metadata(self):\n        \"\"\"Test that meta field defaults to None.\"\"\"\n\n        def dummy_func() -> str:  # pragma: no cover\n            return \"data\"\n\n        resource = FunctionResource(\n            uri=\"resource://test\",\n            name=\"test\",\n            fn=dummy_func,\n        )\n\n        assert resource.meta is None\n"
  },
  {
    "path": "tests/server/mcpserver/servers/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/mcpserver/servers/test_file_server.py",
    "content": "import json\nfrom pathlib import Path\n\nimport pytest\n\nfrom mcp.server.mcpserver import MCPServer\n\n\n@pytest.fixture()\ndef test_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:\n    \"\"\"Create a temporary directory with test files.\"\"\"\n    tmp = tmp_path_factory.mktemp(\"test_files\")\n\n    # Create test files\n    (tmp / \"example.py\").write_text(\"print('hello world')\")\n    (tmp / \"readme.md\").write_text(\"# Test Directory\\nThis is a test.\")\n    (tmp / \"config.json\").write_text('{\"test\": true}')\n\n    return tmp\n\n\n@pytest.fixture\ndef mcp() -> MCPServer:\n    mcp = MCPServer()\n\n    return mcp\n\n\n@pytest.fixture(autouse=True)\ndef resources(mcp: MCPServer, test_dir: Path) -> MCPServer:\n    @mcp.resource(\"dir://test_dir\")\n    def list_test_dir() -> list[str]:\n        \"\"\"List the files in the test directory\"\"\"\n        return [str(f) for f in test_dir.iterdir()]\n\n    @mcp.resource(\"file://test_dir/example.py\")\n    def read_example_py() -> str:\n        \"\"\"Read the example.py file\"\"\"\n        try:\n            return (test_dir / \"example.py\").read_text()\n        except FileNotFoundError:\n            return \"File not found\"\n\n    @mcp.resource(\"file://test_dir/readme.md\")\n    def read_readme_md() -> str:\n        \"\"\"Read the readme.md file\"\"\"\n        try:  # pragma: no cover\n            return (test_dir / \"readme.md\").read_text()\n        except FileNotFoundError:  # pragma: no cover\n            return \"File not found\"\n\n    @mcp.resource(\"file://test_dir/config.json\")\n    def read_config_json() -> str:\n        \"\"\"Read the config.json file\"\"\"\n        try:  # pragma: no cover\n            return (test_dir / \"config.json\").read_text()\n        except FileNotFoundError:  # pragma: no cover\n            return \"File not found\"\n\n    return mcp\n\n\n@pytest.fixture(autouse=True)\ndef tools(mcp: MCPServer, test_dir: Path) -> MCPServer:\n    @mcp.tool()\n    def delete_file(path: str) -> bool:\n        # ensure path is in test_dir\n        if Path(path).resolve().parent != test_dir:  # pragma: no cover\n            raise ValueError(f\"Path must be in test_dir: {path}\")\n        Path(path).unlink()\n        return True\n\n    return mcp\n\n\n@pytest.mark.anyio\nasync def test_list_resources(mcp: MCPServer):\n    resources = await mcp.list_resources()\n    assert len(resources) == 4\n\n    assert [str(r.uri) for r in resources] == [\n        \"dir://test_dir\",\n        \"file://test_dir/example.py\",\n        \"file://test_dir/readme.md\",\n        \"file://test_dir/config.json\",\n    ]\n\n\n@pytest.mark.anyio\nasync def test_read_resource_dir(mcp: MCPServer):\n    res_iter = await mcp.read_resource(\"dir://test_dir\")\n    res_list = list(res_iter)\n    assert len(res_list) == 1\n    res = res_list[0]\n    assert res.mime_type == \"text/plain\"\n\n    files = json.loads(res.content)\n\n    assert sorted([Path(f).name for f in files]) == [\n        \"config.json\",\n        \"example.py\",\n        \"readme.md\",\n    ]\n\n\n@pytest.mark.anyio\nasync def test_read_resource_file(mcp: MCPServer):\n    res_iter = await mcp.read_resource(\"file://test_dir/example.py\")\n    res_list = list(res_iter)\n    assert len(res_list) == 1\n    res = res_list[0]\n    assert res.content == \"print('hello world')\"\n\n\n@pytest.mark.anyio\nasync def test_delete_file(mcp: MCPServer, test_dir: Path):\n    await mcp.call_tool(\"delete_file\", arguments={\"path\": str(test_dir / \"example.py\")})\n    assert not (test_dir / \"example.py\").exists()\n\n\n@pytest.mark.anyio\nasync def test_delete_file_and_check_resources(mcp: MCPServer, test_dir: Path):\n    await mcp.call_tool(\"delete_file\", arguments={\"path\": str(test_dir / \"example.py\")})\n    res_iter = await mcp.read_resource(\"file://test_dir/example.py\")\n    res_list = list(res_iter)\n    assert len(res_list) == 1\n    res = res_list[0]\n    assert res.content == \"File not found\"\n"
  },
  {
    "path": "tests/server/mcpserver/test_elicitation.py",
    "content": "\"\"\"Test the elicitation feature using stdio transport.\"\"\"\n\nfrom typing import Any\n\nimport pytest\nfrom pydantic import BaseModel, Field\n\nfrom mcp import Client, types\nfrom mcp.client.session import ClientSession, ElicitationFnT\nfrom mcp.server.mcpserver import Context, MCPServer\nfrom mcp.shared._context import RequestContext\nfrom mcp.types import ElicitRequestParams, ElicitResult, TextContent\n\n\n# Shared schema for basic tests\nclass AnswerSchema(BaseModel):\n    answer: str = Field(description=\"The user's answer to the question\")\n\n\ndef create_ask_user_tool(mcp: MCPServer):\n    \"\"\"Create a standard ask_user tool that handles all elicitation responses.\"\"\"\n\n    @mcp.tool(description=\"A tool that uses elicitation\")\n    async def ask_user(prompt: str, ctx: Context) -> str:\n        result = await ctx.elicit(message=f\"Tool wants to ask: {prompt}\", schema=AnswerSchema)\n\n        if result.action == \"accept\" and result.data:\n            return f\"User answered: {result.data.answer}\"\n        elif result.action == \"decline\":\n            return \"User declined to answer\"\n        else:  # pragma: no cover\n            return \"User cancelled\"\n\n    return ask_user\n\n\nasync def call_tool_and_assert(\n    mcp: MCPServer,\n    elicitation_callback: ElicitationFnT,\n    tool_name: str,\n    args: dict[str, Any],\n    expected_text: str | None = None,\n    text_contains: list[str] | None = None,\n):\n    \"\"\"Helper to create session, call tool, and assert result.\"\"\"\n    async with Client(mcp, elicitation_callback=elicitation_callback) as client:\n        result = await client.call_tool(tool_name, args)\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n\n        if expected_text is not None:\n            assert result.content[0].text == expected_text\n        elif text_contains is not None:  # pragma: no branch\n            for substring in text_contains:\n                assert substring in result.content[0].text\n\n        return result\n\n\n@pytest.mark.anyio\nasync def test_stdio_elicitation():\n    \"\"\"Test the elicitation feature using stdio transport.\"\"\"\n    mcp = MCPServer(name=\"StdioElicitationServer\")\n    create_ask_user_tool(mcp)\n\n    # Create a custom handler for elicitation requests\n    async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        if params.message == \"Tool wants to ask: What is your name?\":\n            return ElicitResult(action=\"accept\", content={\"answer\": \"Test User\"})\n        else:  # pragma: no cover\n            raise ValueError(f\"Unexpected elicitation message: {params.message}\")\n\n    await call_tool_and_assert(\n        mcp, elicitation_callback, \"ask_user\", {\"prompt\": \"What is your name?\"}, \"User answered: Test User\"\n    )\n\n\n@pytest.mark.anyio\nasync def test_stdio_elicitation_decline():\n    \"\"\"Test elicitation with user declining.\"\"\"\n    mcp = MCPServer(name=\"StdioElicitationDeclineServer\")\n    create_ask_user_tool(mcp)\n\n    async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        return ElicitResult(action=\"decline\")\n\n    await call_tool_and_assert(\n        mcp, elicitation_callback, \"ask_user\", {\"prompt\": \"What is your name?\"}, \"User declined to answer\"\n    )\n\n\n@pytest.mark.anyio\nasync def test_elicitation_schema_validation():\n    \"\"\"Test that elicitation schemas must only contain primitive types.\"\"\"\n    mcp = MCPServer(name=\"ValidationTestServer\")\n\n    def create_validation_tool(name: str, schema_class: type[BaseModel]):\n        @mcp.tool(name=name, description=f\"Tool testing {name}\")\n        async def tool(ctx: Context) -> str:\n            try:\n                await ctx.elicit(message=\"This should fail validation\", schema=schema_class)\n                return \"Should not reach here\"  # pragma: no cover\n            except TypeError as e:\n                return f\"Validation failed as expected: {str(e)}\"\n\n        return tool\n\n    # Test cases for invalid schemas\n    class InvalidListSchema(BaseModel):\n        numbers: list[int] = Field(description=\"List of numbers\")\n\n    class NestedModel(BaseModel):\n        value: str\n\n    class InvalidNestedSchema(BaseModel):\n        nested: NestedModel = Field(description=\"Nested model\")\n\n    create_validation_tool(\"invalid_list\", InvalidListSchema)\n    create_validation_tool(\"nested_model\", InvalidNestedSchema)\n\n    # Dummy callback (won't be called due to validation failure)\n    async def elicitation_callback(\n        context: RequestContext[ClientSession], params: ElicitRequestParams\n    ):  # pragma: no cover\n        return ElicitResult(action=\"accept\", content={})\n\n    async with Client(mcp, elicitation_callback=elicitation_callback) as client:\n        # Test both invalid schemas\n        for tool_name, field_name in [(\"invalid_list\", \"numbers\"), (\"nested_model\", \"nested\")]:\n            result = await client.call_tool(tool_name, {})\n            assert len(result.content) == 1\n            assert isinstance(result.content[0], TextContent)\n            assert \"Validation failed as expected\" in result.content[0].text\n            assert field_name in result.content[0].text\n\n\n@pytest.mark.anyio\nasync def test_elicitation_with_optional_fields():\n    \"\"\"Test that Optional fields work correctly in elicitation schemas.\"\"\"\n    mcp = MCPServer(name=\"OptionalFieldServer\")\n\n    class OptionalSchema(BaseModel):\n        required_name: str = Field(description=\"Your name (required)\")\n        optional_age: int | None = Field(default=None, description=\"Your age (optional)\")\n        optional_email: str | None = Field(default=None, description=\"Your email (optional)\")\n        subscribe: bool | None = Field(default=False, description=\"Subscribe to newsletter?\")\n\n    @mcp.tool(description=\"Tool with optional fields\")\n    async def optional_tool(ctx: Context) -> str:\n        result = await ctx.elicit(message=\"Please provide your information\", schema=OptionalSchema)\n\n        if result.action == \"accept\" and result.data:\n            info = [f\"Name: {result.data.required_name}\"]\n            if result.data.optional_age is not None:\n                info.append(f\"Age: {result.data.optional_age}\")\n            if result.data.optional_email is not None:\n                info.append(f\"Email: {result.data.optional_email}\")\n            info.append(f\"Subscribe: {result.data.subscribe}\")\n            return \", \".join(info)\n        else:  # pragma: no cover\n            return f\"User {result.action}\"\n\n    # Test cases with different field combinations\n    test_cases: list[tuple[dict[str, Any], str]] = [\n        (\n            # All fields provided\n            {\"required_name\": \"John Doe\", \"optional_age\": 30, \"optional_email\": \"john@example.com\", \"subscribe\": True},\n            \"Name: John Doe, Age: 30, Email: john@example.com, Subscribe: True\",\n        ),\n        (\n            # Only required fields\n            {\"required_name\": \"Jane Smith\"},\n            \"Name: Jane Smith, Subscribe: False\",\n        ),\n    ]\n\n    for content, expected in test_cases:\n\n        async def callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n            return ElicitResult(action=\"accept\", content=content)\n\n        await call_tool_and_assert(mcp, callback, \"optional_tool\", {}, expected)\n\n    # Test invalid optional field\n    class InvalidOptionalSchema(BaseModel):\n        name: str = Field(description=\"Name\")\n        optional_list: list[int] | None = Field(default=None, description=\"Invalid optional list\")\n\n    @mcp.tool(description=\"Tool with invalid optional field\")\n    async def invalid_optional_tool(ctx: Context) -> str:\n        try:\n            await ctx.elicit(message=\"This should fail\", schema=InvalidOptionalSchema)\n            return \"Should not reach here\"  # pragma: no cover\n        except TypeError as e:\n            return f\"Validation failed: {str(e)}\"\n\n    async def elicitation_callback(\n        context: RequestContext[ClientSession], params: ElicitRequestParams\n    ):  # pragma: no cover\n        return ElicitResult(action=\"accept\", content={})\n\n    await call_tool_and_assert(\n        mcp,\n        elicitation_callback,\n        \"invalid_optional_tool\",\n        {},\n        text_contains=[\"Validation failed:\", \"optional_list\"],\n    )\n\n    # Test valid list[str] for multi-select enum\n    class ValidMultiSelectSchema(BaseModel):\n        name: str = Field(description=\"Name\")\n        tags: list[str] = Field(description=\"Tags\")\n\n    @mcp.tool(description=\"Tool with valid list[str] field\")\n    async def valid_multiselect_tool(ctx: Context) -> str:\n        result = await ctx.elicit(message=\"Please provide tags\", schema=ValidMultiSelectSchema)\n        if result.action == \"accept\" and result.data:\n            return f\"Name: {result.data.name}, Tags: {', '.join(result.data.tags)}\"\n        return f\"User {result.action}\"  # pragma: no cover\n\n    async def multiselect_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        if \"Please provide tags\" in params.message:\n            return ElicitResult(action=\"accept\", content={\"name\": \"Test\", \"tags\": [\"tag1\", \"tag2\"]})\n        return ElicitResult(action=\"decline\")  # pragma: no cover\n\n    await call_tool_and_assert(mcp, multiselect_callback, \"valid_multiselect_tool\", {}, \"Name: Test, Tags: tag1, tag2\")\n\n    # Test Optional[list[str]] for optional multi-select enum\n    class OptionalMultiSelectSchema(BaseModel):\n        name: str = Field(description=\"Name\")\n        tags: list[str] | None = Field(default=None, description=\"Optional tags\")\n\n    @mcp.tool(description=\"Tool with optional list[str] field\")\n    async def optional_multiselect_tool(ctx: Context) -> str:\n        result = await ctx.elicit(message=\"Please provide optional tags\", schema=OptionalMultiSelectSchema)\n        if result.action == \"accept\" and result.data:\n            tags_str = \", \".join(result.data.tags) if result.data.tags else \"none\"\n            return f\"Name: {result.data.name}, Tags: {tags_str}\"\n        return f\"User {result.action}\"  # pragma: no cover\n\n    async def optional_multiselect_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        if \"Please provide optional tags\" in params.message:\n            return ElicitResult(action=\"accept\", content={\"name\": \"Test\", \"tags\": [\"tag1\", \"tag2\"]})\n        return ElicitResult(action=\"decline\")  # pragma: no cover\n\n    await call_tool_and_assert(\n        mcp, optional_multiselect_callback, \"optional_multiselect_tool\", {}, \"Name: Test, Tags: tag1, tag2\"\n    )\n\n\n@pytest.mark.anyio\nasync def test_elicitation_with_default_values():\n    \"\"\"Test that default values work correctly in elicitation schemas and are included in JSON.\"\"\"\n    mcp = MCPServer(name=\"DefaultValuesServer\")\n\n    class DefaultsSchema(BaseModel):\n        name: str = Field(default=\"Guest\", description=\"User name\")\n        age: int = Field(default=18, description=\"User age\")\n        subscribe: bool = Field(default=True, description=\"Subscribe to newsletter\")\n        email: str = Field(description=\"Email address (required)\")\n\n    @mcp.tool(description=\"Tool with default values\")\n    async def defaults_tool(ctx: Context) -> str:\n        result = await ctx.elicit(message=\"Please provide your information\", schema=DefaultsSchema)\n\n        if result.action == \"accept\" and result.data:\n            return (\n                f\"Name: {result.data.name}, Age: {result.data.age}, \"\n                f\"Subscribe: {result.data.subscribe}, Email: {result.data.email}\"\n            )\n        else:  # pragma: no cover\n            return f\"User {result.action}\"\n\n    # First verify that defaults are present in the JSON schema sent to clients\n    async def callback_schema_verify(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        # Verify the schema includes defaults\n        assert isinstance(params, types.ElicitRequestFormParams), \"Expected form mode elicitation\"\n        schema = params.requested_schema\n        props = schema[\"properties\"]\n\n        assert props[\"name\"][\"default\"] == \"Guest\"\n        assert props[\"age\"][\"default\"] == 18\n        assert props[\"subscribe\"][\"default\"] is True\n        assert \"default\" not in props[\"email\"]  # Required field has no default\n\n        return ElicitResult(action=\"accept\", content={\"email\": \"test@example.com\"})\n\n    await call_tool_and_assert(\n        mcp,\n        callback_schema_verify,\n        \"defaults_tool\",\n        {},\n        \"Name: Guest, Age: 18, Subscribe: True, Email: test@example.com\",\n    )\n\n    # Test overriding defaults\n    async def callback_override(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        return ElicitResult(\n            action=\"accept\", content={\"email\": \"john@example.com\", \"name\": \"John\", \"age\": 25, \"subscribe\": False}\n        )\n\n    await call_tool_and_assert(\n        mcp, callback_override, \"defaults_tool\", {}, \"Name: John, Age: 25, Subscribe: False, Email: john@example.com\"\n    )\n\n\n@pytest.mark.anyio\nasync def test_elicitation_with_enum_titles():\n    \"\"\"Test elicitation with enum schemas using oneOf/anyOf for titles.\"\"\"\n    mcp = MCPServer(name=\"ColorPreferencesApp\")\n\n    # Test single-select with titles using oneOf\n    class FavoriteColorSchema(BaseModel):\n        user_name: str = Field(description=\"Your name\")\n        favorite_color: str = Field(\n            description=\"Select your favorite color\",\n            json_schema_extra={\n                \"oneOf\": [\n                    {\"const\": \"red\", \"title\": \"Red\"},\n                    {\"const\": \"green\", \"title\": \"Green\"},\n                    {\"const\": \"blue\", \"title\": \"Blue\"},\n                    {\"const\": \"yellow\", \"title\": \"Yellow\"},\n                ]\n            },\n        )\n\n    @mcp.tool(description=\"Single color selection\")\n    async def select_favorite_color(ctx: Context) -> str:\n        result = await ctx.elicit(message=\"Select your favorite color\", schema=FavoriteColorSchema)\n        if result.action == \"accept\" and result.data:\n            return f\"User: {result.data.user_name}, Favorite: {result.data.favorite_color}\"\n        return f\"User {result.action}\"  # pragma: no cover\n\n    # Test multi-select with titles using anyOf\n    class FavoriteColorsSchema(BaseModel):\n        user_name: str = Field(description=\"Your name\")\n        favorite_colors: list[str] = Field(\n            description=\"Select your favorite colors\",\n            json_schema_extra={\n                \"items\": {\n                    \"anyOf\": [\n                        {\"const\": \"red\", \"title\": \"Red\"},\n                        {\"const\": \"green\", \"title\": \"Green\"},\n                        {\"const\": \"blue\", \"title\": \"Blue\"},\n                        {\"const\": \"yellow\", \"title\": \"Yellow\"},\n                    ]\n                }\n            },\n        )\n\n    @mcp.tool(description=\"Multiple color selection\")\n    async def select_favorite_colors(ctx: Context) -> str:\n        result = await ctx.elicit(message=\"Select your favorite colors\", schema=FavoriteColorsSchema)\n        if result.action == \"accept\" and result.data:\n            return f\"User: {result.data.user_name}, Colors: {', '.join(result.data.favorite_colors)}\"\n        return f\"User {result.action}\"  # pragma: no cover\n\n    # Test legacy enumNames format\n    class LegacyColorSchema(BaseModel):\n        user_name: str = Field(description=\"Your name\")\n        color: str = Field(\n            description=\"Select a color\",\n            json_schema_extra={\"enum\": [\"red\", \"green\", \"blue\"], \"enumNames\": [\"Red\", \"Green\", \"Blue\"]},\n        )\n\n    @mcp.tool(description=\"Legacy enum format\")\n    async def select_color_legacy(ctx: Context) -> str:\n        result = await ctx.elicit(message=\"Select a color (legacy format)\", schema=LegacyColorSchema)\n        if result.action == \"accept\" and result.data:\n            return f\"User: {result.data.user_name}, Color: {result.data.color}\"\n        return f\"User {result.action}\"  # pragma: no cover\n\n    async def enum_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        if \"colors\" in params.message and \"legacy\" not in params.message:\n            return ElicitResult(action=\"accept\", content={\"user_name\": \"Bob\", \"favorite_colors\": [\"red\", \"green\"]})\n        elif \"color\" in params.message:\n            if \"legacy\" in params.message:\n                return ElicitResult(action=\"accept\", content={\"user_name\": \"Charlie\", \"color\": \"green\"})\n            else:\n                return ElicitResult(action=\"accept\", content={\"user_name\": \"Alice\", \"favorite_color\": \"blue\"})\n        return ElicitResult(action=\"decline\")  # pragma: no cover\n\n    # Test single-select with titles\n    await call_tool_and_assert(mcp, enum_callback, \"select_favorite_color\", {}, \"User: Alice, Favorite: blue\")\n\n    # Test multi-select with titles\n    await call_tool_and_assert(mcp, enum_callback, \"select_favorite_colors\", {}, \"User: Bob, Colors: red, green\")\n\n    # Test legacy enumNames format\n    await call_tool_and_assert(mcp, enum_callback, \"select_color_legacy\", {}, \"User: Charlie, Color: green\")\n"
  },
  {
    "path": "tests/server/mcpserver/test_func_metadata.py",
    "content": "# NOTE: Those were added because we actually want to test wrong type annotations.\n# pyright: reportUnknownParameterType=false\n# pyright: reportMissingParameterType=false\n# pyright: reportUnknownArgumentType=false\n# pyright: reportUnknownLambdaType=false\nfrom collections.abc import Callable\nfrom dataclasses import dataclass\nfrom typing import Annotated, Any, Final, NamedTuple, TypedDict\n\nimport annotated_types\nimport pytest\nfrom dirty_equals import IsPartialDict\nfrom pydantic import BaseModel, Field\n\nfrom mcp.server.mcpserver.exceptions import InvalidSignature\nfrom mcp.server.mcpserver.utilities.func_metadata import func_metadata\nfrom mcp.types import CallToolResult\n\n\nclass SomeInputModelA(BaseModel):\n    pass\n\n\nclass SomeInputModelB(BaseModel):\n    class InnerModel(BaseModel):\n        x: int\n\n    how_many_shrimp: Annotated[int, Field(description=\"How many shrimp in the tank???\")]\n    ok: InnerModel\n    y: None\n\n\ndef complex_arguments_fn(\n    an_int: int,\n    must_be_none: None,\n    must_be_none_dumb_annotation: Annotated[None, \"blah\"],\n    list_of_ints: list[int],\n    # list[str] | str is an interesting case because if it comes in as JSON like\n    # \"[\\\"a\\\", \\\"b\\\"]\" then it will be naively parsed as a string.\n    list_str_or_str: list[str] | str,\n    an_int_annotated_with_field: Annotated[int, Field(description=\"An int with a field\")],\n    an_int_annotated_with_field_and_others: Annotated[\n        int,\n        str,  # Should be ignored, really\n        Field(description=\"An int with a field\"),\n        annotated_types.Gt(1),\n    ],\n    an_int_annotated_with_junk: Annotated[\n        int,\n        \"123\",\n        456,\n    ],\n    field_with_default_via_field_annotation_before_nondefault_arg: Annotated[int, Field(1)],\n    unannotated,\n    my_model_a: SomeInputModelA,\n    my_model_a_forward_ref: \"SomeInputModelA\",\n    my_model_b: SomeInputModelB,\n    an_int_annotated_with_field_default: Annotated[\n        int,\n        Field(1, description=\"An int with a field\"),\n    ],\n    unannotated_with_default=5,\n    my_model_a_with_default: SomeInputModelA = SomeInputModelA(),  # noqa: B008\n    an_int_with_default: int = 1,\n    must_be_none_with_default: None = None,\n    an_int_with_equals_field: int = Field(1, ge=0),\n    int_annotated_with_default: Annotated[int, Field(description=\"hey\")] = 5,\n) -> str:\n    _: Any = (\n        an_int,\n        must_be_none,\n        must_be_none_dumb_annotation,\n        list_of_ints,\n        list_str_or_str,\n        an_int_annotated_with_field,\n        an_int_annotated_with_field_and_others,\n        an_int_annotated_with_junk,\n        field_with_default_via_field_annotation_before_nondefault_arg,\n        unannotated,\n        an_int_annotated_with_field_default,\n        unannotated_with_default,\n        my_model_a,\n        my_model_a_forward_ref,\n        my_model_b,\n        my_model_a_with_default,\n        an_int_with_default,\n        must_be_none_with_default,\n        an_int_with_equals_field,\n        int_annotated_with_default,\n    )\n    return \"ok!\"\n\n\n@pytest.mark.anyio\nasync def test_complex_function_runtime_arg_validation_non_json():\n    \"\"\"Test that basic non-JSON arguments are validated correctly\"\"\"\n    meta = func_metadata(complex_arguments_fn)\n\n    # Test with minimum required arguments\n    result = await meta.call_fn_with_arg_validation(\n        complex_arguments_fn,\n        fn_is_async=False,\n        arguments_to_validate={\n            \"an_int\": 1,\n            \"must_be_none\": None,\n            \"must_be_none_dumb_annotation\": None,\n            \"list_of_ints\": [1, 2, 3],\n            \"list_str_or_str\": \"hello\",\n            \"an_int_annotated_with_field\": 42,\n            \"an_int_annotated_with_field_and_others\": 5,\n            \"an_int_annotated_with_junk\": 100,\n            \"unannotated\": \"test\",\n            \"my_model_a\": {},\n            \"my_model_a_forward_ref\": {},\n            \"my_model_b\": {\"how_many_shrimp\": 5, \"ok\": {\"x\": 1}, \"y\": None},\n        },\n        arguments_to_pass_directly=None,\n    )\n    assert result == \"ok!\"\n\n    # Test with invalid types\n    with pytest.raises(ValueError):\n        await meta.call_fn_with_arg_validation(\n            complex_arguments_fn,\n            fn_is_async=False,\n            arguments_to_validate={\"an_int\": \"not an int\"},\n            arguments_to_pass_directly=None,\n        )\n\n\n@pytest.mark.anyio\nasync def test_complex_function_runtime_arg_validation_with_json():\n    \"\"\"Test that JSON string arguments are parsed and validated correctly\"\"\"\n    meta = func_metadata(complex_arguments_fn)\n\n    result = await meta.call_fn_with_arg_validation(\n        complex_arguments_fn,\n        fn_is_async=False,\n        arguments_to_validate={\n            \"an_int\": 1,\n            \"must_be_none\": None,\n            \"must_be_none_dumb_annotation\": None,\n            \"list_of_ints\": \"[1, 2, 3]\",  # JSON string\n            \"list_str_or_str\": '[\"a\", \"b\", \"c\"]',  # JSON string\n            \"an_int_annotated_with_field\": 42,\n            \"an_int_annotated_with_field_and_others\": \"5\",  # JSON string\n            \"an_int_annotated_with_junk\": 100,\n            \"unannotated\": \"test\",\n            \"my_model_a\": \"{}\",  # JSON string\n            \"my_model_a_forward_ref\": \"{}\",  # JSON string\n            \"my_model_b\": '{\"how_many_shrimp\": 5, \"ok\": {\"x\": 1}, \"y\": null}',\n        },\n        arguments_to_pass_directly=None,\n    )\n    assert result == \"ok!\"\n\n\ndef test_str_vs_list_str():\n    \"\"\"Test handling of string vs list[str] type annotations.\n\n    This is tricky as '\"hello\"' can be parsed as a JSON string or a Python string.\n    We want to make sure it's kept as a python string.\n    \"\"\"\n\n    def func_with_str_types(str_or_list: str | list[str]):  # pragma: no cover\n        return str_or_list\n\n    meta = func_metadata(func_with_str_types)\n\n    # Test string input for union type\n    result = meta.pre_parse_json({\"str_or_list\": \"hello\"})\n    assert result[\"str_or_list\"] == \"hello\"\n\n    # Test string input that contains valid JSON for union type\n    # We want to see here that the JSON-vali string is NOT parsed as JSON, but rather\n    # kept as a raw string\n    result = meta.pre_parse_json({\"str_or_list\": '\"hello\"'})\n    assert result[\"str_or_list\"] == '\"hello\"'\n\n    # Test list input for union type\n    result = meta.pre_parse_json({\"str_or_list\": '[\"hello\", \"world\"]'})\n    assert result[\"str_or_list\"] == [\"hello\", \"world\"]\n\n\ndef test_skip_names():\n    \"\"\"Test that skipped parameters are not included in the model\"\"\"\n\n    def func_with_many_params(keep_this: int, skip_this: str, also_keep: float, also_skip: bool):  # pragma: no cover\n        return keep_this, skip_this, also_keep, also_skip\n\n    # Skip some parameters\n    meta = func_metadata(func_with_many_params, skip_names=[\"skip_this\", \"also_skip\"])\n\n    # Check model fields\n    assert \"keep_this\" in meta.arg_model.model_fields\n    assert \"also_keep\" in meta.arg_model.model_fields\n    assert \"skip_this\" not in meta.arg_model.model_fields\n    assert \"also_skip\" not in meta.arg_model.model_fields\n\n    # Validate that we can call with only non-skipped parameters\n    model: BaseModel = meta.arg_model.model_validate({\"keep_this\": 1, \"also_keep\": 2.5})  # type: ignore\n    assert model.keep_this == 1  # type: ignore\n    assert model.also_keep == 2.5  # type: ignore\n\n\ndef test_structured_output_dict_str_types():\n    \"\"\"Test that dict[str, T] types are handled without wrapping.\"\"\"\n\n    # Test dict[str, Any]\n    def func_dict_any() -> dict[str, Any]:  # pragma: no cover\n        return {\"a\": 1, \"b\": \"hello\", \"c\": [1, 2, 3]}\n\n    meta = func_metadata(func_dict_any)\n\n    assert meta.output_schema == IsPartialDict(type=\"object\", title=\"func_dict_anyDictOutput\")\n\n    # Test dict[str, str]\n    def func_dict_str() -> dict[str, str]:  # pragma: no cover\n        return {\"name\": \"John\", \"city\": \"NYC\"}\n\n    meta = func_metadata(func_dict_str)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"additionalProperties\": {\"type\": \"string\"},\n        \"title\": \"func_dict_strDictOutput\",\n    }\n\n    # Test dict[str, list[int]]\n    def func_dict_list() -> dict[str, list[int]]:  # pragma: no cover\n        return {\"nums\": [1, 2, 3], \"more\": [4, 5, 6]}\n\n    meta = func_metadata(func_dict_list)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"additionalProperties\": {\"type\": \"array\", \"items\": {\"type\": \"integer\"}},\n        \"title\": \"func_dict_listDictOutput\",\n    }\n\n    # Test dict[int, str] - should be wrapped since key is not str\n    def func_dict_int_key() -> dict[int, str]:  # pragma: no cover\n        return {1: \"a\", 2: \"b\"}\n\n    meta = func_metadata(func_dict_int_key)\n    assert meta.output_schema is not None\n    assert \"result\" in meta.output_schema[\"properties\"]\n\n\n@pytest.mark.anyio\nasync def test_lambda_function():\n    \"\"\"Test lambda function schema and validation\"\"\"\n    fn: Callable[[str, int], str] = lambda x, y=5: x  # noqa: E731\n    meta = func_metadata(lambda x, y=5: x)\n\n    # Test schema\n    assert meta.arg_model.model_json_schema() == {\n        \"properties\": {\n            \"x\": {\"title\": \"x\", \"type\": \"string\"},\n            \"y\": {\"default\": 5, \"title\": \"y\", \"type\": \"string\"},\n        },\n        \"required\": [\"x\"],\n        \"title\": \"<lambda>Arguments\",\n        \"type\": \"object\",\n    }\n\n    async def check_call(args):\n        return await meta.call_fn_with_arg_validation(\n            fn,\n            fn_is_async=False,\n            arguments_to_validate=args,\n            arguments_to_pass_directly=None,\n        )\n\n    # Basic calls\n    assert await check_call({\"x\": \"hello\"}) == \"hello\"\n    assert await check_call({\"x\": \"hello\", \"y\": \"world\"}) == \"hello\"\n    assert await check_call({\"x\": '\"hello\"'}) == '\"hello\"'\n\n    # Missing required arg\n    with pytest.raises(ValueError):\n        await check_call({\"y\": \"world\"})\n\n\ndef test_complex_function_json_schema():\n    \"\"\"Test JSON schema generation for complex function arguments.\n\n    Note: Different versions of pydantic output slightly different\n    JSON Schema formats for model fields with defaults. The format changed in 2.9.0:\n\n    1. Before 2.9.0:\n       {\n         \"allOf\": [{\"$ref\": \"#/$defs/Model\"}],\n         \"default\": {}\n       }\n\n    2. Since 2.9.0:\n       {\n         \"$ref\": \"#/$defs/Model\",\n         \"default\": {}\n       }\n\n    Both formats are valid and functionally equivalent. This test accepts either format\n    to ensure compatibility across our supported pydantic versions.\n\n    This change in format does not affect runtime behavior since:\n    1. Both schemas validate the same way\n    2. The actual model classes and validation logic are unchanged\n    3. func_metadata uses model_validate/model_dump, not the schema directly\n    \"\"\"\n    meta = func_metadata(complex_arguments_fn)\n    actual_schema = meta.arg_model.model_json_schema()\n\n    # Create a copy of the actual schema to normalize\n    normalized_schema = actual_schema.copy()\n\n    # Normalize the my_model_a_with_default field to handle both pydantic formats\n    if \"allOf\" in actual_schema[\"properties\"][\"my_model_a_with_default\"]:  # pragma: no cover\n        normalized_schema[\"properties\"][\"my_model_a_with_default\"] = {  # pragma: no cover\n            \"$ref\": \"#/$defs/SomeInputModelA\",\n            \"default\": {},\n        }\n\n    assert normalized_schema == {\n        \"$defs\": {\n            \"InnerModel\": {\n                \"properties\": {\"x\": {\"title\": \"X\", \"type\": \"integer\"}},\n                \"required\": [\"x\"],\n                \"title\": \"InnerModel\",\n                \"type\": \"object\",\n            },\n            \"SomeInputModelA\": {\n                \"properties\": {},\n                \"title\": \"SomeInputModelA\",\n                \"type\": \"object\",\n            },\n            \"SomeInputModelB\": {\n                \"properties\": {\n                    \"how_many_shrimp\": {\n                        \"description\": \"How many shrimp in the tank???\",\n                        \"title\": \"How Many Shrimp\",\n                        \"type\": \"integer\",\n                    },\n                    \"ok\": {\"$ref\": \"#/$defs/InnerModel\"},\n                    \"y\": {\"title\": \"Y\", \"type\": \"null\"},\n                },\n                \"required\": [\"how_many_shrimp\", \"ok\", \"y\"],\n                \"title\": \"SomeInputModelB\",\n                \"type\": \"object\",\n            },\n        },\n        \"properties\": {\n            \"an_int\": {\"title\": \"An Int\", \"type\": \"integer\"},\n            \"must_be_none\": {\"title\": \"Must Be None\", \"type\": \"null\"},\n            \"must_be_none_dumb_annotation\": {\n                \"title\": \"Must Be None Dumb Annotation\",\n                \"type\": \"null\",\n            },\n            \"list_of_ints\": {\n                \"items\": {\"type\": \"integer\"},\n                \"title\": \"List Of Ints\",\n                \"type\": \"array\",\n            },\n            \"list_str_or_str\": {\n                \"anyOf\": [\n                    {\"items\": {\"type\": \"string\"}, \"type\": \"array\"},\n                    {\"type\": \"string\"},\n                ],\n                \"title\": \"List Str Or Str\",\n            },\n            \"an_int_annotated_with_field\": {\n                \"description\": \"An int with a field\",\n                \"title\": \"An Int Annotated With Field\",\n                \"type\": \"integer\",\n            },\n            \"an_int_annotated_with_field_and_others\": {\n                \"description\": \"An int with a field\",\n                \"exclusiveMinimum\": 1,\n                \"title\": \"An Int Annotated With Field And Others\",\n                \"type\": \"integer\",\n            },\n            \"an_int_annotated_with_junk\": {\n                \"title\": \"An Int Annotated With Junk\",\n                \"type\": \"integer\",\n            },\n            \"field_with_default_via_field_annotation_before_nondefault_arg\": {\n                \"default\": 1,\n                \"title\": \"Field With Default Via Field Annotation Before Nondefault Arg\",\n                \"type\": \"integer\",\n            },\n            \"unannotated\": {\"title\": \"unannotated\", \"type\": \"string\"},\n            \"my_model_a\": {\"$ref\": \"#/$defs/SomeInputModelA\"},\n            \"my_model_a_forward_ref\": {\"$ref\": \"#/$defs/SomeInputModelA\"},\n            \"my_model_b\": {\"$ref\": \"#/$defs/SomeInputModelB\"},\n            \"an_int_annotated_with_field_default\": {\n                \"default\": 1,\n                \"description\": \"An int with a field\",\n                \"title\": \"An Int Annotated With Field Default\",\n                \"type\": \"integer\",\n            },\n            \"unannotated_with_default\": {\n                \"default\": 5,\n                \"title\": \"unannotated_with_default\",\n                \"type\": \"string\",\n            },\n            \"my_model_a_with_default\": {\n                \"$ref\": \"#/$defs/SomeInputModelA\",\n                \"default\": {},\n            },\n            \"an_int_with_default\": {\n                \"default\": 1,\n                \"title\": \"An Int With Default\",\n                \"type\": \"integer\",\n            },\n            \"must_be_none_with_default\": {\n                \"default\": None,\n                \"title\": \"Must Be None With Default\",\n                \"type\": \"null\",\n            },\n            \"an_int_with_equals_field\": {\n                \"default\": 1,\n                \"minimum\": 0,\n                \"title\": \"An Int With Equals Field\",\n                \"type\": \"integer\",\n            },\n            \"int_annotated_with_default\": {\n                \"default\": 5,\n                \"description\": \"hey\",\n                \"title\": \"Int Annotated With Default\",\n                \"type\": \"integer\",\n            },\n        },\n        \"required\": [\n            \"an_int\",\n            \"must_be_none\",\n            \"must_be_none_dumb_annotation\",\n            \"list_of_ints\",\n            \"list_str_or_str\",\n            \"an_int_annotated_with_field\",\n            \"an_int_annotated_with_field_and_others\",\n            \"an_int_annotated_with_junk\",\n            \"unannotated\",\n            \"my_model_a\",\n            \"my_model_a_forward_ref\",\n            \"my_model_b\",\n        ],\n        \"title\": \"complex_arguments_fnArguments\",\n        \"type\": \"object\",\n    }\n\n\ndef test_str_vs_int():\n    \"\"\"Test that string values are kept as strings even when they contain numbers,\n    while numbers are parsed correctly.\n    \"\"\"\n\n    def func_with_str_and_int(a: str, b: int):  # pragma: no cover\n        return a\n\n    meta = func_metadata(func_with_str_and_int)\n    result = meta.pre_parse_json({\"a\": \"123\", \"b\": 123})\n    assert result[\"a\"] == \"123\"\n    assert result[\"b\"] == 123\n\n\ndef test_str_annotation_preserves_json_string():\n    \"\"\"Regression test for PR #1113: Ensure that when a parameter is annotated as str,\n    valid JSON strings are NOT parsed into Python objects.\n\n    This test would fail before the fix (JSON string would be parsed to dict)\n    and passes after the fix (JSON string remains as string).\n    \"\"\"\n\n    def process_json_config(config: str, enabled: bool = True) -> str:  # pragma: no cover\n        \"\"\"Function that expects a JSON string as a string parameter.\"\"\"\n        # In real use, this function might validate or transform the JSON string\n        # before parsing it, or pass it to another service as-is\n        return f\"Processing config: {config}\"\n\n    meta = func_metadata(process_json_config)\n\n    # Test case 1: JSON object as string\n    json_obj_str = '{\"database\": \"postgres\", \"port\": 5432}'\n    result = meta.pre_parse_json({\"config\": json_obj_str, \"enabled\": True})\n\n    # The config parameter should remain as a string, NOT be parsed to a dict\n    assert isinstance(result[\"config\"], str)\n    assert result[\"config\"] == json_obj_str\n\n    # Test case 2: JSON array as string\n    json_array_str = '[\"item1\", \"item2\", \"item3\"]'\n    result = meta.pre_parse_json({\"config\": json_array_str})\n\n    # Should remain as string\n    assert isinstance(result[\"config\"], str)\n    assert result[\"config\"] == json_array_str\n\n    # Test case 3: JSON string value (double-encoded)\n    json_string_str = '\"This is a JSON string\"'\n    result = meta.pre_parse_json({\"config\": json_string_str})\n\n    # Should remain as the original string with quotes\n    assert isinstance(result[\"config\"], str)\n    assert result[\"config\"] == json_string_str\n\n    # Test case 4: Complex nested JSON as string\n    complex_json_str = '{\"users\": [{\"id\": 1, \"name\": \"Alice\"}, {\"id\": 2, \"name\": \"Bob\"}], \"count\": 2}'\n    result = meta.pre_parse_json({\"config\": complex_json_str})\n\n    # Should remain as string\n    assert isinstance(result[\"config\"], str)\n    assert result[\"config\"] == complex_json_str\n\n\n@pytest.mark.anyio\nasync def test_str_annotation_runtime_validation():\n    \"\"\"Regression test for PR #1113: Test runtime validation with string parameters\n    containing valid JSON to ensure they are passed as strings, not parsed objects.\n    \"\"\"\n\n    def handle_json_payload(payload: str, strict_mode: bool = False) -> str:\n        \"\"\"Function that processes a JSON payload as a string.\"\"\"\n        # This function expects to receive the raw JSON string\n        # It might parse it later after validation or logging\n        assert isinstance(payload, str), f\"Expected str, got {type(payload)}\"\n        return f\"Handled payload of length {len(payload)}\"\n\n    meta = func_metadata(handle_json_payload)\n\n    # Test with a JSON object string\n    json_payload = '{\"action\": \"create\", \"resource\": \"user\", \"data\": {\"name\": \"Test User\"}}'\n\n    result = await meta.call_fn_with_arg_validation(\n        handle_json_payload,\n        fn_is_async=False,\n        arguments_to_validate={\"payload\": json_payload, \"strict_mode\": True},\n        arguments_to_pass_directly=None,\n    )\n\n    # The function should have received the string and returned successfully\n    assert result == f\"Handled payload of length {len(json_payload)}\"\n\n    # Test with JSON array string\n    json_array_payload = '[\"task1\", \"task2\", \"task3\"]'\n\n    result = await meta.call_fn_with_arg_validation(\n        handle_json_payload,\n        fn_is_async=False,\n        arguments_to_validate={\"payload\": json_array_payload},\n        arguments_to_pass_directly=None,\n    )\n\n    assert result == f\"Handled payload of length {len(json_array_payload)}\"\n\n\n# Tests for structured output functionality\n\n\ndef test_structured_output_requires_return_annotation():\n    \"\"\"Test that structured_output=True requires a return annotation\"\"\"\n\n    def func_no_annotation():  # pragma: no cover\n        return \"hello\"\n\n    def func_none_annotation() -> None:  # pragma: no cover\n        return None\n\n    with pytest.raises(InvalidSignature) as exc_info:\n        func_metadata(func_no_annotation, structured_output=True)\n    assert \"return annotation required\" in str(exc_info.value)\n\n    # None annotation should work\n    meta = func_metadata(func_none_annotation)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\"result\": {\"title\": \"Result\", \"type\": \"null\"}},\n        \"required\": [\"result\"],\n        \"title\": \"func_none_annotationOutput\",\n    }\n\n\ndef test_structured_output_basemodel():\n    \"\"\"Test structured output with BaseModel return types\"\"\"\n\n    class PersonModel(BaseModel):\n        name: str\n        age: int\n        email: str | None = None\n\n    def func_returning_person() -> PersonModel:  # pragma: no cover\n        return PersonModel(name=\"Alice\", age=30)\n\n    meta = func_metadata(func_returning_person)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\n            \"name\": {\"title\": \"Name\", \"type\": \"string\"},\n            \"age\": {\"title\": \"Age\", \"type\": \"integer\"},\n            \"email\": {\"anyOf\": [{\"type\": \"string\"}, {\"type\": \"null\"}], \"default\": None, \"title\": \"Email\"},\n        },\n        \"required\": [\"name\", \"age\"],\n        \"title\": \"PersonModel\",\n    }\n\n\ndef test_structured_output_primitives():\n    \"\"\"Test structured output with primitive return types\"\"\"\n\n    def func_str() -> str:  # pragma: no cover\n        return \"hello\"\n\n    def func_int() -> int:  # pragma: no cover\n        return 42\n\n    def func_float() -> float:  # pragma: no cover\n        return 3.14\n\n    def func_bool() -> bool:  # pragma: no cover\n        return True\n\n    def func_bytes() -> bytes:  # pragma: no cover\n        return b\"data\"\n\n    # Test string\n    meta = func_metadata(func_str)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\"result\": {\"title\": \"Result\", \"type\": \"string\"}},\n        \"required\": [\"result\"],\n        \"title\": \"func_strOutput\",\n    }\n\n    # Test int\n    meta = func_metadata(func_int)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\"result\": {\"title\": \"Result\", \"type\": \"integer\"}},\n        \"required\": [\"result\"],\n        \"title\": \"func_intOutput\",\n    }\n\n    # Test float\n    meta = func_metadata(func_float)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\"result\": {\"title\": \"Result\", \"type\": \"number\"}},\n        \"required\": [\"result\"],\n        \"title\": \"func_floatOutput\",\n    }\n\n    # Test bool\n    meta = func_metadata(func_bool)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\"result\": {\"title\": \"Result\", \"type\": \"boolean\"}},\n        \"required\": [\"result\"],\n        \"title\": \"func_boolOutput\",\n    }\n\n    # Test bytes\n    meta = func_metadata(func_bytes)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\"result\": {\"title\": \"Result\", \"type\": \"string\", \"format\": \"binary\"}},\n        \"required\": [\"result\"],\n        \"title\": \"func_bytesOutput\",\n    }\n\n\ndef test_structured_output_generic_types():\n    \"\"\"Test structured output with generic types (list, dict, Union, etc.)\"\"\"\n\n    def func_list_str() -> list[str]:  # pragma: no cover\n        return [\"a\", \"b\", \"c\"]\n\n    def func_dict_str_int() -> dict[str, int]:  # pragma: no cover\n        return {\"a\": 1, \"b\": 2}\n\n    def func_union() -> str | int:  # pragma: no cover\n        return \"hello\"\n\n    def func_optional() -> str | None:  # pragma: no cover\n        return None\n\n    # Test list\n    meta = func_metadata(func_list_str)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\"result\": {\"title\": \"Result\", \"type\": \"array\", \"items\": {\"type\": \"string\"}}},\n        \"required\": [\"result\"],\n        \"title\": \"func_list_strOutput\",\n    }\n\n    # Test dict[str, int] - should NOT be wrapped\n    meta = func_metadata(func_dict_str_int)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"additionalProperties\": {\"type\": \"integer\"},\n        \"title\": \"func_dict_str_intDictOutput\",\n    }\n\n    # Test Union\n    meta = func_metadata(func_union)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\"result\": {\"title\": \"Result\", \"anyOf\": [{\"type\": \"string\"}, {\"type\": \"integer\"}]}},\n        \"required\": [\"result\"],\n        \"title\": \"func_unionOutput\",\n    }\n\n    # Test Optional\n    meta = func_metadata(func_optional)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\"result\": {\"title\": \"Result\", \"anyOf\": [{\"type\": \"string\"}, {\"type\": \"null\"}]}},\n        \"required\": [\"result\"],\n        \"title\": \"func_optionalOutput\",\n    }\n\n\ndef test_structured_output_dataclass():\n    \"\"\"Test structured output with dataclass return types\"\"\"\n\n    @dataclass\n    class PersonDataClass:\n        name: str\n        age: int\n        email: str | None = None\n        tags: list[str] | None = None\n\n    def func_returning_dataclass() -> PersonDataClass:  # pragma: no cover\n        return PersonDataClass(name=\"Bob\", age=25)\n\n    meta = func_metadata(func_returning_dataclass)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\n            \"name\": {\"title\": \"Name\", \"type\": \"string\"},\n            \"age\": {\"title\": \"Age\", \"type\": \"integer\"},\n            \"email\": {\"anyOf\": [{\"type\": \"string\"}, {\"type\": \"null\"}], \"default\": None, \"title\": \"Email\"},\n            \"tags\": {\n                \"anyOf\": [{\"items\": {\"type\": \"string\"}, \"type\": \"array\"}, {\"type\": \"null\"}],\n                \"default\": None,\n                \"title\": \"Tags\",\n            },\n        },\n        \"required\": [\"name\", \"age\"],\n        \"title\": \"PersonDataClass\",\n    }\n\n\ndef test_structured_output_typeddict():\n    \"\"\"Test structured output with TypedDict return types\"\"\"\n\n    class PersonTypedDictOptional(TypedDict, total=False):\n        name: str\n        age: int\n\n    def func_returning_typeddict_optional() -> PersonTypedDictOptional:  # pragma: no cover\n        return {\"name\": \"Dave\"}  # Only returning one field to test partial dict\n\n    meta = func_metadata(func_returning_typeddict_optional)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\n            \"name\": {\"title\": \"Name\", \"type\": \"string\", \"default\": None},\n            \"age\": {\"title\": \"Age\", \"type\": \"integer\", \"default\": None},\n        },\n        \"title\": \"PersonTypedDictOptional\",\n    }\n\n    # Test with total=True (all required)\n    class PersonTypedDictRequired(TypedDict):\n        name: str\n        age: int\n        email: str | None\n\n    def func_returning_typeddict_required() -> PersonTypedDictRequired:  # pragma: no cover\n        return {\"name\": \"Eve\", \"age\": 40, \"email\": None}  # Testing None value\n\n    meta = func_metadata(func_returning_typeddict_required)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\n            \"name\": {\"title\": \"Name\", \"type\": \"string\"},\n            \"age\": {\"title\": \"Age\", \"type\": \"integer\"},\n            \"email\": {\"anyOf\": [{\"type\": \"string\"}, {\"type\": \"null\"}], \"title\": \"Email\"},\n        },\n        \"required\": [\"name\", \"age\", \"email\"],\n        \"title\": \"PersonTypedDictRequired\",\n    }\n\n\ndef test_structured_output_ordinary_class():\n    \"\"\"Test structured output with ordinary annotated classes\"\"\"\n\n    class PersonClass:\n        name: str\n        age: int\n        email: str | None\n\n        def __init__(self, name: str, age: int, email: str | None = None):  # pragma: no cover\n            self.name = name\n            self.age = age\n            self.email = email\n\n    def func_returning_class() -> PersonClass:  # pragma: no cover\n        return PersonClass(\"Helen\", 55)\n\n    meta = func_metadata(func_returning_class)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\n            \"name\": {\"title\": \"Name\", \"type\": \"string\"},\n            \"age\": {\"title\": \"Age\", \"type\": \"integer\"},\n            \"email\": {\"anyOf\": [{\"type\": \"string\"}, {\"type\": \"null\"}], \"title\": \"Email\"},\n        },\n        \"required\": [\"name\", \"age\", \"email\"],\n        \"title\": \"PersonClass\",\n    }\n\n\ndef test_unstructured_output_unannotated_class():\n    # Test with class that has no annotations\n    class UnannotatedClass:\n        def __init__(self, x, y):  # pragma: no cover\n            self.x = x\n            self.y = y\n\n    def func_returning_unannotated() -> UnannotatedClass:  # pragma: no cover\n        return UnannotatedClass(1, 2)\n\n    meta = func_metadata(func_returning_unannotated)\n    assert meta.output_schema is None\n\n\ndef test_tool_call_result_is_unstructured_and_not_converted():\n    def func_returning_call_tool_result() -> CallToolResult:\n        return CallToolResult(content=[])\n\n    meta = func_metadata(func_returning_call_tool_result)\n\n    assert meta.output_schema is None\n    assert isinstance(meta.convert_result(func_returning_call_tool_result()), CallToolResult)\n\n\ndef test_tool_call_result_annotated_is_structured_and_converted():\n    class PersonClass(BaseModel):\n        name: str\n\n    def func_returning_annotated_tool_call_result() -> Annotated[CallToolResult, PersonClass]:\n        return CallToolResult(content=[], structured_content={\"name\": \"Brandon\"})\n\n    meta = func_metadata(func_returning_annotated_tool_call_result)\n\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\n            \"name\": {\"title\": \"Name\", \"type\": \"string\"},\n        },\n        \"required\": [\"name\"],\n        \"title\": \"PersonClass\",\n    }\n    assert isinstance(meta.convert_result(func_returning_annotated_tool_call_result()), CallToolResult)\n\n\ndef test_tool_call_result_annotated_is_structured_and_invalid():\n    class PersonClass(BaseModel):\n        name: str\n\n    def func_returning_annotated_tool_call_result() -> Annotated[CallToolResult, PersonClass]:\n        return CallToolResult(content=[], structured_content={\"person\": \"Brandon\"})\n\n    meta = func_metadata(func_returning_annotated_tool_call_result)\n\n    with pytest.raises(ValueError):\n        meta.convert_result(func_returning_annotated_tool_call_result())\n\n\ndef test_tool_call_result_in_optional_is_rejected():\n    \"\"\"Test that Optional[CallToolResult] raises InvalidSignature\"\"\"\n\n    def func_optional_call_tool_result() -> CallToolResult | None:  # pragma: no cover\n        return CallToolResult(content=[])\n\n    with pytest.raises(InvalidSignature) as exc_info:\n        func_metadata(func_optional_call_tool_result)\n\n    assert \"Union or Optional\" in str(exc_info.value)\n    assert \"CallToolResult\" in str(exc_info.value)\n\n\ndef test_tool_call_result_in_union_is_rejected():\n    \"\"\"Test that Union[str, CallToolResult] raises InvalidSignature\"\"\"\n\n    def func_union_call_tool_result() -> str | CallToolResult:  # pragma: no cover\n        return CallToolResult(content=[])\n\n    with pytest.raises(InvalidSignature) as exc_info:\n        func_metadata(func_union_call_tool_result)\n\n    assert \"Union or Optional\" in str(exc_info.value)\n    assert \"CallToolResult\" in str(exc_info.value)\n\n\ndef test_tool_call_result_in_pipe_union_is_rejected():\n    \"\"\"Test that str | CallToolResult raises InvalidSignature\"\"\"\n\n    def func_pipe_union_call_tool_result() -> str | CallToolResult:  # pragma: no cover\n        return CallToolResult(content=[])\n\n    with pytest.raises(InvalidSignature) as exc_info:\n        func_metadata(func_pipe_union_call_tool_result)\n\n    assert \"Union or Optional\" in str(exc_info.value)\n    assert \"CallToolResult\" in str(exc_info.value)\n\n\ndef test_structured_output_with_field_descriptions():\n    \"\"\"Test that Field descriptions are preserved in structured output\"\"\"\n\n    class ModelWithDescriptions(BaseModel):\n        name: Annotated[str, Field(description=\"The person's full name\")]\n        age: Annotated[int, Field(description=\"Age in years\", ge=0, le=150)]\n\n    def func_with_descriptions() -> ModelWithDescriptions:  # pragma: no cover\n        return ModelWithDescriptions(name=\"Ian\", age=60)\n\n    meta = func_metadata(func_with_descriptions)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"properties\": {\n            \"name\": {\"title\": \"Name\", \"type\": \"string\", \"description\": \"The person's full name\"},\n            \"age\": {\"title\": \"Age\", \"type\": \"integer\", \"description\": \"Age in years\", \"minimum\": 0, \"maximum\": 150},\n        },\n        \"required\": [\"name\", \"age\"],\n        \"title\": \"ModelWithDescriptions\",\n    }\n\n\ndef test_structured_output_nested_models():\n    \"\"\"Test structured output with nested models\"\"\"\n\n    class Address(BaseModel):\n        street: str\n        city: str\n        zipcode: str\n\n    class PersonWithAddress(BaseModel):\n        name: str\n        address: Address\n\n    def func_nested() -> PersonWithAddress:  # pragma: no cover\n        return PersonWithAddress(name=\"Jack\", address=Address(street=\"123 Main St\", city=\"Anytown\", zipcode=\"12345\"))\n\n    meta = func_metadata(func_nested)\n    assert meta.output_schema == {\n        \"type\": \"object\",\n        \"$defs\": {\n            \"Address\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"street\": {\"title\": \"Street\", \"type\": \"string\"},\n                    \"city\": {\"title\": \"City\", \"type\": \"string\"},\n                    \"zipcode\": {\"title\": \"Zipcode\", \"type\": \"string\"},\n                },\n                \"required\": [\"street\", \"city\", \"zipcode\"],\n                \"title\": \"Address\",\n            }\n        },\n        \"properties\": {\n            \"name\": {\"title\": \"Name\", \"type\": \"string\"},\n            \"address\": {\"$ref\": \"#/$defs/Address\"},\n        },\n        \"required\": [\"name\", \"address\"],\n        \"title\": \"PersonWithAddress\",\n    }\n\n\ndef test_structured_output_unserializable_type_error():\n    \"\"\"Test error when structured_output=True is used with unserializable types\"\"\"\n\n    # Test with a class that has non-serializable default values\n    class ConfigWithCallable:\n        name: str\n        # Callable defaults are not JSON serializable and will trigger Pydantic warnings\n        callback: Callable[[Any], Any] = lambda x: x * 2\n\n    def func_returning_config_with_callable() -> ConfigWithCallable:  # pragma: no cover\n        return ConfigWithCallable()\n\n    # Should work without structured_output=True (returns None for output_schema)\n    meta = func_metadata(func_returning_config_with_callable)\n    assert meta.output_schema is None\n\n    # Should raise error with structured_output=True\n    with pytest.raises(InvalidSignature) as exc_info:\n        func_metadata(func_returning_config_with_callable, structured_output=True)\n    assert \"is not serializable for structured output\" in str(exc_info.value)\n    assert \"ConfigWithCallable\" in str(exc_info.value)\n\n    # Also test with NamedTuple for good measure\n    class Point(NamedTuple):\n        x: int\n        y: int\n\n    def func_returning_namedtuple() -> Point:  # pragma: no cover\n        return Point(1, 2)\n\n    # Should work without structured_output=True (returns None for output_schema)\n    meta = func_metadata(func_returning_namedtuple)\n    assert meta.output_schema is None\n\n    # Should raise error with structured_output=True\n    with pytest.raises(InvalidSignature) as exc_info:\n        func_metadata(func_returning_namedtuple, structured_output=True)\n    assert \"is not serializable for structured output\" in str(exc_info.value)\n    assert \"Point\" in str(exc_info.value)\n\n\ndef test_structured_output_aliases():\n    \"\"\"Test that field aliases are consistent between schema and output\"\"\"\n\n    class ModelWithAliases(BaseModel):\n        field_first: str | None = Field(default=None, alias=\"first\", description=\"The first field.\")\n        field_second: str | None = Field(default=None, alias=\"second\", description=\"The second field.\")\n\n    def func_with_aliases() -> ModelWithAliases:  # pragma: no cover\n        # When aliases are defined, we must use the aliased names to set values\n        return ModelWithAliases(**{\"first\": \"hello\", \"second\": \"world\"})\n\n    meta = func_metadata(func_with_aliases)\n\n    # Check that schema uses aliases\n    assert meta.output_schema is not None\n    assert \"first\" in meta.output_schema[\"properties\"]\n    assert \"second\" in meta.output_schema[\"properties\"]\n    assert \"field_first\" not in meta.output_schema[\"properties\"]\n    assert \"field_second\" not in meta.output_schema[\"properties\"]\n\n    # Check that the actual output uses aliases too\n    result = ModelWithAliases(**{\"first\": \"hello\", \"second\": \"world\"})\n    _, structured_content = meta.convert_result(result)\n\n    # The structured content should use aliases to match the schema\n    assert \"first\" in structured_content\n    assert \"second\" in structured_content\n    assert \"field_first\" not in structured_content\n    assert \"field_second\" not in structured_content\n    assert structured_content[\"first\"] == \"hello\"\n    assert structured_content[\"second\"] == \"world\"\n\n    # Also test the case where we have a model with defaults to ensure aliases work in all cases\n    result_with_defaults = ModelWithAliases()  # Uses default None values\n    _, structured_content_defaults = meta.convert_result(result_with_defaults)\n\n    # Even with defaults, should use aliases in output\n    assert \"first\" in structured_content_defaults\n    assert \"second\" in structured_content_defaults\n    assert \"field_first\" not in structured_content_defaults\n    assert \"field_second\" not in structured_content_defaults\n    assert structured_content_defaults[\"first\"] is None\n    assert structured_content_defaults[\"second\"] is None\n\n\ndef test_basemodel_reserved_names():\n    \"\"\"Test that functions with parameters named after BaseModel methods work correctly\"\"\"\n\n    def func_with_reserved_names(  # pragma: no cover\n        model_dump: str,\n        model_validate: int,\n        dict: list[str],\n        json: dict[str, Any],\n        validate: bool,\n        copy: float,\n        normal_param: str,\n    ) -> str:\n        return f\"{model_dump}, {model_validate}, {dict}, {json}, {validate}, {copy}, {normal_param}\"\n\n    meta = func_metadata(func_with_reserved_names)\n\n    # Check that the schema has all the original parameter names (using aliases)\n    schema = meta.arg_model.model_json_schema(by_alias=True)\n    assert \"model_dump\" in schema[\"properties\"]\n    assert \"model_validate\" in schema[\"properties\"]\n    assert \"dict\" in schema[\"properties\"]\n    assert \"json\" in schema[\"properties\"]\n    assert \"validate\" in schema[\"properties\"]\n    assert \"copy\" in schema[\"properties\"]\n    assert \"normal_param\" in schema[\"properties\"]\n\n\n@pytest.mark.anyio\nasync def test_basemodel_reserved_names_validation():\n    \"\"\"Test that validation and calling works with reserved parameter names\"\"\"\n\n    def func_with_reserved_names(\n        model_dump: str,\n        model_validate: int,\n        dict: list[str],\n        json: dict[str, Any],\n        validate: bool,\n        normal_param: str,\n    ) -> str:\n        return f\"{model_dump}|{model_validate}|{len(dict)}|{json}|{validate}|{normal_param}\"\n\n    meta = func_metadata(func_with_reserved_names)\n\n    # Test validation with reserved names\n    result = await meta.call_fn_with_arg_validation(\n        func_with_reserved_names,\n        fn_is_async=False,\n        arguments_to_validate={\n            \"model_dump\": \"test_dump\",\n            \"model_validate\": 42,\n            \"dict\": [\"a\", \"b\", \"c\"],\n            \"json\": {\"key\": \"value\"},\n            \"validate\": True,\n            \"normal_param\": \"normal\",\n        },\n        arguments_to_pass_directly=None,\n    )\n\n    assert result == \"test_dump|42|3|{'key': 'value'}|True|normal\"\n\n    # Test that the model can still call its own methods\n    model_instance = meta.arg_model.model_validate(\n        {\n            \"model_dump\": \"dump_value\",\n            \"model_validate\": 123,\n            \"dict\": [\"x\", \"y\"],\n            \"json\": {\"foo\": \"bar\"},\n            \"validate\": False,\n            \"normal_param\": \"test\",\n        }\n    )\n\n    # The model should still have its methods accessible\n    assert hasattr(model_instance, \"model_dump\")\n    assert callable(model_instance.model_dump)\n\n    # model_dump_one_level should return the original parameter names\n    dumped = model_instance.model_dump_one_level()\n    assert dumped[\"model_dump\"] == \"dump_value\"\n    assert dumped[\"model_validate\"] == 123\n    assert dumped[\"dict\"] == [\"x\", \"y\"]\n    assert dumped[\"json\"] == {\"foo\": \"bar\"}\n    assert dumped[\"validate\"] is False\n    assert dumped[\"normal_param\"] == \"test\"\n\n\ndef test_basemodel_reserved_names_with_json_preparsing():\n    \"\"\"Test that pre_parse_json works correctly with reserved parameter names\"\"\"\n\n    def func_with_reserved_json(  # pragma: no cover\n        json: dict[str, Any],\n        model_dump: list[int],\n        normal: str,\n    ) -> str:\n        return \"ok\"\n\n    meta = func_metadata(func_with_reserved_json)\n\n    # Test pre-parsing with reserved names\n    result = meta.pre_parse_json(\n        {\n            \"json\": '{\"nested\": \"data\"}',  # JSON string that should be parsed\n            \"model_dump\": \"[1, 2, 3]\",  # JSON string that should be parsed\n            \"normal\": \"plain string\",  # Should remain as string\n        }\n    )\n\n    assert result[\"json\"] == {\"nested\": \"data\"}\n    assert result[\"model_dump\"] == [1, 2, 3]\n    assert result[\"normal\"] == \"plain string\"\n\n\ndef test_disallowed_type_qualifier():\n    def func_disallowed_qualifier() -> Final[int]:  # type: ignore\n        pass  # pragma: no cover\n\n    with pytest.raises(InvalidSignature) as exc_info:\n        func_metadata(func_disallowed_qualifier)\n    assert \"return annotation contains an invalid type qualifier\" in str(exc_info.value)\n\n\ndef test_preserves_pydantic_metadata():\n    def func_with_metadata() -> Annotated[int, Field(gt=1)]: ...  # pragma: no branch\n\n    meta = func_metadata(func_with_metadata)\n\n    assert meta.output_schema is not None\n    assert meta.output_schema[\"properties\"][\"result\"] == {\"exclusiveMinimum\": 1, \"title\": \"Result\", \"type\": \"integer\"}\n"
  },
  {
    "path": "tests/server/mcpserver/test_integration.py",
    "content": "\"\"\"Integration tests for MCPServer server functionality.\n\nThese tests validate the proper functioning of MCPServer features using focused,\nsingle-feature example servers over an in-memory transport.\n\"\"\"\n# TODO(Marcelo): The `examples` package is not being imported as package. We need to solve this.\n# pyright: reportUnknownMemberType=false\n# pyright: reportMissingImports=false\n# pyright: reportUnknownVariableType=false\n# pyright: reportUnknownArgumentType=false\n\nimport json\n\nimport pytest\nfrom inline_snapshot import snapshot\n\nfrom examples.snippets.servers import (\n    basic_prompt,\n    basic_resource,\n    basic_tool,\n    completion,\n    elicitation,\n    mcpserver_quickstart,\n    notifications,\n    sampling,\n    structured_output,\n    tool_progress,\n)\nfrom mcp.client import Client\nfrom mcp.client.session import ClientSession\nfrom mcp.shared._context import RequestContext\nfrom mcp.shared.session import RequestResponder\nfrom mcp.types import (\n    ClientResult,\n    CreateMessageRequestParams,\n    CreateMessageResult,\n    ElicitRequestParams,\n    ElicitResult,\n    GetPromptResult,\n    LoggingMessageNotification,\n    LoggingMessageNotificationParams,\n    NotificationParams,\n    ProgressNotification,\n    ProgressNotificationParams,\n    PromptReference,\n    ReadResourceResult,\n    ResourceListChangedNotification,\n    ResourceTemplateReference,\n    ServerNotification,\n    ServerRequest,\n    TextContent,\n    TextResourceContents,\n    ToolListChangedNotification,\n)\n\npytestmark = pytest.mark.anyio\n\n\nclass NotificationCollector:\n    \"\"\"Collects notifications from the server for testing.\"\"\"\n\n    def __init__(self):\n        self.progress_notifications: list[ProgressNotificationParams] = []\n        self.log_messages: list[LoggingMessageNotificationParams] = []\n        self.resource_notifications: list[NotificationParams | None] = []\n        self.tool_notifications: list[NotificationParams | None] = []\n\n    async def handle_generic_notification(\n        self, message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception\n    ) -> None:\n        \"\"\"Handle any server notification and route to appropriate handler.\"\"\"\n        if isinstance(message, ServerNotification):  # pragma: no branch\n            if isinstance(message, ProgressNotification):\n                self.progress_notifications.append(message.params)\n            elif isinstance(message, LoggingMessageNotification):\n                self.log_messages.append(message.params)\n            elif isinstance(message, ResourceListChangedNotification):\n                self.resource_notifications.append(message.params)\n            elif isinstance(message, ToolListChangedNotification):  # pragma: no cover\n                self.tool_notifications.append(message.params)\n\n\nasync def sampling_callback(\n    context: RequestContext[ClientSession], params: CreateMessageRequestParams\n) -> CreateMessageResult:\n    \"\"\"Sampling callback for tests.\"\"\"\n    return CreateMessageResult(\n        role=\"assistant\",\n        content=TextContent(\n            type=\"text\",\n            text=\"This is a simulated LLM response for testing\",\n        ),\n        model=\"test-model\",\n    )\n\n\nasync def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n    \"\"\"Elicitation callback for tests.\"\"\"\n    # For restaurant booking test\n    if \"No tables available\" in params.message:\n        return ElicitResult(\n            action=\"accept\",\n            content={\"checkAlternative\": True, \"alternativeDate\": \"2024-12-26\"},\n        )\n    else:  # pragma: no cover\n        return ElicitResult(action=\"decline\")\n\n\nasync def test_basic_tools() -> None:\n    \"\"\"Test basic tool functionality.\"\"\"\n    async with Client(basic_tool.mcp) as client:\n        assert client.initialize_result.capabilities.tools is not None\n\n        # Test sum tool\n        tool_result = await client.call_tool(\"sum\", {\"a\": 5, \"b\": 3})\n        assert len(tool_result.content) == 1\n        assert isinstance(tool_result.content[0], TextContent)\n        assert tool_result.content[0].text == \"8\"\n\n        # Test weather tool\n        weather_result = await client.call_tool(\"get_weather\", {\"city\": \"London\"})\n        assert len(weather_result.content) == 1\n        assert isinstance(weather_result.content[0], TextContent)\n        assert \"Weather in London: 22degreesC\" in weather_result.content[0].text\n\n\nasync def test_basic_resources() -> None:\n    \"\"\"Test basic resource functionality.\"\"\"\n    async with Client(basic_resource.mcp) as client:\n        assert client.initialize_result.capabilities.resources is not None\n\n        # Test document resource\n        doc_content = await client.read_resource(\"file://documents/readme\")\n        assert isinstance(doc_content, ReadResourceResult)\n        assert len(doc_content.contents) == 1\n        assert isinstance(doc_content.contents[0], TextResourceContents)\n        assert \"Content of readme\" in doc_content.contents[0].text\n\n        # Test settings resource\n        settings_content = await client.read_resource(\"config://settings\")\n        assert isinstance(settings_content, ReadResourceResult)\n        assert len(settings_content.contents) == 1\n        assert isinstance(settings_content.contents[0], TextResourceContents)\n        settings_json = json.loads(settings_content.contents[0].text)\n        assert settings_json[\"theme\"] == \"dark\"\n        assert settings_json[\"language\"] == \"en\"\n\n\nasync def test_basic_prompts() -> None:\n    \"\"\"Test basic prompt functionality.\"\"\"\n    async with Client(basic_prompt.mcp) as client:\n        assert client.initialize_result.capabilities.prompts is not None\n\n        # Test review_code prompt\n        prompts = await client.list_prompts()\n        review_prompt = next((p for p in prompts.prompts if p.name == \"review_code\"), None)\n        assert review_prompt is not None\n\n        prompt_result = await client.get_prompt(\"review_code\", {\"code\": \"def hello():\\n    print('Hello')\"})\n        assert isinstance(prompt_result, GetPromptResult)\n        assert len(prompt_result.messages) == 1\n        assert isinstance(prompt_result.messages[0].content, TextContent)\n        assert \"Please review this code:\" in prompt_result.messages[0].content.text\n        assert \"def hello():\" in prompt_result.messages[0].content.text\n\n        # Test debug_error prompt\n        debug_result = await client.get_prompt(\n            \"debug_error\", {\"error\": \"TypeError: 'NoneType' object is not subscriptable\"}\n        )\n        assert isinstance(debug_result, GetPromptResult)\n        assert len(debug_result.messages) == 3\n        assert debug_result.messages[0].role == \"user\"\n        assert isinstance(debug_result.messages[0].content, TextContent)\n        assert \"I'm seeing this error:\" in debug_result.messages[0].content.text\n        assert debug_result.messages[1].role == \"user\"\n        assert isinstance(debug_result.messages[1].content, TextContent)\n        assert \"TypeError\" in debug_result.messages[1].content.text\n        assert debug_result.messages[2].role == \"assistant\"\n        assert isinstance(debug_result.messages[2].content, TextContent)\n        assert \"I'll help debug that\" in debug_result.messages[2].content.text\n\n\nasync def test_tool_progress() -> None:\n    \"\"\"Test tool progress reporting.\"\"\"\n    collector = NotificationCollector()\n\n    async def message_handler(message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception):\n        await collector.handle_generic_notification(message)\n        if isinstance(message, Exception):  # pragma: no cover\n            raise message\n\n    async with Client(tool_progress.mcp, message_handler=message_handler) as client:\n        # Test progress callback\n        progress_updates = []\n\n        async def progress_callback(progress: float, total: float | None, message: str | None) -> None:\n            progress_updates.append((progress, total, message))\n\n        # Call tool with progress\n        steps = 3\n        tool_result = await client.call_tool(\n            \"long_running_task\",\n            {\"task_name\": \"Test Task\", \"steps\": steps},\n            progress_callback=progress_callback,\n        )\n        assert tool_result.content == snapshot([TextContent(text=\"Task 'Test Task' completed\")])\n\n        # Verify progress updates\n        assert len(progress_updates) == steps\n        for i, (progress, total, message) in enumerate(progress_updates):\n            expected_progress = (i + 1) / steps\n            assert abs(progress - expected_progress) < 0.01\n            assert total == 1.0\n            assert f\"Step {i + 1}/{steps}\" in message\n\n        # Verify log messages\n        assert len(collector.log_messages) > 0\n\n\nasync def test_sampling() -> None:\n    \"\"\"Test sampling (LLM interaction) functionality.\"\"\"\n    async with Client(sampling.mcp, sampling_callback=sampling_callback) as client:\n        assert client.initialize_result.capabilities.tools is not None\n\n        # Test sampling tool\n        sampling_result = await client.call_tool(\"generate_poem\", {\"topic\": \"nature\"})\n        assert len(sampling_result.content) == 1\n        assert isinstance(sampling_result.content[0], TextContent)\n        assert \"This is a simulated LLM response\" in sampling_result.content[0].text\n\n\nasync def test_elicitation() -> None:\n    \"\"\"Test elicitation (user interaction) functionality.\"\"\"\n    async with Client(elicitation.mcp, elicitation_callback=elicitation_callback) as client:\n        # Test booking with unavailable date (triggers elicitation)\n        booking_result = await client.call_tool(\n            \"book_table\",\n            {\n                \"date\": \"2024-12-25\",  # Unavailable date\n                \"time\": \"19:00\",\n                \"party_size\": 4,\n            },\n        )\n        assert len(booking_result.content) == 1\n        assert isinstance(booking_result.content[0], TextContent)\n        assert \"[SUCCESS] Booked for 2024-12-26\" in booking_result.content[0].text\n\n        # Test booking with available date (no elicitation)\n        booking_result = await client.call_tool(\n            \"book_table\",\n            {\n                \"date\": \"2024-12-20\",  # Available date\n                \"time\": \"20:00\",\n                \"party_size\": 2,\n            },\n        )\n        assert len(booking_result.content) == 1\n        assert isinstance(booking_result.content[0], TextContent)\n        assert \"[SUCCESS] Booked for 2024-12-20 at 20:00\" in booking_result.content[0].text\n\n\nasync def test_notifications() -> None:\n    \"\"\"Test notifications and logging functionality.\"\"\"\n    collector = NotificationCollector()\n\n    async def message_handler(message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception):\n        await collector.handle_generic_notification(message)\n        if isinstance(message, Exception):  # pragma: no cover\n            raise message\n\n    async with Client(notifications.mcp, message_handler=message_handler) as client:\n        # Call tool that generates notifications\n        tool_result = await client.call_tool(\"process_data\", {\"data\": \"test_data\"})\n        assert len(tool_result.content) == 1\n        assert isinstance(tool_result.content[0], TextContent)\n        assert \"Processed: test_data\" in tool_result.content[0].text\n\n        # Verify log messages at different levels\n        assert len(collector.log_messages) >= 4\n        log_levels = {msg.level for msg in collector.log_messages}\n        assert \"debug\" in log_levels\n        assert \"info\" in log_levels\n        assert \"warning\" in log_levels\n        assert \"error\" in log_levels\n\n        # Verify resource list changed notification\n        assert len(collector.resource_notifications) > 0\n\n\nasync def test_completion() -> None:\n    \"\"\"Test completion (autocomplete) functionality.\"\"\"\n    async with Client(completion.mcp) as client:\n        assert client.initialize_result.capabilities.resources is not None\n        assert client.initialize_result.capabilities.prompts is not None\n\n        # Test resource completion\n        completion_result = await client.complete(\n            ref=ResourceTemplateReference(type=\"ref/resource\", uri=\"github://repos/{owner}/{repo}\"),\n            argument={\"name\": \"repo\", \"value\": \"\"},\n            context_arguments={\"owner\": \"modelcontextprotocol\"},\n        )\n\n        assert completion_result is not None\n        assert hasattr(completion_result, \"completion\")\n        assert completion_result.completion is not None\n        assert len(completion_result.completion.values) == 3\n        assert \"python-sdk\" in completion_result.completion.values\n        assert \"typescript-sdk\" in completion_result.completion.values\n        assert \"specification\" in completion_result.completion.values\n\n        # Test prompt completion\n        completion_result = await client.complete(\n            ref=PromptReference(type=\"ref/prompt\", name=\"review_code\"),\n            argument={\"name\": \"language\", \"value\": \"py\"},\n        )\n\n        assert completion_result is not None\n        assert hasattr(completion_result, \"completion\")\n        assert completion_result.completion is not None\n        assert \"python\" in completion_result.completion.values\n        assert all(lang.startswith(\"py\") for lang in completion_result.completion.values)\n\n\nasync def test_mcpserver_quickstart() -> None:\n    \"\"\"Test MCPServer quickstart example.\"\"\"\n    async with Client(mcpserver_quickstart.mcp) as client:\n        # Test add tool\n        tool_result = await client.call_tool(\"add\", {\"a\": 10, \"b\": 20})\n        assert len(tool_result.content) == 1\n        assert isinstance(tool_result.content[0], TextContent)\n        assert tool_result.content[0].text == \"30\"\n\n        # Test greeting resource directly\n        resource_result = await client.read_resource(\"greeting://Alice\")\n        assert len(resource_result.contents) == 1\n        assert isinstance(resource_result.contents[0], TextResourceContents)\n        assert resource_result.contents[0].text == \"Hello, Alice!\"\n\n\nasync def test_structured_output() -> None:\n    \"\"\"Test structured output functionality.\"\"\"\n    async with Client(structured_output.mcp) as client:\n        # Test get_weather tool\n        weather_result = await client.call_tool(\"get_weather\", {\"city\": \"New York\"})\n        assert len(weather_result.content) == 1\n        assert isinstance(weather_result.content[0], TextContent)\n\n        # Check that the result contains expected weather data\n        result_text = weather_result.content[0].text\n        assert \"22.5\" in result_text  # temperature\n        assert \"sunny\" in result_text  # condition\n        assert \"45\" in result_text  # humidity\n        assert \"5.2\" in result_text  # wind_speed\n"
  },
  {
    "path": "tests/server/mcpserver/test_parameter_descriptions.py",
    "content": "\"\"\"Test that parameter descriptions are properly exposed through list_tools\"\"\"\n\nimport pytest\nfrom pydantic import Field\n\nfrom mcp.server.mcpserver import MCPServer\n\n\n@pytest.mark.anyio\nasync def test_parameter_descriptions():\n    mcp = MCPServer(\"Test Server\")\n\n    @mcp.tool()\n    def greet(\n        name: str = Field(description=\"The name to greet\"),\n        title: str = Field(description=\"Optional title\", default=\"\"),\n    ) -> str:  # pragma: no cover\n        \"\"\"A greeting tool\"\"\"\n        return f\"Hello {title} {name}\"\n\n    tools = await mcp.list_tools()\n    assert len(tools) == 1\n    tool = tools[0]\n\n    # Check that parameter descriptions are present in the schema\n    properties = tool.input_schema[\"properties\"]\n    assert \"name\" in properties\n    assert properties[\"name\"][\"description\"] == \"The name to greet\"\n    assert \"title\" in properties\n    assert properties[\"title\"][\"description\"] == \"Optional title\"\n"
  },
  {
    "path": "tests/server/mcpserver/test_server.py",
    "content": "import base64\nfrom pathlib import Path\nfrom typing import Any\nfrom unittest.mock import AsyncMock, MagicMock, patch\n\nimport pytest\nfrom inline_snapshot import snapshot\nfrom pydantic import BaseModel\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount, Route\n\nfrom mcp.client import Client\nfrom mcp.server.context import ServerRequestContext\nfrom mcp.server.experimental.request_context import Experimental\nfrom mcp.server.mcpserver import Context, MCPServer\nfrom mcp.server.mcpserver.exceptions import ToolError\nfrom mcp.server.mcpserver.prompts.base import Message, UserMessage\nfrom mcp.server.mcpserver.resources import FileResource, FunctionResource\nfrom mcp.server.mcpserver.utilities.types import Audio, Image\nfrom mcp.server.transport_security import TransportSecuritySettings\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.types import (\n    AudioContent,\n    BlobResourceContents,\n    Completion,\n    CompletionArgument,\n    CompletionContext,\n    ContentBlock,\n    EmbeddedResource,\n    GetPromptResult,\n    Icon,\n    ImageContent,\n    ListPromptsResult,\n    Prompt,\n    PromptArgument,\n    PromptMessage,\n    PromptReference,\n    ReadResourceResult,\n    Resource,\n    ResourceTemplate,\n    TextContent,\n    TextResourceContents,\n)\n\npytestmark = pytest.mark.anyio\n\n\nclass TestServer:\n    async def test_create_server(self):\n        mcp = MCPServer(\n            title=\"MCPServer Server\",\n            description=\"Server description\",\n            instructions=\"Server instructions\",\n            website_url=\"https://example.com/mcp_server\",\n            version=\"1.0\",\n            icons=[Icon(src=\"https://example.com/icon.png\", mime_type=\"image/png\", sizes=[\"48x48\", \"96x96\"])],\n        )\n        assert mcp.name == \"mcp-server\"\n        assert mcp.title == \"MCPServer Server\"\n        assert mcp.description == \"Server description\"\n        assert mcp.instructions == \"Server instructions\"\n        assert mcp.website_url == \"https://example.com/mcp_server\"\n        assert mcp.version == \"1.0\"\n        assert isinstance(mcp.icons, list)\n        assert len(mcp.icons) == 1\n        assert mcp.icons[0].src == \"https://example.com/icon.png\"\n\n    async def test_sse_app_returns_starlette_app(self):\n        \"\"\"Test that sse_app returns a Starlette application with correct routes.\"\"\"\n        mcp = MCPServer(\"test\")\n        # Use host=\"0.0.0.0\" to avoid auto DNS protection\n        app = mcp.sse_app(host=\"0.0.0.0\")\n\n        assert isinstance(app, Starlette)\n\n        # Verify routes exist\n        sse_routes = [r for r in app.routes if isinstance(r, Route)]\n        mount_routes = [r for r in app.routes if isinstance(r, Mount)]\n\n        assert len(sse_routes) == 1, \"Should have one SSE route\"\n        assert len(mount_routes) == 1, \"Should have one mount route\"\n        assert sse_routes[0].path == \"/sse\"\n        assert mount_routes[0].path == \"/messages\"\n\n    async def test_non_ascii_description(self):\n        \"\"\"Test that MCPServer handles non-ASCII characters in descriptions correctly\"\"\"\n        mcp = MCPServer()\n\n        @mcp.tool(description=(\"🌟 This tool uses emojis and UTF-8 characters: á é í ó ú ñ 漢字 🎉\"))\n        def hello_world(name: str = \"世界\") -> str:\n            return f\"¡Hola, {name}! 👋\"\n\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            assert len(tools.tools) == 1\n            tool = tools.tools[0]\n            assert tool.description is not None\n            assert \"🌟\" in tool.description\n            assert \"漢字\" in tool.description\n            assert \"🎉\" in tool.description\n\n            result = await client.call_tool(\"hello_world\", {})\n            assert len(result.content) == 1\n            content = result.content[0]\n            assert isinstance(content, TextContent)\n            assert \"¡Hola, 世界! 👋\" == content.text\n\n    async def test_add_tool_decorator(self):\n        mcp = MCPServer()\n\n        @mcp.tool()\n        def sum(x: int, y: int) -> int:  # pragma: no cover\n            return x + y\n\n        assert len(mcp._tool_manager.list_tools()) == 1\n\n    async def test_add_tool_decorator_incorrect_usage(self):\n        mcp = MCPServer()\n\n        with pytest.raises(TypeError, match=\"The @tool decorator was used incorrectly\"):\n\n            @mcp.tool  # Missing parentheses #type: ignore\n            def sum(x: int, y: int) -> int:  # pragma: no cover\n                return x + y\n\n    async def test_add_resource_decorator(self):\n        mcp = MCPServer()\n\n        @mcp.resource(\"r://{x}\")\n        def get_data(x: str) -> str:  # pragma: no cover\n            return f\"Data: {x}\"\n\n        assert len(mcp._resource_manager._templates) == 1\n\n    async def test_add_resource_decorator_incorrect_usage(self):\n        mcp = MCPServer()\n\n        with pytest.raises(TypeError, match=\"The @resource decorator was used incorrectly\"):\n\n            @mcp.resource  # Missing parentheses #type: ignore\n            def get_data(x: str) -> str:  # pragma: no cover\n                return f\"Data: {x}\"\n\n\nclass TestDnsRebindingProtection:\n    \"\"\"Tests for automatic DNS rebinding protection on localhost.\n\n    DNS rebinding protection is now configured in sse_app() and streamable_http_app()\n    based on the host parameter passed to those methods.\n    \"\"\"\n\n    def test_auto_enabled_for_127_0_0_1_sse(self):\n        \"\"\"DNS rebinding protection should auto-enable for host=127.0.0.1 in SSE app.\"\"\"\n        mcp = MCPServer()\n        # Call sse_app with host=127.0.0.1 to trigger auto-config\n        # We can't directly inspect the transport_security, but we can verify\n        # the app is created without error\n        app = mcp.sse_app(host=\"127.0.0.1\")\n        assert app is not None\n\n    def test_auto_enabled_for_127_0_0_1_streamable_http(self):\n        \"\"\"DNS rebinding protection should auto-enable for host=127.0.0.1 in StreamableHTTP app.\"\"\"\n        mcp = MCPServer()\n        app = mcp.streamable_http_app(host=\"127.0.0.1\")\n        assert app is not None\n\n    def test_auto_enabled_for_localhost_sse(self):\n        \"\"\"DNS rebinding protection should auto-enable for host=localhost in SSE app.\"\"\"\n        mcp = MCPServer()\n        app = mcp.sse_app(host=\"localhost\")\n        assert app is not None\n\n    def test_auto_enabled_for_ipv6_localhost_sse(self):\n        \"\"\"DNS rebinding protection should auto-enable for host=::1 (IPv6 localhost) in SSE app.\"\"\"\n        mcp = MCPServer()\n        app = mcp.sse_app(host=\"::1\")\n        assert app is not None\n\n    def test_not_auto_enabled_for_other_hosts_sse(self):\n        \"\"\"DNS rebinding protection should NOT auto-enable for other hosts in SSE app.\"\"\"\n        mcp = MCPServer()\n        app = mcp.sse_app(host=\"0.0.0.0\")\n        assert app is not None\n\n    def test_explicit_settings_not_overridden_sse(self):\n        \"\"\"Explicit transport_security settings should not be overridden in SSE app.\"\"\"\n        custom_settings = TransportSecuritySettings(\n            enable_dns_rebinding_protection=False,\n        )\n        mcp = MCPServer()\n        # Explicit transport_security passed to sse_app should be used as-is\n        app = mcp.sse_app(host=\"127.0.0.1\", transport_security=custom_settings)\n        assert app is not None\n\n    def test_explicit_settings_not_overridden_streamable_http(self):\n        \"\"\"Explicit transport_security settings should not be overridden in StreamableHTTP app.\"\"\"\n        custom_settings = TransportSecuritySettings(\n            enable_dns_rebinding_protection=False,\n        )\n        mcp = MCPServer()\n        # Explicit transport_security passed to streamable_http_app should be used as-is\n        app = mcp.streamable_http_app(host=\"127.0.0.1\", transport_security=custom_settings)\n        assert app is not None\n\n\ndef tool_fn(x: int, y: int) -> int:\n    return x + y\n\n\ndef error_tool_fn() -> None:\n    raise ValueError(\"Test error\")\n\n\ndef image_tool_fn(path: str) -> Image:\n    return Image(path)\n\n\ndef audio_tool_fn(path: str) -> Audio:\n    return Audio(path)\n\n\ndef mixed_content_tool_fn() -> list[ContentBlock]:\n    return [\n        TextContent(type=\"text\", text=\"Hello\"),\n        ImageContent(type=\"image\", data=\"abc\", mime_type=\"image/png\"),\n        AudioContent(type=\"audio\", data=\"def\", mime_type=\"audio/wav\"),\n    ]\n\n\nclass TestServerTools:\n    async def test_add_tool(self):\n        mcp = MCPServer()\n        mcp.add_tool(tool_fn)\n        mcp.add_tool(tool_fn)\n        assert len(mcp._tool_manager.list_tools()) == 1\n\n    async def test_list_tools(self):\n        mcp = MCPServer()\n        mcp.add_tool(tool_fn)\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            assert len(tools.tools) == 1\n\n    async def test_call_tool(self):\n        mcp = MCPServer()\n        mcp.add_tool(tool_fn)\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"my_tool\", {\"arg1\": \"value\"})\n            assert not hasattr(result, \"error\")\n            assert len(result.content) > 0\n\n    async def test_tool_exception_handling(self):\n        mcp = MCPServer()\n        mcp.add_tool(error_tool_fn)\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"error_tool_fn\", {})\n            assert len(result.content) == 1\n            content = result.content[0]\n            assert isinstance(content, TextContent)\n            assert \"Test error\" in content.text\n            assert result.is_error is True\n\n    async def test_tool_error_handling(self):\n        mcp = MCPServer()\n        mcp.add_tool(error_tool_fn)\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"error_tool_fn\", {})\n            assert len(result.content) == 1\n            content = result.content[0]\n            assert isinstance(content, TextContent)\n            assert \"Test error\" in content.text\n            assert result.is_error is True\n\n    async def test_tool_error_details(self):\n        \"\"\"Test that exception details are properly formatted in the response\"\"\"\n        mcp = MCPServer()\n        mcp.add_tool(error_tool_fn)\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"error_tool_fn\", {})\n            content = result.content[0]\n            assert isinstance(content, TextContent)\n            assert isinstance(content.text, str)\n            assert \"Test error\" in content.text\n            assert result.is_error is True\n\n    async def test_tool_return_value_conversion(self):\n        mcp = MCPServer()\n        mcp.add_tool(tool_fn)\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"tool_fn\", {\"x\": 1, \"y\": 2})\n            assert len(result.content) == 1\n            content = result.content[0]\n            assert isinstance(content, TextContent)\n            assert content.text == \"3\"\n            # Check structured content - int return type should have structured output\n            assert result.structured_content is not None\n            assert result.structured_content == {\"result\": 3}\n\n    async def test_tool_image_helper(self, tmp_path: Path):\n        # Create a test image\n        image_path = tmp_path / \"test.png\"\n        image_path.write_bytes(b\"fake png data\")\n\n        mcp = MCPServer()\n        mcp.add_tool(image_tool_fn)\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"image_tool_fn\", {\"path\": str(image_path)})\n            assert len(result.content) == 1\n            content = result.content[0]\n            assert isinstance(content, ImageContent)\n            assert content.type == \"image\"\n            assert content.mime_type == \"image/png\"\n            # Verify base64 encoding\n            decoded = base64.b64decode(content.data)\n            assert decoded == b\"fake png data\"\n            # Check structured content - Image return type should NOT have structured output\n            assert result.structured_content is None\n\n    async def test_tool_audio_helper(self, tmp_path: Path):\n        # Create a test audio\n        audio_path = tmp_path / \"test.wav\"\n        audio_path.write_bytes(b\"fake wav data\")\n\n        mcp = MCPServer()\n        mcp.add_tool(audio_tool_fn)\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"audio_tool_fn\", {\"path\": str(audio_path)})\n            assert len(result.content) == 1\n            content = result.content[0]\n            assert isinstance(content, AudioContent)\n            assert content.type == \"audio\"\n            assert content.mime_type == \"audio/wav\"\n            # Verify base64 encoding\n            decoded = base64.b64decode(content.data)\n            assert decoded == b\"fake wav data\"\n            # Check structured content - Image return type should NOT have structured output\n            assert result.structured_content is None\n\n    @pytest.mark.parametrize(\n        \"filename,expected_mime_type\",\n        [\n            (\"test.wav\", \"audio/wav\"),\n            (\"test.mp3\", \"audio/mpeg\"),\n            (\"test.ogg\", \"audio/ogg\"),\n            (\"test.flac\", \"audio/flac\"),\n            (\"test.aac\", \"audio/aac\"),\n            (\"test.m4a\", \"audio/mp4\"),\n            (\"test.unknown\", \"application/octet-stream\"),  # Unknown extension fallback\n        ],\n    )\n    async def test_tool_audio_suffix_detection(self, tmp_path: Path, filename: str, expected_mime_type: str):\n        \"\"\"Test that Audio helper correctly detects MIME types from file suffixes\"\"\"\n        mcp = MCPServer()\n        mcp.add_tool(audio_tool_fn)\n\n        # Create a test audio file with the specific extension\n        audio_path = tmp_path / filename\n        audio_path.write_bytes(b\"fake audio data\")\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"audio_tool_fn\", {\"path\": str(audio_path)})\n            assert len(result.content) == 1\n            content = result.content[0]\n            assert isinstance(content, AudioContent)\n            assert content.type == \"audio\"\n            assert content.mime_type == expected_mime_type\n            # Verify base64 encoding\n            decoded = base64.b64decode(content.data)\n            assert decoded == b\"fake audio data\"\n\n    async def test_tool_mixed_content(self):\n        mcp = MCPServer()\n        mcp.add_tool(mixed_content_tool_fn)\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"mixed_content_tool_fn\", {})\n            assert len(result.content) == 3\n            content1, content2, content3 = result.content\n            assert isinstance(content1, TextContent)\n            assert content1.text == \"Hello\"\n            assert isinstance(content2, ImageContent)\n            assert content2.mime_type == \"image/png\"\n            assert content2.data == \"abc\"\n            assert isinstance(content3, AudioContent)\n            assert content3.mime_type == \"audio/wav\"\n            assert content3.data == \"def\"\n            assert result.structured_content is not None\n            assert \"result\" in result.structured_content\n            structured_result = result.structured_content[\"result\"]\n            assert len(structured_result) == 3\n\n            expected_content = [\n                {\"type\": \"text\", \"text\": \"Hello\"},\n                {\"type\": \"image\", \"data\": \"abc\", \"mimeType\": \"image/png\"},\n                {\"type\": \"audio\", \"data\": \"def\", \"mimeType\": \"audio/wav\"},\n            ]\n\n            for i, expected in enumerate(expected_content):\n                for key, value in expected.items():\n                    assert structured_result[i][key] == value\n\n    async def test_tool_mixed_list_with_audio_and_image(self, tmp_path: Path):\n        \"\"\"Test that lists containing Image objects and other types are handled\n        correctly\"\"\"\n        # Create a test image\n        image_path = tmp_path / \"test.png\"\n        image_path.write_bytes(b\"test image data\")\n\n        # Create a test audio\n        audio_path = tmp_path / \"test.wav\"\n        audio_path.write_bytes(b\"test audio data\")\n\n        # TODO(Marcelo): It seems if we add the proper type hint, it generates an invalid JSON schema.\n        # We need to fix this.\n        def mixed_list_fn() -> list:  # type: ignore\n            return [  # type: ignore\n                \"text message\",\n                Image(image_path),\n                Audio(audio_path),\n                {\"key\": \"value\"},\n                TextContent(type=\"text\", text=\"direct content\"),\n            ]\n\n        mcp = MCPServer()\n        mcp.add_tool(mixed_list_fn)  # type: ignore\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"mixed_list_fn\", {})\n            assert len(result.content) == 5\n            # Check text conversion\n            content1 = result.content[0]\n            assert isinstance(content1, TextContent)\n            assert content1.text == \"text message\"\n            # Check image conversion\n            content2 = result.content[1]\n            assert isinstance(content2, ImageContent)\n            assert content2.mime_type == \"image/png\"\n            assert base64.b64decode(content2.data) == b\"test image data\"\n            # Check audio conversion\n            content3 = result.content[2]\n            assert isinstance(content3, AudioContent)\n            assert content3.mime_type == \"audio/wav\"\n            assert base64.b64decode(content3.data) == b\"test audio data\"\n            # Check dict conversion\n            content4 = result.content[3]\n            assert isinstance(content4, TextContent)\n            assert '\"key\": \"value\"' in content4.text\n            # Check direct TextContent\n            content5 = result.content[4]\n            assert isinstance(content5, TextContent)\n            assert content5.text == \"direct content\"\n            # Check structured content - untyped list with Image objects should NOT have structured output\n            assert result.structured_content is None\n\n    async def test_tool_structured_output_basemodel(self):\n        \"\"\"Test tool with structured output returning BaseModel\"\"\"\n\n        class UserOutput(BaseModel):\n            name: str\n            age: int\n            active: bool = True\n\n        def get_user(user_id: int) -> UserOutput:\n            \"\"\"Get user by ID\"\"\"\n            return UserOutput(name=\"John Doe\", age=30)\n\n        mcp = MCPServer()\n        mcp.add_tool(get_user)\n\n        async with Client(mcp) as client:\n            # Check that the tool has outputSchema\n            tools = await client.list_tools()\n            tool = next(t for t in tools.tools if t.name == \"get_user\")\n            assert tool.output_schema is not None\n            assert tool.output_schema[\"type\"] == \"object\"\n            assert \"name\" in tool.output_schema[\"properties\"]\n            assert \"age\" in tool.output_schema[\"properties\"]\n\n            # Call the tool and check structured output\n            result = await client.call_tool(\"get_user\", {\"user_id\": 123})\n            assert result.is_error is False\n            assert result.structured_content is not None\n            assert result.structured_content == {\"name\": \"John Doe\", \"age\": 30, \"active\": True}\n            # Content should be JSON serialized version\n            assert len(result.content) == 1\n            assert isinstance(result.content[0], TextContent)\n            assert '\"name\": \"John Doe\"' in result.content[0].text\n\n    async def test_tool_structured_output_primitive(self):\n        \"\"\"Test tool with structured output returning primitive type\"\"\"\n\n        def calculate_sum(a: int, b: int) -> int:\n            \"\"\"Add two numbers\"\"\"\n            return a + b\n\n        mcp = MCPServer()\n        mcp.add_tool(calculate_sum)\n\n        async with Client(mcp) as client:\n            # Check that the tool has outputSchema\n            tools = await client.list_tools()\n            tool = next(t for t in tools.tools if t.name == \"calculate_sum\")\n            assert tool.output_schema is not None\n            # Primitive types are wrapped\n            assert tool.output_schema[\"type\"] == \"object\"\n            assert \"result\" in tool.output_schema[\"properties\"]\n            assert tool.output_schema[\"properties\"][\"result\"][\"type\"] == \"integer\"\n\n            # Call the tool\n            result = await client.call_tool(\"calculate_sum\", {\"a\": 5, \"b\": 7})\n            assert result.is_error is False\n            assert result.structured_content is not None\n            assert result.structured_content == {\"result\": 12}\n\n    async def test_tool_structured_output_list(self):\n        \"\"\"Test tool with structured output returning list\"\"\"\n\n        def get_numbers() -> list[int]:\n            \"\"\"Get a list of numbers\"\"\"\n            return [1, 2, 3, 4, 5]\n\n        mcp = MCPServer()\n        mcp.add_tool(get_numbers)\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"get_numbers\", {})\n            assert result.is_error is False\n            assert result.structured_content is not None\n            assert result.structured_content == {\"result\": [1, 2, 3, 4, 5]}\n\n    async def test_tool_structured_output_server_side_validation_error(self):\n        \"\"\"Test that server-side validation errors are handled properly\"\"\"\n\n        def get_numbers() -> list[int]:\n            return [1, 2, 3, 4, [5]]  # type: ignore\n\n        mcp = MCPServer()\n        mcp.add_tool(get_numbers)\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"get_numbers\", {})\n            assert result.is_error is True\n            assert result.structured_content is None\n            assert len(result.content) == 1\n            assert isinstance(result.content[0], TextContent)\n\n    async def test_tool_structured_output_dict_str_any(self):\n        \"\"\"Test tool with dict[str, Any] structured output\"\"\"\n\n        def get_metadata() -> dict[str, Any]:\n            \"\"\"Get metadata dictionary\"\"\"\n            return {\n                \"version\": \"1.0.0\",\n                \"enabled\": True,\n                \"count\": 42,\n                \"tags\": [\"production\", \"stable\"],\n                \"config\": {\"nested\": {\"value\": 123}},\n            }\n\n        mcp = MCPServer()\n        mcp.add_tool(get_metadata)\n\n        async with Client(mcp) as client:\n            # Check schema\n            tools = await client.list_tools()\n            tool = next(t for t in tools.tools if t.name == \"get_metadata\")\n            assert tool.output_schema is not None\n            assert tool.output_schema[\"type\"] == \"object\"\n            # dict[str, Any] should have minimal schema\n            assert (\n                \"additionalProperties\" not in tool.output_schema\n                or tool.output_schema.get(\"additionalProperties\") is True\n            )\n\n            # Call tool\n            result = await client.call_tool(\"get_metadata\", {})\n            assert result.is_error is False\n            assert result.structured_content is not None\n            expected = {\n                \"version\": \"1.0.0\",\n                \"enabled\": True,\n                \"count\": 42,\n                \"tags\": [\"production\", \"stable\"],\n                \"config\": {\"nested\": {\"value\": 123}},\n            }\n            assert result.structured_content == expected\n\n    async def test_tool_structured_output_dict_str_typed(self):\n        \"\"\"Test tool with dict[str, T] structured output for specific T\"\"\"\n\n        def get_settings() -> dict[str, str]:\n            \"\"\"Get settings as string dictionary\"\"\"\n            return {\"theme\": \"dark\", \"language\": \"en\", \"timezone\": \"UTC\"}\n\n        mcp = MCPServer()\n        mcp.add_tool(get_settings)\n\n        async with Client(mcp) as client:\n            # Check schema\n            tools = await client.list_tools()\n            tool = next(t for t in tools.tools if t.name == \"get_settings\")\n            assert tool.output_schema is not None\n            assert tool.output_schema[\"type\"] == \"object\"\n            assert tool.output_schema[\"additionalProperties\"][\"type\"] == \"string\"\n\n            # Call tool\n            result = await client.call_tool(\"get_settings\", {})\n            assert result.is_error is False\n            assert result.structured_content == {\"theme\": \"dark\", \"language\": \"en\", \"timezone\": \"UTC\"}\n\n    async def test_remove_tool(self):\n        \"\"\"Test removing a tool from the server.\"\"\"\n        mcp = MCPServer()\n        mcp.add_tool(tool_fn)\n\n        # Verify tool exists\n        assert len(mcp._tool_manager.list_tools()) == 1\n\n        # Remove the tool\n        mcp.remove_tool(\"tool_fn\")\n\n        # Verify tool is removed\n        assert len(mcp._tool_manager.list_tools()) == 0\n\n    async def test_remove_nonexistent_tool(self):\n        \"\"\"Test that removing a non-existent tool raises ToolError.\"\"\"\n        mcp = MCPServer()\n\n        with pytest.raises(ToolError, match=\"Unknown tool: nonexistent\"):\n            mcp.remove_tool(\"nonexistent\")\n\n    async def test_remove_tool_and_list(self):\n        \"\"\"Test that a removed tool doesn't appear in list_tools.\"\"\"\n        mcp = MCPServer()\n        mcp.add_tool(tool_fn)\n        mcp.add_tool(error_tool_fn)\n\n        # Verify both tools exist\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            assert len(tools.tools) == 2\n            tool_names = [t.name for t in tools.tools]\n            assert \"tool_fn\" in tool_names\n            assert \"error_tool_fn\" in tool_names\n\n        # Remove one tool\n        mcp.remove_tool(\"tool_fn\")\n\n        # Verify only one tool remains\n        async with Client(mcp) as client:\n            tools = await client.list_tools()\n            assert len(tools.tools) == 1\n            assert tools.tools[0].name == \"error_tool_fn\"\n\n    async def test_remove_tool_and_call(self):\n        \"\"\"Test that calling a removed tool fails appropriately.\"\"\"\n        mcp = MCPServer()\n        mcp.add_tool(tool_fn)\n\n        # Verify tool works before removal\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"tool_fn\", {\"x\": 1, \"y\": 2})\n            assert not result.is_error\n            content = result.content[0]\n            assert isinstance(content, TextContent)\n            assert content.text == \"3\"\n\n        # Remove the tool\n        mcp.remove_tool(\"tool_fn\")\n\n        # Verify calling removed tool returns an error\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"tool_fn\", {\"x\": 1, \"y\": 2})\n            assert result.is_error\n            content = result.content[0]\n            assert isinstance(content, TextContent)\n            assert \"Unknown tool\" in content.text\n\n\nclass TestServerResources:\n    async def test_text_resource(self):\n        mcp = MCPServer()\n\n        def get_text():\n            return \"Hello, world!\"\n\n        resource = FunctionResource(uri=\"resource://test\", name=\"test\", fn=get_text)\n        mcp.add_resource(resource)\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"resource://test\")\n\n            assert isinstance(result.contents[0], TextResourceContents)\n            assert result.contents[0].text == \"Hello, world!\"\n\n    async def test_read_unknown_resource(self):\n        \"\"\"Test that reading an unknown resource raises MCPError.\"\"\"\n        mcp = MCPServer()\n\n        async with Client(mcp) as client:\n            with pytest.raises(MCPError, match=\"Unknown resource: unknown://missing\"):\n                await client.read_resource(\"unknown://missing\")\n\n    async def test_read_resource_error(self):\n        \"\"\"Test that resource read errors are properly wrapped in MCPError.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://failing\")\n        def failing_resource():\n            raise ValueError(\"Resource read failed\")\n\n        async with Client(mcp) as client:\n            with pytest.raises(MCPError, match=\"Error reading resource resource://failing\"):\n                await client.read_resource(\"resource://failing\")\n\n    async def test_binary_resource(self):\n        mcp = MCPServer()\n\n        def get_binary():\n            return b\"Binary data\"\n\n        resource = FunctionResource(\n            uri=\"resource://binary\",\n            name=\"binary\",\n            fn=get_binary,\n            mime_type=\"application/octet-stream\",\n        )\n        mcp.add_resource(resource)\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"resource://binary\")\n\n            assert isinstance(result.contents[0], BlobResourceContents)\n            assert result.contents[0].blob == base64.b64encode(b\"Binary data\").decode()\n\n    async def test_file_resource_text(self, tmp_path: Path):\n        mcp = MCPServer()\n\n        # Create a text file\n        text_file = tmp_path / \"test.txt\"\n        text_file.write_text(\"Hello from file!\")\n\n        resource = FileResource(uri=\"file://test.txt\", name=\"test.txt\", path=text_file)\n        mcp.add_resource(resource)\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"file://test.txt\")\n\n            assert isinstance(result.contents[0], TextResourceContents)\n            assert result.contents[0].text == \"Hello from file!\"\n\n    async def test_file_resource_binary(self, tmp_path: Path):\n        mcp = MCPServer()\n\n        # Create a binary file\n        binary_file = tmp_path / \"test.bin\"\n        binary_file.write_bytes(b\"Binary file data\")\n\n        resource = FileResource(\n            uri=\"file://test.bin\",\n            name=\"test.bin\",\n            path=binary_file,\n            mime_type=\"application/octet-stream\",\n        )\n        mcp.add_resource(resource)\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"file://test.bin\")\n\n            assert isinstance(result.contents[0], BlobResourceContents)\n            assert result.contents[0].blob == base64.b64encode(b\"Binary file data\").decode()\n\n    async def test_function_resource(self):\n        mcp = MCPServer()\n\n        @mcp.resource(\"function://test\", name=\"test_get_data\")\n        def get_data() -> str:  # pragma: no cover\n            \"\"\"get_data returns a string\"\"\"\n            return \"Hello, world!\"\n\n        async with Client(mcp) as client:\n            resources = await client.list_resources()\n            assert len(resources.resources) == 1\n            resource = resources.resources[0]\n            assert resource.description == \"get_data returns a string\"\n            assert resource.uri == \"function://test\"\n            assert resource.name == \"test_get_data\"\n            assert resource.mime_type == \"text/plain\"\n\n\nclass TestServerResourceTemplates:\n    async def test_resource_with_params(self):\n        \"\"\"Test that a resource with function parameters raises an error if the URI\n        parameters don't match\"\"\"\n        mcp = MCPServer()\n\n        with pytest.raises(ValueError, match=\"Mismatch between URI parameters\"):\n\n            @mcp.resource(\"resource://data\")\n            def get_data_fn(param: str) -> str:  # pragma: no cover\n                return f\"Data: {param}\"\n\n    async def test_resource_with_uri_params(self):\n        \"\"\"Test that a resource with URI parameters is automatically a template\"\"\"\n        mcp = MCPServer()\n\n        with pytest.raises(ValueError, match=\"Mismatch between URI parameters\"):\n\n            @mcp.resource(\"resource://{param}\")\n            def get_data() -> str:  # pragma: no cover\n                return \"Data\"\n\n    async def test_resource_with_untyped_params(self):\n        \"\"\"Test that a resource with untyped parameters raises an error\"\"\"\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://{param}\")\n        def get_data(param) -> str:  # type: ignore  # pragma: no cover\n            return \"Data\"\n\n    async def test_resource_matching_params(self):\n        \"\"\"Test that a resource with matching URI and function parameters works\"\"\"\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://{name}/data\")\n        def get_data(name: str) -> str:\n            return f\"Data for {name}\"\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"resource://test/data\")\n\n            assert isinstance(result.contents[0], TextResourceContents)\n            assert result.contents[0].text == \"Data for test\"\n\n    async def test_resource_mismatched_params(self):\n        \"\"\"Test that mismatched parameters raise an error\"\"\"\n        mcp = MCPServer()\n\n        with pytest.raises(ValueError, match=\"Mismatch between URI parameters\"):\n\n            @mcp.resource(\"resource://{name}/data\")\n            def get_data(user: str) -> str:  # pragma: no cover\n                return f\"Data for {user}\"\n\n    async def test_resource_multiple_params(self):\n        \"\"\"Test that multiple parameters work correctly\"\"\"\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://{org}/{repo}/data\")\n        def get_data(org: str, repo: str) -> str:\n            return f\"Data for {org}/{repo}\"\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"resource://cursor/myrepo/data\")\n\n            assert isinstance(result.contents[0], TextResourceContents)\n            assert result.contents[0].text == \"Data for cursor/myrepo\"\n\n    async def test_resource_multiple_mismatched_params(self):\n        \"\"\"Test that mismatched parameters raise an error\"\"\"\n        mcp = MCPServer()\n\n        with pytest.raises(ValueError, match=\"Mismatch between URI parameters\"):\n\n            @mcp.resource(\"resource://{org}/{repo}/data\")\n            def get_data_mismatched(org: str, repo_2: str) -> str:  # pragma: no cover\n                return f\"Data for {org}\"\n\n        \"\"\"Test that a resource with no parameters works as a regular resource\"\"\"\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://static\")\n        def get_static_data() -> str:\n            return \"Static data\"\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"resource://static\")\n\n            assert isinstance(result.contents[0], TextResourceContents)\n            assert result.contents[0].text == \"Static data\"\n\n    async def test_template_to_resource_conversion(self):\n        \"\"\"Test that templates are properly converted to resources when accessed\"\"\"\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://{name}/data\")\n        def get_data(name: str) -> str:\n            return f\"Data for {name}\"\n\n        # Should be registered as a template\n        assert len(mcp._resource_manager._templates) == 1\n        assert len(await mcp.list_resources()) == 0\n\n        # When accessed, should create a concrete resource\n        resource = await mcp._resource_manager.get_resource(\"resource://test/data\", Context())\n        assert isinstance(resource, FunctionResource)\n        result = await resource.read()\n        assert result == \"Data for test\"\n\n    async def test_resource_template_includes_mime_type(self):\n        \"\"\"Test that list resource templates includes the correct mimeType.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://{user}/csv\", mime_type=\"text/csv\")\n        def get_csv(user: str) -> str:\n            return f\"csv for {user}\"\n\n        templates = await mcp.list_resource_templates()\n        assert templates == snapshot(\n            [\n                ResourceTemplate(\n                    name=\"get_csv\", uri_template=\"resource://{user}/csv\", description=\"\", mime_type=\"text/csv\"\n                )\n            ]\n        )\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"resource://bob/csv\")\n            assert result == snapshot(\n                ReadResourceResult(\n                    contents=[TextResourceContents(uri=\"resource://bob/csv\", mime_type=\"text/csv\", text=\"csv for bob\")]\n                )\n            )\n\n\nclass TestServerResourceMetadata:\n    \"\"\"Test MCPServer @resource decorator meta parameter for list operations.\n\n    Meta flows: @resource decorator -> resource/template storage -> list_resources/list_resource_templates.\n    Note: read_resource does NOT pass meta to protocol response (lowlevel/server.py only extracts content/mime_type).\n    \"\"\"\n\n    async def test_resource_decorator_with_metadata(self):\n        \"\"\"Test that @resource decorator accepts and passes meta parameter.\"\"\"\n        # Tests static resource flow: decorator -> FunctionResource -> list_resources (server.py:544,635,361)\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://config\", meta={\"ui\": {\"component\": \"file-viewer\"}, \"priority\": \"high\"})\n        def get_config() -> str: ...  # pragma: no branch\n\n        resources = await mcp.list_resources()\n        assert resources == snapshot(\n            [\n                Resource(\n                    name=\"get_config\",\n                    uri=\"resource://config\",\n                    description=\"\",\n                    mime_type=\"text/plain\",\n                    meta={\"ui\": {\"component\": \"file-viewer\"}, \"priority\": \"high\"},  # type: ignore[reportCallIssue]\n                )\n            ]\n        )\n\n    async def test_resource_template_decorator_with_metadata(self):\n        \"\"\"Test that @resource decorator passes meta to templates.\"\"\"\n        # Tests template resource flow: decorator -> add_template() -> list_resource_templates (server.py:544,622,377)\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://{city}/weather\", meta={\"api_version\": \"v2\", \"deprecated\": False})\n        def get_weather(city: str) -> str: ...  # pragma: no branch\n\n        templates = await mcp.list_resource_templates()\n        assert templates == snapshot(\n            [\n                ResourceTemplate(\n                    name=\"get_weather\",\n                    uri_template=\"resource://{city}/weather\",\n                    description=\"\",\n                    mime_type=\"text/plain\",\n                    meta={\"api_version\": \"v2\", \"deprecated\": False},  # type: ignore[reportCallIssue]\n                )\n            ]\n        )\n\n    async def test_read_resource_returns_meta(self):\n        \"\"\"Test that read_resource includes meta in response.\"\"\"\n        # Tests end-to-end: Resource.meta -> ReadResourceContents.meta -> protocol _meta (lowlevel/server.py:341,371)\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://data\", meta={\"version\": \"1.0\", \"category\": \"config\"})\n        def get_data() -> str:\n            return \"test data\"\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"resource://data\")\n            assert result == snapshot(\n                ReadResourceResult(\n                    contents=[\n                        TextResourceContents(\n                            uri=\"resource://data\",\n                            mime_type=\"text/plain\",\n                            meta={\"version\": \"1.0\", \"category\": \"config\"},  # type: ignore[reportUnknownMemberType]\n                            text=\"test data\",\n                        )\n                    ]\n                )\n            )\n\n\nclass TestContextInjection:\n    \"\"\"Test context injection in tools, resources, and prompts.\"\"\"\n\n    async def test_context_detection(self):\n        \"\"\"Test that context parameters are properly detected.\"\"\"\n        mcp = MCPServer()\n\n        def tool_with_context(x: int, ctx: Context) -> str:  # pragma: no cover\n            return f\"Request {ctx.request_id}: {x}\"\n\n        tool = mcp._tool_manager.add_tool(tool_with_context)\n        assert tool.context_kwarg == \"ctx\"\n\n    async def test_context_injection(self):\n        \"\"\"Test that context is properly injected into tool calls.\"\"\"\n        mcp = MCPServer()\n\n        def tool_with_context(x: int, ctx: Context) -> str:\n            assert ctx.request_id is not None\n            return f\"Request {ctx.request_id}: {x}\"\n\n        mcp.add_tool(tool_with_context)\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"tool_with_context\", {\"x\": 42})\n            assert len(result.content) == 1\n            content = result.content[0]\n            assert isinstance(content, TextContent)\n            assert \"Request\" in content.text\n            assert \"42\" in content.text\n\n    async def test_async_context(self):\n        \"\"\"Test that context works in async functions.\"\"\"\n        mcp = MCPServer()\n\n        async def async_tool(x: int, ctx: Context) -> str:\n            assert ctx.request_id is not None\n            return f\"Async request {ctx.request_id}: {x}\"\n\n        mcp.add_tool(async_tool)\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"async_tool\", {\"x\": 42})\n            assert len(result.content) == 1\n            content = result.content[0]\n            assert isinstance(content, TextContent)\n            assert \"Async request\" in content.text\n            assert \"42\" in content.text\n\n    async def test_context_logging(self):\n        \"\"\"Test that context logging methods work.\"\"\"\n        mcp = MCPServer()\n\n        async def logging_tool(msg: str, ctx: Context) -> str:\n            await ctx.debug(\"Debug message\")\n            await ctx.info(\"Info message\")\n            await ctx.warning(\"Warning message\")\n            await ctx.error(\"Error message\")\n            return f\"Logged messages for {msg}\"\n\n        mcp.add_tool(logging_tool)\n\n        with patch(\"mcp.server.session.ServerSession.send_log_message\") as mock_log:\n            async with Client(mcp) as client:\n                result = await client.call_tool(\"logging_tool\", {\"msg\": \"test\"})\n                assert len(result.content) == 1\n                content = result.content[0]\n                assert isinstance(content, TextContent)\n                assert \"Logged messages for test\" in content.text\n\n                assert mock_log.call_count == 4\n                mock_log.assert_any_call(level=\"debug\", data=\"Debug message\", logger=None, related_request_id=\"1\")\n                mock_log.assert_any_call(level=\"info\", data=\"Info message\", logger=None, related_request_id=\"1\")\n                mock_log.assert_any_call(level=\"warning\", data=\"Warning message\", logger=None, related_request_id=\"1\")\n                mock_log.assert_any_call(level=\"error\", data=\"Error message\", logger=None, related_request_id=\"1\")\n\n    async def test_optional_context(self):\n        \"\"\"Test that context is optional.\"\"\"\n        mcp = MCPServer()\n\n        def no_context(x: int) -> int:\n            return x * 2\n\n        mcp.add_tool(no_context)\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"no_context\", {\"x\": 21})\n            assert len(result.content) == 1\n            content = result.content[0]\n            assert isinstance(content, TextContent)\n            assert content.text == \"42\"\n\n    async def test_context_resource_access(self):\n        \"\"\"Test that context can access resources.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.resource(\"test://data\")\n        def test_resource() -> str:\n            return \"resource data\"\n\n        @mcp.tool()\n        async def tool_with_resource(ctx: Context) -> str:\n            r_iter = await ctx.read_resource(\"test://data\")\n            r_list = list(r_iter)\n            assert len(r_list) == 1\n            r = r_list[0]\n            return f\"Read resource: {r.content} with mime type {r.mime_type}\"\n\n        async with Client(mcp) as client:\n            result = await client.call_tool(\"tool_with_resource\", {})\n            assert len(result.content) == 1\n            content = result.content[0]\n            assert isinstance(content, TextContent)\n            assert \"Read resource: resource data\" in content.text\n\n    async def test_resource_with_context(self):\n        \"\"\"Test that resources can receive context parameter.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://context/{name}\")\n        def resource_with_context(name: str, ctx: Context) -> str:\n            \"\"\"Resource that receives context.\"\"\"\n            assert ctx is not None\n            return f\"Resource {name} - context injected\"\n\n        # Verify template has context_kwarg set\n        templates = mcp._resource_manager.list_templates()\n        assert len(templates) == 1\n        template = templates[0]\n        assert hasattr(template, \"context_kwarg\")\n        assert template.context_kwarg == \"ctx\"\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"resource://context/test\")\n\n            assert len(result.contents) == 1\n            content = result.contents[0]\n            assert isinstance(content, TextResourceContents)\n            # Should have either request_id or indication that context was injected\n            assert \"Resource test - context injected\" == content.text\n\n    async def test_resource_without_context(self):\n        \"\"\"Test that resources without context work normally.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://nocontext/{name}\")\n        def resource_no_context(name: str) -> str:\n            \"\"\"Resource without context.\"\"\"\n            return f\"Resource {name} works\"\n\n        # Verify template has no context_kwarg\n        templates = mcp._resource_manager.list_templates()\n        assert len(templates) == 1\n        template = templates[0]\n        assert template.context_kwarg is None\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"resource://nocontext/test\")\n            assert result == snapshot(\n                ReadResourceResult(\n                    contents=[\n                        TextResourceContents(\n                            uri=\"resource://nocontext/test\", mime_type=\"text/plain\", text=\"Resource test works\"\n                        )\n                    ]\n                )\n            )\n\n    async def test_resource_context_custom_name(self):\n        \"\"\"Test resource context with custom parameter name.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.resource(\"resource://custom/{id}\")\n        def resource_custom_ctx(id: str, my_ctx: Context) -> str:\n            \"\"\"Resource with custom context parameter name.\"\"\"\n            assert my_ctx is not None\n            return f\"Resource {id} with context\"\n\n        # Verify template detects custom context parameter\n        templates = mcp._resource_manager.list_templates()\n        assert len(templates) == 1\n        template = templates[0]\n        assert template.context_kwarg == \"my_ctx\"\n\n        async with Client(mcp) as client:\n            result = await client.read_resource(\"resource://custom/123\")\n            assert result == snapshot(\n                ReadResourceResult(\n                    contents=[\n                        TextResourceContents(\n                            uri=\"resource://custom/123\", mime_type=\"text/plain\", text=\"Resource 123 with context\"\n                        )\n                    ]\n                )\n            )\n\n    async def test_prompt_with_context(self):\n        \"\"\"Test that prompts can receive context parameter.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.prompt(\"prompt_with_ctx\")\n        def prompt_with_context(text: str, ctx: Context) -> str:\n            \"\"\"Prompt that expects context.\"\"\"\n            assert ctx is not None\n            return f\"Prompt '{text}' - context injected\"\n\n        # Test via client\n        async with Client(mcp) as client:\n            # Try calling without passing ctx explicitly\n            result = await client.get_prompt(\"prompt_with_ctx\", {\"text\": \"test\"})\n            # If this succeeds, check if context was injected\n            assert len(result.messages) == 1\n            content = result.messages[0].content\n            assert isinstance(content, TextContent)\n            assert \"Prompt 'test' - context injected\" in content.text\n\n    async def test_prompt_without_context(self):\n        \"\"\"Test that prompts without context work normally.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.prompt(\"prompt_no_ctx\")\n        def prompt_no_context(text: str) -> str:\n            \"\"\"Prompt without context.\"\"\"\n            return f\"Prompt '{text}' works\"\n\n        # Test via client\n        async with Client(mcp) as client:\n            result = await client.get_prompt(\"prompt_no_ctx\", {\"text\": \"test\"})\n            assert len(result.messages) == 1\n            message = result.messages[0]\n            content = message.content\n            assert isinstance(content, TextContent)\n            assert content.text == \"Prompt 'test' works\"\n\n\nclass TestServerPrompts:\n    \"\"\"Test prompt functionality in MCPServer server.\"\"\"\n\n    async def test_get_prompt_direct_call_without_context(self):\n        \"\"\"Test calling mcp.get_prompt() directly without passing context.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.prompt()\n        def fn() -> str:\n            return \"Hello, world!\"\n\n        result = await mcp.get_prompt(\"fn\")\n        content = result.messages[0].content\n        assert isinstance(content, TextContent)\n        assert content.text == \"Hello, world!\"\n\n    async def test_prompt_decorator(self):\n        \"\"\"Test that the prompt decorator registers prompts correctly.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.prompt()\n        def fn() -> str:\n            return \"Hello, world!\"\n\n        prompts = mcp._prompt_manager.list_prompts()\n        assert len(prompts) == 1\n        assert prompts[0].name == \"fn\"\n        # Don't compare functions directly since validate_call wraps them\n        content = await prompts[0].render(None, Context())\n        assert isinstance(content[0].content, TextContent)\n        assert content[0].content.text == \"Hello, world!\"\n\n    async def test_prompt_decorator_with_name(self):\n        \"\"\"Test prompt decorator with custom name.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.prompt(name=\"custom_name\")\n        def fn() -> str:\n            return \"Hello, world!\"\n\n        prompts = mcp._prompt_manager.list_prompts()\n        assert len(prompts) == 1\n        assert prompts[0].name == \"custom_name\"\n        content = await prompts[0].render(None, Context())\n        assert isinstance(content[0].content, TextContent)\n        assert content[0].content.text == \"Hello, world!\"\n\n    async def test_prompt_decorator_with_description(self):\n        \"\"\"Test prompt decorator with custom description.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.prompt(description=\"A custom description\")\n        def fn() -> str:\n            return \"Hello, world!\"\n\n        prompts = mcp._prompt_manager.list_prompts()\n        assert len(prompts) == 1\n        assert prompts[0].description == \"A custom description\"\n        content = await prompts[0].render(None, Context())\n        assert isinstance(content[0].content, TextContent)\n        assert content[0].content.text == \"Hello, world!\"\n\n    def test_prompt_decorator_error(self):\n        \"\"\"Test error when decorator is used incorrectly.\"\"\"\n        mcp = MCPServer()\n        with pytest.raises(TypeError, match=\"decorator was used incorrectly\"):\n\n            @mcp.prompt  # type: ignore\n            def fn() -> str: ...  # pragma: no branch\n\n    async def test_list_prompts(self):\n        \"\"\"Test listing prompts through MCP protocol.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.prompt()\n        def fn(name: str, optional: str = \"default\") -> str: ...  # pragma: no branch\n\n        async with Client(mcp) as client:\n            result = await client.list_prompts()\n            assert result == snapshot(\n                ListPromptsResult(\n                    prompts=[\n                        Prompt(\n                            name=\"fn\",\n                            description=\"\",\n                            arguments=[\n                                PromptArgument(name=\"name\", required=True),\n                                PromptArgument(name=\"optional\", required=False),\n                            ],\n                        )\n                    ]\n                )\n            )\n\n    async def test_get_prompt(self):\n        \"\"\"Test getting a prompt through MCP protocol.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.prompt()\n        def fn(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        async with Client(mcp) as client:\n            result = await client.get_prompt(\"fn\", {\"name\": \"World\"})\n            assert result == snapshot(\n                GetPromptResult(\n                    description=\"\",\n                    messages=[PromptMessage(role=\"user\", content=TextContent(text=\"Hello, World!\"))],\n                )\n            )\n\n    async def test_get_prompt_with_description(self):\n        \"\"\"Test getting a prompt through MCP protocol.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.prompt(description=\"Test prompt description\")\n        def fn(name: str) -> str:\n            return f\"Hello, {name}!\"\n\n        async with Client(mcp) as client:\n            result = await client.get_prompt(\"fn\", {\"name\": \"World\"})\n            assert result.description == \"Test prompt description\"\n\n    async def test_get_prompt_with_docstring_description(self):\n        \"\"\"Test prompt uses docstring as description when not explicitly provided.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.prompt()\n        def fn(name: str) -> str:\n            \"\"\"This is the function docstring.\"\"\"\n            return f\"Hello, {name}!\"\n\n        async with Client(mcp) as client:\n            result = await client.get_prompt(\"fn\", {\"name\": \"World\"})\n            assert result == snapshot(\n                GetPromptResult(\n                    description=\"This is the function docstring.\",\n                    messages=[PromptMessage(role=\"user\", content=TextContent(text=\"Hello, World!\"))],\n                )\n            )\n\n    async def test_get_prompt_with_resource(self):\n        \"\"\"Test getting a prompt that returns resource content.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.prompt()\n        def fn() -> Message:\n            return UserMessage(\n                content=EmbeddedResource(\n                    type=\"resource\",\n                    resource=TextResourceContents(uri=\"file://file.txt\", text=\"File contents\", mime_type=\"text/plain\"),\n                )\n            )\n\n        async with Client(mcp) as client:\n            result = await client.get_prompt(\"fn\")\n            assert result == snapshot(\n                GetPromptResult(\n                    description=\"\",\n                    messages=[\n                        PromptMessage(\n                            role=\"user\",\n                            content=EmbeddedResource(\n                                resource=TextResourceContents(\n                                    uri=\"file://file.txt\", mime_type=\"text/plain\", text=\"File contents\"\n                                )\n                            ),\n                        )\n                    ],\n                )\n            )\n\n    async def test_get_unknown_prompt(self):\n        \"\"\"Test error when getting unknown prompt.\"\"\"\n        mcp = MCPServer()\n\n        async with Client(mcp) as client:\n            with pytest.raises(MCPError, match=\"Unknown prompt\"):\n                await client.get_prompt(\"unknown\")\n\n    async def test_get_prompt_missing_args(self):\n        \"\"\"Test error when required arguments are missing.\"\"\"\n        mcp = MCPServer()\n\n        @mcp.prompt()\n        def prompt_fn(name: str) -> str: ...  # pragma: no branch\n\n        async with Client(mcp) as client:\n            with pytest.raises(MCPError, match=\"Missing required arguments\"):\n                await client.get_prompt(\"prompt_fn\")\n\n\nasync def test_completion_decorator() -> None:\n    \"\"\"Test that the completion decorator registers a working handler.\"\"\"\n    mcp = MCPServer()\n\n    @mcp.completion()\n    async def handle_completion(\n        ref: PromptReference, argument: CompletionArgument, context: CompletionContext | None\n    ) -> Completion:\n        assert argument.name == \"style\"\n        return Completion(values=[\"bold\", \"italic\", \"underline\"])\n\n    async with Client(mcp) as client:\n        ref = PromptReference(type=\"ref/prompt\", name=\"test\")\n        result = await client.complete(ref=ref, argument={\"name\": \"style\", \"value\": \"b\"})\n        assert result.completion.values == [\"bold\", \"italic\", \"underline\"]\n\n\ndef test_streamable_http_no_redirect() -> None:\n    \"\"\"Test that streamable HTTP routes are correctly configured.\"\"\"\n    mcp = MCPServer()\n    # streamable_http_path defaults to \"/mcp\"\n    app = mcp.streamable_http_app()\n\n    # Find routes by type - streamable_http_app creates Route objects, not Mount objects\n    streamable_routes = [r for r in app.routes if isinstance(r, Route) and hasattr(r, \"path\") and r.path == \"/mcp\"]\n\n    # Verify routes exist\n    assert len(streamable_routes) == 1, \"Should have one streamable route\"\n\n    # Verify path values\n    assert streamable_routes[0].path == \"/mcp\", \"Streamable route path should be /mcp\"\n\n\nasync def test_report_progress_passes_related_request_id():\n    \"\"\"Test that report_progress passes the request_id as related_request_id.\n\n    Without related_request_id, the streamable HTTP transport cannot route\n    progress notifications to the correct SSE stream, causing them to be\n    silently dropped. See #953 and #2001.\n    \"\"\"\n    mock_session = AsyncMock()\n    mock_session.send_progress_notification = AsyncMock()\n\n    request_context = ServerRequestContext(\n        request_id=\"req-abc-123\",\n        session=mock_session,\n        meta={\"progress_token\": \"tok-1\"},\n        lifespan_context=None,\n        experimental=Experimental(),\n    )\n\n    ctx = Context(request_context=request_context, mcp_server=MagicMock())\n\n    await ctx.report_progress(50, 100, message=\"halfway\")\n\n    mock_session.send_progress_notification.assert_awaited_once_with(\n        progress_token=\"tok-1\",\n        progress=50,\n        total=100,\n        message=\"halfway\",\n        related_request_id=\"req-abc-123\",\n    )\n"
  },
  {
    "path": "tests/server/mcpserver/test_title.py",
    "content": "\"\"\"Integration tests for title field functionality.\"\"\"\n\nimport pytest\n\nfrom mcp import Client\nfrom mcp.server.mcpserver import MCPServer\nfrom mcp.server.mcpserver.resources import FunctionResource\nfrom mcp.shared.metadata_utils import get_display_name\nfrom mcp.types import Prompt, Resource, ResourceTemplate, Tool, ToolAnnotations\n\n\n@pytest.mark.anyio\nasync def test_server_name_title_description_version():\n    \"\"\"Test that server title and description are set and retrievable correctly.\"\"\"\n    mcp = MCPServer(\n        name=\"TestServer\",\n        title=\"Test Server Title\",\n        description=\"This is a test server description.\",\n        version=\"1.0\",\n    )\n\n    assert mcp.title == \"Test Server Title\"\n    assert mcp.description == \"This is a test server description.\"\n    assert mcp.version == \"1.0\"\n\n    # Start server and connect client\n    async with Client(mcp) as client:\n        # Access initialization result from session\n        init_result = await client.session.initialize()\n        assert init_result.server_info.name == \"TestServer\"\n        assert init_result.server_info.title == \"Test Server Title\"\n        assert init_result.server_info.description == \"This is a test server description.\"\n        assert init_result.server_info.version == \"1.0\"\n\n\n@pytest.mark.anyio\nasync def test_tool_title_precedence():\n    \"\"\"Test that tool title precedence works correctly: title > annotations.title > name.\"\"\"\n    # Create server with various tool configurations\n    mcp = MCPServer(name=\"TitleTestServer\")\n\n    # Tool with only name\n    @mcp.tool(description=\"Basic tool\")\n    def basic_tool(message: str) -> str:  # pragma: no cover\n        return message\n\n    # Tool with title\n    @mcp.tool(description=\"Tool with title\", title=\"User-Friendly Tool\")\n    def tool_with_title(message: str) -> str:  # pragma: no cover\n        return message\n\n    # Tool with annotations.title (when title is not supported on decorator)\n    # We'll need to add this manually after registration\n    @mcp.tool(description=\"Tool with annotations\")\n    def tool_with_annotations(message: str) -> str:  # pragma: no cover\n        return message\n\n    # Tool with both title and annotations.title\n    @mcp.tool(description=\"Tool with both\", title=\"Primary Title\")\n    def tool_with_both(message: str) -> str:  # pragma: no cover\n        return message\n\n    # Start server and connect client\n    async with Client(mcp) as client:\n        # List tools\n        tools_result = await client.list_tools()\n        tools = {tool.name: tool for tool in tools_result.tools}\n\n        # Verify basic tool uses name\n        assert \"basic_tool\" in tools\n        basic = tools[\"basic_tool\"]\n        # Since we haven't implemented get_display_name yet, we'll check the raw fields\n        assert basic.title is None\n        assert basic.name == \"basic_tool\"\n\n        # Verify tool with title\n        assert \"tool_with_title\" in tools\n        titled = tools[\"tool_with_title\"]\n        assert titled.title == \"User-Friendly Tool\"\n\n        # For now, we'll skip the annotations.title test as it requires modifying\n        # the tool after registration, which we'll implement later\n\n        # Verify tool with both uses title over annotations.title\n        assert \"tool_with_both\" in tools\n        both = tools[\"tool_with_both\"]\n        assert both.title == \"Primary Title\"\n\n\n@pytest.mark.anyio\nasync def test_prompt_title():\n    \"\"\"Test that prompt titles work correctly.\"\"\"\n    mcp = MCPServer(name=\"PromptTitleServer\")\n\n    # Prompt with only name\n    @mcp.prompt(description=\"Basic prompt\")\n    def basic_prompt(topic: str) -> str:  # pragma: no cover\n        return f\"Tell me about {topic}\"\n\n    # Prompt with title\n    @mcp.prompt(description=\"Titled prompt\", title=\"Ask About Topic\")\n    def titled_prompt(topic: str) -> str:  # pragma: no cover\n        return f\"Tell me about {topic}\"\n\n    # Start server and connect client\n    async with Client(mcp) as client:\n        # List prompts\n        prompts_result = await client.list_prompts()\n        prompts = {prompt.name: prompt for prompt in prompts_result.prompts}\n\n        # Verify basic prompt uses name\n        assert \"basic_prompt\" in prompts\n        basic = prompts[\"basic_prompt\"]\n        assert basic.title is None\n        assert basic.name == \"basic_prompt\"\n\n        # Verify prompt with title\n        assert \"titled_prompt\" in prompts\n        titled = prompts[\"titled_prompt\"]\n        assert titled.title == \"Ask About Topic\"\n\n\n@pytest.mark.anyio\nasync def test_resource_title():\n    \"\"\"Test that resource titles work correctly.\"\"\"\n    mcp = MCPServer(name=\"ResourceTitleServer\")\n\n    # Static resource without title\n    def get_basic_data() -> str:  # pragma: no cover\n        return \"Basic data\"\n\n    basic_resource = FunctionResource(\n        uri=\"resource://basic\",\n        name=\"basic_resource\",\n        description=\"Basic resource\",\n        fn=get_basic_data,\n    )\n    mcp.add_resource(basic_resource)\n\n    # Static resource with title\n    def get_titled_data() -> str:  # pragma: no cover\n        return \"Titled data\"\n\n    titled_resource = FunctionResource(\n        uri=\"resource://titled\",\n        name=\"titled_resource\",\n        title=\"User-Friendly Resource\",\n        description=\"Resource with title\",\n        fn=get_titled_data,\n    )\n    mcp.add_resource(titled_resource)\n\n    # Dynamic resource without title\n    @mcp.resource(\"resource://dynamic/{id}\")\n    def dynamic_resource(id: str) -> str:  # pragma: no cover\n        return f\"Data for {id}\"\n\n    # Dynamic resource with title (when supported)\n    @mcp.resource(\"resource://titled-dynamic/{id}\", title=\"Dynamic Data\")\n    def titled_dynamic_resource(id: str) -> str:  # pragma: no cover\n        return f\"Data for {id}\"\n\n    # Start server and connect client\n    async with Client(mcp) as client:\n        # List resources\n        resources_result = await client.list_resources()\n        resources = {str(res.uri): res for res in resources_result.resources}\n\n        # Verify basic resource uses name\n        assert \"resource://basic\" in resources\n        basic = resources[\"resource://basic\"]\n        assert basic.title is None\n        assert basic.name == \"basic_resource\"\n\n        # Verify resource with title\n        assert \"resource://titled\" in resources\n        titled = resources[\"resource://titled\"]\n        assert titled.title == \"User-Friendly Resource\"\n\n        # List resource templates\n        templates_result = await client.list_resource_templates()\n        templates = {tpl.uri_template: tpl for tpl in templates_result.resource_templates}\n\n        # Verify dynamic resource template\n        assert \"resource://dynamic/{id}\" in templates\n        dynamic = templates[\"resource://dynamic/{id}\"]\n        assert dynamic.title is None\n        assert dynamic.name == \"dynamic_resource\"\n\n        # Verify titled dynamic resource template (when supported)\n        if \"resource://titled-dynamic/{id}\" in templates:  # pragma: no branch\n            titled_dynamic = templates[\"resource://titled-dynamic/{id}\"]\n            assert titled_dynamic.title == \"Dynamic Data\"\n\n\n@pytest.mark.anyio\nasync def test_get_display_name_utility():\n    \"\"\"Test the get_display_name utility function.\"\"\"\n\n    # Test tool precedence: title > annotations.title > name\n    tool_name_only = Tool(name=\"test_tool\", input_schema={})\n    assert get_display_name(tool_name_only) == \"test_tool\"\n\n    tool_with_title = Tool(name=\"test_tool\", title=\"Test Tool\", input_schema={})\n    assert get_display_name(tool_with_title) == \"Test Tool\"\n\n    tool_with_annotations = Tool(name=\"test_tool\", input_schema={}, annotations=ToolAnnotations(title=\"Annotated Tool\"))\n    assert get_display_name(tool_with_annotations) == \"Annotated Tool\"\n\n    tool_with_both = Tool(\n        name=\"test_tool\", title=\"Primary Title\", input_schema={}, annotations=ToolAnnotations(title=\"Secondary Title\")\n    )\n    assert get_display_name(tool_with_both) == \"Primary Title\"\n\n    # Test other types: title > name\n    resource = Resource(uri=\"file://test\", name=\"test_res\")\n    assert get_display_name(resource) == \"test_res\"\n\n    resource_with_title = Resource(uri=\"file://test\", name=\"test_res\", title=\"Test Resource\")\n    assert get_display_name(resource_with_title) == \"Test Resource\"\n\n    prompt = Prompt(name=\"test_prompt\")\n    assert get_display_name(prompt) == \"test_prompt\"\n\n    prompt_with_title = Prompt(name=\"test_prompt\", title=\"Test Prompt\")\n    assert get_display_name(prompt_with_title) == \"Test Prompt\"\n\n    template = ResourceTemplate(uri_template=\"file://{id}\", name=\"test_template\")\n    assert get_display_name(template) == \"test_template\"\n\n    template_with_title = ResourceTemplate(uri_template=\"file://{id}\", name=\"test_template\", title=\"Test Template\")\n    assert get_display_name(template_with_title) == \"Test Template\"\n"
  },
  {
    "path": "tests/server/mcpserver/test_tool_manager.py",
    "content": "import json\nimport logging\nfrom dataclasses import dataclass\nfrom typing import Any, TypedDict\n\nimport pytest\nfrom pydantic import BaseModel\n\nfrom mcp.server.context import LifespanContextT, RequestT\nfrom mcp.server.mcpserver import Context, MCPServer\nfrom mcp.server.mcpserver.exceptions import ToolError\nfrom mcp.server.mcpserver.tools import Tool, ToolManager\nfrom mcp.server.mcpserver.utilities.func_metadata import ArgModelBase, FuncMetadata\nfrom mcp.types import TextContent, ToolAnnotations\n\n\nclass TestAddTools:\n    def test_basic_function(self):\n        \"\"\"Test registering and running a basic function.\"\"\"\n\n        def sum(a: int, b: int) -> int:  # pragma: no cover\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        manager = ToolManager()\n        manager.add_tool(sum)\n\n        tool = manager.get_tool(\"sum\")\n        assert tool is not None\n        assert tool.name == \"sum\"\n        assert tool.description == \"Add two numbers.\"\n        assert tool.is_async is False\n        assert tool.parameters[\"properties\"][\"a\"][\"type\"] == \"integer\"\n        assert tool.parameters[\"properties\"][\"b\"][\"type\"] == \"integer\"\n\n    def test_init_with_tools(self, caplog: pytest.LogCaptureFixture):\n        def sum(a: int, b: int) -> int:  # pragma: no cover\n            return a + b\n\n        class AddArguments(ArgModelBase):\n            a: int\n            b: int\n\n        fn_metadata = FuncMetadata(arg_model=AddArguments)\n\n        original_tool = Tool(\n            name=\"sum\",\n            title=\"Add Tool\",\n            description=\"Add two numbers.\",\n            fn=sum,\n            fn_metadata=fn_metadata,\n            is_async=False,\n            parameters=AddArguments.model_json_schema(),\n            context_kwarg=None,\n            annotations=None,\n        )\n        manager = ToolManager(tools=[original_tool])\n        saved_tool = manager.get_tool(\"sum\")\n        assert saved_tool == original_tool\n\n        # warn on duplicate tools\n        with caplog.at_level(logging.WARNING):\n            manager = ToolManager(True, tools=[original_tool, original_tool])\n            assert \"Tool already exists: sum\" in caplog.text\n\n    @pytest.mark.anyio\n    async def test_async_function(self):\n        \"\"\"Test registering and running an async function.\"\"\"\n\n        async def fetch_data(url: str) -> str:  # pragma: no cover\n            \"\"\"Fetch data from URL.\"\"\"\n            return f\"Data from {url}\"\n\n        manager = ToolManager()\n        manager.add_tool(fetch_data)\n\n        tool = manager.get_tool(\"fetch_data\")\n        assert tool is not None\n        assert tool.name == \"fetch_data\"\n        assert tool.description == \"Fetch data from URL.\"\n        assert tool.is_async is True\n        assert tool.parameters[\"properties\"][\"url\"][\"type\"] == \"string\"\n\n    def test_pydantic_model_function(self):\n        \"\"\"Test registering a function that takes a Pydantic model.\"\"\"\n\n        class UserInput(BaseModel):\n            name: str\n            age: int\n\n        def create_user(user: UserInput, flag: bool) -> dict[str, Any]:  # pragma: no cover\n            \"\"\"Create a new user.\"\"\"\n            return {\"id\": 1, **user.model_dump()}\n\n        manager = ToolManager()\n        manager.add_tool(create_user)\n\n        tool = manager.get_tool(\"create_user\")\n        assert tool is not None\n        assert tool.name == \"create_user\"\n        assert tool.description == \"Create a new user.\"\n        assert tool.is_async is False\n        assert \"name\" in tool.parameters[\"$defs\"][\"UserInput\"][\"properties\"]\n        assert \"age\" in tool.parameters[\"$defs\"][\"UserInput\"][\"properties\"]\n        assert \"flag\" in tool.parameters[\"properties\"]\n\n    def test_add_callable_object(self):\n        \"\"\"Test registering a callable object.\"\"\"\n\n        class MyTool:\n            def __init__(self):\n                self.__name__ = \"MyTool\"\n\n            def __call__(self, x: int) -> int:  # pragma: no cover\n                return x * 2\n\n        manager = ToolManager()\n        tool = manager.add_tool(MyTool())\n        assert tool.name == \"MyTool\"\n        assert tool.is_async is False\n        assert tool.parameters[\"properties\"][\"x\"][\"type\"] == \"integer\"\n\n    @pytest.mark.anyio\n    async def test_add_async_callable_object(self):\n        \"\"\"Test registering an async callable object.\"\"\"\n\n        class MyAsyncTool:\n            def __init__(self):\n                self.__name__ = \"MyAsyncTool\"\n\n            async def __call__(self, x: int) -> int:  # pragma: no cover\n                return x * 2\n\n        manager = ToolManager()\n        tool = manager.add_tool(MyAsyncTool())\n        assert tool.name == \"MyAsyncTool\"\n        assert tool.is_async is True\n        assert tool.parameters[\"properties\"][\"x\"][\"type\"] == \"integer\"\n\n    def test_add_invalid_tool(self):\n        manager = ToolManager()\n        with pytest.raises(AttributeError):\n            manager.add_tool(1)  # type: ignore\n\n    def test_add_lambda(self):\n        manager = ToolManager()\n        tool = manager.add_tool(lambda x: x, name=\"my_tool\")  # type: ignore[reportUnknownLambdaType]\n        assert tool.name == \"my_tool\"\n\n    def test_add_lambda_with_no_name(self):\n        manager = ToolManager()\n        with pytest.raises(ValueError, match=\"You must provide a name for lambda functions\"):\n            manager.add_tool(lambda x: x)  # type: ignore[reportUnknownLambdaType]\n\n    def test_warn_on_duplicate_tools(self, caplog: pytest.LogCaptureFixture):\n        \"\"\"Test warning on duplicate tools.\"\"\"\n\n        def f(x: int) -> int:  # pragma: no cover\n            return x\n\n        manager = ToolManager()\n        manager.add_tool(f)\n        with caplog.at_level(logging.WARNING):\n            manager.add_tool(f)\n            assert \"Tool already exists: f\" in caplog.text\n\n    def test_disable_warn_on_duplicate_tools(self, caplog: pytest.LogCaptureFixture):\n        \"\"\"Test disabling warning on duplicate tools.\"\"\"\n\n        def f(x: int) -> int:  # pragma: no cover\n            return x\n\n        manager = ToolManager()\n        manager.add_tool(f)\n        manager.warn_on_duplicate_tools = False\n        with caplog.at_level(logging.WARNING):\n            manager.add_tool(f)\n            assert \"Tool already exists: f\" not in caplog.text\n\n\nclass TestCallTools:\n    @pytest.mark.anyio\n    async def test_call_tool(self):\n        def sum(a: int, b: int) -> int:\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        manager = ToolManager()\n        manager.add_tool(sum)\n        result = await manager.call_tool(\"sum\", {\"a\": 1, \"b\": 2}, Context())\n        assert result == 3\n\n    @pytest.mark.anyio\n    async def test_call_async_tool(self):\n        async def double(n: int) -> int:\n            \"\"\"Double a number.\"\"\"\n            return n * 2\n\n        manager = ToolManager()\n        manager.add_tool(double)\n        result = await manager.call_tool(\"double\", {\"n\": 5}, Context())\n        assert result == 10\n\n    @pytest.mark.anyio\n    async def test_call_object_tool(self):\n        class MyTool:\n            def __init__(self):\n                self.__name__ = \"MyTool\"\n\n            def __call__(self, x: int) -> int:\n                return x * 2\n\n        manager = ToolManager()\n        tool = manager.add_tool(MyTool())\n        result = await tool.run({\"x\": 5}, Context())\n        assert result == 10\n\n    @pytest.mark.anyio\n    async def test_call_async_object_tool(self):\n        class MyAsyncTool:\n            def __init__(self):\n                self.__name__ = \"MyAsyncTool\"\n\n            async def __call__(self, x: int) -> int:\n                return x * 2\n\n        manager = ToolManager()\n        tool = manager.add_tool(MyAsyncTool())\n        result = await tool.run({\"x\": 5}, Context())\n        assert result == 10\n\n    @pytest.mark.anyio\n    async def test_call_tool_with_default_args(self):\n        def sum(a: int, b: int = 1) -> int:\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        manager = ToolManager()\n        manager.add_tool(sum)\n        result = await manager.call_tool(\"sum\", {\"a\": 1}, Context())\n        assert result == 2\n\n    @pytest.mark.anyio\n    async def test_call_tool_with_missing_args(self):\n        def sum(a: int, b: int) -> int:  # pragma: no cover\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        manager = ToolManager()\n        manager.add_tool(sum)\n        with pytest.raises(ToolError):\n            await manager.call_tool(\"sum\", {\"a\": 1}, Context())\n\n    @pytest.mark.anyio\n    async def test_call_unknown_tool(self):\n        manager = ToolManager()\n        with pytest.raises(ToolError):\n            await manager.call_tool(\"unknown\", {\"a\": 1}, Context())\n\n    @pytest.mark.anyio\n    async def test_call_tool_with_list_int_input(self):\n        def sum_vals(vals: list[int]) -> int:\n            return sum(vals)\n\n        manager = ToolManager()\n        manager.add_tool(sum_vals)\n        # Try both with plain list and with JSON list\n        result = await manager.call_tool(\"sum_vals\", {\"vals\": \"[1, 2, 3]\"}, Context())\n        assert result == 6\n        result = await manager.call_tool(\"sum_vals\", {\"vals\": [1, 2, 3]}, Context())\n        assert result == 6\n\n    @pytest.mark.anyio\n    async def test_call_tool_with_list_str_or_str_input(self):\n        def concat_strs(vals: list[str] | str) -> str:\n            return vals if isinstance(vals, str) else \"\".join(vals)\n\n        manager = ToolManager()\n        manager.add_tool(concat_strs)\n        # Try both with plain python object and with JSON list\n        result = await manager.call_tool(\"concat_strs\", {\"vals\": [\"a\", \"b\", \"c\"]}, Context())\n        assert result == \"abc\"\n        result = await manager.call_tool(\"concat_strs\", {\"vals\": '[\"a\", \"b\", \"c\"]'}, Context())\n        assert result == \"abc\"\n        result = await manager.call_tool(\"concat_strs\", {\"vals\": \"a\"}, Context())\n        assert result == \"a\"\n        result = await manager.call_tool(\"concat_strs\", {\"vals\": '\"a\"'}, Context())\n        assert result == '\"a\"'\n\n    @pytest.mark.anyio\n    async def test_call_tool_with_complex_model(self):\n        class MyShrimpTank(BaseModel):\n            class Shrimp(BaseModel):\n                name: str\n\n            shrimp: list[Shrimp]\n            x: None\n\n        def name_shrimp(tank: MyShrimpTank) -> list[str]:\n            return [x.name for x in tank.shrimp]\n\n        manager = ToolManager()\n        manager.add_tool(name_shrimp)\n        result = await manager.call_tool(\n            \"name_shrimp\",\n            {\"tank\": {\"x\": None, \"shrimp\": [{\"name\": \"rex\"}, {\"name\": \"gertrude\"}]}},\n            Context(),\n        )\n        assert result == [\"rex\", \"gertrude\"]\n        result = await manager.call_tool(\n            \"name_shrimp\",\n            {\"tank\": '{\"x\": null, \"shrimp\": [{\"name\": \"rex\"}, {\"name\": \"gertrude\"}]}'},\n            Context(),\n        )\n        assert result == [\"rex\", \"gertrude\"]\n\n\nclass TestToolSchema:\n    @pytest.mark.anyio\n    async def test_context_arg_excluded_from_schema(self):\n        def something(a: int, ctx: Context) -> int:  # pragma: no cover\n            return a\n\n        manager = ToolManager()\n        tool = manager.add_tool(something)\n        assert \"ctx\" not in json.dumps(tool.parameters)\n        assert \"Context\" not in json.dumps(tool.parameters)\n        assert \"ctx\" not in tool.fn_metadata.arg_model.model_fields\n\n\nclass TestContextHandling:\n    \"\"\"Test context handling in the tool manager.\"\"\"\n\n    def test_context_parameter_detection(self):\n        \"\"\"Test that context parameters are properly detected in\n        Tool.from_function().\"\"\"\n\n        def tool_with_context(x: int, ctx: Context) -> str:  # pragma: no cover\n            return str(x)\n\n        manager = ToolManager()\n        tool = manager.add_tool(tool_with_context)\n        assert tool.context_kwarg == \"ctx\"\n\n        def tool_without_context(x: int) -> str:  # pragma: no cover\n            return str(x)\n\n        tool = manager.add_tool(tool_without_context)\n        assert tool.context_kwarg is None\n\n        def tool_with_parametrized_context(x: int, ctx: Context[LifespanContextT, RequestT]) -> str:  # pragma: no cover\n            return str(x)\n\n        tool = manager.add_tool(tool_with_parametrized_context)\n        assert tool.context_kwarg == \"ctx\"\n\n    @pytest.mark.anyio\n    async def test_context_injection(self):\n        \"\"\"Test that context is properly injected during tool execution.\"\"\"\n\n        def tool_with_context(x: int, ctx: Context) -> str:\n            assert isinstance(ctx, Context)\n            return str(x)\n\n        manager = ToolManager()\n        manager.add_tool(tool_with_context)\n\n        result = await manager.call_tool(\"tool_with_context\", {\"x\": 42}, context=Context())\n        assert result == \"42\"\n\n    @pytest.mark.anyio\n    async def test_context_injection_async(self):\n        \"\"\"Test that context is properly injected in async tools.\"\"\"\n\n        async def async_tool(x: int, ctx: Context) -> str:\n            assert isinstance(ctx, Context)\n            return str(x)\n\n        manager = ToolManager()\n        manager.add_tool(async_tool)\n\n        result = await manager.call_tool(\"async_tool\", {\"x\": 42}, context=Context())\n        assert result == \"42\"\n\n    @pytest.mark.anyio\n    async def test_context_error_handling(self):\n        \"\"\"Test error handling when context injection fails.\"\"\"\n\n        def tool_with_context(x: int, ctx: Context) -> str:\n            raise ValueError(\"Test error\")\n\n        manager = ToolManager()\n        manager.add_tool(tool_with_context)\n\n        with pytest.raises(ToolError, match=\"Error executing tool tool_with_context\"):\n            await manager.call_tool(\"tool_with_context\", {\"x\": 42}, context=Context())\n\n\nclass TestToolAnnotations:\n    def test_tool_annotations(self):\n        \"\"\"Test that tool annotations are correctly added to tools.\"\"\"\n\n        def read_data(path: str) -> str:  # pragma: no cover\n            \"\"\"Read data from a file.\"\"\"\n            return f\"Data from {path}\"\n\n        annotations = ToolAnnotations(\n            title=\"File Reader\",\n            read_only_hint=True,\n            open_world_hint=False,\n        )\n\n        manager = ToolManager()\n        tool = manager.add_tool(read_data, annotations=annotations)\n\n        assert tool.annotations is not None\n        assert tool.annotations.title == \"File Reader\"\n        assert tool.annotations.read_only_hint is True\n        assert tool.annotations.open_world_hint is False\n\n    @pytest.mark.anyio\n    async def test_tool_annotations_in_mcpserver(self):\n        \"\"\"Test that tool annotations are included in MCPTool conversion.\"\"\"\n\n        app = MCPServer()\n\n        @app.tool(annotations=ToolAnnotations(title=\"Echo Tool\", read_only_hint=True))\n        def echo(message: str) -> str:  # pragma: no cover\n            \"\"\"Echo a message back.\"\"\"\n            return message\n\n        tools = await app.list_tools()\n        assert len(tools) == 1\n        assert tools[0].annotations is not None\n        assert tools[0].annotations.title == \"Echo Tool\"\n        assert tools[0].annotations.read_only_hint is True\n\n\nclass TestStructuredOutput:\n    \"\"\"Test structured output functionality in tools.\"\"\"\n\n    @pytest.mark.anyio\n    async def test_tool_with_basemodel_output(self):\n        \"\"\"Test tool with BaseModel return type.\"\"\"\n\n        class UserOutput(BaseModel):\n            name: str\n            age: int\n\n        def get_user(user_id: int) -> UserOutput:\n            \"\"\"Get user by ID.\"\"\"\n            return UserOutput(name=\"John\", age=30)\n\n        manager = ToolManager()\n        manager.add_tool(get_user)\n        result = await manager.call_tool(\"get_user\", {\"user_id\": 1}, Context(), convert_result=True)\n        # don't test unstructured output here, just the structured conversion\n        assert len(result) == 2 and result[1] == {\"name\": \"John\", \"age\": 30}\n\n    @pytest.mark.anyio\n    async def test_tool_with_primitive_output(self):\n        \"\"\"Test tool with primitive return type.\"\"\"\n\n        def double_number(n: int) -> int:\n            \"\"\"Double a number.\"\"\"\n            return 10\n\n        manager = ToolManager()\n        manager.add_tool(double_number)\n        result = await manager.call_tool(\"double_number\", {\"n\": 5}, Context())\n        assert result == 10\n        result = await manager.call_tool(\"double_number\", {\"n\": 5}, Context(), convert_result=True)\n        assert isinstance(result[0][0], TextContent) and result[1] == {\"result\": 10}\n\n    @pytest.mark.anyio\n    async def test_tool_with_typeddict_output(self):\n        \"\"\"Test tool with TypedDict return type.\"\"\"\n\n        class UserDict(TypedDict):\n            name: str\n            age: int\n\n        expected_output = {\"name\": \"Alice\", \"age\": 25}\n\n        def get_user_dict(user_id: int) -> UserDict:\n            \"\"\"Get user as dict.\"\"\"\n            return UserDict(name=\"Alice\", age=25)\n\n        manager = ToolManager()\n        manager.add_tool(get_user_dict)\n        result = await manager.call_tool(\"get_user_dict\", {\"user_id\": 1}, Context())\n        assert result == expected_output\n\n    @pytest.mark.anyio\n    async def test_tool_with_dataclass_output(self):\n        \"\"\"Test tool with dataclass return type.\"\"\"\n\n        @dataclass\n        class Person:\n            name: str\n            age: int\n\n        expected_output = {\"name\": \"Bob\", \"age\": 40}\n\n        def get_person() -> Person:\n            \"\"\"Get a person.\"\"\"\n            return Person(\"Bob\", 40)\n\n        manager = ToolManager()\n        manager.add_tool(get_person)\n        result = await manager.call_tool(\"get_person\", {}, Context(), convert_result=True)\n        # don't test unstructured output here, just the structured conversion\n        assert len(result) == 2 and result[1] == expected_output\n\n    @pytest.mark.anyio\n    async def test_tool_with_list_output(self):\n        \"\"\"Test tool with list return type.\"\"\"\n\n        expected_list = [1, 2, 3, 4, 5]\n        expected_output = {\"result\": expected_list}\n\n        def get_numbers() -> list[int]:\n            \"\"\"Get a list of numbers.\"\"\"\n            return expected_list\n\n        manager = ToolManager()\n        manager.add_tool(get_numbers)\n        result = await manager.call_tool(\"get_numbers\", {}, Context())\n        assert result == expected_list\n        result = await manager.call_tool(\"get_numbers\", {}, Context(), convert_result=True)\n        assert isinstance(result[0][0], TextContent) and result[1] == expected_output\n\n    @pytest.mark.anyio\n    async def test_tool_without_structured_output(self):\n        \"\"\"Test that tools work normally when structured_output=False.\"\"\"\n\n        def get_dict() -> dict[str, Any]:\n            \"\"\"Get a dict.\"\"\"\n            return {\"key\": \"value\"}\n\n        manager = ToolManager()\n        manager.add_tool(get_dict, structured_output=False)\n        result = await manager.call_tool(\"get_dict\", {}, Context())\n        assert isinstance(result, dict)\n        assert result == {\"key\": \"value\"}\n\n    def test_tool_output_schema_property(self):\n        \"\"\"Test that Tool.output_schema property works correctly.\"\"\"\n\n        class UserOutput(BaseModel):\n            name: str\n            age: int\n\n        def get_user() -> UserOutput:  # pragma: no cover\n            return UserOutput(name=\"Test\", age=25)\n\n        manager = ToolManager()\n        tool = manager.add_tool(get_user)\n\n        # Test that output_schema is populated\n        expected_schema = {\n            \"properties\": {\"name\": {\"type\": \"string\", \"title\": \"Name\"}, \"age\": {\"type\": \"integer\", \"title\": \"Age\"}},\n            \"required\": [\"name\", \"age\"],\n            \"title\": \"UserOutput\",\n            \"type\": \"object\",\n        }\n        assert tool.output_schema == expected_schema\n\n    @pytest.mark.anyio\n    async def test_tool_with_dict_str_any_output(self):\n        \"\"\"Test tool with dict[str, Any] return type.\"\"\"\n\n        def get_config() -> dict[str, Any]:\n            \"\"\"Get configuration\"\"\"\n            return {\"debug\": True, \"port\": 8080, \"features\": [\"auth\", \"logging\"]}\n\n        manager = ToolManager()\n        tool = manager.add_tool(get_config)\n\n        # Check output schema\n        assert tool.output_schema is not None\n        assert tool.output_schema[\"type\"] == \"object\"\n        assert \"properties\" not in tool.output_schema  # dict[str, Any] has no constraints\n\n        # Test raw result\n        result = await manager.call_tool(\"get_config\", {}, Context())\n        expected = {\"debug\": True, \"port\": 8080, \"features\": [\"auth\", \"logging\"]}\n        assert result == expected\n\n        # Test converted result\n        result = await manager.call_tool(\"get_config\", {}, Context())\n        assert result == expected\n\n    @pytest.mark.anyio\n    async def test_tool_with_dict_str_typed_output(self):\n        \"\"\"Test tool with dict[str, T] return type for specific T.\"\"\"\n\n        def get_scores() -> dict[str, int]:\n            \"\"\"Get player scores\"\"\"\n            return {\"alice\": 100, \"bob\": 85, \"charlie\": 92}\n\n        manager = ToolManager()\n        tool = manager.add_tool(get_scores)\n\n        # Check output schema\n        assert tool.output_schema is not None\n        assert tool.output_schema[\"type\"] == \"object\"\n        assert tool.output_schema[\"additionalProperties\"][\"type\"] == \"integer\"\n\n        # Test raw result\n        result = await manager.call_tool(\"get_scores\", {}, Context())\n        expected = {\"alice\": 100, \"bob\": 85, \"charlie\": 92}\n        assert result == expected\n\n        # Test converted result\n        result = await manager.call_tool(\"get_scores\", {}, Context())\n        assert result == expected\n\n\nclass TestToolMetadata:\n    \"\"\"Test tool metadata functionality.\"\"\"\n\n    def test_add_tool_with_metadata(self):\n        \"\"\"Test adding a tool with metadata via ToolManager.\"\"\"\n\n        def process_data(input_data: str) -> str:  # pragma: no cover\n            \"\"\"Process some data.\"\"\"\n            return f\"Processed: {input_data}\"\n\n        metadata = {\"ui\": {\"type\": \"form\", \"fields\": [\"input\"]}, \"version\": \"1.0\"}\n\n        manager = ToolManager()\n        tool = manager.add_tool(process_data, meta=metadata)\n\n        assert tool.meta is not None\n        assert tool.meta == metadata\n        assert tool.meta[\"ui\"][\"type\"] == \"form\"\n        assert tool.meta[\"version\"] == \"1.0\"\n\n    def test_add_tool_without_metadata(self):\n        \"\"\"Test that tools without metadata have None as meta value.\"\"\"\n\n        def simple_tool(x: int) -> int:  # pragma: no cover\n            \"\"\"Simple tool.\"\"\"\n            return x * 2\n\n        manager = ToolManager()\n        tool = manager.add_tool(simple_tool)\n\n        assert tool.meta is None\n\n    @pytest.mark.anyio\n    async def test_metadata_in_mcpserver_decorator(self):\n        \"\"\"Test that metadata is correctly added via MCPServer.tool decorator.\"\"\"\n\n        app = MCPServer()\n\n        metadata = {\"client\": {\"ui_component\": \"file_picker\"}, \"priority\": \"high\"}\n\n        @app.tool(meta=metadata)\n        def upload_file(filename: str) -> str:  # pragma: no cover\n            \"\"\"Upload a file.\"\"\"\n            return f\"Uploaded: {filename}\"\n\n        # Get the tool from the tool manager\n        tool = app._tool_manager.get_tool(\"upload_file\")\n        assert tool is not None\n        assert tool.meta is not None\n        assert tool.meta == metadata\n        assert tool.meta[\"client\"][\"ui_component\"] == \"file_picker\"\n        assert tool.meta[\"priority\"] == \"high\"\n\n    @pytest.mark.anyio\n    async def test_metadata_in_list_tools(self):\n        \"\"\"Test that metadata is included in MCPTool when listing tools.\"\"\"\n\n        app = MCPServer()\n\n        metadata = {\n            \"ui\": {\"input_type\": \"textarea\", \"rows\": 5},\n            \"tags\": [\"text\", \"processing\"],\n        }\n\n        @app.tool(meta=metadata)\n        def analyze_text(text: str) -> dict[str, Any]:  # pragma: no cover\n            \"\"\"Analyze text content.\"\"\"\n            return {\"length\": len(text), \"words\": len(text.split())}\n\n        tools = await app.list_tools()\n        assert len(tools) == 1\n        assert tools[0].meta is not None\n        assert tools[0].meta == metadata\n\n    @pytest.mark.anyio\n    async def test_multiple_tools_with_different_metadata(self):\n        \"\"\"Test multiple tools with different metadata values.\"\"\"\n\n        app = MCPServer()\n\n        metadata1 = {\"ui\": \"form\", \"version\": 1}\n        metadata2 = {\"ui\": \"picker\", \"experimental\": True}\n\n        @app.tool(meta=metadata1)\n        def tool1(x: int) -> int:  # pragma: no cover\n            \"\"\"First tool.\"\"\"\n            return x\n\n        @app.tool(meta=metadata2)\n        def tool2(y: str) -> str:  # pragma: no cover\n            \"\"\"Second tool.\"\"\"\n            return y\n\n        @app.tool()\n        def tool3(z: bool) -> bool:  # pragma: no cover\n            \"\"\"Third tool without metadata.\"\"\"\n            return z\n\n        tools = await app.list_tools()\n        assert len(tools) == 3\n\n        # Find tools by name and check metadata\n        tools_by_name = {t.name: t for t in tools}\n\n        assert tools_by_name[\"tool1\"].meta == metadata1\n        assert tools_by_name[\"tool2\"].meta == metadata2\n        assert tools_by_name[\"tool3\"].meta is None\n\n    def test_metadata_with_complex_structure(self):\n        \"\"\"Test metadata with complex nested structures.\"\"\"\n\n        def complex_tool(data: str) -> str:  # pragma: no cover\n            \"\"\"Tool with complex metadata.\"\"\"\n            return data\n\n        metadata = {\n            \"ui\": {\n                \"components\": [\n                    {\"type\": \"input\", \"name\": \"field1\", \"validation\": {\"required\": True, \"minLength\": 5}},\n                    {\"type\": \"select\", \"name\": \"field2\", \"options\": [\"a\", \"b\", \"c\"]},\n                ],\n                \"layout\": {\"columns\": 2, \"responsive\": True},\n            },\n            \"permissions\": [\"read\", \"write\"],\n            \"tags\": [\"data-processing\", \"user-input\"],\n            \"version\": 2,\n        }\n\n        manager = ToolManager()\n        tool = manager.add_tool(complex_tool, meta=metadata)\n\n        assert tool.meta is not None\n        assert tool.meta[\"ui\"][\"components\"][0][\"validation\"][\"minLength\"] == 5\n        assert tool.meta[\"ui\"][\"layout\"][\"columns\"] == 2\n        assert \"read\" in tool.meta[\"permissions\"]\n        assert \"data-processing\" in tool.meta[\"tags\"]\n\n    def test_metadata_empty_dict(self):\n        \"\"\"Test that empty dict metadata is preserved.\"\"\"\n\n        def tool_with_empty_meta(x: int) -> int:  # pragma: no cover\n            \"\"\"Tool with empty metadata.\"\"\"\n            return x\n\n        manager = ToolManager()\n        tool = manager.add_tool(tool_with_empty_meta, meta={})\n\n        assert tool.meta is not None\n        assert tool.meta == {}\n\n    @pytest.mark.anyio\n    async def test_metadata_with_annotations(self):\n        \"\"\"Test that metadata and annotations can coexist.\"\"\"\n\n        app = MCPServer()\n\n        metadata = {\"custom\": \"value\"}\n        annotations = ToolAnnotations(title=\"Combined Tool\", read_only_hint=True)\n\n        @app.tool(meta=metadata, annotations=annotations)\n        def combined_tool(data: str) -> str:  # pragma: no cover\n            \"\"\"Tool with both metadata and annotations.\"\"\"\n            return data\n\n        tools = await app.list_tools()\n        assert len(tools) == 1\n        assert tools[0].meta == metadata\n        assert tools[0].annotations is not None\n        assert tools[0].annotations.title == \"Combined Tool\"\n        assert tools[0].annotations.read_only_hint is True\n\n\nclass TestRemoveTools:\n    \"\"\"Test tool removal functionality in the tool manager.\"\"\"\n\n    def test_remove_existing_tool(self):\n        \"\"\"Test removing an existing tool.\"\"\"\n\n        def add(a: int, b: int) -> int:  # pragma: no cover\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        manager = ToolManager()\n        manager.add_tool(add)\n\n        # Verify tool exists\n        assert manager.get_tool(\"add\") is not None\n        assert len(manager.list_tools()) == 1\n\n        # Remove the tool - should not raise any exception\n        manager.remove_tool(\"add\")\n\n        # Verify tool is removed\n        assert manager.get_tool(\"add\") is None\n        assert len(manager.list_tools()) == 0\n\n    def test_remove_nonexistent_tool(self):\n        \"\"\"Test removing a non-existent tool raises ToolError.\"\"\"\n        manager = ToolManager()\n\n        with pytest.raises(ToolError, match=\"Unknown tool: nonexistent\"):\n            manager.remove_tool(\"nonexistent\")\n\n    def test_remove_tool_from_multiple_tools(self):\n        \"\"\"Test removing one tool when multiple tools exist.\"\"\"\n\n        def add(a: int, b: int) -> int:  # pragma: no cover\n            \"\"\"Add two numbers.\"\"\"\n            return a + b\n\n        def multiply(a: int, b: int) -> int:  # pragma: no cover\n            \"\"\"Multiply two numbers.\"\"\"\n            return a * b\n\n        def divide(a: int, b: int) -> float:  # pragma: no cover\n            \"\"\"Divide two numbers.\"\"\"\n            return a / b\n\n        manager = ToolManager()\n        manager.add_tool(add)\n        manager.add_tool(multiply)\n        manager.add_tool(divide)\n\n        # Verify all tools exist\n        assert len(manager.list_tools()) == 3\n        assert manager.get_tool(\"add\") is not None\n        assert manager.get_tool(\"multiply\") is not None\n        assert manager.get_tool(\"divide\") is not None\n\n        # Remove middle tool\n        manager.remove_tool(\"multiply\")\n\n        # Verify only multiply is removed\n        assert len(manager.list_tools()) == 2\n        assert manager.get_tool(\"add\") is not None\n        assert manager.get_tool(\"multiply\") is None\n        assert manager.get_tool(\"divide\") is not None\n\n    @pytest.mark.anyio\n    async def test_call_removed_tool_raises_error(self):\n        \"\"\"Test that calling a removed tool raises ToolError.\"\"\"\n\n        def greet(name: str) -> str:\n            \"\"\"Greet someone.\"\"\"\n            return f\"Hello, {name}!\"\n\n        manager = ToolManager()\n        manager.add_tool(greet)\n\n        # Verify tool works before removal\n        result = await manager.call_tool(\"greet\", {\"name\": \"World\"}, Context())\n        assert result == \"Hello, World!\"\n\n        # Remove the tool\n        manager.remove_tool(\"greet\")\n\n        # Verify calling removed tool raises error\n        with pytest.raises(ToolError, match=\"Unknown tool: greet\"):\n            await manager.call_tool(\"greet\", {\"name\": \"World\"}, Context())\n\n    def test_remove_tool_case_sensitive(self):\n        \"\"\"Test that tool removal is case-sensitive.\"\"\"\n\n        def test_func() -> str:  # pragma: no cover\n            \"\"\"Test function.\"\"\"\n            return \"test\"\n\n        manager = ToolManager()\n        manager.add_tool(test_func)\n\n        # Verify tool exists\n        assert manager.get_tool(\"test_func\") is not None\n\n        # Try to remove with different case - should raise ToolError\n        with pytest.raises(ToolError, match=\"Unknown tool: Test_Func\"):\n            manager.remove_tool(\"Test_Func\")\n\n        # Verify original tool still exists\n        assert manager.get_tool(\"test_func\") is not None\n\n        # Remove with correct case\n        manager.remove_tool(\"test_func\")\n        assert manager.get_tool(\"test_func\") is None\n"
  },
  {
    "path": "tests/server/mcpserver/test_url_elicitation.py",
    "content": "\"\"\"Test URL mode elicitation feature (SEP 1036).\"\"\"\n\nimport anyio\nimport pytest\nfrom pydantic import BaseModel, Field\n\nfrom mcp import Client, types\nfrom mcp.client.session import ClientSession\nfrom mcp.server.elicitation import CancelledElicitation, DeclinedElicitation, elicit_url\nfrom mcp.server.mcpserver import Context, MCPServer\nfrom mcp.shared._context import RequestContext\nfrom mcp.types import ElicitRequestParams, ElicitResult, TextContent\n\n\n@pytest.mark.anyio\nasync def test_url_elicitation_accept():\n    \"\"\"Test URL mode elicitation with user acceptance.\"\"\"\n    mcp = MCPServer(name=\"URLElicitationServer\")\n\n    @mcp.tool(description=\"A tool that uses URL elicitation\")\n    async def request_api_key(ctx: Context) -> str:\n        result = await ctx.session.elicit_url(\n            message=\"Please provide your API key to continue.\",\n            url=\"https://example.com/api_key_setup\",\n            elicitation_id=\"test-elicitation-001\",\n        )\n        # Test only checks accept path\n        return f\"User {result.action}\"\n\n    # Create elicitation callback that accepts URL mode\n    async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        assert params.mode == \"url\"\n        assert params.url == \"https://example.com/api_key_setup\"\n        assert params.elicitation_id == \"test-elicitation-001\"\n        assert params.message == \"Please provide your API key to continue.\"\n        return ElicitResult(action=\"accept\")\n\n    async with Client(mcp, elicitation_callback=elicitation_callback) as client:\n        result = await client.call_tool(\"request_api_key\", {})\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"User accept\"\n\n\n@pytest.mark.anyio\nasync def test_url_elicitation_decline():\n    \"\"\"Test URL mode elicitation with user declining.\"\"\"\n    mcp = MCPServer(name=\"URLElicitationDeclineServer\")\n\n    @mcp.tool(description=\"A tool that uses URL elicitation\")\n    async def oauth_flow(ctx: Context) -> str:\n        result = await ctx.session.elicit_url(\n            message=\"Authorize access to your files.\",\n            url=\"https://example.com/oauth/authorize\",\n            elicitation_id=\"oauth-001\",\n        )\n        # Test only checks decline path\n        return f\"User {result.action} authorization\"\n\n    async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        assert params.mode == \"url\"\n        return ElicitResult(action=\"decline\")\n\n    async with Client(mcp, elicitation_callback=elicitation_callback) as client:\n        result = await client.call_tool(\"oauth_flow\", {})\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"User decline authorization\"\n\n\n@pytest.mark.anyio\nasync def test_url_elicitation_cancel():\n    \"\"\"Test URL mode elicitation with user cancelling.\"\"\"\n    mcp = MCPServer(name=\"URLElicitationCancelServer\")\n\n    @mcp.tool(description=\"A tool that uses URL elicitation\")\n    async def payment_flow(ctx: Context) -> str:\n        result = await ctx.session.elicit_url(\n            message=\"Complete payment to proceed.\",\n            url=\"https://example.com/payment\",\n            elicitation_id=\"payment-001\",\n        )\n        # Test only checks cancel path\n        return f\"User {result.action} payment\"\n\n    async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        assert params.mode == \"url\"\n        return ElicitResult(action=\"cancel\")\n\n    async with Client(mcp, elicitation_callback=elicitation_callback) as client:\n        result = await client.call_tool(\"payment_flow\", {})\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"User cancel payment\"\n\n\n@pytest.mark.anyio\nasync def test_url_elicitation_helper_function():\n    \"\"\"Test the elicit_url helper function.\"\"\"\n    mcp = MCPServer(name=\"URLElicitationHelperServer\")\n\n    @mcp.tool(description=\"Tool using elicit_url helper\")\n    async def setup_credentials(ctx: Context) -> str:\n        result = await elicit_url(\n            session=ctx.session,\n            message=\"Set up your credentials\",\n            url=\"https://example.com/setup\",\n            elicitation_id=\"setup-001\",\n        )\n        # Test only checks accept path - return the type name\n        return type(result).__name__\n\n    async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        return ElicitResult(action=\"accept\")\n\n    async with Client(mcp, elicitation_callback=elicitation_callback) as client:\n        result = await client.call_tool(\"setup_credentials\", {})\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"AcceptedUrlElicitation\"\n\n\n@pytest.mark.anyio\nasync def test_url_no_content_in_response():\n    \"\"\"Test that URL mode elicitation responses don't include content field.\"\"\"\n    mcp = MCPServer(name=\"URLContentCheckServer\")\n\n    @mcp.tool(description=\"Check URL response format\")\n    async def check_url_response(ctx: Context) -> str:\n        result = await ctx.session.elicit_url(\n            message=\"Test message\",\n            url=\"https://example.com/test\",\n            elicitation_id=\"test-001\",\n        )\n\n        # URL mode responses should not have content\n        assert result.content is None\n        return f\"Action: {result.action}, Content: {result.content}\"\n\n    async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        # Verify that this is URL mode\n        assert params.mode == \"url\"\n        assert isinstance(params, types.ElicitRequestURLParams)\n        # URL params have url and elicitation_id, not requested_schema\n        assert params.url == \"https://example.com/test\"\n        assert params.elicitation_id == \"test-001\"\n        # Return without content - this is correct for URL mode\n        return ElicitResult(action=\"accept\")\n\n    async with Client(mcp, elicitation_callback=elicitation_callback) as client:\n        result = await client.call_tool(\"check_url_response\", {})\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert \"Content: None\" in result.content[0].text\n\n\n@pytest.mark.anyio\nasync def test_form_mode_still_works():\n    \"\"\"Ensure form mode elicitation still works after SEP 1036.\"\"\"\n    mcp = MCPServer(name=\"FormModeBackwardCompatServer\")\n\n    class NameSchema(BaseModel):\n        name: str = Field(description=\"Your name\")\n\n    @mcp.tool(description=\"Test form mode\")\n    async def ask_name(ctx: Context) -> str:\n        result = await ctx.elicit(message=\"What is your name?\", schema=NameSchema)\n        # Test only checks accept path with data\n        assert result.action == \"accept\"\n        assert result.data is not None\n        return f\"Hello, {result.data.name}!\"\n\n    async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        # Verify form mode parameters\n        assert params.mode == \"form\"\n        assert isinstance(params, types.ElicitRequestFormParams)\n        # Form params have requested_schema, not url/elicitation_id\n        assert params.requested_schema is not None\n        return ElicitResult(action=\"accept\", content={\"name\": \"Alice\"})\n\n    async with Client(mcp, elicitation_callback=elicitation_callback) as client:\n        result = await client.call_tool(\"ask_name\", {})\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Hello, Alice!\"\n\n\n@pytest.mark.anyio\nasync def test_elicit_complete_notification():\n    \"\"\"Test that elicitation completion notifications can be sent and received.\"\"\"\n    mcp = MCPServer(name=\"ElicitCompleteServer\")\n\n    # Track if the notification was sent\n    notification_sent = False\n\n    @mcp.tool(description=\"Tool that sends completion notification\")\n    async def trigger_elicitation(ctx: Context) -> str:\n        nonlocal notification_sent\n\n        # Simulate an async operation (e.g., user completing auth in browser)\n        elicitation_id = \"complete-test-001\"\n\n        # Send completion notification\n        await ctx.session.send_elicit_complete(elicitation_id)\n        notification_sent = True\n\n        return \"Elicitation completed\"\n\n    async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        return ElicitResult(action=\"accept\")  # pragma: no cover\n\n    async with Client(mcp, elicitation_callback=elicitation_callback) as client:\n        result = await client.call_tool(\"trigger_elicitation\", {})\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Elicitation completed\"\n\n        # Give time for notification to be processed\n        await anyio.sleep(0.1)\n\n        # Verify the notification was sent\n        assert notification_sent\n\n\n@pytest.mark.anyio\nasync def test_url_elicitation_required_error_code():\n    \"\"\"Test that the URL_ELICITATION_REQUIRED error code is correct.\"\"\"\n    # Verify the error code matches the specification (SEP 1036)\n    assert types.URL_ELICITATION_REQUIRED == -32042, (\n        \"URL_ELICITATION_REQUIRED error code must be -32042 per SEP 1036 specification\"\n    )\n\n\n@pytest.mark.anyio\nasync def test_elicit_url_typed_results():\n    \"\"\"Test that elicit_url returns properly typed result objects.\"\"\"\n    mcp = MCPServer(name=\"TypedResultsServer\")\n\n    @mcp.tool(description=\"Test declined result\")\n    async def test_decline(ctx: Context) -> str:\n        result = await elicit_url(\n            session=ctx.session,\n            message=\"Test decline\",\n            url=\"https://example.com/decline\",\n            elicitation_id=\"decline-001\",\n        )\n\n        if isinstance(result, DeclinedElicitation):\n            return \"Declined\"\n        return \"Not declined\"  # pragma: no cover\n\n    @mcp.tool(description=\"Test cancelled result\")\n    async def test_cancel(ctx: Context) -> str:\n        result = await elicit_url(\n            session=ctx.session,\n            message=\"Test cancel\",\n            url=\"https://example.com/cancel\",\n            elicitation_id=\"cancel-001\",\n        )\n\n        if isinstance(result, CancelledElicitation):\n            return \"Cancelled\"\n        return \"Not cancelled\"  # pragma: no cover\n\n    # Test declined result\n    async def decline_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        return ElicitResult(action=\"decline\")\n\n    async with Client(mcp, elicitation_callback=decline_callback) as client:\n        result = await client.call_tool(\"test_decline\", {})\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Declined\"\n\n    # Test cancelled result\n    async def cancel_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        return ElicitResult(action=\"cancel\")\n\n    async with Client(mcp, elicitation_callback=cancel_callback) as client:\n        result = await client.call_tool(\"test_cancel\", {})\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Cancelled\"\n\n\n@pytest.mark.anyio\nasync def test_deprecated_elicit_method():\n    \"\"\"Test the deprecated elicit() method for backward compatibility.\"\"\"\n    mcp = MCPServer(name=\"DeprecatedElicitServer\")\n\n    class EmailSchema(BaseModel):\n        email: str = Field(description=\"Email address\")\n\n    @mcp.tool(description=\"Test deprecated elicit method\")\n    async def use_deprecated_elicit(ctx: Context) -> str:\n        # Use the deprecated elicit() method which should call elicit_form()\n        result = await ctx.session.elicit(\n            message=\"Enter your email\",\n            requested_schema=EmailSchema.model_json_schema(),\n        )\n\n        if result.action == \"accept\" and result.content:\n            return f\"Email: {result.content.get('email', 'none')}\"\n        return \"No email provided\"  # pragma: no cover\n\n    async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        # Verify this is form mode\n        assert params.mode == \"form\"\n        assert params.requested_schema is not None\n        return ElicitResult(action=\"accept\", content={\"email\": \"test@example.com\"})\n\n    async with Client(mcp, elicitation_callback=elicitation_callback) as client:\n        result = await client.call_tool(\"use_deprecated_elicit\", {})\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Email: test@example.com\"\n\n\n@pytest.mark.anyio\nasync def test_ctx_elicit_url_convenience_method():\n    \"\"\"Test the ctx.elicit_url() convenience method (vs ctx.session.elicit_url()).\"\"\"\n    mcp = MCPServer(name=\"CtxElicitUrlServer\")\n\n    @mcp.tool(description=\"A tool that uses ctx.elicit_url() directly\")\n    async def direct_elicit_url(ctx: Context) -> str:\n        # Use ctx.elicit_url() directly instead of ctx.session.elicit_url()\n        result = await ctx.elicit_url(\n            message=\"Test the convenience method\",\n            url=\"https://example.com/test\",\n            elicitation_id=\"ctx-test-001\",\n        )\n        return f\"Result: {result.action}\"\n\n    async def elicitation_callback(context: RequestContext[ClientSession], params: ElicitRequestParams):\n        assert params.mode == \"url\"\n        assert params.elicitation_id == \"ctx-test-001\"\n        return ElicitResult(action=\"accept\")\n\n    async with Client(mcp, elicitation_callback=elicitation_callback) as client:\n        result = await client.call_tool(\"direct_elicit_url\", {})\n        assert isinstance(result.content[0], TextContent)\n        assert result.content[0].text == \"Result: accept\"\n"
  },
  {
    "path": "tests/server/mcpserver/test_url_elicitation_error_throw.py",
    "content": "\"\"\"Test that UrlElicitationRequiredError is properly propagated as MCP error.\"\"\"\n\nimport pytest\nfrom inline_snapshot import snapshot\n\nfrom mcp import Client, ErrorData, types\nfrom mcp.server.mcpserver import Context, MCPServer\nfrom mcp.shared.exceptions import MCPError, UrlElicitationRequiredError\n\n\n@pytest.mark.anyio\nasync def test_url_elicitation_error_thrown_from_tool():\n    \"\"\"Test that UrlElicitationRequiredError raised from a tool is received as MCPError by client.\"\"\"\n    mcp = MCPServer(name=\"UrlElicitationErrorServer\")\n\n    @mcp.tool(description=\"A tool that raises UrlElicitationRequiredError\")\n    async def connect_service(service_name: str, ctx: Context) -> str:\n        # This tool cannot proceed without authorization\n        raise UrlElicitationRequiredError(\n            [\n                types.ElicitRequestURLParams(\n                    mode=\"url\",\n                    message=f\"Authorization required to connect to {service_name}\",\n                    url=f\"https://{service_name}.example.com/oauth/authorize\",\n                    elicitation_id=f\"{service_name}-auth-001\",\n                )\n            ]\n        )\n\n    async with Client(mcp) as client:\n        with pytest.raises(MCPError) as exc_info:\n            await client.call_tool(\"connect_service\", {\"service_name\": \"github\"})\n\n        assert exc_info.value.error == snapshot(\n            ErrorData(\n                code=types.URL_ELICITATION_REQUIRED,\n                message=\"URL elicitation required\",\n                data={\n                    \"elicitations\": [\n                        {\n                            \"mode\": \"url\",\n                            \"message\": \"Authorization required to connect to github\",\n                            \"url\": \"https://github.example.com/oauth/authorize\",\n                            \"elicitationId\": \"github-auth-001\",\n                        }\n                    ]\n                },\n            )\n        )\n\n\n@pytest.mark.anyio\nasync def test_url_elicitation_error_from_error():\n    \"\"\"Test that client can reconstruct UrlElicitationRequiredError from MCPError.\"\"\"\n    mcp = MCPServer(name=\"UrlElicitationErrorServer\")\n\n    @mcp.tool(description=\"A tool that raises UrlElicitationRequiredError with multiple elicitations\")\n    async def multi_auth(ctx: Context) -> str:\n        raise UrlElicitationRequiredError(\n            [\n                types.ElicitRequestURLParams(\n                    mode=\"url\",\n                    message=\"GitHub authorization required\",\n                    url=\"https://github.example.com/oauth\",\n                    elicitation_id=\"github-auth\",\n                ),\n                types.ElicitRequestURLParams(\n                    mode=\"url\",\n                    message=\"Google Drive authorization required\",\n                    url=\"https://drive.google.com/oauth\",\n                    elicitation_id=\"gdrive-auth\",\n                ),\n            ]\n        )\n\n    async with Client(mcp) as client:\n        # Call the tool and catch the error\n        with pytest.raises(MCPError) as exc_info:\n            await client.call_tool(\"multi_auth\", {})\n\n        # Reconstruct the typed error\n        mcp_error = exc_info.value\n        assert mcp_error.code == types.URL_ELICITATION_REQUIRED\n\n        url_error = UrlElicitationRequiredError.from_error(mcp_error.error)\n\n        # Verify the reconstructed error has both elicitations\n        assert len(url_error.elicitations) == 2\n        assert url_error.elicitations[0].elicitation_id == \"github-auth\"\n        assert url_error.elicitations[1].elicitation_id == \"gdrive-auth\"\n\n\n@pytest.mark.anyio\nasync def test_normal_exceptions_still_return_error_result():\n    \"\"\"Test that normal exceptions still return CallToolResult with is_error=True.\"\"\"\n    mcp = MCPServer(name=\"NormalErrorServer\")\n\n    @mcp.tool(description=\"A tool that raises a normal exception\")\n    async def failing_tool(ctx: Context) -> str:\n        raise ValueError(\"Something went wrong\")\n\n    async with Client(mcp) as client:\n        # Normal exceptions should be returned as error results, not MCPError\n        result = await client.call_tool(\"failing_tool\", {})\n        assert result.is_error is True\n        assert len(result.content) == 1\n        assert isinstance(result.content[0], types.TextContent)\n        assert \"Something went wrong\" in result.content[0].text\n"
  },
  {
    "path": "tests/server/mcpserver/tools/__init__.py",
    "content": ""
  },
  {
    "path": "tests/server/mcpserver/tools/test_base.py",
    "content": "from mcp.server.mcpserver import Context\nfrom mcp.server.mcpserver.tools.base import Tool\n\n\ndef test_context_detected_in_union_annotation():\n    def my_tool(x: int, ctx: Context | None) -> str:\n        raise NotImplementedError\n\n    tool = Tool.from_function(my_tool)\n    assert tool.context_kwarg == \"ctx\"\n"
  },
  {
    "path": "tests/server/test_cancel_handling.py",
    "content": "\"\"\"Test that cancelled requests don't cause double responses.\"\"\"\n\nimport anyio\nimport pytest\n\nfrom mcp import Client\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.types import (\n    CallToolRequest,\n    CallToolRequestParams,\n    CallToolResult,\n    CancelledNotification,\n    CancelledNotificationParams,\n    ListToolsResult,\n    PaginatedRequestParams,\n    TextContent,\n    Tool,\n)\n\n\n@pytest.mark.anyio\nasync def test_server_remains_functional_after_cancel():\n    \"\"\"Verify server can handle new requests after a cancellation.\"\"\"\n\n    # Track tool calls\n    call_count = 0\n    ev_first_call = anyio.Event()\n    first_request_id = None\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        return ListToolsResult(\n            tools=[\n                Tool(\n                    name=\"test_tool\",\n                    description=\"Tool for testing\",\n                    input_schema={},\n                )\n            ]\n        )\n\n    async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:\n        nonlocal call_count, first_request_id\n        if params.name == \"test_tool\":\n            call_count += 1\n            if call_count == 1:\n                first_request_id = ctx.request_id\n                ev_first_call.set()\n                await anyio.sleep(5)  # First call is slow\n            return CallToolResult(content=[TextContent(type=\"text\", text=f\"Call number: {call_count}\")])\n        raise ValueError(f\"Unknown tool: {params.name}\")  # pragma: no cover\n\n    server = Server(\"test-server\", on_list_tools=handle_list_tools, on_call_tool=handle_call_tool)\n\n    async with Client(server) as client:\n        # First request (will be cancelled)\n        async def first_request():\n            try:\n                await client.session.send_request(\n                    CallToolRequest(params=CallToolRequestParams(name=\"test_tool\", arguments={})),\n                    CallToolResult,\n                )\n                pytest.fail(\"First request should have been cancelled\")  # pragma: no cover\n            except MCPError:\n                pass  # Expected\n\n        # Start first request\n        async with anyio.create_task_group() as tg:\n            tg.start_soon(first_request)\n\n            # Wait for it to start\n            await ev_first_call.wait()\n\n            # Cancel it\n            assert first_request_id is not None\n            await client.session.send_notification(\n                CancelledNotification(\n                    params=CancelledNotificationParams(request_id=first_request_id, reason=\"Testing server recovery\"),\n                )\n            )\n\n        # Second request (should work normally)\n        result = await client.call_tool(\"test_tool\", {})\n\n        # Verify second request completed successfully\n        assert len(result.content) == 1\n        # Type narrowing for pyright\n        content = result.content[0]\n        assert content.type == \"text\"\n        assert isinstance(content, TextContent)\n        assert content.text == \"Call number: 2\"\n        assert call_count == 2\n"
  },
  {
    "path": "tests/server/test_completion_with_context.py",
    "content": "\"\"\"Tests for completion handler with context functionality.\"\"\"\n\nimport pytest\n\nfrom mcp import Client\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.types import (\n    CompleteRequestParams,\n    CompleteResult,\n    Completion,\n    PromptReference,\n    ResourceTemplateReference,\n)\n\n\n@pytest.mark.anyio\nasync def test_completion_handler_receives_context():\n    \"\"\"Test that the completion handler receives context correctly.\"\"\"\n    # Track what the handler receives\n    received_params: CompleteRequestParams | None = None\n\n    async def handle_completion(ctx: ServerRequestContext, params: CompleteRequestParams) -> CompleteResult:\n        nonlocal received_params\n        received_params = params\n        return CompleteResult(completion=Completion(values=[\"test-completion\"], total=1, has_more=False))\n\n    server = Server(\"test-server\", on_completion=handle_completion)\n\n    async with Client(server) as client:\n        # Test with context\n        result = await client.complete(\n            ref=ResourceTemplateReference(type=\"ref/resource\", uri=\"test://resource/{param}\"),\n            argument={\"name\": \"param\", \"value\": \"test\"},\n            context_arguments={\"previous\": \"value\"},\n        )\n\n        # Verify handler received the context\n        assert received_params is not None\n        assert received_params.context is not None\n        assert received_params.context.arguments == {\"previous\": \"value\"}\n        assert result.completion.values == [\"test-completion\"]\n\n\n@pytest.mark.anyio\nasync def test_completion_backward_compatibility():\n    \"\"\"Test that completion works without context (backward compatibility).\"\"\"\n    context_was_none = False\n\n    async def handle_completion(ctx: ServerRequestContext, params: CompleteRequestParams) -> CompleteResult:\n        nonlocal context_was_none\n        context_was_none = params.context is None\n        return CompleteResult(completion=Completion(values=[\"no-context-completion\"], total=1, has_more=False))\n\n    server = Server(\"test-server\", on_completion=handle_completion)\n\n    async with Client(server) as client:\n        # Test without context\n        result = await client.complete(\n            ref=PromptReference(type=\"ref/prompt\", name=\"test-prompt\"), argument={\"name\": \"arg\", \"value\": \"val\"}\n        )\n\n        # Verify context was None\n        assert context_was_none\n        assert result.completion.values == [\"no-context-completion\"]\n\n\n@pytest.mark.anyio\nasync def test_dependent_completion_scenario():\n    \"\"\"Test a real-world scenario with dependent completions.\"\"\"\n\n    async def handle_completion(ctx: ServerRequestContext, params: CompleteRequestParams) -> CompleteResult:\n        # Simulate database/table completion scenario\n        assert isinstance(params.ref, ResourceTemplateReference)\n        assert params.ref.uri == \"db://{database}/{table}\"\n\n        if params.argument.name == \"database\":\n            return CompleteResult(\n                completion=Completion(values=[\"users_db\", \"products_db\", \"analytics_db\"], total=3, has_more=False)\n            )\n\n        assert params.argument.name == \"table\"\n        assert params.context and params.context.arguments\n        db = params.context.arguments.get(\"database\")\n        if db == \"users_db\":\n            return CompleteResult(\n                completion=Completion(values=[\"users\", \"sessions\", \"permissions\"], total=3, has_more=False)\n            )\n        else:\n            assert db == \"products_db\"\n            return CompleteResult(\n                completion=Completion(values=[\"products\", \"categories\", \"inventory\"], total=3, has_more=False)\n            )\n\n    server = Server(\"test-server\", on_completion=handle_completion)\n\n    async with Client(server) as client:\n        # First, complete database\n        db_result = await client.complete(\n            ref=ResourceTemplateReference(type=\"ref/resource\", uri=\"db://{database}/{table}\"),\n            argument={\"name\": \"database\", \"value\": \"\"},\n        )\n        assert \"users_db\" in db_result.completion.values\n        assert \"products_db\" in db_result.completion.values\n\n        # Then complete table with database context\n        table_result = await client.complete(\n            ref=ResourceTemplateReference(type=\"ref/resource\", uri=\"db://{database}/{table}\"),\n            argument={\"name\": \"table\", \"value\": \"\"},\n            context_arguments={\"database\": \"users_db\"},\n        )\n        assert table_result.completion.values == [\"users\", \"sessions\", \"permissions\"]\n\n        # Different database gives different tables\n        table_result2 = await client.complete(\n            ref=ResourceTemplateReference(type=\"ref/resource\", uri=\"db://{database}/{table}\"),\n            argument={\"name\": \"table\", \"value\": \"\"},\n            context_arguments={\"database\": \"products_db\"},\n        )\n        assert table_result2.completion.values == [\"products\", \"categories\", \"inventory\"]\n\n\n@pytest.mark.anyio\nasync def test_completion_error_on_missing_context():\n    \"\"\"Test that server can raise error when required context is missing.\"\"\"\n\n    async def handle_completion(ctx: ServerRequestContext, params: CompleteRequestParams) -> CompleteResult:\n        assert isinstance(params.ref, ResourceTemplateReference)\n        assert params.ref.uri == \"db://{database}/{table}\"\n        assert params.argument.name == \"table\"\n\n        if not params.context or not params.context.arguments or \"database\" not in params.context.arguments:\n            raise ValueError(\"Please select a database first to see available tables\")\n\n        db = params.context.arguments.get(\"database\")\n        assert db == \"test_db\"\n        return CompleteResult(completion=Completion(values=[\"users\", \"orders\", \"products\"], total=3, has_more=False))\n\n    server = Server(\"test-server\", on_completion=handle_completion)\n\n    async with Client(server) as client:\n        # Try to complete table without database context - should raise error\n        with pytest.raises(Exception) as exc_info:\n            await client.complete(\n                ref=ResourceTemplateReference(type=\"ref/resource\", uri=\"db://{database}/{table}\"),\n                argument={\"name\": \"table\", \"value\": \"\"},\n            )\n\n        # Verify error message\n        assert \"Please select a database first\" in str(exc_info.value)\n\n        # Now complete with proper context - should work normally\n        result_with_context = await client.complete(\n            ref=ResourceTemplateReference(type=\"ref/resource\", uri=\"db://{database}/{table}\"),\n            argument={\"name\": \"table\", \"value\": \"\"},\n            context_arguments={\"database\": \"test_db\"},\n        )\n\n        # Should get normal completions\n        assert result_with_context.completion.values == [\"users\", \"orders\", \"products\"]\n"
  },
  {
    "path": "tests/server/test_lifespan.py",
    "content": "\"\"\"Tests for lifespan functionality in both low-level and MCPServer servers.\"\"\"\n\nfrom collections.abc import AsyncIterator\nfrom contextlib import asynccontextmanager\n\nimport anyio\nimport pytest\nfrom pydantic import TypeAdapter\n\nfrom mcp.server import ServerRequestContext\nfrom mcp.server.lowlevel.server import NotificationOptions, Server\nfrom mcp.server.mcpserver import Context, MCPServer\nfrom mcp.server.models import InitializationOptions\nfrom mcp.shared.message import SessionMessage\nfrom mcp.types import (\n    CallToolRequestParams,\n    CallToolResult,\n    ClientCapabilities,\n    Implementation,\n    InitializeRequestParams,\n    JSONRPCMessage,\n    JSONRPCNotification,\n    JSONRPCRequest,\n    JSONRPCResponse,\n    TextContent,\n)\n\n\n@pytest.mark.anyio\nasync def test_lowlevel_server_lifespan():\n    \"\"\"Test that lifespan works in low-level server.\"\"\"\n\n    @asynccontextmanager\n    async def test_lifespan(server: Server) -> AsyncIterator[dict[str, bool]]:\n        \"\"\"Test lifespan context that tracks startup/shutdown.\"\"\"\n        context = {\"started\": False, \"shutdown\": False}\n        try:\n            context[\"started\"] = True\n            yield context\n        finally:\n            context[\"shutdown\"] = True\n\n    # Create a tool that accesses lifespan context\n    async def check_lifespan(\n        ctx: ServerRequestContext[dict[str, bool]], params: CallToolRequestParams\n    ) -> CallToolResult:\n        assert isinstance(ctx.lifespan_context, dict)\n        assert ctx.lifespan_context[\"started\"]\n        assert not ctx.lifespan_context[\"shutdown\"]\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"true\")])\n\n    server = Server[dict[str, bool]](\"test\", lifespan=test_lifespan, on_call_tool=check_lifespan)\n\n    # Create memory streams for testing\n    send_stream1, receive_stream1 = anyio.create_memory_object_stream[SessionMessage](100)\n    send_stream2, receive_stream2 = anyio.create_memory_object_stream[SessionMessage](100)\n\n    # Run server in background task\n    async with anyio.create_task_group() as tg, send_stream1, receive_stream1, send_stream2, receive_stream2:\n\n        async def run_server():\n            await server.run(\n                receive_stream1,\n                send_stream2,\n                InitializationOptions(\n                    server_name=\"test\",\n                    server_version=\"0.1.0\",\n                    capabilities=server.get_capabilities(\n                        notification_options=NotificationOptions(),\n                        experimental_capabilities={},\n                    ),\n                ),\n                raise_exceptions=True,\n            )\n\n        tg.start_soon(run_server)\n\n        # Initialize the server\n        params = InitializeRequestParams(\n            protocol_version=\"2024-11-05\",\n            capabilities=ClientCapabilities(),\n            client_info=Implementation(name=\"test-client\", version=\"0.1.0\"),\n        )\n        await send_stream1.send(\n            SessionMessage(\n                JSONRPCRequest(\n                    jsonrpc=\"2.0\",\n                    id=1,\n                    method=\"initialize\",\n                    params=TypeAdapter(InitializeRequestParams).dump_python(params),\n                )\n            )\n        )\n        response = await receive_stream2.receive()\n        response = response.message\n\n        # Send initialized notification\n        await send_stream1.send(SessionMessage(JSONRPCNotification(jsonrpc=\"2.0\", method=\"notifications/initialized\")))\n\n        # Call the tool to verify lifespan context\n        await send_stream1.send(\n            SessionMessage(\n                JSONRPCRequest(\n                    jsonrpc=\"2.0\",\n                    id=2,\n                    method=\"tools/call\",\n                    params={\"name\": \"check_lifespan\", \"arguments\": {}},\n                )\n            )\n        )\n\n        # Get response and verify\n        response = await receive_stream2.receive()\n        response = response.message\n        assert isinstance(response, JSONRPCMessage)\n        assert isinstance(response, JSONRPCResponse)\n        assert response.result[\"content\"][0][\"text\"] == \"true\"\n\n        # Cancel server task\n        tg.cancel_scope.cancel()\n\n\n@pytest.mark.anyio\nasync def test_mcpserver_server_lifespan():\n    \"\"\"Test that lifespan works in MCPServer server.\"\"\"\n\n    @asynccontextmanager\n    async def test_lifespan(server: MCPServer) -> AsyncIterator[dict[str, bool]]:\n        \"\"\"Test lifespan context that tracks startup/shutdown.\"\"\"\n        context = {\"started\": False, \"shutdown\": False}\n        try:\n            context[\"started\"] = True\n            yield context\n        finally:\n            context[\"shutdown\"] = True\n\n    server = MCPServer(\"test\", lifespan=test_lifespan)\n\n    # Create memory streams for testing\n    send_stream1, receive_stream1 = anyio.create_memory_object_stream[SessionMessage](100)\n    send_stream2, receive_stream2 = anyio.create_memory_object_stream[SessionMessage](100)\n\n    # Add a tool that checks lifespan context\n    @server.tool()\n    def check_lifespan(ctx: Context) -> bool:\n        \"\"\"Tool that checks lifespan context.\"\"\"\n        assert isinstance(ctx.request_context.lifespan_context, dict)\n        assert ctx.request_context.lifespan_context[\"started\"]\n        assert not ctx.request_context.lifespan_context[\"shutdown\"]\n        return True\n\n    # Run server in background task\n    async with anyio.create_task_group() as tg, send_stream1, receive_stream1, send_stream2, receive_stream2:\n\n        async def run_server():\n            await server._lowlevel_server.run(\n                receive_stream1,\n                send_stream2,\n                server._lowlevel_server.create_initialization_options(),\n                raise_exceptions=True,\n            )\n\n        tg.start_soon(run_server)\n\n        # Initialize the server\n        params = InitializeRequestParams(\n            protocol_version=\"2024-11-05\",\n            capabilities=ClientCapabilities(),\n            client_info=Implementation(name=\"test-client\", version=\"0.1.0\"),\n        )\n        await send_stream1.send(\n            SessionMessage(\n                JSONRPCRequest(\n                    jsonrpc=\"2.0\",\n                    id=1,\n                    method=\"initialize\",\n                    params=TypeAdapter(InitializeRequestParams).dump_python(params),\n                )\n            )\n        )\n        response = await receive_stream2.receive()\n        response = response.message\n\n        # Send initialized notification\n        await send_stream1.send(SessionMessage(JSONRPCNotification(jsonrpc=\"2.0\", method=\"notifications/initialized\")))\n\n        # Call the tool to verify lifespan context\n        await send_stream1.send(\n            SessionMessage(\n                JSONRPCRequest(\n                    jsonrpc=\"2.0\",\n                    id=2,\n                    method=\"tools/call\",\n                    params={\"name\": \"check_lifespan\", \"arguments\": {}},\n                )\n            )\n        )\n\n        # Get response and verify\n        response = await receive_stream2.receive()\n        response = response.message\n        assert isinstance(response, JSONRPCMessage)\n        assert isinstance(response, JSONRPCResponse)\n        assert response.result[\"content\"][0][\"text\"] == \"true\"\n\n        # Cancel server task\n        tg.cancel_scope.cancel()\n"
  },
  {
    "path": "tests/server/test_lowlevel_exception_handling.py",
    "content": "from unittest.mock import AsyncMock, Mock\n\nimport anyio\nimport pytest\n\nfrom mcp import types\nfrom mcp.server.lowlevel.server import Server\nfrom mcp.server.session import ServerSession\nfrom mcp.shared.message import SessionMessage\nfrom mcp.shared.session import RequestResponder\n\n\n@pytest.mark.anyio\nasync def test_exception_handling_with_raise_exceptions_true():\n    \"\"\"Transport exceptions are re-raised when raise_exceptions=True.\"\"\"\n    server = Server(\"test-server\")\n    session = Mock(spec=ServerSession)\n\n    test_exception = RuntimeError(\"Test error\")\n\n    with pytest.raises(RuntimeError, match=\"Test error\"):\n        await server._handle_message(test_exception, session, {}, raise_exceptions=True)\n\n\n@pytest.mark.anyio\nasync def test_exception_handling_with_raise_exceptions_false():\n    \"\"\"Transport exceptions are logged locally but not sent to the client.\n\n    The transport that reported the error is likely broken; writing back\n    through it races with stream closure (#1967, #2064). The TypeScript,\n    Go, and C# SDKs all log locally only.\n    \"\"\"\n    server = Server(\"test-server\")\n    session = Mock(spec=ServerSession)\n    session.send_log_message = AsyncMock()\n\n    await server._handle_message(RuntimeError(\"Test error\"), session, {}, raise_exceptions=False)\n\n    session.send_log_message.assert_not_called()\n\n\n@pytest.mark.anyio\nasync def test_normal_message_handling_not_affected():\n    \"\"\"Test that normal messages still work correctly\"\"\"\n    server = Server(\"test-server\")\n    session = Mock(spec=ServerSession)\n\n    # Create a mock RequestResponder\n    responder = Mock(spec=RequestResponder)\n    responder.request = types.PingRequest(method=\"ping\")\n    responder.__enter__ = Mock(return_value=responder)\n    responder.__exit__ = Mock(return_value=None)\n\n    # Mock the _handle_request method to avoid complex setup\n    server._handle_request = AsyncMock()\n\n    # Should handle normally without any exception handling\n    await server._handle_message(responder, session, {}, raise_exceptions=False)\n\n    # Verify _handle_request was called\n    server._handle_request.assert_called_once()\n\n\n@pytest.mark.anyio\nasync def test_server_run_exits_cleanly_when_transport_yields_exception_then_closes():\n    \"\"\"Regression test for #1967 / #2064.\n\n    Exercises the real Server.run() path with real memory streams, reproducing\n    what happens in stateless streamable HTTP when a POST handler throws:\n\n    1. Transport yields an Exception into the read stream\n       (streamable_http.py does this in its broad POST-handler except).\n    2. Transport closes the read stream (terminate() in stateless mode).\n    3. _receive_loop exits its `async with read_stream, write_stream:` block,\n       closing the write stream.\n    4. Meanwhile _handle_message(exc) was spawned via tg.start_soon and runs\n       after the write stream is closed.\n\n    Before the fix, _handle_message tried to send_log_message through the\n    closed write stream, raising ClosedResourceError inside the TaskGroup\n    and crashing server.run(). After the fix, it only logs locally.\n    \"\"\"\n    server = Server(\"test-server\")\n\n    read_send, read_recv = anyio.create_memory_object_stream[SessionMessage | Exception](1)\n    # Zero-buffer on the write stream forces send() to block until received.\n    # With no receiver, a send() sits blocked until _receive_loop exits its\n    # `async with self._read_stream, self._write_stream:` block and closes the\n    # stream, at which point the blocked send raises ClosedResourceError.\n    # This deterministically reproduces the race without sleeps.\n    write_send, write_recv = anyio.create_memory_object_stream[SessionMessage](0)\n\n    # What the streamable HTTP transport does: push the exception, then close.\n    read_send.send_nowait(RuntimeError(\"simulated transport error\"))\n    read_send.close()\n\n    with anyio.fail_after(5):\n        # stateless=True so server.run doesn't wait for initialize handshake.\n        # Before this fix, this raised ExceptionGroup(ClosedResourceError).\n        await server.run(read_recv, write_send, server.create_initialization_options(), stateless=True)\n\n    # write_send was closed inside _receive_loop's `async with`; receive_nowait\n    # raises EndOfStream iff the buffer is empty (i.e., server wrote nothing).\n    with pytest.raises(anyio.EndOfStream):\n        write_recv.receive_nowait()\n    write_recv.close()\n"
  },
  {
    "path": "tests/server/test_lowlevel_tool_annotations.py",
    "content": "\"\"\"Tests for tool annotations in low-level server.\"\"\"\n\nimport pytest\n\nfrom mcp import Client\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.types import ListToolsResult, PaginatedRequestParams, Tool, ToolAnnotations\n\n\n@pytest.mark.anyio\nasync def test_lowlevel_server_tool_annotations():\n    \"\"\"Test that tool annotations work in low-level server.\"\"\"\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        return ListToolsResult(\n            tools=[\n                Tool(\n                    name=\"echo\",\n                    description=\"Echo a message back\",\n                    input_schema={\n                        \"type\": \"object\",\n                        \"properties\": {\n                            \"message\": {\"type\": \"string\"},\n                        },\n                        \"required\": [\"message\"],\n                    },\n                    annotations=ToolAnnotations(\n                        title=\"Echo Tool\",\n                        read_only_hint=True,\n                    ),\n                )\n            ]\n        )\n\n    server = Server(\"test\", on_list_tools=handle_list_tools)\n\n    async with Client(server) as client:\n        tools_result = await client.list_tools()\n\n        assert len(tools_result.tools) == 1\n        assert tools_result.tools[0].name == \"echo\"\n        assert tools_result.tools[0].annotations is not None\n        assert tools_result.tools[0].annotations.title == \"Echo Tool\"\n        assert tools_result.tools[0].annotations.read_only_hint is True\n"
  },
  {
    "path": "tests/server/test_read_resource.py",
    "content": "import base64\n\nimport pytest\n\nfrom mcp import Client\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.types import (\n    BlobResourceContents,\n    ReadResourceRequestParams,\n    ReadResourceResult,\n    TextResourceContents,\n)\n\npytestmark = pytest.mark.anyio\n\n\nasync def test_read_resource_text():\n    async def handle_read_resource(ctx: ServerRequestContext, params: ReadResourceRequestParams) -> ReadResourceResult:\n        return ReadResourceResult(\n            contents=[TextResourceContents(uri=str(params.uri), text=\"Hello World\", mime_type=\"text/plain\")]\n        )\n\n    server = Server(\"test\", on_read_resource=handle_read_resource)\n\n    async with Client(server) as client:\n        result = await client.read_resource(\"test://resource\")\n        assert len(result.contents) == 1\n\n        content = result.contents[0]\n        assert isinstance(content, TextResourceContents)\n        assert content.text == \"Hello World\"\n        assert content.mime_type == \"text/plain\"\n\n\nasync def test_read_resource_binary():\n    binary_data = b\"Hello World\"\n\n    async def handle_read_resource(ctx: ServerRequestContext, params: ReadResourceRequestParams) -> ReadResourceResult:\n        return ReadResourceResult(\n            contents=[\n                BlobResourceContents(\n                    uri=str(params.uri),\n                    blob=base64.b64encode(binary_data).decode(\"utf-8\"),\n                    mime_type=\"application/octet-stream\",\n                )\n            ]\n        )\n\n    server = Server(\"test\", on_read_resource=handle_read_resource)\n\n    async with Client(server) as client:\n        result = await client.read_resource(\"test://resource\")\n        assert len(result.contents) == 1\n\n        content = result.contents[0]\n        assert isinstance(content, BlobResourceContents)\n        assert content.mime_type == \"application/octet-stream\"\n        assert base64.b64decode(content.blob) == binary_data\n"
  },
  {
    "path": "tests/server/test_session.py",
    "content": "from typing import Any\n\nimport anyio\nimport pytest\n\nfrom mcp import types\nfrom mcp.client.session import ClientSession\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.server.lowlevel import NotificationOptions\nfrom mcp.server.models import InitializationOptions\nfrom mcp.server.session import ServerSession\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.message import SessionMessage\nfrom mcp.shared.session import RequestResponder\nfrom mcp.types import (\n    ClientNotification,\n    CompletionsCapability,\n    InitializedNotification,\n    PromptsCapability,\n    ResourcesCapability,\n    ServerCapabilities,\n)\n\n\n@pytest.mark.anyio\nasync def test_server_session_initialize():\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](1)\n\n    # Create a message handler to catch exceptions\n    async def message_handler(  # pragma: no cover\n        message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,\n    ) -> None:\n        if isinstance(message, Exception):\n            raise message\n\n    received_initialized = False\n\n    async def run_server():\n        nonlocal received_initialized\n\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(\n                server_name=\"mcp\",\n                server_version=\"0.1.0\",\n                capabilities=ServerCapabilities(),\n            ),\n        ) as server_session:\n            async for message in server_session.incoming_messages:  # pragma: no branch\n                if isinstance(message, Exception):  # pragma: no cover\n                    raise message\n\n                if isinstance(message, ClientNotification) and isinstance(\n                    message, InitializedNotification\n                ):  # pragma: no branch\n                    received_initialized = True\n                    return\n\n    try:\n        async with (\n            ClientSession(\n                server_to_client_receive,\n                client_to_server_send,\n                message_handler=message_handler,\n            ) as client_session,\n            anyio.create_task_group() as tg,\n        ):\n            tg.start_soon(run_server)\n\n            await client_session.initialize()\n    except anyio.ClosedResourceError:  # pragma: no cover\n        pass\n\n    assert received_initialized\n\n\n@pytest.mark.anyio\nasync def test_server_capabilities():\n    notification_options = NotificationOptions()\n    experimental_capabilities: dict[str, Any] = {}\n\n    async def noop_list_prompts(\n        ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n    ) -> types.ListPromptsResult:\n        raise NotImplementedError\n\n    async def noop_list_resources(\n        ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n    ) -> types.ListResourcesResult:\n        raise NotImplementedError\n\n    async def noop_completion(ctx: ServerRequestContext, params: types.CompleteRequestParams) -> types.CompleteResult:\n        raise NotImplementedError\n\n    # No capabilities\n    server = Server(\"test\")\n    caps = server.get_capabilities(notification_options, experimental_capabilities)\n    assert caps.prompts is None\n    assert caps.resources is None\n    assert caps.completions is None\n\n    # With prompts handler\n    server = Server(\"test\", on_list_prompts=noop_list_prompts)\n    caps = server.get_capabilities(notification_options, experimental_capabilities)\n    assert caps.prompts == PromptsCapability(list_changed=False)\n    assert caps.resources is None\n    assert caps.completions is None\n\n    # With prompts + resources handlers\n    server = Server(\"test\", on_list_prompts=noop_list_prompts, on_list_resources=noop_list_resources)\n    caps = server.get_capabilities(notification_options, experimental_capabilities)\n    assert caps.prompts == PromptsCapability(list_changed=False)\n    assert caps.resources == ResourcesCapability(subscribe=False, list_changed=False)\n    assert caps.completions is None\n\n    # With prompts + resources + completion handlers\n    server = Server(\n        \"test\",\n        on_list_prompts=noop_list_prompts,\n        on_list_resources=noop_list_resources,\n        on_completion=noop_completion,\n    )\n    caps = server.get_capabilities(notification_options, experimental_capabilities)\n    assert caps.prompts == PromptsCapability(list_changed=False)\n    assert caps.resources == ResourcesCapability(subscribe=False, list_changed=False)\n    assert caps.completions == CompletionsCapability()\n\n\n@pytest.mark.anyio\nasync def test_server_session_initialize_with_older_protocol_version():\n    \"\"\"Test that server accepts and responds with older protocol (2024-11-05).\"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1)\n\n    received_initialized = False\n    received_protocol_version = None\n\n    async def run_server():\n        nonlocal received_initialized\n\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(\n                server_name=\"mcp\",\n                server_version=\"0.1.0\",\n                capabilities=ServerCapabilities(),\n            ),\n        ) as server_session:\n            async for message in server_session.incoming_messages:  # pragma: no branch\n                if isinstance(message, Exception):  # pragma: no cover\n                    raise message\n\n                if isinstance(message, types.ClientNotification) and isinstance(\n                    message, InitializedNotification\n                ):  # pragma: no branch\n                    received_initialized = True\n                    return\n\n    async def mock_client():\n        nonlocal received_protocol_version\n\n        # Send initialization request with older protocol version (2024-11-05)\n        await client_to_server_send.send(\n            SessionMessage(\n                types.JSONRPCRequest(\n                    jsonrpc=\"2.0\",\n                    id=1,\n                    method=\"initialize\",\n                    params=types.InitializeRequestParams(\n                        protocol_version=\"2024-11-05\",\n                        capabilities=types.ClientCapabilities(),\n                        client_info=types.Implementation(name=\"test-client\", version=\"1.0.0\"),\n                    ).model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                )\n            )\n        )\n\n        # Wait for the initialize response\n        init_response_message = await server_to_client_receive.receive()\n        assert isinstance(init_response_message.message, types.JSONRPCResponse)\n        result_data = init_response_message.message.result\n        init_result = types.InitializeResult.model_validate(result_data)\n\n        # Check that the server responded with the requested protocol version\n        received_protocol_version = init_result.protocol_version\n        assert received_protocol_version == \"2024-11-05\"\n\n        # Send initialized notification\n        await client_to_server_send.send(\n            SessionMessage(types.JSONRPCNotification(jsonrpc=\"2.0\", method=\"notifications/initialized\"))\n        )\n\n    async with (\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n        anyio.create_task_group() as tg,\n    ):\n        tg.start_soon(run_server)\n        tg.start_soon(mock_client)\n\n    assert received_initialized\n    assert received_protocol_version == \"2024-11-05\"\n\n\n@pytest.mark.anyio\nasync def test_ping_request_before_initialization():\n    \"\"\"Test that ping requests are allowed before initialization is complete.\"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1)\n\n    ping_response_received = False\n    ping_response_id = None\n\n    async def run_server():\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(\n                server_name=\"mcp\",\n                server_version=\"0.1.0\",\n                capabilities=ServerCapabilities(),\n            ),\n        ) as server_session:\n            async for message in server_session.incoming_messages:  # pragma: no branch\n                if isinstance(message, Exception):  # pragma: no cover\n                    raise message\n\n                # We should receive a ping request before initialization\n                if isinstance(message, RequestResponder) and isinstance(\n                    message.request, types.PingRequest\n                ):  # pragma: no branch\n                    # Respond to the ping\n                    with message:\n                        await message.respond(types.EmptyResult())\n                    return\n\n    async def mock_client():\n        nonlocal ping_response_received, ping_response_id\n\n        # Send ping request before any initialization\n        await client_to_server_send.send(SessionMessage(types.JSONRPCRequest(jsonrpc=\"2.0\", id=42, method=\"ping\")))\n\n        # Wait for the ping response\n        ping_response_message = await server_to_client_receive.receive()\n        assert isinstance(ping_response_message.message, types.JSONRPCResponse)\n\n        ping_response_received = True\n        ping_response_id = ping_response_message.message.id\n\n    async with (\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n        anyio.create_task_group() as tg,\n    ):\n        tg.start_soon(run_server)\n        tg.start_soon(mock_client)\n\n    assert ping_response_received\n    assert ping_response_id == 42\n\n\n@pytest.mark.anyio\nasync def test_create_message_tool_result_validation():\n    \"\"\"Test tool_use/tool_result validation in create_message.\"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1)\n\n    async with (\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(\n                server_name=\"test\",\n                server_version=\"0.1.0\",\n                capabilities=ServerCapabilities(),\n            ),\n        ) as session:\n            # Set up client params with sampling.tools capability for the test\n            session._client_params = types.InitializeRequestParams(\n                protocol_version=types.LATEST_PROTOCOL_VERSION,\n                capabilities=types.ClientCapabilities(\n                    sampling=types.SamplingCapability(tools=types.SamplingToolsCapability())\n                ),\n                client_info=types.Implementation(name=\"test\", version=\"1.0\"),\n            )\n\n            tool = types.Tool(name=\"test_tool\", input_schema={\"type\": \"object\"})\n            text = types.TextContent(type=\"text\", text=\"hello\")\n            tool_use = types.ToolUseContent(type=\"tool_use\", id=\"call_1\", name=\"test_tool\", input={})\n            tool_result = types.ToolResultContent(type=\"tool_result\", tool_use_id=\"call_1\", content=[])\n\n            # Case 1: tool_result mixed with other content\n            with pytest.raises(ValueError, match=\"only tool_result content\"):\n                await session.create_message(\n                    messages=[\n                        types.SamplingMessage(role=\"user\", content=text),\n                        types.SamplingMessage(role=\"assistant\", content=tool_use),\n                        types.SamplingMessage(role=\"user\", content=[tool_result, text]),  # mixed!\n                    ],\n                    max_tokens=100,\n                    tools=[tool],\n                )\n\n            # Case 2: tool_result without previous message\n            with pytest.raises(ValueError, match=\"requires a previous message\"):\n                await session.create_message(\n                    messages=[types.SamplingMessage(role=\"user\", content=tool_result)],\n                    max_tokens=100,\n                    tools=[tool],\n                )\n\n            # Case 3: tool_result without previous tool_use\n            with pytest.raises(ValueError, match=\"do not match any tool_use\"):\n                await session.create_message(\n                    messages=[\n                        types.SamplingMessage(role=\"user\", content=text),\n                        types.SamplingMessage(role=\"user\", content=tool_result),\n                    ],\n                    max_tokens=100,\n                    tools=[tool],\n                )\n\n            # Case 4: mismatched tool IDs\n            with pytest.raises(ValueError, match=\"ids of tool_result blocks and tool_use blocks\"):\n                await session.create_message(\n                    messages=[\n                        types.SamplingMessage(role=\"user\", content=text),\n                        types.SamplingMessage(role=\"assistant\", content=tool_use),\n                        types.SamplingMessage(\n                            role=\"user\",\n                            content=types.ToolResultContent(type=\"tool_result\", tool_use_id=\"wrong_id\", content=[]),\n                        ),\n                    ],\n                    max_tokens=100,\n                    tools=[tool],\n                )\n\n            # Case 5: text-only message with tools (no tool_results) - passes validation\n            # Covers has_tool_results=False branch.\n            # We use move_on_after because validation happens synchronously before\n            # send_request, which would block indefinitely waiting for a response.\n            # The timeout lets validation pass, then cancels the blocked send.\n            with anyio.move_on_after(0.01):\n                await session.create_message(\n                    messages=[types.SamplingMessage(role=\"user\", content=text)],\n                    max_tokens=100,\n                    tools=[tool],\n                )\n\n            # Case 6: valid matching tool_result/tool_use IDs - passes validation\n            # Covers tool_use_ids == tool_result_ids branch.\n            # (see Case 5 comment for move_on_after explanation)\n            with anyio.move_on_after(0.01):\n                await session.create_message(\n                    messages=[\n                        types.SamplingMessage(role=\"user\", content=text),\n                        types.SamplingMessage(role=\"assistant\", content=tool_use),\n                        types.SamplingMessage(role=\"user\", content=tool_result),\n                    ],\n                    max_tokens=100,\n                    tools=[tool],\n                )\n\n            # Case 7: validation runs even without `tools` parameter\n            # (tool loop continuation may omit tools while containing tool_result)\n            with pytest.raises(ValueError, match=\"do not match any tool_use\"):\n                await session.create_message(\n                    messages=[\n                        types.SamplingMessage(role=\"user\", content=text),\n                        types.SamplingMessage(role=\"user\", content=tool_result),\n                    ],\n                    max_tokens=100,\n                    # Note: no tools parameter\n                )\n\n            # Case 8: empty messages list - skips validation entirely\n            # Covers the `if messages:` branch (line 280->302)\n            with anyio.move_on_after(0.01):  # pragma: no branch\n                await session.create_message(messages=[], max_tokens=100)\n\n\n@pytest.mark.anyio\nasync def test_create_message_without_tools_capability():\n    \"\"\"Test that create_message raises MCPError when tools are provided without capability.\"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1)\n\n    async with (\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(\n                server_name=\"test\",\n                server_version=\"0.1.0\",\n                capabilities=ServerCapabilities(),\n            ),\n        ) as session:\n            # Set up client params WITHOUT sampling.tools capability\n            session._client_params = types.InitializeRequestParams(\n                protocol_version=types.LATEST_PROTOCOL_VERSION,\n                capabilities=types.ClientCapabilities(sampling=types.SamplingCapability()),\n                client_info=types.Implementation(name=\"test\", version=\"1.0\"),\n            )\n\n            tool = types.Tool(name=\"test_tool\", input_schema={\"type\": \"object\"})\n            text = types.TextContent(type=\"text\", text=\"hello\")\n\n            # Should raise MCPError when tools are provided but client lacks capability\n            with pytest.raises(MCPError) as exc_info:\n                await session.create_message(\n                    messages=[types.SamplingMessage(role=\"user\", content=text)],\n                    max_tokens=100,\n                    tools=[tool],\n                )\n            assert \"does not support sampling tools capability\" in exc_info.value.error.message\n\n            # Should also raise MCPError when tool_choice is provided\n            with pytest.raises(MCPError) as exc_info:\n                await session.create_message(\n                    messages=[types.SamplingMessage(role=\"user\", content=text)],\n                    max_tokens=100,\n                    tool_choice=types.ToolChoice(mode=\"auto\"),\n                )\n            assert \"does not support sampling tools capability\" in exc_info.value.error.message\n\n\n@pytest.mark.anyio\nasync def test_other_requests_blocked_before_initialization():\n    \"\"\"Test that non-ping requests are still blocked before initialization.\"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1)\n\n    error_response_received = False\n    error_code = None\n\n    async def run_server():\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(\n                server_name=\"mcp\",\n                server_version=\"0.1.0\",\n                capabilities=ServerCapabilities(),\n            ),\n        ):\n            # Server should handle the request and send an error response\n            # No need to process incoming_messages since the error is handled automatically\n            await anyio.sleep(0.1)  # Give time for the request to be processed\n\n    async def mock_client():\n        nonlocal error_response_received, error_code\n\n        # Try to send a non-ping request before initialization\n        await client_to_server_send.send(\n            SessionMessage(types.JSONRPCRequest(jsonrpc=\"2.0\", id=1, method=\"prompts/list\"))\n        )\n\n        # Wait for the error response\n        error_message = await server_to_client_receive.receive()\n        if isinstance(error_message.message, types.JSONRPCError):  # pragma: no branch\n            error_response_received = True\n            error_code = error_message.message.error.code\n\n    async with (\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n        anyio.create_task_group() as tg,\n    ):\n        tg.start_soon(run_server)\n        tg.start_soon(mock_client)\n\n    assert error_response_received\n    assert error_code == types.INVALID_PARAMS\n"
  },
  {
    "path": "tests/server/test_session_race_condition.py",
    "content": "\"\"\"Test for race condition fix in initialization flow.\n\nThis test verifies that requests can be processed immediately after\nresponding to InitializeRequest, without waiting for InitializedNotification.\n\nThis is critical for HTTP transport where requests can arrive in any order.\n\"\"\"\n\nimport anyio\nimport pytest\n\nfrom mcp import types\nfrom mcp.server.models import InitializationOptions\nfrom mcp.server.session import ServerSession\nfrom mcp.shared.message import SessionMessage\nfrom mcp.shared.session import RequestResponder\nfrom mcp.types import ServerCapabilities, Tool\n\n\n@pytest.mark.anyio\nasync def test_request_immediately_after_initialize_response():\n    \"\"\"Test that requests are accepted immediately after initialize response.\n\n    This reproduces the race condition in stateful HTTP mode where:\n    1. Client sends InitializeRequest\n    2. Server responds with InitializeResult\n    3. Client immediately sends tools/list (before server receives InitializedNotification)\n    4. Without fix: Server rejects with \"Received request before initialization was complete\"\n    5. With fix: Server accepts and processes the request\n\n    This test simulates the HTTP transport behavior where InitializedNotification\n    may arrive in a separate POST request after other requests.\n    \"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](10)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](10)\n\n    tools_list_success = False\n    error_received = None\n\n    async def run_server():\n        nonlocal tools_list_success\n\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(\n                server_name=\"test-server\",\n                server_version=\"1.0.0\",\n                capabilities=ServerCapabilities(\n                    tools=types.ToolsCapability(list_changed=False),\n                ),\n            ),\n        ) as server_session:\n            async for message in server_session.incoming_messages:  # pragma: no branch\n                if isinstance(message, Exception):  # pragma: no cover\n                    raise message\n\n                # Handle tools/list request\n                if isinstance(message, RequestResponder):\n                    if isinstance(message.request, types.ListToolsRequest):  # pragma: no branch\n                        tools_list_success = True\n                        # Respond with a tool list\n                        with message:\n                            await message.respond(\n                                types.ListToolsResult(\n                                    tools=[\n                                        Tool(\n                                            name=\"example_tool\",\n                                            description=\"An example tool\",\n                                            input_schema={\"type\": \"object\", \"properties\": {}},\n                                        )\n                                    ]\n                                )\n                            )\n\n                # Handle InitializedNotification\n                if isinstance(message, types.ClientNotification):\n                    if isinstance(message, types.InitializedNotification):  # pragma: no branch\n                        # Done - exit gracefully\n                        return\n\n    async def mock_client():\n        nonlocal error_received\n\n        # Step 1: Send InitializeRequest\n        await client_to_server_send.send(\n            SessionMessage(\n                types.JSONRPCRequest(\n                    jsonrpc=\"2.0\",\n                    id=1,\n                    method=\"initialize\",\n                    params=types.InitializeRequestParams(\n                        protocol_version=types.LATEST_PROTOCOL_VERSION,\n                        capabilities=types.ClientCapabilities(),\n                        client_info=types.Implementation(name=\"test-client\", version=\"1.0.0\"),\n                    ).model_dump(by_alias=True, mode=\"json\", exclude_none=True),\n                )\n            )\n        )\n\n        # Step 2: Wait for InitializeResult\n        init_msg = await server_to_client_receive.receive()\n        assert isinstance(init_msg.message, types.JSONRPCResponse)\n\n        # Step 3: Immediately send tools/list BEFORE InitializedNotification\n        # This is the race condition scenario\n        await client_to_server_send.send(SessionMessage(types.JSONRPCRequest(jsonrpc=\"2.0\", id=2, method=\"tools/list\")))\n\n        # Step 4: Check the response\n        tools_msg = await server_to_client_receive.receive()\n        if isinstance(tools_msg.message, types.JSONRPCError):  # pragma: no cover\n            error_received = tools_msg.message.error.message\n\n        # Step 5: Send InitializedNotification\n        await client_to_server_send.send(\n            SessionMessage(types.JSONRPCNotification(jsonrpc=\"2.0\", method=\"notifications/initialized\"))\n        )\n\n    async with (\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n        anyio.create_task_group() as tg,\n    ):\n        tg.start_soon(run_server)\n        tg.start_soon(mock_client)\n\n    # With the PR fix: tools_list_success should be True, error_received should be None\n    # Without the fix: error_received would contain \"Received request before initialization was complete\"\n    assert tools_list_success, f\"tools/list should have succeeded. Error received: {error_received}\"\n    assert error_received is None, f\"Expected no error, but got: {error_received}\"\n"
  },
  {
    "path": "tests/server/test_sse_security.py",
    "content": "\"\"\"Tests for SSE server DNS rebinding protection.\"\"\"\n\nimport logging\nimport multiprocessing\nimport socket\n\nimport httpx\nimport pytest\nimport uvicorn\nfrom starlette.applications import Starlette\nfrom starlette.requests import Request\nfrom starlette.responses import Response\nfrom starlette.routing import Mount, Route\n\nfrom mcp.server import Server\nfrom mcp.server.sse import SseServerTransport\nfrom mcp.server.transport_security import TransportSecuritySettings\nfrom mcp.types import Tool\nfrom tests.test_helpers import wait_for_server\n\nlogger = logging.getLogger(__name__)\nSERVER_NAME = \"test_sse_security_server\"\n\n\n@pytest.fixture\ndef server_port() -> int:\n    with socket.socket() as s:\n        s.bind((\"127.0.0.1\", 0))\n        return s.getsockname()[1]\n\n\n@pytest.fixture\ndef server_url(server_port: int) -> str:  # pragma: no cover\n    return f\"http://127.0.0.1:{server_port}\"\n\n\nclass SecurityTestServer(Server):  # pragma: no cover\n    def __init__(self):\n        super().__init__(SERVER_NAME)\n\n    async def on_list_tools(self) -> list[Tool]:\n        return []\n\n\ndef run_server_with_settings(port: int, security_settings: TransportSecuritySettings | None = None):  # pragma: no cover\n    \"\"\"Run the SSE server with specified security settings.\"\"\"\n    app = SecurityTestServer()\n    sse_transport = SseServerTransport(\"/messages/\", security_settings)\n\n    async def handle_sse(request: Request):\n        try:\n            async with sse_transport.connect_sse(request.scope, request.receive, request._send) as streams:\n                if streams:\n                    await app.run(streams[0], streams[1], app.create_initialization_options())\n        except ValueError as e:\n            # Validation error was already handled inside connect_sse\n            logger.debug(f\"SSE connection failed validation: {e}\")\n        return Response()\n\n    routes = [\n        Route(\"/sse\", endpoint=handle_sse),\n        Mount(\"/messages/\", app=sse_transport.handle_post_message),\n    ]\n\n    starlette_app = Starlette(routes=routes)\n    uvicorn.run(starlette_app, host=\"127.0.0.1\", port=port, log_level=\"error\")\n\n\ndef start_server_process(port: int, security_settings: TransportSecuritySettings | None = None):\n    \"\"\"Start server in a separate process.\"\"\"\n    process = multiprocessing.Process(target=run_server_with_settings, args=(port, security_settings))\n    process.start()\n    # Wait for server to be ready to accept connections\n    wait_for_server(port)\n    return process\n\n\n@pytest.mark.anyio\nasync def test_sse_security_default_settings(server_port: int):\n    \"\"\"Test SSE with default security settings (protection disabled).\"\"\"\n    process = start_server_process(server_port)\n\n    try:\n        headers = {\"Host\": \"evil.com\", \"Origin\": \"http://evil.com\"}\n\n        async with httpx.AsyncClient(timeout=5.0) as client:\n            async with client.stream(\"GET\", f\"http://127.0.0.1:{server_port}/sse\", headers=headers) as response:\n                assert response.status_code == 200\n    finally:\n        process.terminate()\n        process.join()\n\n\n@pytest.mark.anyio\nasync def test_sse_security_invalid_host_header(server_port: int):\n    \"\"\"Test SSE with invalid Host header.\"\"\"\n    # Enable security by providing settings with an empty allowed_hosts list\n    security_settings = TransportSecuritySettings(enable_dns_rebinding_protection=True, allowed_hosts=[\"example.com\"])\n    process = start_server_process(server_port, security_settings)\n\n    try:\n        # Test with invalid host header\n        headers = {\"Host\": \"evil.com\"}\n\n        async with httpx.AsyncClient() as client:\n            response = await client.get(f\"http://127.0.0.1:{server_port}/sse\", headers=headers)\n            assert response.status_code == 421\n            assert response.text == \"Invalid Host header\"\n\n    finally:\n        process.terminate()\n        process.join()\n\n\n@pytest.mark.anyio\nasync def test_sse_security_invalid_origin_header(server_port: int):\n    \"\"\"Test SSE with invalid Origin header.\"\"\"\n    # Configure security to allow the host but restrict origins\n    security_settings = TransportSecuritySettings(\n        enable_dns_rebinding_protection=True, allowed_hosts=[\"127.0.0.1:*\"], allowed_origins=[\"http://localhost:*\"]\n    )\n    process = start_server_process(server_port, security_settings)\n\n    try:\n        # Test with invalid origin header\n        headers = {\"Origin\": \"http://evil.com\"}\n\n        async with httpx.AsyncClient() as client:\n            response = await client.get(f\"http://127.0.0.1:{server_port}/sse\", headers=headers)\n            assert response.status_code == 403\n            assert response.text == \"Invalid Origin header\"\n\n    finally:\n        process.terminate()\n        process.join()\n\n\n@pytest.mark.anyio\nasync def test_sse_security_post_invalid_content_type(server_port: int):\n    \"\"\"Test POST endpoint with invalid Content-Type header.\"\"\"\n    # Configure security to allow the host\n    security_settings = TransportSecuritySettings(\n        enable_dns_rebinding_protection=True, allowed_hosts=[\"127.0.0.1:*\"], allowed_origins=[\"http://127.0.0.1:*\"]\n    )\n    process = start_server_process(server_port, security_settings)\n\n    try:\n        async with httpx.AsyncClient(timeout=5.0) as client:\n            # Test POST with invalid content type\n            fake_session_id = \"12345678123456781234567812345678\"\n            response = await client.post(\n                f\"http://127.0.0.1:{server_port}/messages/?session_id={fake_session_id}\",\n                headers={\"Content-Type\": \"text/plain\"},\n                content=\"test\",\n            )\n            assert response.status_code == 400\n            assert response.text == \"Invalid Content-Type header\"\n\n            # Test POST with missing content type\n            response = await client.post(\n                f\"http://127.0.0.1:{server_port}/messages/?session_id={fake_session_id}\", content=\"test\"\n            )\n            assert response.status_code == 400\n            assert response.text == \"Invalid Content-Type header\"\n\n    finally:\n        process.terminate()\n        process.join()\n\n\n@pytest.mark.anyio\nasync def test_sse_security_disabled(server_port: int):\n    \"\"\"Test SSE with security disabled.\"\"\"\n    settings = TransportSecuritySettings(enable_dns_rebinding_protection=False)\n    process = start_server_process(server_port, settings)\n\n    try:\n        # Test with invalid host header - should still work\n        headers = {\"Host\": \"evil.com\"}\n\n        async with httpx.AsyncClient(timeout=5.0) as client:\n            # For SSE endpoints, we need to use stream to avoid timeout\n            async with client.stream(\"GET\", f\"http://127.0.0.1:{server_port}/sse\", headers=headers) as response:\n                # Should connect successfully even with invalid host\n                assert response.status_code == 200\n\n    finally:\n        process.terminate()\n        process.join()\n\n\n@pytest.mark.anyio\nasync def test_sse_security_custom_allowed_hosts(server_port: int):\n    \"\"\"Test SSE with custom allowed hosts.\"\"\"\n    settings = TransportSecuritySettings(\n        enable_dns_rebinding_protection=True,\n        allowed_hosts=[\"localhost\", \"127.0.0.1\", \"custom.host\"],\n        allowed_origins=[\"http://localhost\", \"http://127.0.0.1\", \"http://custom.host\"],\n    )\n    process = start_server_process(server_port, settings)\n\n    try:\n        # Test with custom allowed host\n        headers = {\"Host\": \"custom.host\"}\n\n        async with httpx.AsyncClient(timeout=5.0) as client:\n            # For SSE endpoints, we need to use stream to avoid timeout\n            async with client.stream(\"GET\", f\"http://127.0.0.1:{server_port}/sse\", headers=headers) as response:\n                # Should connect successfully with custom host\n                assert response.status_code == 200\n\n        # Test with non-allowed host\n        headers = {\"Host\": \"evil.com\"}\n\n        async with httpx.AsyncClient() as client:\n            response = await client.get(f\"http://127.0.0.1:{server_port}/sse\", headers=headers)\n            assert response.status_code == 421\n            assert response.text == \"Invalid Host header\"\n\n    finally:\n        process.terminate()\n        process.join()\n\n\n@pytest.mark.anyio\nasync def test_sse_security_wildcard_ports(server_port: int):\n    \"\"\"Test SSE with wildcard port patterns.\"\"\"\n    settings = TransportSecuritySettings(\n        enable_dns_rebinding_protection=True,\n        allowed_hosts=[\"localhost:*\", \"127.0.0.1:*\"],\n        allowed_origins=[\"http://localhost:*\", \"http://127.0.0.1:*\"],\n    )\n    process = start_server_process(server_port, settings)\n\n    try:\n        # Test with various port numbers\n        for test_port in [8080, 3000, 9999]:\n            headers = {\"Host\": f\"localhost:{test_port}\"}\n\n            async with httpx.AsyncClient(timeout=5.0) as client:\n                # For SSE endpoints, we need to use stream to avoid timeout\n                async with client.stream(\"GET\", f\"http://127.0.0.1:{server_port}/sse\", headers=headers) as response:\n                    # Should connect successfully with any port\n                    assert response.status_code == 200\n\n            headers = {\"Origin\": f\"http://localhost:{test_port}\"}\n\n            async with httpx.AsyncClient(timeout=5.0) as client:\n                # For SSE endpoints, we need to use stream to avoid timeout\n                async with client.stream(\"GET\", f\"http://127.0.0.1:{server_port}/sse\", headers=headers) as response:\n                    # Should connect successfully with any port\n                    assert response.status_code == 200\n\n    finally:\n        process.terminate()\n        process.join()\n\n\n@pytest.mark.anyio\nasync def test_sse_security_post_valid_content_type(server_port: int):\n    \"\"\"Test POST endpoint with valid Content-Type headers.\"\"\"\n    # Configure security to allow the host\n    security_settings = TransportSecuritySettings(\n        enable_dns_rebinding_protection=True, allowed_hosts=[\"127.0.0.1:*\"], allowed_origins=[\"http://127.0.0.1:*\"]\n    )\n    process = start_server_process(server_port, security_settings)\n\n    try:\n        async with httpx.AsyncClient() as client:\n            # Test with various valid content types\n            valid_content_types = [\n                \"application/json\",\n                \"application/json; charset=utf-8\",\n                \"application/json;charset=utf-8\",\n                \"APPLICATION/JSON\",  # Case insensitive\n            ]\n\n            for content_type in valid_content_types:\n                # Use a valid UUID format (even though session won't exist)\n                fake_session_id = \"12345678123456781234567812345678\"\n                response = await client.post(\n                    f\"http://127.0.0.1:{server_port}/messages/?session_id={fake_session_id}\",\n                    headers={\"Content-Type\": content_type},\n                    json={\"test\": \"data\"},\n                )\n                # Will get 404 because session doesn't exist, but that's OK\n                # We're testing that it passes the content-type check\n                assert response.status_code == 404\n                assert response.text == \"Could not find session\"\n\n    finally:\n        process.terminate()\n        process.join()\n"
  },
  {
    "path": "tests/server/test_stateless_mode.py",
    "content": "\"\"\"Tests for stateless HTTP mode limitations.\n\nStateless HTTP mode does not support server-to-client requests because there\nis no persistent connection for bidirectional communication. These tests verify\nthat appropriate errors are raised when attempting to use unsupported features.\n\nSee: https://github.com/modelcontextprotocol/python-sdk/issues/1097\n\"\"\"\n\nfrom collections.abc import AsyncGenerator\nfrom typing import Any\n\nimport anyio\nimport pytest\n\nfrom mcp import types\nfrom mcp.server.models import InitializationOptions\nfrom mcp.server.session import ServerSession\nfrom mcp.shared.exceptions import StatelessModeNotSupported\nfrom mcp.shared.message import SessionMessage\nfrom mcp.types import ServerCapabilities\n\n\n@pytest.fixture\nasync def stateless_session() -> AsyncGenerator[ServerSession, None]:\n    \"\"\"Create a stateless ServerSession for testing.\"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1)\n\n    init_options = InitializationOptions(\n        server_name=\"test\",\n        server_version=\"0.1.0\",\n        capabilities=ServerCapabilities(),\n    )\n\n    async with (\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            init_options,\n            stateless=True,\n        ) as session:\n            yield session\n\n\n@pytest.mark.anyio\nasync def test_list_roots_fails_in_stateless_mode(stateless_session: ServerSession):\n    \"\"\"Test that list_roots raises StatelessModeNotSupported in stateless mode.\"\"\"\n    with pytest.raises(StatelessModeNotSupported, match=\"list_roots\"):\n        await stateless_session.list_roots()\n\n\n@pytest.mark.anyio\nasync def test_create_message_fails_in_stateless_mode(stateless_session: ServerSession):\n    \"\"\"Test that create_message raises StatelessModeNotSupported in stateless mode.\"\"\"\n    with pytest.raises(StatelessModeNotSupported, match=\"sampling\"):\n        await stateless_session.create_message(\n            messages=[\n                types.SamplingMessage(\n                    role=\"user\",\n                    content=types.TextContent(type=\"text\", text=\"hello\"),\n                )\n            ],\n            max_tokens=100,\n        )\n\n\n@pytest.mark.anyio\nasync def test_elicit_form_fails_in_stateless_mode(stateless_session: ServerSession):\n    \"\"\"Test that elicit_form raises StatelessModeNotSupported in stateless mode.\"\"\"\n    with pytest.raises(StatelessModeNotSupported, match=\"elicitation\"):\n        await stateless_session.elicit_form(\n            message=\"Please provide input\",\n            requested_schema={\"type\": \"object\", \"properties\": {}},\n        )\n\n\n@pytest.mark.anyio\nasync def test_elicit_url_fails_in_stateless_mode(stateless_session: ServerSession):\n    \"\"\"Test that elicit_url raises StatelessModeNotSupported in stateless mode.\"\"\"\n    with pytest.raises(StatelessModeNotSupported, match=\"elicitation\"):\n        await stateless_session.elicit_url(\n            message=\"Please authenticate\",\n            url=\"https://example.com/auth\",\n            elicitation_id=\"test-123\",\n        )\n\n\n@pytest.mark.anyio\nasync def test_elicit_deprecated_fails_in_stateless_mode(stateless_session: ServerSession):\n    \"\"\"Test that the deprecated elicit method also fails in stateless mode.\"\"\"\n    with pytest.raises(StatelessModeNotSupported, match=\"elicitation\"):\n        await stateless_session.elicit(\n            message=\"Please provide input\",\n            requested_schema={\"type\": \"object\", \"properties\": {}},\n        )\n\n\n@pytest.mark.anyio\nasync def test_stateless_error_message_is_actionable(stateless_session: ServerSession):\n    \"\"\"Test that the error message provides actionable guidance.\"\"\"\n    with pytest.raises(StatelessModeNotSupported) as exc_info:\n        await stateless_session.list_roots()\n\n    error_message = str(exc_info.value)\n    # Should mention it's stateless mode\n    assert \"stateless HTTP mode\" in error_message\n    # Should explain why it doesn't work\n    assert \"server-to-client requests\" in error_message\n    # Should tell user how to fix it\n    assert \"stateless_http=False\" in error_message\n\n\n@pytest.mark.anyio\nasync def test_exception_has_method_attribute(stateless_session: ServerSession):\n    \"\"\"Test that the exception has a method attribute for programmatic access.\"\"\"\n    with pytest.raises(StatelessModeNotSupported) as exc_info:\n        await stateless_session.list_roots()\n\n    assert exc_info.value.method == \"list_roots\"\n\n\n@pytest.fixture\nasync def stateful_session() -> AsyncGenerator[ServerSession, None]:\n    \"\"\"Create a stateful ServerSession for testing.\"\"\"\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](1)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage | Exception](1)\n\n    init_options = InitializationOptions(\n        server_name=\"test\",\n        server_version=\"0.1.0\",\n        capabilities=ServerCapabilities(),\n    )\n\n    async with (\n        client_to_server_send,\n        client_to_server_receive,\n        server_to_client_send,\n        server_to_client_receive,\n    ):\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            init_options,\n            stateless=False,\n        ) as session:\n            yield session\n\n\n@pytest.mark.anyio\nasync def test_stateful_mode_does_not_raise_stateless_error(\n    stateful_session: ServerSession, monkeypatch: pytest.MonkeyPatch\n):\n    \"\"\"Test that StatelessModeNotSupported is not raised in stateful mode.\n\n    We mock send_request to avoid blocking on I/O while still verifying\n    that the stateless check passes.\n    \"\"\"\n    send_request_called = False\n\n    async def mock_send_request(*_: Any, **__: Any) -> types.ListRootsResult:\n        nonlocal send_request_called\n        send_request_called = True\n        return types.ListRootsResult(roots=[])\n\n    monkeypatch.setattr(stateful_session, \"send_request\", mock_send_request)\n\n    # This should NOT raise StatelessModeNotSupported\n    result = await stateful_session.list_roots()\n\n    assert send_request_called\n    assert isinstance(result, types.ListRootsResult)\n"
  },
  {
    "path": "tests/server/test_stdio.py",
    "content": "import io\nimport sys\nfrom io import TextIOWrapper\n\nimport anyio\nimport pytest\n\nfrom mcp.server.stdio import stdio_server\nfrom mcp.shared.message import SessionMessage\nfrom mcp.types import JSONRPCMessage, JSONRPCRequest, JSONRPCResponse, jsonrpc_message_adapter\n\n\n@pytest.mark.anyio\nasync def test_stdio_server():\n    stdin = io.StringIO()\n    stdout = io.StringIO()\n\n    messages = [\n        JSONRPCRequest(jsonrpc=\"2.0\", id=1, method=\"ping\"),\n        JSONRPCResponse(jsonrpc=\"2.0\", id=2, result={}),\n    ]\n\n    for message in messages:\n        stdin.write(message.model_dump_json(by_alias=True, exclude_none=True) + \"\\n\")\n    stdin.seek(0)\n\n    async with stdio_server(stdin=anyio.AsyncFile(stdin), stdout=anyio.AsyncFile(stdout)) as (\n        read_stream,\n        write_stream,\n    ):\n        received_messages: list[JSONRPCMessage] = []\n        async with read_stream:\n            async for message in read_stream:\n                if isinstance(message, Exception):  # pragma: no cover\n                    raise message\n                received_messages.append(message.message)\n                if len(received_messages) == 2:\n                    break\n\n        # Verify received messages\n        assert len(received_messages) == 2\n        assert received_messages[0] == JSONRPCRequest(jsonrpc=\"2.0\", id=1, method=\"ping\")\n        assert received_messages[1] == JSONRPCResponse(jsonrpc=\"2.0\", id=2, result={})\n\n        # Test sending responses from the server\n        responses = [\n            JSONRPCRequest(jsonrpc=\"2.0\", id=3, method=\"ping\"),\n            JSONRPCResponse(jsonrpc=\"2.0\", id=4, result={}),\n        ]\n\n        async with write_stream:\n            for response in responses:\n                session_message = SessionMessage(response)\n                await write_stream.send(session_message)\n\n    stdout.seek(0)\n    output_lines = stdout.readlines()\n    assert len(output_lines) == 2\n\n    received_responses = [jsonrpc_message_adapter.validate_json(line.strip()) for line in output_lines]\n    assert len(received_responses) == 2\n    assert received_responses[0] == JSONRPCRequest(jsonrpc=\"2.0\", id=3, method=\"ping\")\n    assert received_responses[1] == JSONRPCResponse(jsonrpc=\"2.0\", id=4, result={})\n\n\n@pytest.mark.anyio\nasync def test_stdio_server_invalid_utf8(monkeypatch: pytest.MonkeyPatch):\n    \"\"\"Non-UTF-8 bytes on stdin must not crash the server.\n\n    Invalid bytes are replaced with U+FFFD, which then fails JSON parsing and\n    is delivered as an in-stream exception. Subsequent valid messages must\n    still be processed.\n    \"\"\"\n    # \\xff\\xfe are invalid UTF-8 start bytes.\n    valid = JSONRPCRequest(jsonrpc=\"2.0\", id=1, method=\"ping\")\n    raw_stdin = io.BytesIO(b\"\\xff\\xfe\\n\" + valid.model_dump_json(by_alias=True, exclude_none=True).encode() + b\"\\n\")\n\n    # Replace sys.stdin with a wrapper whose .buffer is our raw bytes, so that\n    # stdio_server()'s default path wraps it with errors='replace'.\n    monkeypatch.setattr(sys, \"stdin\", TextIOWrapper(raw_stdin, encoding=\"utf-8\"))\n    monkeypatch.setattr(sys, \"stdout\", TextIOWrapper(io.BytesIO(), encoding=\"utf-8\"))\n\n    with anyio.fail_after(5):\n        async with stdio_server() as (read_stream, write_stream):\n            await write_stream.aclose()\n            async with read_stream:  # pragma: no branch\n                # First line: \\xff\\xfe -> U+FFFD U+FFFD -> JSON parse fails -> exception in stream\n                first = await read_stream.receive()\n                assert isinstance(first, Exception)\n\n                # Second line: valid message still comes through\n                second = await read_stream.receive()\n                assert isinstance(second, SessionMessage)\n                assert second.message == valid\n"
  },
  {
    "path": "tests/server/test_streamable_http_manager.py",
    "content": "\"\"\"Tests for StreamableHTTPSessionManager.\"\"\"\n\nimport json\nimport logging\nfrom typing import Any\nfrom unittest.mock import AsyncMock, patch\n\nimport anyio\nimport httpx\nimport pytest\nfrom starlette.types import Message\n\nfrom mcp import Client\nfrom mcp.client.streamable_http import streamable_http_client\nfrom mcp.server import Server, ServerRequestContext, streamable_http_manager\nfrom mcp.server.streamable_http import MCP_SESSION_ID_HEADER, StreamableHTTPServerTransport\nfrom mcp.server.streamable_http_manager import StreamableHTTPSessionManager\nfrom mcp.types import INVALID_REQUEST, ListToolsResult, PaginatedRequestParams\n\n\n@pytest.mark.anyio\nasync def test_run_can_only_be_called_once():\n    \"\"\"Test that run() can only be called once per instance.\"\"\"\n    app = Server(\"test-server\")\n    manager = StreamableHTTPSessionManager(app=app)\n\n    # First call should succeed\n    async with manager.run():\n        pass\n\n    # Second call should raise RuntimeError\n    with pytest.raises(RuntimeError) as excinfo:\n        async with manager.run():\n            pass  # pragma: no cover\n\n    assert \"StreamableHTTPSessionManager .run() can only be called once per instance\" in str(excinfo.value)\n\n\n@pytest.mark.anyio\nasync def test_run_prevents_concurrent_calls():\n    \"\"\"Test that concurrent calls to run() are prevented.\"\"\"\n    app = Server(\"test-server\")\n    manager = StreamableHTTPSessionManager(app=app)\n\n    errors: list[Exception] = []\n\n    async def try_run():\n        try:\n            async with manager.run():\n                # Simulate some work\n                await anyio.sleep(0.1)\n        except RuntimeError as e:\n            errors.append(e)\n\n    # Try to run concurrently\n    async with anyio.create_task_group() as tg:\n        tg.start_soon(try_run)\n        tg.start_soon(try_run)\n\n    # One should succeed, one should fail\n    assert len(errors) == 1\n    assert \"StreamableHTTPSessionManager .run() can only be called once per instance\" in str(errors[0])\n\n\n@pytest.mark.anyio\nasync def test_handle_request_without_run_raises_error():\n    \"\"\"Test that handle_request raises error if run() hasn't been called.\"\"\"\n    app = Server(\"test-server\")\n    manager = StreamableHTTPSessionManager(app=app)\n\n    # Mock ASGI parameters\n    scope = {\"type\": \"http\", \"method\": \"POST\", \"path\": \"/test\"}\n\n    async def receive():  # pragma: no cover\n        return {\"type\": \"http.request\", \"body\": b\"\"}\n\n    async def send(message: Message):  # pragma: no cover\n        pass\n\n    # Should raise error because run() hasn't been called\n    with pytest.raises(RuntimeError) as excinfo:\n        await manager.handle_request(scope, receive, send)\n\n    assert \"Task group is not initialized. Make sure to use run().\" in str(excinfo.value)\n\n\nclass TestException(Exception):\n    __test__ = False  # Prevent pytest from collecting this as a test class\n    pass\n\n\n@pytest.fixture\nasync def running_manager():\n    app = Server(\"test-cleanup-server\")\n    # It's important that the app instance used by the manager is the one we can patch\n    manager = StreamableHTTPSessionManager(app=app)\n    async with manager.run():\n        # Patch app.run here if it's simpler, or patch it within the test\n        yield manager, app\n\n\n@pytest.mark.anyio\nasync def test_stateful_session_cleanup_on_graceful_exit(running_manager: tuple[StreamableHTTPSessionManager, Server]):\n    manager, app = running_manager\n\n    mock_mcp_run = AsyncMock(return_value=None)\n    # This will be called by StreamableHTTPSessionManager's run_server -> self.app.run\n    app.run = mock_mcp_run\n\n    sent_messages: list[Message] = []\n\n    async def mock_send(message: Message):\n        sent_messages.append(message)\n\n    scope = {\n        \"type\": \"http\",\n        \"method\": \"POST\",\n        \"path\": \"/mcp\",\n        \"headers\": [(b\"content-type\", b\"application/json\")],\n    }\n\n    async def mock_receive():  # pragma: no cover\n        return {\"type\": \"http.request\", \"body\": b\"\", \"more_body\": False}\n\n    # Trigger session creation\n    await manager.handle_request(scope, mock_receive, mock_send)\n\n    # Extract session ID from response headers\n    session_id = None\n    for msg in sent_messages:  # pragma: no branch\n        if msg[\"type\"] == \"http.response.start\":  # pragma: no branch\n            for header_name, header_value in msg.get(\"headers\", []):  # pragma: no branch\n                if header_name.decode().lower() == MCP_SESSION_ID_HEADER.lower():\n                    session_id = header_value.decode()\n                    break\n            if session_id:  # Break outer loop if session_id is found  # pragma: no branch\n                break\n\n    assert session_id is not None, \"Session ID not found in response headers\"\n\n    # Ensure MCPServer.run was called\n    mock_mcp_run.assert_called_once()\n\n    # At this point, mock_mcp_run has completed, and the finally block in\n    # StreamableHTTPSessionManager's run_server should have executed.\n\n    # To ensure the task spawned by handle_request finishes and cleanup occurs:\n    # Give other tasks a chance to run. This is important for the finally block.\n    await anyio.sleep(0.01)\n\n    assert session_id not in manager._server_instances, (\n        \"Session ID should be removed from _server_instances after graceful exit\"\n    )\n    assert not manager._server_instances, \"No sessions should be tracked after the only session exits gracefully\"\n\n\n@pytest.mark.anyio\nasync def test_stateful_session_cleanup_on_exception(running_manager: tuple[StreamableHTTPSessionManager, Server]):\n    manager, app = running_manager\n\n    mock_mcp_run = AsyncMock(side_effect=TestException(\"Simulated crash\"))\n    app.run = mock_mcp_run\n\n    sent_messages: list[Message] = []\n\n    async def mock_send(message: Message):\n        sent_messages.append(message)\n        # If an exception occurs, the transport might try to send an error response\n        # For this test, we mostly care that the session is established enough\n        # to get an ID\n        if message[\"type\"] == \"http.response.start\" and message[\"status\"] >= 500:  # pragma: no cover\n            pass  # Expected if TestException propagates that far up the transport\n\n    scope = {\n        \"type\": \"http\",\n        \"method\": \"POST\",\n        \"path\": \"/mcp\",\n        \"headers\": [(b\"content-type\", b\"application/json\")],\n    }\n\n    async def mock_receive():  # pragma: no cover\n        return {\"type\": \"http.request\", \"body\": b\"\", \"more_body\": False}\n\n    # Trigger session creation\n    await manager.handle_request(scope, mock_receive, mock_send)\n\n    session_id = None\n    for msg in sent_messages:  # pragma: no branch\n        if msg[\"type\"] == \"http.response.start\":  # pragma: no branch\n            for header_name, header_value in msg.get(\"headers\", []):  # pragma: no branch\n                if header_name.decode().lower() == MCP_SESSION_ID_HEADER.lower():\n                    session_id = header_value.decode()\n                    break\n            if session_id:  # Break outer loop if session_id is found  # pragma: no branch\n                break\n\n    assert session_id is not None, \"Session ID not found in response headers\"\n\n    mock_mcp_run.assert_called_once()\n\n    # Give other tasks a chance to run to ensure the finally block executes\n    await anyio.sleep(0.01)\n\n    assert session_id not in manager._server_instances, (\n        \"Session ID should be removed from _server_instances after an exception\"\n    )\n    assert not manager._server_instances, \"No sessions should be tracked after the only session crashes\"\n\n\n@pytest.mark.anyio\nasync def test_stateless_requests_memory_cleanup():\n    \"\"\"Test that stateless requests actually clean up resources using real transports.\"\"\"\n    app = Server(\"test-stateless-real-cleanup\")\n    manager = StreamableHTTPSessionManager(app=app, stateless=True)\n\n    # Track created transport instances\n    created_transports: list[StreamableHTTPServerTransport] = []\n\n    # Patch StreamableHTTPServerTransport constructor to track instances\n\n    original_constructor = StreamableHTTPServerTransport\n\n    def track_transport(*args: Any, **kwargs: Any) -> StreamableHTTPServerTransport:\n        transport = original_constructor(*args, **kwargs)\n        created_transports.append(transport)\n        return transport\n\n    with patch.object(streamable_http_manager, \"StreamableHTTPServerTransport\", side_effect=track_transport):\n        async with manager.run():\n            # Mock app.run to complete immediately\n            app.run = AsyncMock(return_value=None)\n\n            # Send a simple request\n            sent_messages: list[Message] = []\n\n            async def mock_send(message: Message):\n                sent_messages.append(message)\n\n            scope = {\n                \"type\": \"http\",\n                \"method\": \"POST\",\n                \"path\": \"/mcp\",\n                \"headers\": [\n                    (b\"content-type\", b\"application/json\"),\n                    (b\"accept\", b\"application/json, text/event-stream\"),\n                ],\n            }\n\n            # Empty body to trigger early return\n            async def mock_receive():\n                return {\n                    \"type\": \"http.request\",\n                    \"body\": b\"\",\n                    \"more_body\": False,\n                }\n\n            # Send a request\n            await manager.handle_request(scope, mock_receive, mock_send)\n\n            # Verify transport was created\n            assert len(created_transports) == 1, \"Should have created one transport\"\n\n            transport = created_transports[0]\n\n            # The key assertion - transport should be terminated\n            assert transport._terminated, \"Transport should be terminated after stateless request\"\n\n            # Verify internal state is cleaned up\n            assert len(transport._request_streams) == 0, \"Transport should have no active request streams\"\n\n\n@pytest.mark.anyio\nasync def test_unknown_session_id_returns_404(caplog: pytest.LogCaptureFixture):\n    \"\"\"Test that requests with unknown session IDs return HTTP 404 per MCP spec.\"\"\"\n    app = Server(\"test-unknown-session\")\n    manager = StreamableHTTPSessionManager(app=app)\n\n    async with manager.run():\n        sent_messages: list[Message] = []\n        response_body = b\"\"\n\n        async def mock_send(message: Message):\n            nonlocal response_body\n            sent_messages.append(message)\n            if message[\"type\"] == \"http.response.body\":\n                response_body += message.get(\"body\", b\"\")\n\n        # Request with a non-existent session ID\n        scope = {\n            \"type\": \"http\",\n            \"method\": \"POST\",\n            \"path\": \"/mcp\",\n            \"headers\": [\n                (b\"content-type\", b\"application/json\"),\n                (b\"accept\", b\"application/json, text/event-stream\"),\n                (b\"mcp-session-id\", b\"non-existent-session-id\"),\n            ],\n        }\n\n        async def mock_receive():\n            return {\"type\": \"http.request\", \"body\": b\"{}\", \"more_body\": False}  # pragma: no cover\n\n        with caplog.at_level(logging.INFO):\n            await manager.handle_request(scope, mock_receive, mock_send)\n\n        # Find the response start message\n        response_start = next(\n            (msg for msg in sent_messages if msg[\"type\"] == \"http.response.start\"),\n            None,\n        )\n        assert response_start is not None, \"Should have sent a response\"\n        assert response_start[\"status\"] == 404, \"Should return HTTP 404 for unknown session ID\"\n\n        # Verify JSON-RPC error format\n        error_data = json.loads(response_body)\n        assert error_data[\"jsonrpc\"] == \"2.0\"\n        assert error_data[\"id\"] is None\n        assert error_data[\"error\"][\"code\"] == INVALID_REQUEST\n        assert error_data[\"error\"][\"message\"] == \"Session not found\"\n        assert \"Rejected request with unknown or expired session ID: non-existent-session-id\" in caplog.text\n\n\n@pytest.mark.anyio\nasync def test_e2e_streamable_http_server_cleanup():\n    host = \"testserver\"\n\n    async def handle_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:\n        return ListToolsResult(tools=[])\n\n    app = Server(\"test-server\", on_list_tools=handle_list_tools)\n    mcp_app = app.streamable_http_app(host=host)\n    async with (\n        mcp_app.router.lifespan_context(mcp_app),\n        httpx.ASGITransport(mcp_app) as transport,\n        httpx.AsyncClient(transport=transport) as http_client,\n        Client(streamable_http_client(f\"http://{host}/mcp\", http_client=http_client)) as client,\n    ):\n        await client.list_tools()\n\n\n@pytest.mark.anyio\nasync def test_idle_session_is_reaped():\n    \"\"\"After idle timeout fires, the session returns 404.\"\"\"\n    app = Server(\"test-idle-reap\")\n    manager = StreamableHTTPSessionManager(app=app, session_idle_timeout=0.05)\n\n    async with manager.run():\n        sent_messages: list[Message] = []\n\n        async def mock_send(message: Message):\n            sent_messages.append(message)\n\n        scope = {\n            \"type\": \"http\",\n            \"method\": \"POST\",\n            \"path\": \"/mcp\",\n            \"headers\": [(b\"content-type\", b\"application/json\")],\n        }\n\n        async def mock_receive():  # pragma: no cover\n            return {\"type\": \"http.request\", \"body\": b\"\", \"more_body\": False}\n\n        await manager.handle_request(scope, mock_receive, mock_send)\n\n        session_id = None\n        for msg in sent_messages:  # pragma: no branch\n            if msg[\"type\"] == \"http.response.start\":  # pragma: no branch\n                for header_name, header_value in msg.get(\"headers\", []):  # pragma: no branch\n                    if header_name.decode().lower() == MCP_SESSION_ID_HEADER.lower():\n                        session_id = header_value.decode()\n                        break\n                if session_id:  # pragma: no branch\n                    break\n\n        assert session_id is not None, \"Session ID not found in response headers\"\n\n        # Wait for the 50ms idle timeout to fire and cleanup to complete\n        await anyio.sleep(0.1)\n\n        # Verify via public API: old session ID now returns 404\n        response_messages: list[Message] = []\n\n        async def capture_send(message: Message):\n            response_messages.append(message)\n\n        scope_with_session = {\n            \"type\": \"http\",\n            \"method\": \"POST\",\n            \"path\": \"/mcp\",\n            \"headers\": [\n                (b\"content-type\", b\"application/json\"),\n                (b\"mcp-session-id\", session_id.encode()),\n            ],\n        }\n\n        await manager.handle_request(scope_with_session, mock_receive, capture_send)\n\n        response_start = next(\n            (msg for msg in response_messages if msg[\"type\"] == \"http.response.start\"),\n            None,\n        )\n        assert response_start is not None\n        assert response_start[\"status\"] == 404\n\n\ndef test_session_idle_timeout_rejects_non_positive():\n    with pytest.raises(ValueError, match=\"positive number\"):\n        StreamableHTTPSessionManager(app=Server(\"test\"), session_idle_timeout=-1)\n    with pytest.raises(ValueError, match=\"positive number\"):\n        StreamableHTTPSessionManager(app=Server(\"test\"), session_idle_timeout=0)\n\n\ndef test_session_idle_timeout_rejects_stateless():\n    with pytest.raises(RuntimeError, match=\"not supported in stateless\"):\n        StreamableHTTPSessionManager(app=Server(\"test\"), session_idle_timeout=30, stateless=True)\n"
  },
  {
    "path": "tests/server/test_streamable_http_security.py",
    "content": "\"\"\"Tests for StreamableHTTP server DNS rebinding protection.\"\"\"\n\nimport multiprocessing\nimport socket\nfrom collections.abc import AsyncGenerator\nfrom contextlib import asynccontextmanager\n\nimport httpx\nimport pytest\nimport uvicorn\nfrom starlette.applications import Starlette\nfrom starlette.routing import Mount\nfrom starlette.types import Receive, Scope, Send\n\nfrom mcp.server import Server\nfrom mcp.server.streamable_http_manager import StreamableHTTPSessionManager\nfrom mcp.server.transport_security import TransportSecuritySettings\nfrom mcp.types import Tool\nfrom tests.test_helpers import wait_for_server\n\nSERVER_NAME = \"test_streamable_http_security_server\"\n\n\n@pytest.fixture\ndef server_port() -> int:\n    with socket.socket() as s:\n        s.bind((\"127.0.0.1\", 0))\n        return s.getsockname()[1]\n\n\n@pytest.fixture\ndef server_url(server_port: int) -> str:  # pragma: no cover\n    return f\"http://127.0.0.1:{server_port}\"\n\n\nclass SecurityTestServer(Server):  # pragma: no cover\n    def __init__(self):\n        super().__init__(SERVER_NAME)\n\n    async def on_list_tools(self) -> list[Tool]:\n        return []\n\n\ndef run_server_with_settings(port: int, security_settings: TransportSecuritySettings | None = None):  # pragma: no cover\n    \"\"\"Run the StreamableHTTP server with specified security settings.\"\"\"\n    app = SecurityTestServer()\n\n    # Create session manager with security settings\n    session_manager = StreamableHTTPSessionManager(\n        app=app,\n        json_response=False,\n        stateless=False,\n        security_settings=security_settings,\n    )\n\n    # Create the ASGI handler\n    async def handle_streamable_http(scope: Scope, receive: Receive, send: Send) -> None:\n        await session_manager.handle_request(scope, receive, send)\n\n    # Create Starlette app with lifespan\n    @asynccontextmanager\n    async def lifespan(app: Starlette) -> AsyncGenerator[None, None]:\n        async with session_manager.run():\n            yield\n\n    routes = [\n        Mount(\"/\", app=handle_streamable_http),\n    ]\n\n    starlette_app = Starlette(routes=routes, lifespan=lifespan)\n    uvicorn.run(starlette_app, host=\"127.0.0.1\", port=port, log_level=\"error\")\n\n\ndef start_server_process(port: int, security_settings: TransportSecuritySettings | None = None):\n    \"\"\"Start server in a separate process.\"\"\"\n    process = multiprocessing.Process(target=run_server_with_settings, args=(port, security_settings))\n    process.start()\n    # Wait for server to be ready to accept connections\n    wait_for_server(port)\n    return process\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_security_default_settings(server_port: int):\n    \"\"\"Test StreamableHTTP with default security settings (protection enabled).\"\"\"\n    process = start_server_process(server_port)\n\n    try:\n        # Test with valid localhost headers\n        async with httpx.AsyncClient(timeout=5.0) as client:\n            # POST request to initialize session\n            response = await client.post(\n                f\"http://127.0.0.1:{server_port}/\",\n                json={\"jsonrpc\": \"2.0\", \"method\": \"initialize\", \"id\": 1, \"params\": {}},\n                headers={\n                    \"Accept\": \"application/json, text/event-stream\",\n                    \"Content-Type\": \"application/json\",\n                },\n            )\n            assert response.status_code == 200\n            assert \"mcp-session-id\" in response.headers\n\n    finally:\n        process.terminate()\n        process.join()\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_security_invalid_host_header(server_port: int):\n    \"\"\"Test StreamableHTTP with invalid Host header.\"\"\"\n    security_settings = TransportSecuritySettings(enable_dns_rebinding_protection=True)\n    process = start_server_process(server_port, security_settings)\n\n    try:\n        # Test with invalid host header\n        headers = {\n            \"Host\": \"evil.com\",\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        }\n\n        async with httpx.AsyncClient(timeout=5.0) as client:\n            response = await client.post(\n                f\"http://127.0.0.1:{server_port}/\",\n                json={\"jsonrpc\": \"2.0\", \"method\": \"initialize\", \"id\": 1, \"params\": {}},\n                headers=headers,\n            )\n            assert response.status_code == 421\n            assert response.text == \"Invalid Host header\"\n\n    finally:\n        process.terminate()\n        process.join()\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_security_invalid_origin_header(server_port: int):\n    \"\"\"Test StreamableHTTP with invalid Origin header.\"\"\"\n    security_settings = TransportSecuritySettings(enable_dns_rebinding_protection=True, allowed_hosts=[\"127.0.0.1:*\"])\n    process = start_server_process(server_port, security_settings)\n\n    try:\n        # Test with invalid origin header\n        headers = {\n            \"Origin\": \"http://evil.com\",\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        }\n\n        async with httpx.AsyncClient(timeout=5.0) as client:\n            response = await client.post(\n                f\"http://127.0.0.1:{server_port}/\",\n                json={\"jsonrpc\": \"2.0\", \"method\": \"initialize\", \"id\": 1, \"params\": {}},\n                headers=headers,\n            )\n            assert response.status_code == 403\n            assert response.text == \"Invalid Origin header\"\n\n    finally:\n        process.terminate()\n        process.join()\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_security_invalid_content_type(server_port: int):\n    \"\"\"Test StreamableHTTP POST with invalid Content-Type header.\"\"\"\n    process = start_server_process(server_port)\n\n    try:\n        async with httpx.AsyncClient(timeout=5.0) as client:\n            # Test POST with invalid content type\n            response = await client.post(\n                f\"http://127.0.0.1:{server_port}/\",\n                headers={\n                    \"Content-Type\": \"text/plain\",\n                    \"Accept\": \"application/json, text/event-stream\",\n                },\n                content=\"test\",\n            )\n            assert response.status_code == 400\n            assert response.text == \"Invalid Content-Type header\"\n\n            # Test POST with missing content type\n            response = await client.post(\n                f\"http://127.0.0.1:{server_port}/\",\n                headers={\"Accept\": \"application/json, text/event-stream\"},\n                content=\"test\",\n            )\n            assert response.status_code == 400\n            assert response.text == \"Invalid Content-Type header\"\n\n    finally:\n        process.terminate()\n        process.join()\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_security_disabled(server_port: int):\n    \"\"\"Test StreamableHTTP with security disabled.\"\"\"\n    settings = TransportSecuritySettings(enable_dns_rebinding_protection=False)\n    process = start_server_process(server_port, settings)\n\n    try:\n        # Test with invalid host header - should still work\n        headers = {\n            \"Host\": \"evil.com\",\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        }\n\n        async with httpx.AsyncClient(timeout=5.0) as client:\n            response = await client.post(\n                f\"http://127.0.0.1:{server_port}/\",\n                json={\"jsonrpc\": \"2.0\", \"method\": \"initialize\", \"id\": 1, \"params\": {}},\n                headers=headers,\n            )\n            # Should connect successfully even with invalid host\n            assert response.status_code == 200\n\n    finally:\n        process.terminate()\n        process.join()\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_security_custom_allowed_hosts(server_port: int):\n    \"\"\"Test StreamableHTTP with custom allowed hosts.\"\"\"\n    settings = TransportSecuritySettings(\n        enable_dns_rebinding_protection=True,\n        allowed_hosts=[\"localhost\", \"127.0.0.1\", \"custom.host\"],\n        allowed_origins=[\"http://localhost\", \"http://127.0.0.1\", \"http://custom.host\"],\n    )\n    process = start_server_process(server_port, settings)\n\n    try:\n        # Test with custom allowed host\n        headers = {\n            \"Host\": \"custom.host\",\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        }\n\n        async with httpx.AsyncClient(timeout=5.0) as client:\n            response = await client.post(\n                f\"http://127.0.0.1:{server_port}/\",\n                json={\"jsonrpc\": \"2.0\", \"method\": \"initialize\", \"id\": 1, \"params\": {}},\n                headers=headers,\n            )\n            # Should connect successfully with custom host\n            assert response.status_code == 200\n    finally:\n        process.terminate()\n        process.join()\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_security_get_request(server_port: int):\n    \"\"\"Test StreamableHTTP GET request with security.\"\"\"\n    security_settings = TransportSecuritySettings(enable_dns_rebinding_protection=True, allowed_hosts=[\"127.0.0.1\"])\n    process = start_server_process(server_port, security_settings)\n\n    try:\n        # Test GET request with invalid host header\n        headers = {\n            \"Host\": \"evil.com\",\n            \"Accept\": \"text/event-stream\",\n        }\n\n        async with httpx.AsyncClient(timeout=5.0) as client:\n            response = await client.get(f\"http://127.0.0.1:{server_port}/\", headers=headers)\n            assert response.status_code == 421\n            assert response.text == \"Invalid Host header\"\n\n        # Test GET request with valid host header\n        headers = {\n            \"Host\": \"127.0.0.1\",\n            \"Accept\": \"text/event-stream\",\n        }\n\n        async with httpx.AsyncClient(timeout=5.0) as client:\n            # GET requests need a session ID in StreamableHTTP\n            # So it will fail with \"Missing session ID\" not security error\n            response = await client.get(f\"http://127.0.0.1:{server_port}/\", headers=headers)\n            # This should pass security but fail on session validation\n            assert response.status_code == 400\n            body = response.json()\n            assert \"Missing session ID\" in body[\"error\"][\"message\"]\n\n    finally:\n        process.terminate()\n        process.join()\n"
  },
  {
    "path": "tests/server/test_validation.py",
    "content": "\"\"\"Tests for server validation functions.\"\"\"\n\nimport pytest\n\nfrom mcp.server.validation import (\n    check_sampling_tools_capability,\n    validate_sampling_tools,\n    validate_tool_use_result_messages,\n)\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.types import (\n    ClientCapabilities,\n    SamplingCapability,\n    SamplingMessage,\n    SamplingToolsCapability,\n    TextContent,\n    Tool,\n    ToolChoice,\n    ToolResultContent,\n    ToolUseContent,\n)\n\n# Tests for check_sampling_tools_capability function\n\n\ndef test_check_sampling_tools_capability_returns_false_when_caps_none() -> None:\n    \"\"\"Returns False when client_caps is None.\"\"\"\n    assert check_sampling_tools_capability(None) is False\n\n\ndef test_check_sampling_tools_capability_returns_false_when_sampling_none() -> None:\n    \"\"\"Returns False when client_caps.sampling is None.\"\"\"\n    caps = ClientCapabilities()\n    assert check_sampling_tools_capability(caps) is False\n\n\ndef test_check_sampling_tools_capability_returns_false_when_tools_none() -> None:\n    \"\"\"Returns False when client_caps.sampling.tools is None.\"\"\"\n    caps = ClientCapabilities(sampling=SamplingCapability())\n    assert check_sampling_tools_capability(caps) is False\n\n\ndef test_check_sampling_tools_capability_returns_true_when_tools_present() -> None:\n    \"\"\"Returns True when sampling.tools is present.\"\"\"\n    caps = ClientCapabilities(sampling=SamplingCapability(tools=SamplingToolsCapability()))\n    assert check_sampling_tools_capability(caps) is True\n\n\n# Tests for validate_sampling_tools function\n\n\ndef test_validate_sampling_tools_no_error_when_tools_none() -> None:\n    \"\"\"No error when tools and tool_choice are None.\"\"\"\n    validate_sampling_tools(None, None, None)  # Should not raise\n\n\ndef test_validate_sampling_tools_raises_when_tools_provided_but_no_capability() -> None:\n    \"\"\"Raises MCPError when tools provided but client doesn't support.\"\"\"\n    tool = Tool(name=\"test\", input_schema={\"type\": \"object\"})\n    with pytest.raises(MCPError) as exc_info:\n        validate_sampling_tools(None, [tool], None)\n    assert \"sampling tools capability\" in str(exc_info.value)\n\n\ndef test_validate_sampling_tools_raises_when_tool_choice_provided_but_no_capability() -> None:\n    \"\"\"Raises MCPError when tool_choice provided but client doesn't support.\"\"\"\n    with pytest.raises(MCPError) as exc_info:\n        validate_sampling_tools(None, None, ToolChoice(mode=\"auto\"))\n    assert \"sampling tools capability\" in str(exc_info.value)\n\n\ndef test_validate_sampling_tools_no_error_when_capability_present() -> None:\n    \"\"\"No error when client has sampling.tools capability.\"\"\"\n    caps = ClientCapabilities(sampling=SamplingCapability(tools=SamplingToolsCapability()))\n    tool = Tool(name=\"test\", input_schema={\"type\": \"object\"})\n    validate_sampling_tools(caps, [tool], ToolChoice(mode=\"auto\"))  # Should not raise\n\n\n# Tests for validate_tool_use_result_messages function\n\n\ndef test_validate_tool_use_result_messages_no_error_for_empty_messages() -> None:\n    \"\"\"No error when messages list is empty.\"\"\"\n    validate_tool_use_result_messages([])  # Should not raise\n\n\ndef test_validate_tool_use_result_messages_no_error_for_simple_text_messages() -> None:\n    \"\"\"No error for simple text messages.\"\"\"\n    messages = [\n        SamplingMessage(role=\"user\", content=TextContent(type=\"text\", text=\"Hello\")),\n        SamplingMessage(role=\"assistant\", content=TextContent(type=\"text\", text=\"Hi\")),\n    ]\n    validate_tool_use_result_messages(messages)  # Should not raise\n\n\ndef test_validate_tool_use_result_messages_raises_when_tool_result_mixed_with_other_content() -> None:\n    \"\"\"Raises when tool_result is mixed with other content types.\"\"\"\n    messages = [\n        SamplingMessage(\n            role=\"user\",\n            content=[\n                ToolResultContent(type=\"tool_result\", tool_use_id=\"123\"),\n                TextContent(type=\"text\", text=\"also this\"),\n            ],\n        ),\n    ]\n    with pytest.raises(ValueError, match=\"only tool_result content\"):\n        validate_tool_use_result_messages(messages)\n\n\ndef test_validate_tool_use_result_messages_raises_when_tool_result_without_previous_tool_use() -> None:\n    \"\"\"Raises when tool_result appears without preceding tool_use.\"\"\"\n    messages = [\n        SamplingMessage(\n            role=\"user\",\n            content=ToolResultContent(type=\"tool_result\", tool_use_id=\"123\"),\n        ),\n    ]\n    with pytest.raises(ValueError, match=\"previous message containing tool_use\"):\n        validate_tool_use_result_messages(messages)\n\n\ndef test_validate_tool_use_result_messages_raises_when_tool_result_ids_dont_match_tool_use() -> None:\n    \"\"\"Raises when tool_result IDs don't match tool_use IDs.\"\"\"\n    messages = [\n        SamplingMessage(\n            role=\"assistant\",\n            content=ToolUseContent(type=\"tool_use\", id=\"tool-1\", name=\"test\", input={}),\n        ),\n        SamplingMessage(\n            role=\"user\",\n            content=ToolResultContent(type=\"tool_result\", tool_use_id=\"tool-2\"),\n        ),\n    ]\n    with pytest.raises(ValueError, match=\"do not match\"):\n        validate_tool_use_result_messages(messages)\n\n\ndef test_validate_tool_use_result_messages_no_error_when_tool_result_matches_tool_use() -> None:\n    \"\"\"No error when tool_result IDs match tool_use IDs.\"\"\"\n    messages = [\n        SamplingMessage(\n            role=\"assistant\",\n            content=ToolUseContent(type=\"tool_use\", id=\"tool-1\", name=\"test\", input={}),\n        ),\n        SamplingMessage(\n            role=\"user\",\n            content=ToolResultContent(type=\"tool_result\", tool_use_id=\"tool-1\"),\n        ),\n    ]\n    validate_tool_use_result_messages(messages)  # Should not raise\n"
  },
  {
    "path": "tests/shared/__init__.py",
    "content": ""
  },
  {
    "path": "tests/shared/test_auth.py",
    "content": "\"\"\"Tests for OAuth 2.0 shared code.\"\"\"\n\nfrom mcp.shared.auth import OAuthMetadata\n\n\ndef test_oauth():\n    \"\"\"Should not throw when parsing OAuth metadata.\"\"\"\n    OAuthMetadata.model_validate(\n        {\n            \"issuer\": \"https://example.com\",\n            \"authorization_endpoint\": \"https://example.com/oauth2/authorize\",\n            \"token_endpoint\": \"https://example.com/oauth2/token\",\n            \"scopes_supported\": [\"read\", \"write\"],\n            \"response_types_supported\": [\"code\", \"token\"],\n            \"token_endpoint_auth_methods_supported\": [\"client_secret_basic\", \"client_secret_post\"],\n        }\n    )\n\n\ndef test_oidc():\n    \"\"\"Should not throw when parsing OIDC metadata.\"\"\"\n    OAuthMetadata.model_validate(\n        {\n            \"issuer\": \"https://example.com\",\n            \"authorization_endpoint\": \"https://example.com/oauth2/authorize\",\n            \"token_endpoint\": \"https://example.com/oauth2/token\",\n            \"end_session_endpoint\": \"https://example.com/logout\",\n            \"id_token_signing_alg_values_supported\": [\"RS256\"],\n            \"jwks_uri\": \"https://example.com/.well-known/jwks.json\",\n            \"response_types_supported\": [\"code\", \"token\"],\n            \"revocation_endpoint\": \"https://example.com/oauth2/revoke\",\n            \"scopes_supported\": [\"openid\", \"read\", \"write\"],\n            \"subject_types_supported\": [\"public\"],\n            \"token_endpoint_auth_methods_supported\": [\"client_secret_basic\", \"client_secret_post\"],\n            \"userinfo_endpoint\": \"https://example.com/oauth2/userInfo\",\n        }\n    )\n\n\ndef test_oauth_with_jarm():\n    \"\"\"Should not throw when parsing OAuth metadata that includes JARM response modes.\"\"\"\n    OAuthMetadata.model_validate(\n        {\n            \"issuer\": \"https://example.com\",\n            \"authorization_endpoint\": \"https://example.com/oauth2/authorize\",\n            \"token_endpoint\": \"https://example.com/oauth2/token\",\n            \"scopes_supported\": [\"read\", \"write\"],\n            \"response_types_supported\": [\"code\", \"token\"],\n            \"response_modes_supported\": [\n                \"query\",\n                \"fragment\",\n                \"form_post\",\n                \"query.jwt\",\n                \"fragment.jwt\",\n                \"form_post.jwt\",\n                \"jwt\",\n            ],\n            \"token_endpoint_auth_methods_supported\": [\"client_secret_basic\", \"client_secret_post\"],\n        }\n    )\n"
  },
  {
    "path": "tests/shared/test_auth_utils.py",
    "content": "\"\"\"Tests for OAuth 2.0 Resource Indicators utilities.\"\"\"\n\nfrom pydantic import HttpUrl\n\nfrom mcp.shared.auth_utils import check_resource_allowed, resource_url_from_server_url\n\n# Tests for resource_url_from_server_url function\n\n\ndef test_resource_url_from_server_url_removes_fragment():\n    \"\"\"Fragment should be removed per RFC 8707.\"\"\"\n    assert resource_url_from_server_url(\"https://example.com/path#fragment\") == \"https://example.com/path\"\n    assert resource_url_from_server_url(\"https://example.com/#fragment\") == \"https://example.com/\"\n\n\ndef test_resource_url_from_server_url_preserves_path():\n    \"\"\"Path should be preserved.\"\"\"\n    assert (\n        resource_url_from_server_url(\"https://example.com/path/to/resource\") == \"https://example.com/path/to/resource\"\n    )\n    assert resource_url_from_server_url(\"https://example.com/\") == \"https://example.com/\"\n    assert resource_url_from_server_url(\"https://example.com\") == \"https://example.com\"\n\n\ndef test_resource_url_from_server_url_preserves_query():\n    \"\"\"Query parameters should be preserved.\"\"\"\n    assert resource_url_from_server_url(\"https://example.com/path?foo=bar\") == \"https://example.com/path?foo=bar\"\n    assert resource_url_from_server_url(\"https://example.com/?key=value\") == \"https://example.com/?key=value\"\n\n\ndef test_resource_url_from_server_url_preserves_port():\n    \"\"\"Non-default ports should be preserved.\"\"\"\n    assert resource_url_from_server_url(\"https://example.com:8443/path\") == \"https://example.com:8443/path\"\n    assert resource_url_from_server_url(\"http://example.com:8080/\") == \"http://example.com:8080/\"\n\n\ndef test_resource_url_from_server_url_lowercase_scheme_and_host():\n    \"\"\"Scheme and host should be lowercase for canonical form.\"\"\"\n    assert resource_url_from_server_url(\"HTTPS://EXAMPLE.COM/path\") == \"https://example.com/path\"\n    assert resource_url_from_server_url(\"Http://Example.Com:8080/\") == \"http://example.com:8080/\"\n\n\ndef test_resource_url_from_server_url_handles_pydantic_urls():\n    \"\"\"Should handle Pydantic URL types.\"\"\"\n    url = HttpUrl(\"https://example.com/path\")\n    assert resource_url_from_server_url(url) == \"https://example.com/path\"\n\n\n# Tests for check_resource_allowed function\n\n\ndef test_check_resource_allowed_identical_urls():\n    \"\"\"Identical URLs should match.\"\"\"\n    assert check_resource_allowed(\"https://example.com/path\", \"https://example.com/path\") is True\n    assert check_resource_allowed(\"https://example.com/\", \"https://example.com/\") is True\n    assert check_resource_allowed(\"https://example.com\", \"https://example.com\") is True\n\n\ndef test_check_resource_allowed_different_schemes():\n    \"\"\"Different schemes should not match.\"\"\"\n    assert check_resource_allowed(\"https://example.com/path\", \"http://example.com/path\") is False\n    assert check_resource_allowed(\"http://example.com/\", \"https://example.com/\") is False\n\n\ndef test_check_resource_allowed_different_domains():\n    \"\"\"Different domains should not match.\"\"\"\n    assert check_resource_allowed(\"https://example.com/path\", \"https://example.org/path\") is False\n    assert check_resource_allowed(\"https://sub.example.com/\", \"https://example.com/\") is False\n\n\ndef test_check_resource_allowed_different_ports():\n    \"\"\"Different ports should not match.\"\"\"\n    assert check_resource_allowed(\"https://example.com:8443/path\", \"https://example.com/path\") is False\n    assert check_resource_allowed(\"https://example.com:8080/\", \"https://example.com:8443/\") is False\n\n\ndef test_check_resource_allowed_hierarchical_matching():\n    \"\"\"Child paths should match parent paths.\"\"\"\n    # Parent resource allows child resources\n    assert check_resource_allowed(\"https://example.com/api/v1/users\", \"https://example.com/api\") is True\n    assert check_resource_allowed(\"https://example.com/api/v1\", \"https://example.com/api\") is True\n    assert check_resource_allowed(\"https://example.com/mcp/server\", \"https://example.com/mcp\") is True\n\n    # Exact match\n    assert check_resource_allowed(\"https://example.com/api\", \"https://example.com/api\") is True\n\n    # Parent cannot use child's token\n    assert check_resource_allowed(\"https://example.com/api\", \"https://example.com/api/v1\") is False\n    assert check_resource_allowed(\"https://example.com/\", \"https://example.com/api\") is False\n\n\ndef test_check_resource_allowed_path_boundary_matching():\n    \"\"\"Path matching should respect boundaries.\"\"\"\n    # Should not match partial path segments\n    assert check_resource_allowed(\"https://example.com/apiextra\", \"https://example.com/api\") is False\n    assert check_resource_allowed(\"https://example.com/api123\", \"https://example.com/api\") is False\n\n    # Should match with trailing slash\n    assert check_resource_allowed(\"https://example.com/api/\", \"https://example.com/api\") is True\n    assert check_resource_allowed(\"https://example.com/api/v1\", \"https://example.com/api/\") is True\n\n\ndef test_check_resource_allowed_trailing_slash_handling():\n    \"\"\"Trailing slashes should be handled correctly.\"\"\"\n    # With and without trailing slashes\n    assert check_resource_allowed(\"https://example.com/api/\", \"https://example.com/api\") is True\n    assert check_resource_allowed(\"https://example.com/api\", \"https://example.com/api/\") is True\n    assert check_resource_allowed(\"https://example.com/api/v1\", \"https://example.com/api\") is True\n    assert check_resource_allowed(\"https://example.com/api/v1\", \"https://example.com/api/\") is True\n\n\ndef test_check_resource_allowed_case_insensitive_origin():\n    \"\"\"Origin comparison should be case-insensitive.\"\"\"\n    assert check_resource_allowed(\"https://EXAMPLE.COM/path\", \"https://example.com/path\") is True\n    assert check_resource_allowed(\"HTTPS://example.com/path\", \"https://example.com/path\") is True\n    assert check_resource_allowed(\"https://Example.Com:8080/api\", \"https://example.com:8080/api\") is True\n\n\ndef test_check_resource_allowed_empty_paths():\n    \"\"\"Empty paths should be handled correctly.\"\"\"\n    assert check_resource_allowed(\"https://example.com\", \"https://example.com\") is True\n    assert check_resource_allowed(\"https://example.com/\", \"https://example.com\") is True\n    assert check_resource_allowed(\"https://example.com/api\", \"https://example.com\") is True\n"
  },
  {
    "path": "tests/shared/test_exceptions.py",
    "content": "\"\"\"Tests for MCP exception classes.\"\"\"\n\nimport pytest\n\nfrom mcp.shared.exceptions import MCPError, UrlElicitationRequiredError\nfrom mcp.types import URL_ELICITATION_REQUIRED, ElicitRequestURLParams, ErrorData\n\n\ndef test_url_elicitation_required_error_create_with_single_elicitation() -> None:\n    \"\"\"Test creating error with a single elicitation.\"\"\"\n    elicitation = ElicitRequestURLParams(\n        mode=\"url\",\n        message=\"Auth required\",\n        url=\"https://example.com/auth\",\n        elicitation_id=\"test-123\",\n    )\n    error = UrlElicitationRequiredError([elicitation])\n\n    assert error.error.code == URL_ELICITATION_REQUIRED\n    assert error.error.message == \"URL elicitation required\"\n    assert len(error.elicitations) == 1\n    assert error.elicitations[0].elicitation_id == \"test-123\"\n\n\ndef test_url_elicitation_required_error_create_with_multiple_elicitations() -> None:\n    \"\"\"Test creating error with multiple elicitations uses plural message.\"\"\"\n    elicitations = [\n        ElicitRequestURLParams(\n            mode=\"url\",\n            message=\"Auth 1\",\n            url=\"https://example.com/auth1\",\n            elicitation_id=\"test-1\",\n        ),\n        ElicitRequestURLParams(\n            mode=\"url\",\n            message=\"Auth 2\",\n            url=\"https://example.com/auth2\",\n            elicitation_id=\"test-2\",\n        ),\n    ]\n    error = UrlElicitationRequiredError(elicitations)\n\n    assert error.error.message == \"URL elicitations required\"  # Plural\n    assert len(error.elicitations) == 2\n\n\ndef test_url_elicitation_required_error_custom_message() -> None:\n    \"\"\"Test creating error with a custom message.\"\"\"\n    elicitation = ElicitRequestURLParams(\n        mode=\"url\",\n        message=\"Auth required\",\n        url=\"https://example.com/auth\",\n        elicitation_id=\"test-123\",\n    )\n    error = UrlElicitationRequiredError([elicitation], message=\"Custom message\")\n\n    assert error.error.message == \"Custom message\"\n\n\ndef test_url_elicitation_required_error_from_error_data() -> None:\n    \"\"\"Test reconstructing error from ErrorData.\"\"\"\n    error_data = ErrorData(\n        code=URL_ELICITATION_REQUIRED,\n        message=\"URL elicitation required\",\n        data={\n            \"elicitations\": [\n                {\n                    \"mode\": \"url\",\n                    \"message\": \"Auth required\",\n                    \"url\": \"https://example.com/auth\",\n                    \"elicitationId\": \"test-123\",\n                }\n            ]\n        },\n    )\n\n    error = UrlElicitationRequiredError.from_error(error_data)\n\n    assert len(error.elicitations) == 1\n    assert error.elicitations[0].elicitation_id == \"test-123\"\n    assert error.elicitations[0].url == \"https://example.com/auth\"\n\n\ndef test_url_elicitation_required_error_from_error_data_wrong_code() -> None:\n    \"\"\"Test that from_error raises ValueError for wrong error code.\"\"\"\n    error_data = ErrorData(\n        code=-32600,  # Wrong code\n        message=\"Some other error\",\n        data={},\n    )\n\n    with pytest.raises(ValueError, match=\"Expected error code\"):\n        UrlElicitationRequiredError.from_error(error_data)\n\n\ndef test_url_elicitation_required_error_serialization_roundtrip() -> None:\n    \"\"\"Test that error can be serialized and reconstructed.\"\"\"\n    original = UrlElicitationRequiredError(\n        [\n            ElicitRequestURLParams(\n                mode=\"url\",\n                message=\"Auth required\",\n                url=\"https://example.com/auth\",\n                elicitation_id=\"test-123\",\n            )\n        ]\n    )\n\n    # Simulate serialization over wire\n    error_data = original.error\n\n    # Reconstruct\n    reconstructed = UrlElicitationRequiredError.from_error(error_data)\n\n    assert reconstructed.elicitations[0].elicitation_id == original.elicitations[0].elicitation_id\n    assert reconstructed.elicitations[0].url == original.elicitations[0].url\n    assert reconstructed.elicitations[0].message == original.elicitations[0].message\n\n\ndef test_url_elicitation_required_error_data_contains_elicitations() -> None:\n    \"\"\"Test that error data contains properly serialized elicitations.\"\"\"\n    elicitation = ElicitRequestURLParams(\n        mode=\"url\",\n        message=\"Please authenticate\",\n        url=\"https://example.com/oauth\",\n        elicitation_id=\"oauth-flow-1\",\n    )\n    error = UrlElicitationRequiredError([elicitation])\n\n    assert error.error.data is not None\n    assert \"elicitations\" in error.error.data\n    elicit_data = error.error.data[\"elicitations\"][0]\n    assert elicit_data[\"mode\"] == \"url\"\n    assert elicit_data[\"message\"] == \"Please authenticate\"\n    assert elicit_data[\"url\"] == \"https://example.com/oauth\"\n    assert elicit_data[\"elicitationId\"] == \"oauth-flow-1\"\n\n\ndef test_url_elicitation_required_error_inherits_from_mcp_error() -> None:\n    \"\"\"Test that UrlElicitationRequiredError inherits from MCPError.\"\"\"\n    elicitation = ElicitRequestURLParams(\n        mode=\"url\",\n        message=\"Auth required\",\n        url=\"https://example.com/auth\",\n        elicitation_id=\"test-123\",\n    )\n    error = UrlElicitationRequiredError([elicitation])\n\n    assert isinstance(error, MCPError)\n    assert isinstance(error, Exception)\n\n\ndef test_url_elicitation_required_error_exception_message() -> None:\n    \"\"\"Test that exception message is set correctly.\"\"\"\n    elicitation = ElicitRequestURLParams(\n        mode=\"url\",\n        message=\"Auth required\",\n        url=\"https://example.com/auth\",\n        elicitation_id=\"test-123\",\n    )\n    error = UrlElicitationRequiredError([elicitation])\n\n    # The exception's string representation should match the message\n    assert str(error) == \"URL elicitation required\"\n"
  },
  {
    "path": "tests/shared/test_httpx_utils.py",
    "content": "\"\"\"Tests for httpx utility functions.\"\"\"\n\nimport httpx\n\nfrom mcp.shared._httpx_utils import create_mcp_http_client\n\n\ndef test_default_settings():\n    \"\"\"Test that default settings are applied correctly.\"\"\"\n    client = create_mcp_http_client()\n\n    assert client.follow_redirects is True\n    assert client.timeout.connect == 30.0\n\n\ndef test_custom_parameters():\n    \"\"\"Test custom headers and timeout are set correctly.\"\"\"\n    headers = {\"Authorization\": \"Bearer token\"}\n    timeout = httpx.Timeout(60.0)\n\n    client = create_mcp_http_client(headers, timeout)\n\n    assert client.headers[\"Authorization\"] == \"Bearer token\"\n    assert client.timeout.connect == 60.0\n"
  },
  {
    "path": "tests/shared/test_progress_notifications.py",
    "content": "from typing import Any\nfrom unittest.mock import patch\n\nimport anyio\nimport pytest\n\nfrom mcp import Client, types\nfrom mcp.client.session import ClientSession\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.server.lowlevel import NotificationOptions\nfrom mcp.server.models import InitializationOptions\nfrom mcp.server.session import ServerSession\nfrom mcp.shared.message import SessionMessage\nfrom mcp.shared.session import RequestResponder\n\n\n@pytest.mark.anyio\nasync def test_bidirectional_progress_notifications():\n    \"\"\"Test that both client and server can send progress notifications.\"\"\"\n    # Create memory streams for client/server\n    server_to_client_send, server_to_client_receive = anyio.create_memory_object_stream[SessionMessage](5)\n    client_to_server_send, client_to_server_receive = anyio.create_memory_object_stream[SessionMessage](5)\n\n    # Run a server session so we can send progress updates in tool\n    async def run_server():\n        # Create a server session\n        async with ServerSession(\n            client_to_server_receive,\n            server_to_client_send,\n            InitializationOptions(\n                server_name=\"ProgressTestServer\",\n                server_version=\"0.1.0\",\n                capabilities=server.get_capabilities(NotificationOptions(), {}),\n            ),\n        ) as server_session:\n            async for message in server_session.incoming_messages:\n                try:\n                    await server._handle_message(message, server_session, {})\n                except Exception as e:  # pragma: no cover\n                    raise e\n\n    # Track progress updates\n    server_progress_updates: list[dict[str, Any]] = []\n    client_progress_updates: list[dict[str, Any]] = []\n\n    # Progress tokens\n    server_progress_token = \"server_token_123\"\n    client_progress_token = \"client_token_456\"\n\n    # Register progress handler\n    async def handle_progress(ctx: ServerRequestContext, params: types.ProgressNotificationParams) -> None:\n        server_progress_updates.append(\n            {\n                \"token\": params.progress_token,\n                \"progress\": params.progress,\n                \"total\": params.total,\n                \"message\": params.message,\n            }\n        )\n\n    # Register list tool handler\n    async def handle_list_tools(\n        ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n    ) -> types.ListToolsResult:\n        return types.ListToolsResult(\n            tools=[\n                types.Tool(\n                    name=\"test_tool\",\n                    description=\"A tool that sends progress notifications <o/\",\n                    input_schema={},\n                )\n            ]\n        )\n\n    # Register tool handler\n    async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:\n        # Make sure we received a progress token\n        if params.name == \"test_tool\":\n            assert params.meta is not None\n            progress_token = params.meta.get(\"progress_token\")\n            assert progress_token is not None\n            assert progress_token == client_progress_token\n\n            # Send progress notifications using ctx.session\n            await ctx.session.send_progress_notification(\n                progress_token=progress_token,\n                progress=0.25,\n                total=1.0,\n                message=\"Server progress 25%\",\n            )\n\n            await ctx.session.send_progress_notification(\n                progress_token=progress_token,\n                progress=0.5,\n                total=1.0,\n                message=\"Server progress 50%\",\n            )\n\n            await ctx.session.send_progress_notification(\n                progress_token=progress_token,\n                progress=1.0,\n                total=1.0,\n                message=\"Server progress 100%\",\n            )\n\n            return types.CallToolResult(content=[types.TextContent(type=\"text\", text=\"Tool executed successfully\")])\n\n        raise ValueError(f\"Unknown tool: {params.name}\")  # pragma: no cover\n\n    # Create a server with progress capability\n    server = Server(\n        name=\"ProgressTestServer\",\n        on_progress=handle_progress,\n        on_list_tools=handle_list_tools,\n        on_call_tool=handle_call_tool,\n    )\n\n    # Client message handler to store progress notifications\n    async def handle_client_message(\n        message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,\n    ) -> None:\n        if isinstance(message, Exception):  # pragma: no cover\n            raise message\n\n        if isinstance(message, types.ServerNotification):  # pragma: no branch\n            if isinstance(message, types.ProgressNotification):  # pragma: no branch\n                params = message.params\n                client_progress_updates.append(\n                    {\n                        \"token\": params.progress_token,\n                        \"progress\": params.progress,\n                        \"total\": params.total,\n                        \"message\": params.message,\n                    }\n                )\n\n    # Test using client\n    async with (\n        ClientSession(\n            server_to_client_receive,\n            client_to_server_send,\n            message_handler=handle_client_message,\n        ) as client_session,\n        anyio.create_task_group() as tg,\n    ):\n        # Start the server in a background task\n        tg.start_soon(run_server)\n\n        # Initialize the client connection\n        await client_session.initialize()\n\n        # Call list_tools with progress token\n        await client_session.list_tools()\n\n        # Call test_tool with progress token\n        await client_session.call_tool(\"test_tool\", meta={\"progress_token\": client_progress_token})\n\n        # Send progress notifications from client to server\n        await client_session.send_progress_notification(\n            progress_token=server_progress_token,\n            progress=0.33,\n            total=1.0,\n            message=\"Client progress 33%\",\n        )\n\n        await client_session.send_progress_notification(\n            progress_token=server_progress_token,\n            progress=0.66,\n            total=1.0,\n            message=\"Client progress 66%\",\n        )\n\n        await client_session.send_progress_notification(\n            progress_token=server_progress_token,\n            progress=1.0,\n            total=1.0,\n            message=\"Client progress 100%\",\n        )\n\n        # Wait and exit\n        await anyio.sleep(0.5)\n        tg.cancel_scope.cancel()\n\n    # Verify client received progress updates from server\n    assert len(client_progress_updates) == 3\n    assert client_progress_updates[0][\"token\"] == client_progress_token\n    assert client_progress_updates[0][\"progress\"] == 0.25\n    assert client_progress_updates[0][\"message\"] == \"Server progress 25%\"\n    assert client_progress_updates[2][\"progress\"] == 1.0\n\n    # Verify server received progress updates from client\n    assert len(server_progress_updates) == 3\n    assert server_progress_updates[0][\"token\"] == server_progress_token\n    assert server_progress_updates[0][\"progress\"] == 0.33\n    assert server_progress_updates[0][\"message\"] == \"Client progress 33%\"\n    assert server_progress_updates[2][\"progress\"] == 1.0\n\n\n@pytest.mark.anyio\nasync def test_progress_callback_exception_logging():\n    \"\"\"Test that exceptions in progress callbacks are logged and \\\n        don't crash the session.\"\"\"\n    # Track logged warnings\n    logged_errors: list[str] = []\n\n    def mock_log_exception(msg: str, *args: Any, **kwargs: Any) -> None:\n        logged_errors.append(msg % args if args else msg)\n\n    # Create a progress callback that raises an exception\n    async def failing_progress_callback(progress: float, total: float | None, message: str | None) -> None:\n        raise ValueError(\"Progress callback failed!\")\n\n    # Create a server with a tool that sends progress notifications\n    async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:\n        if params.name == \"progress_tool\":\n            assert ctx.request_id is not None\n            # Send a progress notification\n            await ctx.session.send_progress_notification(\n                progress_token=ctx.request_id,\n                progress=50.0,\n                total=100.0,\n                message=\"Halfway done\",\n            )\n            return types.CallToolResult(content=[types.TextContent(type=\"text\", text=\"progress_result\")])\n        raise ValueError(f\"Unknown tool: {params.name}\")  # pragma: no cover\n\n    async def handle_list_tools(\n        ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n    ) -> types.ListToolsResult:\n        return types.ListToolsResult(\n            tools=[\n                types.Tool(\n                    name=\"progress_tool\",\n                    description=\"A tool that sends progress notifications\",\n                    input_schema={},\n                )\n            ]\n        )\n\n    server = Server(\n        name=\"TestProgressServer\",\n        on_call_tool=handle_call_tool,\n        on_list_tools=handle_list_tools,\n    )\n\n    # Test with mocked logging\n    with patch(\"mcp.shared.session.logging.exception\", side_effect=mock_log_exception):\n        async with Client(server) as client:\n            # Call tool with a failing progress callback\n            result = await client.call_tool(\n                \"progress_tool\",\n                arguments={},\n                progress_callback=failing_progress_callback,\n            )\n\n            # Verify the request completed successfully despite the callback failure\n            assert len(result.content) == 1\n            content = result.content[0]\n            assert isinstance(content, types.TextContent)\n            assert content.text == \"progress_result\"\n\n            # Check that a warning was logged for the progress callback exception\n            assert len(logged_errors) > 0\n            assert any(\"Progress callback raised an exception\" in warning for warning in logged_errors)\n"
  },
  {
    "path": "tests/shared/test_session.py",
    "content": "import anyio\nimport pytest\n\nfrom mcp import Client, types\nfrom mcp.client.session import ClientSession\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.shared.memory import create_client_server_memory_streams\nfrom mcp.shared.message import SessionMessage\nfrom mcp.shared.session import RequestResponder\nfrom mcp.types import (\n    PARSE_ERROR,\n    CancelledNotification,\n    CancelledNotificationParams,\n    ClientResult,\n    EmptyResult,\n    ErrorData,\n    JSONRPCError,\n    JSONRPCRequest,\n    JSONRPCResponse,\n    ServerNotification,\n    ServerRequest,\n)\n\n\n@pytest.mark.anyio\nasync def test_in_flight_requests_cleared_after_completion():\n    \"\"\"Verify that _in_flight is empty after all requests complete.\"\"\"\n    server = Server(name=\"test server\")\n    async with Client(server) as client:\n        # Send a request and wait for response\n        response = await client.send_ping()\n        assert isinstance(response, EmptyResult)\n\n        # Verify _in_flight is empty\n        assert len(client.session._in_flight) == 0\n\n\n@pytest.mark.anyio\nasync def test_request_cancellation():\n    \"\"\"Test that requests can be cancelled while in-flight.\"\"\"\n    ev_tool_called = anyio.Event()\n    ev_cancelled = anyio.Event()\n    request_id = None\n\n    # Create a server with a slow tool\n    async def handle_call_tool(ctx: ServerRequestContext, params: types.CallToolRequestParams) -> types.CallToolResult:\n        nonlocal request_id, ev_tool_called\n        if params.name == \"slow_tool\":\n            request_id = ctx.request_id\n            ev_tool_called.set()\n            await anyio.sleep(10)  # Long enough to ensure we can cancel\n            return types.CallToolResult(content=[])  # pragma: no cover\n        raise ValueError(f\"Unknown tool: {params.name}\")  # pragma: no cover\n\n    async def handle_list_tools(\n        ctx: ServerRequestContext, params: types.PaginatedRequestParams | None\n    ) -> types.ListToolsResult:\n        raise NotImplementedError\n\n    server = Server(\n        name=\"TestSessionServer\",\n        on_call_tool=handle_call_tool,\n        on_list_tools=handle_list_tools,\n    )\n\n    async def make_request(client: Client):\n        nonlocal ev_cancelled\n        try:\n            await client.session.send_request(\n                types.CallToolRequest(\n                    params=types.CallToolRequestParams(name=\"slow_tool\", arguments={}),\n                ),\n                types.CallToolResult,\n            )\n            pytest.fail(\"Request should have been cancelled\")  # pragma: no cover\n        except MCPError as e:\n            # Expected - request was cancelled\n            assert \"Request cancelled\" in str(e)\n            ev_cancelled.set()\n\n    async with Client(server) as client:\n        async with anyio.create_task_group() as tg:  # pragma: no branch\n            tg.start_soon(make_request, client)\n\n            # Wait for the request to be in-flight\n            with anyio.fail_after(1):  # Timeout after 1 second\n                await ev_tool_called.wait()\n\n            # Send cancellation notification\n            assert request_id is not None\n            await client.session.send_notification(\n                CancelledNotification(params=CancelledNotificationParams(request_id=request_id))\n            )\n\n            # Give cancellation time to process\n            with anyio.fail_after(1):  # pragma: no branch\n                await ev_cancelled.wait()\n\n\n@pytest.mark.anyio\nasync def test_response_id_type_mismatch_string_to_int():\n    \"\"\"Test that responses with string IDs are correctly matched to requests sent with\n    integer IDs.\n\n    This handles the case where a server returns \"id\": \"0\" (string) but the client\n    sent \"id\": 0 (integer). Without ID type normalization, this would cause a timeout.\n    \"\"\"\n    ev_response_received = anyio.Event()\n    result_holder: list[types.EmptyResult] = []\n\n    async with create_client_server_memory_streams() as (client_streams, server_streams):\n        client_read, client_write = client_streams\n        server_read, server_write = server_streams\n\n        async def mock_server():\n            \"\"\"Receive a request and respond with a string ID instead of integer.\"\"\"\n            message = await server_read.receive()\n            assert isinstance(message, SessionMessage)\n            root = message.message\n            assert isinstance(root, JSONRPCRequest)\n            # Get the original request ID (which is an integer)\n            request_id = root.id\n            assert isinstance(request_id, int), f\"Expected int, got {type(request_id)}\"\n\n            # Respond with the ID as a string (simulating a buggy server)\n            response = JSONRPCResponse(\n                jsonrpc=\"2.0\",\n                id=str(request_id),  # Convert to string to simulate mismatch\n                result={},\n            )\n            await server_write.send(SessionMessage(message=response))\n\n        async def make_request(client_session: ClientSession):\n            nonlocal result_holder\n            # Send a ping request (uses integer ID internally)\n            result = await client_session.send_ping()\n            result_holder.append(result)\n            ev_response_received.set()\n\n        async with (\n            anyio.create_task_group() as tg,\n            ClientSession(read_stream=client_read, write_stream=client_write) as client_session,\n        ):\n            tg.start_soon(mock_server)\n            tg.start_soon(make_request, client_session)\n\n            with anyio.fail_after(2):  # pragma: no branch\n                await ev_response_received.wait()\n\n    assert len(result_holder) == 1\n    assert isinstance(result_holder[0], EmptyResult)\n\n\n@pytest.mark.anyio\nasync def test_error_response_id_type_mismatch_string_to_int():\n    \"\"\"Test that error responses with string IDs are correctly matched to requests\n    sent with integer IDs.\n\n    This handles the case where a server returns an error with \"id\": \"0\" (string)\n    but the client sent \"id\": 0 (integer).\n    \"\"\"\n    ev_error_received = anyio.Event()\n    error_holder: list[MCPError | Exception] = []\n\n    async with create_client_server_memory_streams() as (client_streams, server_streams):\n        client_read, client_write = client_streams\n        server_read, server_write = server_streams\n\n        async def mock_server():\n            \"\"\"Receive a request and respond with an error using a string ID.\"\"\"\n            message = await server_read.receive()\n            assert isinstance(message, SessionMessage)\n            root = message.message\n            assert isinstance(root, JSONRPCRequest)\n            request_id = root.id\n            assert isinstance(request_id, int)\n\n            # Respond with an error, using the ID as a string\n            error_response = JSONRPCError(\n                jsonrpc=\"2.0\",\n                id=str(request_id),  # Convert to string to simulate mismatch\n                error=ErrorData(code=-32600, message=\"Test error\"),\n            )\n            await server_write.send(SessionMessage(message=error_response))\n\n        async def make_request(client_session: ClientSession):\n            nonlocal error_holder\n            try:\n                await client_session.send_ping()\n                pytest.fail(\"Expected MCPError to be raised\")  # pragma: no cover\n            except MCPError as e:\n                error_holder.append(e)\n                ev_error_received.set()\n\n        async with (\n            anyio.create_task_group() as tg,\n            ClientSession(read_stream=client_read, write_stream=client_write) as client_session,\n        ):\n            tg.start_soon(mock_server)\n            tg.start_soon(make_request, client_session)\n\n            with anyio.fail_after(2):  # pragma: no branch\n                await ev_error_received.wait()\n\n    assert len(error_holder) == 1\n    assert \"Test error\" in str(error_holder[0])\n\n\n@pytest.mark.anyio\nasync def test_response_id_non_numeric_string_no_match():\n    \"\"\"Test that responses with non-numeric string IDs don't incorrectly match\n    integer request IDs.\n\n    If a server returns \"id\": \"abc\" (non-numeric string), it should not match\n    a request sent with \"id\": 0 (integer).\n    \"\"\"\n    ev_timeout = anyio.Event()\n\n    async with create_client_server_memory_streams() as (client_streams, server_streams):\n        client_read, client_write = client_streams\n        server_read, server_write = server_streams\n\n        async def mock_server():\n            \"\"\"Receive a request and respond with a non-numeric string ID.\"\"\"\n            message = await server_read.receive()\n            assert isinstance(message, SessionMessage)\n\n            # Respond with a non-numeric string ID (should not match)\n            response = JSONRPCResponse(\n                jsonrpc=\"2.0\",\n                id=\"not_a_number\",  # Non-numeric string\n                result={},\n            )\n            await server_write.send(SessionMessage(message=response))\n\n        async def make_request(client_session: ClientSession):\n            try:\n                # Use a short timeout since we expect this to fail\n                await client_session.send_request(\n                    types.PingRequest(),\n                    types.EmptyResult,\n                    request_read_timeout_seconds=0.5,\n                )\n                pytest.fail(\"Expected timeout\")  # pragma: no cover\n            except MCPError as e:\n                assert \"Timed out\" in str(e)\n                ev_timeout.set()\n\n        async with (\n            anyio.create_task_group() as tg,\n            ClientSession(read_stream=client_read, write_stream=client_write) as client_session,\n        ):\n            tg.start_soon(mock_server)\n            tg.start_soon(make_request, client_session)\n\n            with anyio.fail_after(2):  # pragma: no branch\n                await ev_timeout.wait()\n\n\n@pytest.mark.anyio\nasync def test_connection_closed():\n    \"\"\"Test that pending requests are cancelled when the connection is closed remotely.\"\"\"\n\n    ev_closed = anyio.Event()\n    ev_response = anyio.Event()\n\n    async with create_client_server_memory_streams() as (client_streams, server_streams):\n        client_read, client_write = client_streams\n        server_read, server_write = server_streams\n\n        async def make_request(client_session: ClientSession):\n            \"\"\"Send a request in a separate task\"\"\"\n            nonlocal ev_response\n            try:\n                # any request will do\n                await client_session.initialize()\n                pytest.fail(\"Request should have errored\")  # pragma: no cover\n            except MCPError as e:\n                # Expected - request errored\n                assert \"Connection closed\" in str(e)\n                ev_response.set()\n\n        async def mock_server():\n            \"\"\"Wait for a request, then close the connection\"\"\"\n            nonlocal ev_closed\n            # Wait for a request\n            await server_read.receive()\n            # Close the connection, as if the server exited\n            server_write.close()\n            server_read.close()\n            ev_closed.set()\n\n        async with (\n            anyio.create_task_group() as tg,\n            ClientSession(read_stream=client_read, write_stream=client_write) as client_session,\n        ):\n            tg.start_soon(make_request, client_session)\n            tg.start_soon(mock_server)\n\n            with anyio.fail_after(1):\n                await ev_closed.wait()\n            with anyio.fail_after(1):  # pragma: no branch\n                await ev_response.wait()\n\n\n@pytest.mark.anyio\nasync def test_null_id_error_surfaced_via_message_handler():\n    \"\"\"Test that a JSONRPCError with id=None is surfaced to the message handler.\n\n    Per JSON-RPC 2.0, error responses use id=null when the request id could not\n    be determined (e.g., parse errors). These cannot be correlated to any pending\n    request, so they are forwarded to the message handler as MCPError.\n    \"\"\"\n    ev_error_received = anyio.Event()\n    error_holder: list[MCPError] = []\n\n    async def capture_errors(\n        message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception,\n    ) -> None:\n        assert isinstance(message, MCPError)\n        error_holder.append(message)\n        ev_error_received.set()\n\n    sent_error = ErrorData(code=PARSE_ERROR, message=\"Parse error\")\n\n    async with create_client_server_memory_streams() as (client_streams, server_streams):\n        client_read, client_write = client_streams\n        _server_read, server_write = server_streams\n\n        async def mock_server():\n            \"\"\"Send a null-id error (simulating a parse error).\"\"\"\n            error_response = JSONRPCError(jsonrpc=\"2.0\", id=None, error=sent_error)\n            await server_write.send(SessionMessage(message=error_response))\n\n        async with (\n            anyio.create_task_group() as tg,\n            ClientSession(\n                read_stream=client_read,\n                write_stream=client_write,\n                message_handler=capture_errors,\n            ) as _client_session,\n        ):\n            tg.start_soon(mock_server)\n\n            with anyio.fail_after(2):  # pragma: no branch\n                await ev_error_received.wait()\n\n    assert len(error_holder) == 1\n    assert error_holder[0].error == sent_error\n\n\n@pytest.mark.anyio\nasync def test_null_id_error_does_not_affect_pending_request():\n    \"\"\"Test that a null-id error doesn't interfere with an in-flight request.\n\n    When a null-id error arrives while a request is pending, the error should\n    go to the message handler and the pending request should still complete\n    normally with its own response.\n    \"\"\"\n    ev_error_received = anyio.Event()\n    ev_response_received = anyio.Event()\n    error_holder: list[MCPError] = []\n    result_holder: list[EmptyResult] = []\n\n    async def capture_errors(\n        message: RequestResponder[ServerRequest, ClientResult] | ServerNotification | Exception,\n    ) -> None:\n        assert isinstance(message, MCPError)\n        error_holder.append(message)\n        ev_error_received.set()\n\n    sent_error = ErrorData(code=PARSE_ERROR, message=\"Parse error\")\n\n    async with create_client_server_memory_streams() as (client_streams, server_streams):\n        client_read, client_write = client_streams\n        server_read, server_write = server_streams\n\n        async def mock_server():\n            \"\"\"Read a request, inject a null-id error, then respond normally.\"\"\"\n            message = await server_read.receive()\n            assert isinstance(message, SessionMessage)\n            assert isinstance(message.message, JSONRPCRequest)\n            request_id = message.message.id\n\n            # First, send a null-id error (should go to message handler)\n            await server_write.send(SessionMessage(message=JSONRPCError(jsonrpc=\"2.0\", id=None, error=sent_error)))\n\n            # Then, respond normally to the pending request\n            await server_write.send(SessionMessage(message=JSONRPCResponse(jsonrpc=\"2.0\", id=request_id, result={})))\n\n        async def make_request(client_session: ClientSession):\n            result = await client_session.send_ping()\n            result_holder.append(result)\n            ev_response_received.set()\n\n        async with (\n            anyio.create_task_group() as tg,\n            ClientSession(\n                read_stream=client_read,\n                write_stream=client_write,\n                message_handler=capture_errors,\n            ) as client_session,\n        ):\n            tg.start_soon(mock_server)\n            tg.start_soon(make_request, client_session)\n\n            with anyio.fail_after(2):  # pragma: no branch\n                await ev_error_received.wait()\n                await ev_response_received.wait()\n\n    # Null-id error reached the message handler\n    assert len(error_holder) == 1\n    assert error_holder[0].error == sent_error\n\n    # Pending request completed successfully\n    assert len(result_holder) == 1\n    assert isinstance(result_holder[0], EmptyResult)\n"
  },
  {
    "path": "tests/shared/test_sse.py",
    "content": "import json\nimport multiprocessing\nimport socket\nfrom collections.abc import AsyncGenerator, Generator\nfrom typing import Any\nfrom unittest.mock import AsyncMock, MagicMock, Mock, patch\nfrom urllib.parse import urlparse\n\nimport anyio\nimport httpx\nimport pytest\nimport uvicorn\nfrom httpx_sse import ServerSentEvent\nfrom inline_snapshot import snapshot\nfrom starlette.applications import Starlette\nfrom starlette.requests import Request\nfrom starlette.responses import Response\nfrom starlette.routing import Mount, Route\n\nimport mcp.client.sse\nfrom mcp import types\nfrom mcp.client.session import ClientSession\nfrom mcp.client.sse import _extract_session_id_from_endpoint, sse_client\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.server.sse import SseServerTransport\nfrom mcp.server.transport_security import TransportSecuritySettings\nfrom mcp.shared.exceptions import MCPError\nfrom mcp.types import (\n    CallToolRequestParams,\n    CallToolResult,\n    EmptyResult,\n    Implementation,\n    InitializeResult,\n    JSONRPCResponse,\n    ListToolsResult,\n    PaginatedRequestParams,\n    ReadResourceRequestParams,\n    ReadResourceResult,\n    ServerCapabilities,\n    TextContent,\n    TextResourceContents,\n    Tool,\n)\nfrom tests.test_helpers import wait_for_server\n\nSERVER_NAME = \"test_server_for_SSE\"\n\n\n@pytest.fixture\ndef server_port() -> int:\n    with socket.socket() as s:\n        s.bind((\"127.0.0.1\", 0))\n        return s.getsockname()[1]\n\n\n@pytest.fixture\ndef server_url(server_port: int) -> str:\n    return f\"http://127.0.0.1:{server_port}\"\n\n\nasync def _handle_read_resource(  # pragma: no cover\n    ctx: ServerRequestContext, params: ReadResourceRequestParams\n) -> ReadResourceResult:\n    uri = str(params.uri)\n    parsed = urlparse(uri)\n    if parsed.scheme == \"foobar\":\n        text = f\"Read {parsed.netloc}\"\n    elif parsed.scheme == \"slow\":\n        await anyio.sleep(2.0)\n        text = f\"Slow response from {parsed.netloc}\"\n    else:\n        raise MCPError(code=404, message=\"OOPS! no resource with that URI was found\")\n    return ReadResourceResult(contents=[TextResourceContents(uri=uri, text=text, mime_type=\"text/plain\")])\n\n\nasync def _handle_list_tools(  # pragma: no cover\n    ctx: ServerRequestContext, params: PaginatedRequestParams | None\n) -> ListToolsResult:\n    return ListToolsResult(\n        tools=[\n            Tool(\n                name=\"test_tool\",\n                description=\"A test tool\",\n                input_schema={\"type\": \"object\", \"properties\": {}},\n            )\n        ]\n    )\n\n\nasync def _handle_call_tool(  # pragma: no cover\n    ctx: ServerRequestContext, params: CallToolRequestParams\n) -> CallToolResult:\n    return CallToolResult(content=[TextContent(type=\"text\", text=f\"Called {params.name}\")])\n\n\ndef _create_server() -> Server:  # pragma: no cover\n    return Server(\n        SERVER_NAME,\n        on_read_resource=_handle_read_resource,\n        on_list_tools=_handle_list_tools,\n        on_call_tool=_handle_call_tool,\n    )\n\n\n# Test fixtures\ndef make_server_app() -> Starlette:  # pragma: no cover\n    \"\"\"Create test Starlette app with SSE transport\"\"\"\n    # Configure security with allowed hosts/origins for testing\n    security_settings = TransportSecuritySettings(\n        allowed_hosts=[\"127.0.0.1:*\", \"localhost:*\"], allowed_origins=[\"http://127.0.0.1:*\", \"http://localhost:*\"]\n    )\n    sse = SseServerTransport(\"/messages/\", security_settings=security_settings)\n    server = _create_server()\n\n    async def handle_sse(request: Request) -> Response:\n        async with sse.connect_sse(request.scope, request.receive, request._send) as streams:\n            await server.run(streams[0], streams[1], server.create_initialization_options())\n        return Response()\n\n    app = Starlette(\n        routes=[\n            Route(\"/sse\", endpoint=handle_sse),\n            Mount(\"/messages/\", app=sse.handle_post_message),\n        ]\n    )\n\n    return app\n\n\ndef run_server(server_port: int) -> None:  # pragma: no cover\n    app = make_server_app()\n    server = uvicorn.Server(config=uvicorn.Config(app=app, host=\"127.0.0.1\", port=server_port, log_level=\"error\"))\n    print(f\"starting server on {server_port}\")\n    server.run()\n\n\n@pytest.fixture()\ndef server(server_port: int) -> Generator[None, None, None]:\n    proc = multiprocessing.Process(target=run_server, kwargs={\"server_port\": server_port}, daemon=True)\n    print(\"starting process\")\n    proc.start()\n\n    # Wait for server to be running\n    print(\"waiting for server to start\")\n    wait_for_server(server_port)\n\n    yield\n\n    print(\"killing server\")\n    # Signal the server to stop\n    proc.kill()\n    proc.join(timeout=2)\n    if proc.is_alive():  # pragma: no cover\n        print(\"server process failed to terminate\")\n\n\n@pytest.fixture()\nasync def http_client(server: None, server_url: str) -> AsyncGenerator[httpx.AsyncClient, None]:\n    \"\"\"Create test client\"\"\"\n    async with httpx.AsyncClient(base_url=server_url) as client:\n        yield client\n\n\n# Tests\n@pytest.mark.anyio\nasync def test_raw_sse_connection(http_client: httpx.AsyncClient) -> None:\n    \"\"\"Test the SSE connection establishment simply with an HTTP client.\"\"\"\n    async with anyio.create_task_group():\n\n        async def connection_test() -> None:\n            async with http_client.stream(\"GET\", \"/sse\") as response:\n                assert response.status_code == 200\n                assert response.headers[\"content-type\"] == \"text/event-stream; charset=utf-8\"\n\n                line_number = 0\n                async for line in response.aiter_lines():  # pragma: no branch\n                    if line_number == 0:\n                        assert line == \"event: endpoint\"\n                    elif line_number == 1:\n                        assert line.startswith(\"data: /messages/?session_id=\")\n                    else:\n                        return\n                    line_number += 1\n\n        # Add timeout to prevent test from hanging if it fails\n        with anyio.fail_after(3):\n            await connection_test()\n\n\n@pytest.mark.anyio\nasync def test_sse_client_basic_connection(server: None, server_url: str) -> None:\n    async with sse_client(server_url + \"/sse\") as streams:\n        async with ClientSession(*streams) as session:\n            # Test initialization\n            result = await session.initialize()\n            assert isinstance(result, InitializeResult)\n            assert result.server_info.name == SERVER_NAME\n\n            # Test ping\n            ping_result = await session.send_ping()\n            assert isinstance(ping_result, EmptyResult)\n\n\n@pytest.mark.anyio\nasync def test_sse_client_on_session_created(server: None, server_url: str) -> None:\n    captured: list[str] = []\n\n    async with sse_client(server_url + \"/sse\", on_session_created=captured.append) as streams:\n        async with ClientSession(*streams) as session:\n            result = await session.initialize()\n            assert isinstance(result, InitializeResult)\n            # Callback fires when the endpoint event arrives, before sse_client yields.\n            assert len(captured) == 1\n            assert len(captured[0]) > 0\n\n\n@pytest.mark.parametrize(\n    \"endpoint_url,expected\",\n    [\n        (\"/messages?sessionId=abc123\", \"abc123\"),\n        (\"/messages?session_id=def456\", \"def456\"),\n        (\"/messages?sessionId=abc&session_id=def\", \"abc\"),\n        (\"/messages?other=value\", None),\n        (\"/messages\", None),\n        (\"\", None),\n    ],\n)\ndef test_extract_session_id_from_endpoint(endpoint_url: str, expected: str | None) -> None:\n    assert _extract_session_id_from_endpoint(endpoint_url) == expected\n\n\n@pytest.mark.anyio\nasync def test_sse_client_on_session_created_not_called_when_no_session_id(\n    server: None, server_url: str, monkeypatch: pytest.MonkeyPatch\n) -> None:\n    callback_mock = Mock()\n\n    def mock_extract(url: str) -> None:\n        return None\n\n    monkeypatch.setattr(mcp.client.sse, \"_extract_session_id_from_endpoint\", mock_extract)\n\n    async with sse_client(server_url + \"/sse\", on_session_created=callback_mock) as streams:\n        async with ClientSession(*streams) as session:\n            result = await session.initialize()\n            assert isinstance(result, InitializeResult)\n            # Callback would have fired by now (endpoint event arrives before\n            # sse_client yields); if it hasn't, it won't.\n            callback_mock.assert_not_called()\n\n\n@pytest.fixture\nasync def initialized_sse_client_session(server: None, server_url: str) -> AsyncGenerator[ClientSession, None]:\n    async with sse_client(server_url + \"/sse\", sse_read_timeout=0.5) as streams:\n        async with ClientSession(*streams) as session:\n            await session.initialize()\n            yield session\n\n\n@pytest.mark.anyio\nasync def test_sse_client_happy_request_and_response(\n    initialized_sse_client_session: ClientSession,\n) -> None:\n    session = initialized_sse_client_session\n    response = await session.read_resource(uri=\"foobar://should-work\")\n    assert len(response.contents) == 1\n    assert isinstance(response.contents[0], TextResourceContents)\n    assert response.contents[0].text == \"Read should-work\"\n\n\n@pytest.mark.anyio\nasync def test_sse_client_exception_handling(\n    initialized_sse_client_session: ClientSession,\n) -> None:\n    session = initialized_sse_client_session\n    with pytest.raises(MCPError, match=\"OOPS! no resource with that URI was found\"):\n        await session.read_resource(uri=\"xxx://will-not-work\")\n\n\n@pytest.mark.anyio\n@pytest.mark.skip(\"this test highlights a possible bug in SSE read timeout exception handling\")\nasync def test_sse_client_timeout(  # pragma: no cover\n    initialized_sse_client_session: ClientSession,\n) -> None:\n    session = initialized_sse_client_session\n\n    # sanity check that normal, fast responses are working\n    response = await session.read_resource(uri=\"foobar://1\")\n    assert isinstance(response, ReadResourceResult)\n\n    with anyio.move_on_after(3):\n        with pytest.raises(MCPError, match=\"Read timed out\"):\n            response = await session.read_resource(uri=\"slow://2\")\n            # we should receive an error here\n        return\n\n    pytest.fail(\"the client should have timed out and returned an error already\")\n\n\ndef run_mounted_server(server_port: int) -> None:  # pragma: no cover\n    app = make_server_app()\n    main_app = Starlette(routes=[Mount(\"/mounted_app\", app=app)])\n    server = uvicorn.Server(config=uvicorn.Config(app=main_app, host=\"127.0.0.1\", port=server_port, log_level=\"error\"))\n    print(f\"starting server on {server_port}\")\n    server.run()\n\n\n@pytest.fixture()\ndef mounted_server(server_port: int) -> Generator[None, None, None]:\n    proc = multiprocessing.Process(target=run_mounted_server, kwargs={\"server_port\": server_port}, daemon=True)\n    print(\"starting process\")\n    proc.start()\n\n    # Wait for server to be running\n    print(\"waiting for server to start\")\n    wait_for_server(server_port)\n\n    yield\n\n    print(\"killing server\")\n    # Signal the server to stop\n    proc.kill()\n    proc.join(timeout=2)\n    if proc.is_alive():  # pragma: no cover\n        print(\"server process failed to terminate\")\n\n\n@pytest.mark.anyio\nasync def test_sse_client_basic_connection_mounted_app(mounted_server: None, server_url: str) -> None:\n    async with sse_client(server_url + \"/mounted_app/sse\") as streams:\n        async with ClientSession(*streams) as session:\n            # Test initialization\n            result = await session.initialize()\n            assert isinstance(result, InitializeResult)\n            assert result.server_info.name == SERVER_NAME\n\n            # Test ping\n            ping_result = await session.send_ping()\n            assert isinstance(ping_result, EmptyResult)\n\n\nasync def _handle_context_call_tool(  # pragma: no cover\n    ctx: ServerRequestContext, params: CallToolRequestParams\n) -> CallToolResult:\n    headers_info: dict[str, Any] = {}\n    if ctx.request:\n        headers_info = dict(ctx.request.headers)\n\n    if params.name == \"echo_headers\":\n        return CallToolResult(content=[TextContent(type=\"text\", text=json.dumps(headers_info))])\n    elif params.name == \"echo_context\":\n        context_data = {\n            \"request_id\": (params.arguments or {}).get(\"request_id\"),\n            \"headers\": headers_info,\n        }\n        return CallToolResult(content=[TextContent(type=\"text\", text=json.dumps(context_data))])\n\n    return CallToolResult(content=[TextContent(type=\"text\", text=f\"Called {params.name}\")])\n\n\nasync def _handle_context_list_tools(  # pragma: no cover\n    ctx: ServerRequestContext, params: PaginatedRequestParams | None\n) -> ListToolsResult:\n    return ListToolsResult(\n        tools=[\n            Tool(\n                name=\"echo_headers\",\n                description=\"Echoes request headers\",\n                input_schema={\"type\": \"object\", \"properties\": {}},\n            ),\n            Tool(\n                name=\"echo_context\",\n                description=\"Echoes request context\",\n                input_schema={\n                    \"type\": \"object\",\n                    \"properties\": {\"request_id\": {\"type\": \"string\"}},\n                    \"required\": [\"request_id\"],\n                },\n            ),\n        ]\n    )\n\n\ndef run_context_server(server_port: int) -> None:  # pragma: no cover\n    \"\"\"Run a server that captures request context\"\"\"\n    # Configure security with allowed hosts/origins for testing\n    security_settings = TransportSecuritySettings(\n        allowed_hosts=[\"127.0.0.1:*\", \"localhost:*\"], allowed_origins=[\"http://127.0.0.1:*\", \"http://localhost:*\"]\n    )\n    sse = SseServerTransport(\"/messages/\", security_settings=security_settings)\n    context_server = Server(\n        \"request_context_server\",\n        on_call_tool=_handle_context_call_tool,\n        on_list_tools=_handle_context_list_tools,\n    )\n\n    async def handle_sse(request: Request) -> Response:\n        async with sse.connect_sse(request.scope, request.receive, request._send) as streams:\n            await context_server.run(streams[0], streams[1], context_server.create_initialization_options())\n        return Response()\n\n    app = Starlette(\n        routes=[\n            Route(\"/sse\", endpoint=handle_sse),\n            Mount(\"/messages/\", app=sse.handle_post_message),\n        ]\n    )\n\n    server = uvicorn.Server(config=uvicorn.Config(app=app, host=\"127.0.0.1\", port=server_port, log_level=\"error\"))\n    print(f\"starting context server on {server_port}\")\n    server.run()\n\n\n@pytest.fixture()\ndef context_server(server_port: int) -> Generator[None, None, None]:\n    \"\"\"Fixture that provides a server with request context capture\"\"\"\n    proc = multiprocessing.Process(target=run_context_server, kwargs={\"server_port\": server_port}, daemon=True)\n    print(\"starting context server process\")\n    proc.start()\n\n    # Wait for server to be running\n    print(\"waiting for context server to start\")\n    wait_for_server(server_port)\n\n    yield\n\n    print(\"killing context server\")\n    proc.kill()\n    proc.join(timeout=2)\n    if proc.is_alive():  # pragma: no cover\n        print(\"context server process failed to terminate\")\n\n\n@pytest.mark.anyio\nasync def test_request_context_propagation(context_server: None, server_url: str) -> None:\n    \"\"\"Test that request context is properly propagated through SSE transport.\"\"\"\n    # Test with custom headers\n    custom_headers = {\n        \"Authorization\": \"Bearer test-token\",\n        \"X-Custom-Header\": \"test-value\",\n        \"X-Trace-Id\": \"trace-123\",\n    }\n\n    async with sse_client(server_url + \"/sse\", headers=custom_headers) as (\n        read_stream,\n        write_stream,\n    ):\n        async with ClientSession(read_stream, write_stream) as session:\n            # Initialize the session\n            result = await session.initialize()\n            assert isinstance(result, InitializeResult)\n\n            # Call the tool that echoes headers back\n            tool_result = await session.call_tool(\"echo_headers\", {})\n\n            # Parse the JSON response\n\n            assert len(tool_result.content) == 1\n            headers_data = json.loads(tool_result.content[0].text if tool_result.content[0].type == \"text\" else \"{}\")\n\n            # Verify headers were propagated\n            assert headers_data.get(\"authorization\") == \"Bearer test-token\"\n            assert headers_data.get(\"x-custom-header\") == \"test-value\"\n            assert headers_data.get(\"x-trace-id\") == \"trace-123\"\n\n\n@pytest.mark.anyio\nasync def test_request_context_isolation(context_server: None, server_url: str) -> None:\n    \"\"\"Test that request contexts are isolated between different SSE clients.\"\"\"\n    contexts: list[dict[str, Any]] = []\n\n    # Create multiple clients with different headers\n    for i in range(3):\n        headers = {\"X-Request-Id\": f\"request-{i}\", \"X-Custom-Value\": f\"value-{i}\"}\n\n        async with sse_client(server_url + \"/sse\", headers=headers) as (\n            read_stream,\n            write_stream,\n        ):\n            async with ClientSession(read_stream, write_stream) as session:\n                await session.initialize()\n\n                # Call the tool that echoes context\n                tool_result = await session.call_tool(\"echo_context\", {\"request_id\": f\"request-{i}\"})\n\n                assert len(tool_result.content) == 1\n                context_data = json.loads(\n                    tool_result.content[0].text if tool_result.content[0].type == \"text\" else \"{}\"\n                )\n                contexts.append(context_data)\n\n    # Verify each request had its own context\n    assert len(contexts) == 3\n    for i, ctx in enumerate(contexts):\n        assert ctx[\"request_id\"] == f\"request-{i}\"\n        assert ctx[\"headers\"].get(\"x-request-id\") == f\"request-{i}\"\n        assert ctx[\"headers\"].get(\"x-custom-value\") == f\"value-{i}\"\n\n\ndef test_sse_message_id_coercion():\n    \"\"\"Previously, the `RequestId` would coerce a string that looked like an integer into an integer.\n\n    See <https://github.com/modelcontextprotocol/python-sdk/pull/851> for more details.\n\n    As per the JSON-RPC 2.0 specification, the id in the response object needs to be the same type as the id in the\n    request object. In other words, we can't perform the coercion.\n\n    See <https://www.jsonrpc.org/specification#response_object> for more details.\n    \"\"\"\n    json_message = '{\"jsonrpc\": \"2.0\", \"id\": \"123\", \"method\": \"ping\", \"params\": null}'\n    msg = types.JSONRPCRequest.model_validate_json(json_message)\n    assert msg == snapshot(types.JSONRPCRequest(method=\"ping\", jsonrpc=\"2.0\", id=\"123\"))\n\n    json_message = '{\"jsonrpc\": \"2.0\", \"id\": 123, \"method\": \"ping\", \"params\": null}'\n    msg = types.JSONRPCRequest.model_validate_json(json_message)\n    assert msg == snapshot(types.JSONRPCRequest(method=\"ping\", jsonrpc=\"2.0\", id=123))\n\n\n@pytest.mark.parametrize(\n    \"endpoint, expected_result\",\n    [\n        # Valid endpoints - should normalize and work\n        (\"/messages/\", \"/messages/\"),\n        (\"messages/\", \"/messages/\"),\n        (\"/\", \"/\"),\n        # Invalid endpoints - should raise ValueError\n        (\"http://example.com/messages/\", ValueError),\n        (\"//example.com/messages/\", ValueError),\n        (\"ftp://example.com/messages/\", ValueError),\n        (\"/messages/?param=value\", ValueError),\n        (\"/messages/#fragment\", ValueError),\n    ],\n)\ndef test_sse_server_transport_endpoint_validation(endpoint: str, expected_result: str | type[Exception]):\n    \"\"\"Test that SseServerTransport properly validates and normalizes endpoints.\"\"\"\n    if isinstance(expected_result, type):\n        # Test invalid endpoints that should raise an exception\n        with pytest.raises(expected_result, match=\"is not a relative path.*expecting a relative path\"):\n            SseServerTransport(endpoint)\n    else:\n        # Test valid endpoints that should normalize correctly\n        sse = SseServerTransport(endpoint)\n        assert sse._endpoint == expected_result\n        assert sse._endpoint.startswith(\"/\")\n\n\n@pytest.mark.anyio\nasync def test_sse_client_handles_empty_keepalive_pings() -> None:\n    \"\"\"Test that SSE client properly handles empty data lines (keep-alive pings).\n\n    Per the MCP spec (Streamable HTTP transport): \"The server SHOULD immediately\n    send an SSE event consisting of an event ID and an empty data field in order\n    to prime the client to reconnect.\"\n\n    This test mocks the SSE event stream to include empty \"message\" events and\n    verifies the client skips them without crashing.\n    \"\"\"\n    # Build a proper JSON-RPC response using types (not hardcoded strings)\n    init_result = InitializeResult(\n        protocol_version=\"2024-11-05\",\n        capabilities=ServerCapabilities(),\n        server_info=Implementation(name=\"test\", version=\"1.0\"),\n    )\n    response = JSONRPCResponse(\n        jsonrpc=\"2.0\",\n        id=1,\n        result=init_result.model_dump(by_alias=True, exclude_none=True),\n    )\n    response_json = response.model_dump_json(by_alias=True, exclude_none=True)\n\n    # Create mock SSE events using httpx_sse's ServerSentEvent\n    async def mock_aiter_sse() -> AsyncGenerator[ServerSentEvent, None]:\n        # First: endpoint event\n        yield ServerSentEvent(event=\"endpoint\", data=\"/messages/?session_id=abc123\")\n        # Empty data keep-alive ping - this is what we're testing\n        yield ServerSentEvent(event=\"message\", data=\"\")\n        # Real JSON-RPC response\n        yield ServerSentEvent(event=\"message\", data=response_json)\n\n    mock_event_source = MagicMock()\n    mock_event_source.aiter_sse.return_value = mock_aiter_sse()\n    mock_event_source.response = MagicMock()\n    mock_event_source.response.raise_for_status = MagicMock()\n\n    mock_aconnect_sse = MagicMock()\n    mock_aconnect_sse.__aenter__ = AsyncMock(return_value=mock_event_source)\n    mock_aconnect_sse.__aexit__ = AsyncMock(return_value=None)\n\n    mock_client = MagicMock()\n    mock_client.__aenter__ = AsyncMock(return_value=mock_client)\n    mock_client.__aexit__ = AsyncMock(return_value=None)\n    mock_client.post = AsyncMock(return_value=MagicMock(status_code=200, raise_for_status=MagicMock()))\n\n    with (\n        patch(\"mcp.client.sse.create_mcp_http_client\", return_value=mock_client),\n        patch(\"mcp.client.sse.aconnect_sse\", return_value=mock_aconnect_sse),\n    ):\n        async with sse_client(\"http://test/sse\") as (read_stream, _):\n            # Read the message - should skip the empty one and get the real response\n            msg = await read_stream.receive()\n            # If we get here without error, the empty message was skipped successfully\n            assert not isinstance(msg, Exception)\n            assert isinstance(msg.message, types.JSONRPCResponse)\n            assert msg.message.id == 1\n\n\n@pytest.mark.anyio\nasync def test_sse_session_cleanup_on_disconnect(server: None, server_url: str) -> None:\n    \"\"\"Regression test for https://github.com/modelcontextprotocol/python-sdk/issues/1227\n\n    When a client disconnects, the server should remove the session from\n    _read_stream_writers. Without this cleanup, stale sessions accumulate and\n    POST requests to disconnected sessions return 202 Accepted followed by a\n    ClosedResourceError when the server tries to write to the dead stream.\n    \"\"\"\n    captured: list[str] = []\n\n    # Connect a client session, then disconnect\n    async with sse_client(server_url + \"/sse\", on_session_created=captured.append) as streams:\n        async with ClientSession(*streams) as session:\n            await session.initialize()\n\n    # After disconnect, POST to the stale session should return 404\n    # (not 202 as it did before the fix)\n    async with httpx.AsyncClient() as client:\n        response = await client.post(\n            f\"{server_url}/messages/?session_id={captured[0]}\",\n            json={\"jsonrpc\": \"2.0\", \"method\": \"ping\", \"id\": 99},\n            headers={\"Content-Type\": \"application/json\"},\n        )\n        assert response.status_code == 404\n"
  },
  {
    "path": "tests/shared/test_streamable_http.py",
    "content": "\"\"\"Tests for the StreamableHTTP server and client transport.\n\nContains tests for both server and client sides of the StreamableHTTP transport.\n\"\"\"\n\nfrom __future__ import annotations as _annotations\n\nimport json\nimport multiprocessing\nimport socket\nimport time\nimport traceback\nfrom collections.abc import AsyncIterator, Generator\nfrom contextlib import asynccontextmanager\nfrom dataclasses import dataclass, field\nfrom typing import Any\nfrom unittest.mock import MagicMock\nfrom urllib.parse import urlparse\n\nimport anyio\nimport httpx\nimport pytest\nimport requests\nimport uvicorn\nfrom httpx_sse import ServerSentEvent\nfrom starlette.applications import Starlette\nfrom starlette.requests import Request\nfrom starlette.routing import Mount\n\nfrom mcp import MCPError, types\nfrom mcp.client.session import ClientSession\nfrom mcp.client.streamable_http import StreamableHTTPTransport, streamable_http_client\nfrom mcp.server import Server, ServerRequestContext\nfrom mcp.server.streamable_http import (\n    MCP_PROTOCOL_VERSION_HEADER,\n    MCP_SESSION_ID_HEADER,\n    SESSION_ID_PATTERN,\n    EventCallback,\n    EventId,\n    EventMessage,\n    EventStore,\n    StreamableHTTPServerTransport,\n    StreamId,\n)\nfrom mcp.server.streamable_http_manager import StreamableHTTPSessionManager\nfrom mcp.server.transport_security import TransportSecuritySettings\nfrom mcp.shared._context import RequestContext\nfrom mcp.shared._httpx_utils import (\n    MCP_DEFAULT_SSE_READ_TIMEOUT,\n    MCP_DEFAULT_TIMEOUT,\n    create_mcp_http_client,\n)\nfrom mcp.shared.message import ClientMessageMetadata, ServerMessageMetadata, SessionMessage\nfrom mcp.shared.session import RequestResponder\nfrom mcp.types import (\n    CallToolRequestParams,\n    CallToolResult,\n    InitializeResult,\n    JSONRPCRequest,\n    ListToolsResult,\n    PaginatedRequestParams,\n    ReadResourceRequestParams,\n    ReadResourceResult,\n    TextContent,\n    TextResourceContents,\n    Tool,\n)\nfrom tests.test_helpers import wait_for_server\n\n# Test constants\nSERVER_NAME = \"test_streamable_http_server\"\nTEST_SESSION_ID = \"test-session-id-12345\"\nINIT_REQUEST = {\n    \"jsonrpc\": \"2.0\",\n    \"method\": \"initialize\",\n    \"params\": {\n        \"clientInfo\": {\"name\": \"test-client\", \"version\": \"1.0\"},\n        \"protocolVersion\": \"2025-03-26\",\n        \"capabilities\": {},\n    },\n    \"id\": \"init-1\",\n}\n\n\n# Helper functions\ndef extract_protocol_version_from_sse(response: requests.Response) -> str:\n    \"\"\"Extract the negotiated protocol version from an SSE initialization response.\"\"\"\n    assert response.headers.get(\"Content-Type\") == \"text/event-stream\"\n    for line in response.text.splitlines():\n        if line.startswith(\"data: \"):\n            init_data = json.loads(line[6:])\n            return init_data[\"result\"][\"protocolVersion\"]\n    raise ValueError(\"Could not extract protocol version from SSE response\")  # pragma: no cover\n\n\n# Simple in-memory event store for testing\nclass SimpleEventStore(EventStore):\n    \"\"\"Simple in-memory event store for testing.\"\"\"\n\n    def __init__(self):\n        self._events: list[tuple[StreamId, EventId, types.JSONRPCMessage | None]] = []\n        self._event_id_counter = 0\n\n    async def store_event(self, stream_id: StreamId, message: types.JSONRPCMessage | None) -> EventId:\n        \"\"\"Store an event and return its ID.\"\"\"\n        self._event_id_counter += 1\n        event_id = str(self._event_id_counter)\n        self._events.append((stream_id, event_id, message))\n        return event_id\n\n    async def replay_events_after(  # pragma: no cover\n        self,\n        last_event_id: EventId,\n        send_callback: EventCallback,\n    ) -> StreamId | None:\n        \"\"\"Replay events after the specified ID.\"\"\"\n        # Find the stream ID of the last event\n        target_stream_id = None\n        for stream_id, event_id, _ in self._events:\n            if event_id == last_event_id:\n                target_stream_id = stream_id\n                break\n\n        if target_stream_id is None:\n            # If event ID not found, return None\n            return None\n\n        # Convert last_event_id to int for comparison\n        last_event_id_int = int(last_event_id)\n\n        # Replay only events from the same stream with ID > last_event_id\n        for stream_id, event_id, message in self._events:\n            if stream_id == target_stream_id and int(event_id) > last_event_id_int:\n                # Skip priming events (None message)\n                if message is not None:\n                    await send_callback(EventMessage(message, event_id))\n\n        return target_stream_id\n\n\n@dataclass\nclass ServerState:\n    lock: anyio.Event = field(default_factory=anyio.Event)\n\n\n@asynccontextmanager\nasync def _server_lifespan(_server: Server[ServerState]) -> AsyncIterator[ServerState]:  # pragma: no cover\n    yield ServerState()\n\n\nasync def _handle_read_resource(  # pragma: no cover\n    ctx: ServerRequestContext[ServerState], params: ReadResourceRequestParams\n) -> ReadResourceResult:\n    uri = str(params.uri)\n    parsed = urlparse(uri)\n    if parsed.scheme == \"foobar\":\n        text = f\"Read {parsed.netloc}\"\n    elif parsed.scheme == \"slow\":\n        await anyio.sleep(2.0)\n        text = f\"Slow response from {parsed.netloc}\"\n    else:\n        raise ValueError(f\"Unknown resource: {uri}\")\n    return ReadResourceResult(contents=[TextResourceContents(uri=uri, text=text, mime_type=\"text/plain\")])\n\n\nasync def _handle_list_tools(  # pragma: no cover\n    ctx: ServerRequestContext[ServerState], params: PaginatedRequestParams | None\n) -> ListToolsResult:\n    return ListToolsResult(\n        tools=[\n            Tool(\n                name=\"test_tool\",\n                description=\"A test tool\",\n                input_schema={\"type\": \"object\", \"properties\": {}},\n            ),\n            Tool(\n                name=\"test_tool_with_standalone_notification\",\n                description=\"A test tool that sends a notification\",\n                input_schema={\"type\": \"object\", \"properties\": {}},\n            ),\n            Tool(\n                name=\"long_running_with_checkpoints\",\n                description=\"A long-running tool that sends periodic notifications\",\n                input_schema={\"type\": \"object\", \"properties\": {}},\n            ),\n            Tool(\n                name=\"test_sampling_tool\",\n                description=\"A tool that triggers server-side sampling\",\n                input_schema={\"type\": \"object\", \"properties\": {}},\n            ),\n            Tool(\n                name=\"wait_for_lock_with_notification\",\n                description=\"A tool that sends a notification and waits for lock\",\n                input_schema={\"type\": \"object\", \"properties\": {}},\n            ),\n            Tool(\n                name=\"release_lock\",\n                description=\"A tool that releases the lock\",\n                input_schema={\"type\": \"object\", \"properties\": {}},\n            ),\n            Tool(\n                name=\"tool_with_stream_close\",\n                description=\"A tool that closes SSE stream mid-operation\",\n                input_schema={\"type\": \"object\", \"properties\": {}},\n            ),\n            Tool(\n                name=\"tool_with_multiple_notifications_and_close\",\n                description=\"Tool that sends notification1, closes stream, sends notification2, notification3\",\n                input_schema={\"type\": \"object\", \"properties\": {}},\n            ),\n            Tool(\n                name=\"tool_with_multiple_stream_closes\",\n                description=\"Tool that closes SSE stream multiple times during execution\",\n                input_schema={\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"checkpoints\": {\"type\": \"integer\", \"default\": 3},\n                        \"sleep_time\": {\"type\": \"number\", \"default\": 0.2},\n                    },\n                },\n            ),\n            Tool(\n                name=\"tool_with_standalone_stream_close\",\n                description=\"Tool that closes standalone GET stream mid-operation\",\n                input_schema={\"type\": \"object\", \"properties\": {}},\n            ),\n        ]\n    )\n\n\nasync def _handle_call_tool(  # pragma: no cover\n    ctx: ServerRequestContext[ServerState], params: CallToolRequestParams\n) -> CallToolResult:\n    name = params.name\n    args = params.arguments or {}\n\n    # When the tool is called, send a notification to test GET stream\n    if name == \"test_tool_with_standalone_notification\":\n        await ctx.session.send_resource_updated(uri=\"http://test_resource\")\n        return CallToolResult(content=[TextContent(type=\"text\", text=f\"Called {name}\")])\n\n    elif name == \"long_running_with_checkpoints\":\n        await ctx.session.send_log_message(\n            level=\"info\",\n            data=\"Tool started\",\n            logger=\"tool\",\n            related_request_id=ctx.request_id,\n        )\n\n        await anyio.sleep(0.1)\n\n        await ctx.session.send_log_message(\n            level=\"info\",\n            data=\"Tool is almost done\",\n            logger=\"tool\",\n            related_request_id=ctx.request_id,\n        )\n\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"Completed!\")])\n\n    elif name == \"test_sampling_tool\":\n        sampling_result = await ctx.session.create_message(\n            messages=[\n                types.SamplingMessage(\n                    role=\"user\",\n                    content=types.TextContent(type=\"text\", text=\"Server needs client sampling\"),\n                )\n            ],\n            max_tokens=100,\n            related_request_id=ctx.request_id,\n        )\n\n        if sampling_result.content.type == \"text\":\n            response = sampling_result.content.text\n        else:\n            response = str(sampling_result.content)\n        return CallToolResult(\n            content=[\n                TextContent(\n                    type=\"text\",\n                    text=f\"Response from sampling: {response}\",\n                )\n            ]\n        )\n\n    elif name == \"wait_for_lock_with_notification\":\n        await ctx.session.send_log_message(\n            level=\"info\",\n            data=\"First notification before lock\",\n            logger=\"lock_tool\",\n            related_request_id=ctx.request_id,\n        )\n\n        await ctx.lifespan_context.lock.wait()\n\n        await ctx.session.send_log_message(\n            level=\"info\",\n            data=\"Second notification after lock\",\n            logger=\"lock_tool\",\n            related_request_id=ctx.request_id,\n        )\n\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"Completed\")])\n\n    elif name == \"release_lock\":\n        ctx.lifespan_context.lock.set()\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"Lock released\")])\n\n    elif name == \"tool_with_stream_close\":\n        await ctx.session.send_log_message(\n            level=\"info\",\n            data=\"Before close\",\n            logger=\"stream_close_tool\",\n            related_request_id=ctx.request_id,\n        )\n        assert ctx.close_sse_stream is not None\n        await ctx.close_sse_stream()\n        await anyio.sleep(0.1)\n        await ctx.session.send_log_message(\n            level=\"info\",\n            data=\"After close\",\n            logger=\"stream_close_tool\",\n            related_request_id=ctx.request_id,\n        )\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"Done\")])\n\n    elif name == \"tool_with_multiple_notifications_and_close\":\n        await ctx.session.send_log_message(\n            level=\"info\",\n            data=\"notification1\",\n            logger=\"multi_notif_tool\",\n            related_request_id=ctx.request_id,\n        )\n        assert ctx.close_sse_stream is not None\n        await ctx.close_sse_stream()\n        await anyio.sleep(0.1)\n        await ctx.session.send_log_message(\n            level=\"info\",\n            data=\"notification2\",\n            logger=\"multi_notif_tool\",\n            related_request_id=ctx.request_id,\n        )\n        await ctx.session.send_log_message(\n            level=\"info\",\n            data=\"notification3\",\n            logger=\"multi_notif_tool\",\n            related_request_id=ctx.request_id,\n        )\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"All notifications sent\")])\n\n    elif name == \"tool_with_multiple_stream_closes\":\n        num_checkpoints = args.get(\"checkpoints\", 3)\n        sleep_time = args.get(\"sleep_time\", 0.2)\n\n        for i in range(num_checkpoints):\n            await ctx.session.send_log_message(\n                level=\"info\",\n                data=f\"checkpoint_{i}\",\n                logger=\"multi_close_tool\",\n                related_request_id=ctx.request_id,\n            )\n\n            if ctx.close_sse_stream:\n                await ctx.close_sse_stream()\n\n            await anyio.sleep(sleep_time)\n\n        return CallToolResult(content=[TextContent(type=\"text\", text=f\"Completed {num_checkpoints} checkpoints\")])\n\n    elif name == \"tool_with_standalone_stream_close\":\n        await ctx.session.send_resource_updated(uri=\"http://notification_1\")\n        await anyio.sleep(0.1)\n\n        if ctx.close_standalone_sse_stream:\n            await ctx.close_standalone_sse_stream()\n\n        await anyio.sleep(1.5)\n        await ctx.session.send_resource_updated(uri=\"http://notification_2\")\n\n        return CallToolResult(content=[TextContent(type=\"text\", text=\"Standalone stream close test done\")])\n\n    return CallToolResult(content=[TextContent(type=\"text\", text=f\"Called {name}\")])\n\n\ndef _create_server() -> Server[ServerState]:  # pragma: no cover\n    return Server(\n        SERVER_NAME,\n        lifespan=_server_lifespan,\n        on_read_resource=_handle_read_resource,\n        on_list_tools=_handle_list_tools,\n        on_call_tool=_handle_call_tool,\n    )\n\n\ndef create_app(\n    is_json_response_enabled: bool = False,\n    event_store: EventStore | None = None,\n    retry_interval: int | None = None,\n) -> Starlette:  # pragma: no cover\n    \"\"\"Create a Starlette application for testing using the session manager.\n\n    Args:\n        is_json_response_enabled: If True, use JSON responses instead of SSE streams.\n        event_store: Optional event store for testing resumability.\n        retry_interval: Retry interval in milliseconds for SSE polling.\n    \"\"\"\n    # Create server instance\n    server = _create_server()\n\n    # Create the session manager\n    security_settings = TransportSecuritySettings(\n        allowed_hosts=[\"127.0.0.1:*\", \"localhost:*\"], allowed_origins=[\"http://127.0.0.1:*\", \"http://localhost:*\"]\n    )\n    session_manager = StreamableHTTPSessionManager(\n        app=server,\n        event_store=event_store,\n        json_response=is_json_response_enabled,\n        security_settings=security_settings,\n        retry_interval=retry_interval,\n    )\n\n    # Create an ASGI application that uses the session manager\n    app = Starlette(\n        debug=True,\n        routes=[\n            Mount(\"/mcp\", app=session_manager.handle_request),\n        ],\n        lifespan=lambda app: session_manager.run(),\n    )\n\n    return app\n\n\ndef run_server(\n    port: int,\n    is_json_response_enabled: bool = False,\n    event_store: EventStore | None = None,\n    retry_interval: int | None = None,\n) -> None:  # pragma: no cover\n    \"\"\"Run the test server.\n\n    Args:\n        port: Port to listen on.\n        is_json_response_enabled: If True, use JSON responses instead of SSE streams.\n        event_store: Optional event store for testing resumability.\n        retry_interval: Retry interval in milliseconds for SSE polling.\n    \"\"\"\n\n    app = create_app(is_json_response_enabled, event_store, retry_interval)\n    # Configure server\n    config = uvicorn.Config(\n        app=app,\n        host=\"127.0.0.1\",\n        port=port,\n        log_level=\"info\",\n        limit_concurrency=10,\n        timeout_keep_alive=5,\n        access_log=False,\n    )\n\n    # Start the server\n    server = uvicorn.Server(config=config)\n\n    # This is important to catch exceptions and prevent test hangs\n    try:\n        server.run()\n    except Exception:\n        traceback.print_exc()\n\n\n# Test fixtures - using same approach as SSE tests\n@pytest.fixture\ndef basic_server_port() -> int:\n    \"\"\"Find an available port for the basic server.\"\"\"\n    with socket.socket() as s:\n        s.bind((\"127.0.0.1\", 0))\n        return s.getsockname()[1]\n\n\n@pytest.fixture\ndef json_server_port() -> int:\n    \"\"\"Find an available port for the JSON response server.\"\"\"\n    with socket.socket() as s:\n        s.bind((\"127.0.0.1\", 0))\n        return s.getsockname()[1]\n\n\n@pytest.fixture\ndef basic_server(basic_server_port: int) -> Generator[None, None, None]:\n    \"\"\"Start a basic server.\"\"\"\n    proc = multiprocessing.Process(target=run_server, kwargs={\"port\": basic_server_port}, daemon=True)\n    proc.start()\n\n    # Wait for server to be running\n    wait_for_server(basic_server_port)\n\n    yield\n\n    # Clean up\n    proc.kill()\n    proc.join(timeout=2)\n\n\n@pytest.fixture\ndef event_store() -> SimpleEventStore:\n    \"\"\"Create a test event store.\"\"\"\n    return SimpleEventStore()\n\n\n@pytest.fixture\ndef event_server_port() -> int:\n    \"\"\"Find an available port for the event store server.\"\"\"\n    with socket.socket() as s:\n        s.bind((\"127.0.0.1\", 0))\n        return s.getsockname()[1]\n\n\n@pytest.fixture\ndef event_server(\n    event_server_port: int, event_store: SimpleEventStore\n) -> Generator[tuple[SimpleEventStore, str], None, None]:\n    \"\"\"Start a server with event store and retry_interval enabled.\"\"\"\n    proc = multiprocessing.Process(\n        target=run_server,\n        kwargs={\"port\": event_server_port, \"event_store\": event_store, \"retry_interval\": 500},\n        daemon=True,\n    )\n    proc.start()\n\n    # Wait for server to be running\n    wait_for_server(event_server_port)\n\n    yield event_store, f\"http://127.0.0.1:{event_server_port}\"\n\n    # Clean up\n    proc.kill()\n    proc.join(timeout=2)\n\n\n@pytest.fixture\ndef json_response_server(json_server_port: int) -> Generator[None, None, None]:\n    \"\"\"Start a server with JSON response enabled.\"\"\"\n    proc = multiprocessing.Process(\n        target=run_server,\n        kwargs={\"port\": json_server_port, \"is_json_response_enabled\": True},\n        daemon=True,\n    )\n    proc.start()\n\n    # Wait for server to be running\n    wait_for_server(json_server_port)\n\n    yield\n\n    # Clean up\n    proc.kill()\n    proc.join(timeout=2)\n\n\n@pytest.fixture\ndef basic_server_url(basic_server_port: int) -> str:\n    \"\"\"Get the URL for the basic test server.\"\"\"\n    return f\"http://127.0.0.1:{basic_server_port}\"\n\n\n@pytest.fixture\ndef json_server_url(json_server_port: int) -> str:\n    \"\"\"Get the URL for the JSON response test server.\"\"\"\n    return f\"http://127.0.0.1:{json_server_port}\"\n\n\n# Basic request validation tests\ndef test_accept_header_validation(basic_server: None, basic_server_url: str):\n    \"\"\"Test that Accept header is properly validated.\"\"\"\n    # Test without Accept header (suppress requests library default Accept: */*)\n    session = requests.Session()\n    session.headers.pop(\"Accept\")\n    response = session.post(\n        f\"{basic_server_url}/mcp\",\n        headers={\"Content-Type\": \"application/json\"},\n        json={\"jsonrpc\": \"2.0\", \"method\": \"initialize\", \"id\": 1},\n    )\n    assert response.status_code == 406\n    assert \"Not Acceptable\" in response.text\n\n\n@pytest.mark.parametrize(\n    \"accept_header\",\n    [\n        \"*/*\",\n        \"application/*, text/*\",\n        \"text/*, application/json\",\n        \"application/json, text/*\",\n        \"*/*;q=0.8\",\n        \"application/*;q=0.9, text/*;q=0.8\",\n    ],\n)\ndef test_accept_header_wildcard(basic_server: None, basic_server_url: str, accept_header: str):\n    \"\"\"Test that wildcard Accept headers are accepted per RFC 7231.\"\"\"\n    response = requests.post(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            \"Accept\": accept_header,\n            \"Content-Type\": \"application/json\",\n        },\n        json=INIT_REQUEST,\n    )\n    assert response.status_code == 200\n\n\n@pytest.mark.parametrize(\n    \"accept_header\",\n    [\n        \"text/html\",\n        \"application/*\",\n        \"text/*\",\n    ],\n)\ndef test_accept_header_incompatible(basic_server: None, basic_server_url: str, accept_header: str):\n    \"\"\"Test that incompatible Accept headers are rejected for SSE mode.\"\"\"\n    response = requests.post(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            \"Accept\": accept_header,\n            \"Content-Type\": \"application/json\",\n        },\n        json=INIT_REQUEST,\n    )\n    assert response.status_code == 406\n    assert \"Not Acceptable\" in response.text\n\n\ndef test_content_type_validation(basic_server: None, basic_server_url: str):\n    \"\"\"Test that Content-Type header is properly validated.\"\"\"\n    # Test with incorrect Content-Type\n    response = requests.post(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"text/plain\",\n        },\n        data=\"This is not JSON\",\n    )\n\n    assert response.status_code == 400\n    assert \"Invalid Content-Type\" in response.text\n\n\ndef test_json_validation(basic_server: None, basic_server_url: str):\n    \"\"\"Test that JSON content is properly validated.\"\"\"\n    # Test with invalid JSON\n    response = requests.post(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        },\n        data=\"this is not valid json\",\n    )\n    assert response.status_code == 400\n    assert \"Parse error\" in response.text\n\n\ndef test_json_parsing(basic_server: None, basic_server_url: str):\n    \"\"\"Test that JSON content is properly parse.\"\"\"\n    # Test with valid JSON but invalid JSON-RPC\n    response = requests.post(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        },\n        json={\"foo\": \"bar\"},\n    )\n    assert response.status_code == 400\n    assert \"Validation error\" in response.text\n\n\ndef test_method_not_allowed(basic_server: None, basic_server_url: str):\n    \"\"\"Test that unsupported HTTP methods are rejected.\"\"\"\n    # Test with unsupported method (PUT)\n    response = requests.put(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        },\n        json={\"jsonrpc\": \"2.0\", \"method\": \"initialize\", \"id\": 1},\n    )\n    assert response.status_code == 405\n    assert \"Method Not Allowed\" in response.text\n\n\ndef test_session_validation(basic_server: None, basic_server_url: str):\n    \"\"\"Test session ID validation.\"\"\"\n    # session_id not used directly in this test\n\n    # Test without session ID\n    response = requests.post(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        },\n        json={\"jsonrpc\": \"2.0\", \"method\": \"list_tools\", \"id\": 1},\n    )\n    assert response.status_code == 400\n    assert \"Missing session ID\" in response.text\n\n\ndef test_session_id_pattern():\n    \"\"\"Test that SESSION_ID_PATTERN correctly validates session IDs.\"\"\"\n    # Valid session IDs (visible ASCII characters from 0x21 to 0x7E)\n    valid_session_ids = [\n        \"test-session-id\",\n        \"1234567890\",\n        \"session!@#$%^&*()_+-=[]{}|;:,.<>?/\",\n        \"~`\",\n    ]\n\n    for session_id in valid_session_ids:\n        assert SESSION_ID_PATTERN.match(session_id) is not None\n        # Ensure fullmatch matches too (whole string)\n        assert SESSION_ID_PATTERN.fullmatch(session_id) is not None\n\n    # Invalid session IDs\n    invalid_session_ids = [\n        \"\",  # Empty string\n        \" test\",  # Space (0x20)\n        \"test\\t\",  # Tab\n        \"test\\n\",  # Newline\n        \"test\\r\",  # Carriage return\n        \"test\" + chr(0x7F),  # DEL character\n        \"test\" + chr(0x80),  # Extended ASCII\n        \"test\" + chr(0x00),  # Null character\n        \"test\" + chr(0x20),  # Space (0x20)\n    ]\n\n    for session_id in invalid_session_ids:\n        # For invalid IDs, either match will fail or fullmatch will fail\n        if SESSION_ID_PATTERN.match(session_id) is not None:\n            # If match succeeds, fullmatch should fail (partial match case)\n            assert SESSION_ID_PATTERN.fullmatch(session_id) is None\n\n\ndef test_streamable_http_transport_init_validation():\n    \"\"\"Test that StreamableHTTPServerTransport validates session ID on init.\"\"\"\n    # Valid session ID should initialize without errors\n    valid_transport = StreamableHTTPServerTransport(mcp_session_id=\"valid-id\")\n    assert valid_transport.mcp_session_id == \"valid-id\"\n\n    # None should be accepted\n    none_transport = StreamableHTTPServerTransport(mcp_session_id=None)\n    assert none_transport.mcp_session_id is None\n\n    # Invalid session ID should raise ValueError\n    with pytest.raises(ValueError) as excinfo:\n        StreamableHTTPServerTransport(mcp_session_id=\"invalid id with space\")\n    assert \"Session ID must only contain visible ASCII characters\" in str(excinfo.value)\n\n    # Test with control characters\n    with pytest.raises(ValueError):\n        StreamableHTTPServerTransport(mcp_session_id=\"test\\nid\")\n\n    with pytest.raises(ValueError):\n        StreamableHTTPServerTransport(mcp_session_id=\"test\\n\")\n\n\ndef test_session_termination(basic_server: None, basic_server_url: str):\n    \"\"\"Test session termination via DELETE and subsequent request handling.\"\"\"\n    response = requests.post(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        },\n        json=INIT_REQUEST,\n    )\n    assert response.status_code == 200\n\n    # Extract negotiated protocol version from SSE response\n    negotiated_version = extract_protocol_version_from_sse(response)\n\n    # Now terminate the session\n    session_id = response.headers.get(MCP_SESSION_ID_HEADER)\n    response = requests.delete(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            MCP_SESSION_ID_HEADER: session_id,\n            MCP_PROTOCOL_VERSION_HEADER: negotiated_version,\n        },\n    )\n    assert response.status_code == 200\n\n    # Try to use the terminated session\n    response = requests.post(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n            MCP_SESSION_ID_HEADER: session_id,\n        },\n        json={\"jsonrpc\": \"2.0\", \"method\": \"ping\", \"id\": 2},\n    )\n    assert response.status_code == 404\n    assert \"Session has been terminated\" in response.text\n\n\ndef test_response(basic_server: None, basic_server_url: str):\n    \"\"\"Test response handling for a valid request.\"\"\"\n    mcp_url = f\"{basic_server_url}/mcp\"\n    response = requests.post(\n        mcp_url,\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        },\n        json=INIT_REQUEST,\n    )\n    assert response.status_code == 200\n\n    # Extract negotiated protocol version from SSE response\n    negotiated_version = extract_protocol_version_from_sse(response)\n\n    # Now get the session ID\n    session_id = response.headers.get(MCP_SESSION_ID_HEADER)\n\n    # Try to use the session with proper headers\n    tools_response = requests.post(\n        mcp_url,\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n            MCP_SESSION_ID_HEADER: session_id,  # Use the session ID we got earlier\n            MCP_PROTOCOL_VERSION_HEADER: negotiated_version,\n        },\n        json={\"jsonrpc\": \"2.0\", \"method\": \"tools/list\", \"id\": \"tools-1\"},\n        stream=True,\n    )\n    assert tools_response.status_code == 200\n    assert tools_response.headers.get(\"Content-Type\") == \"text/event-stream\"\n\n\ndef test_json_response(json_response_server: None, json_server_url: str):\n    \"\"\"Test response handling when is_json_response_enabled is True.\"\"\"\n    mcp_url = f\"{json_server_url}/mcp\"\n    response = requests.post(\n        mcp_url,\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        },\n        json=INIT_REQUEST,\n    )\n    assert response.status_code == 200\n    assert response.headers.get(\"Content-Type\") == \"application/json\"\n\n\ndef test_json_response_accept_json_only(json_response_server: None, json_server_url: str):\n    \"\"\"Test that json_response servers only require application/json in Accept header.\"\"\"\n    mcp_url = f\"{json_server_url}/mcp\"\n    response = requests.post(\n        mcp_url,\n        headers={\n            \"Accept\": \"application/json\",\n            \"Content-Type\": \"application/json\",\n        },\n        json=INIT_REQUEST,\n    )\n    assert response.status_code == 200\n    assert response.headers.get(\"Content-Type\") == \"application/json\"\n\n\ndef test_json_response_missing_accept_header(json_response_server: None, json_server_url: str):\n    \"\"\"Test that json_response servers reject requests without Accept header.\"\"\"\n    mcp_url = f\"{json_server_url}/mcp\"\n    # Suppress requests library default Accept: */* header\n    session = requests.Session()\n    session.headers.pop(\"Accept\")\n    response = session.post(\n        mcp_url,\n        headers={\n            \"Content-Type\": \"application/json\",\n        },\n        json=INIT_REQUEST,\n    )\n    assert response.status_code == 406\n    assert \"Not Acceptable\" in response.text\n\n\ndef test_json_response_incorrect_accept_header(json_response_server: None, json_server_url: str):\n    \"\"\"Test that json_response servers reject requests with incorrect Accept header.\"\"\"\n    mcp_url = f\"{json_server_url}/mcp\"\n    # Test with only text/event-stream (wrong for JSON server)\n    response = requests.post(\n        mcp_url,\n        headers={\n            \"Accept\": \"text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        },\n        json=INIT_REQUEST,\n    )\n    assert response.status_code == 406\n    assert \"Not Acceptable\" in response.text\n\n\n@pytest.mark.parametrize(\n    \"accept_header\",\n    [\n        \"*/*\",\n        \"application/*\",\n        \"application/*;q=0.9\",\n    ],\n)\ndef test_json_response_wildcard_accept_header(json_response_server: None, json_server_url: str, accept_header: str):\n    \"\"\"Test that json_response servers accept wildcard Accept headers per RFC 7231.\"\"\"\n    mcp_url = f\"{json_server_url}/mcp\"\n    response = requests.post(\n        mcp_url,\n        headers={\n            \"Accept\": accept_header,\n            \"Content-Type\": \"application/json\",\n        },\n        json=INIT_REQUEST,\n    )\n    assert response.status_code == 200\n    assert response.headers.get(\"Content-Type\") == \"application/json\"\n\n\ndef test_get_sse_stream(basic_server: None, basic_server_url: str):\n    \"\"\"Test establishing an SSE stream via GET request.\"\"\"\n    # First, we need to initialize a session\n    mcp_url = f\"{basic_server_url}/mcp\"\n    init_response = requests.post(\n        mcp_url,\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        },\n        json=INIT_REQUEST,\n    )\n    assert init_response.status_code == 200\n\n    # Get the session ID\n    session_id = init_response.headers.get(MCP_SESSION_ID_HEADER)\n    assert session_id is not None\n\n    # Extract negotiated protocol version from SSE response\n    init_data = None\n    assert init_response.headers.get(\"Content-Type\") == \"text/event-stream\"\n    for line in init_response.text.splitlines():  # pragma: no branch\n        if line.startswith(\"data: \"):\n            init_data = json.loads(line[6:])\n            break\n    assert init_data is not None\n    negotiated_version = init_data[\"result\"][\"protocolVersion\"]\n\n    # Now attempt to establish an SSE stream via GET\n    get_response = requests.get(\n        mcp_url,\n        headers={\n            \"Accept\": \"text/event-stream\",\n            MCP_SESSION_ID_HEADER: session_id,\n            MCP_PROTOCOL_VERSION_HEADER: negotiated_version,\n        },\n        stream=True,\n    )\n\n    # Verify we got a successful response with the right content type\n    assert get_response.status_code == 200\n    assert get_response.headers.get(\"Content-Type\") == \"text/event-stream\"\n\n    # Test that a second GET request gets rejected (only one stream allowed)\n    second_get = requests.get(\n        mcp_url,\n        headers={\n            \"Accept\": \"text/event-stream\",\n            MCP_SESSION_ID_HEADER: session_id,\n            MCP_PROTOCOL_VERSION_HEADER: negotiated_version,\n        },\n        stream=True,\n    )\n\n    # Should get CONFLICT (409) since there's already a stream\n    # Note: This might fail if the first stream fully closed before this runs,\n    # but generally it should work in the test environment where it runs quickly\n    assert second_get.status_code == 409\n\n\ndef test_get_validation(basic_server: None, basic_server_url: str):\n    \"\"\"Test validation for GET requests.\"\"\"\n    # First, we need to initialize a session\n    mcp_url = f\"{basic_server_url}/mcp\"\n    init_response = requests.post(\n        mcp_url,\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        },\n        json=INIT_REQUEST,\n    )\n    assert init_response.status_code == 200\n\n    # Get the session ID\n    session_id = init_response.headers.get(MCP_SESSION_ID_HEADER)\n    assert session_id is not None\n\n    # Extract negotiated protocol version from SSE response\n    init_data = None\n    assert init_response.headers.get(\"Content-Type\") == \"text/event-stream\"\n    for line in init_response.text.splitlines():  # pragma: no branch\n        if line.startswith(\"data: \"):\n            init_data = json.loads(line[6:])\n            break\n    assert init_data is not None\n    negotiated_version = init_data[\"result\"][\"protocolVersion\"]\n\n    # Test without Accept header (suppress requests library default Accept: */*)\n    session = requests.Session()\n    session.headers.pop(\"Accept\")\n    response = session.get(\n        mcp_url,\n        headers={\n            MCP_SESSION_ID_HEADER: session_id,\n            MCP_PROTOCOL_VERSION_HEADER: negotiated_version,\n        },\n        stream=True,\n    )\n    assert response.status_code == 406\n    assert \"Not Acceptable\" in response.text\n\n    # Test with wrong Accept header\n    response = requests.get(\n        mcp_url,\n        headers={\n            \"Accept\": \"application/json\",\n            MCP_SESSION_ID_HEADER: session_id,\n            MCP_PROTOCOL_VERSION_HEADER: negotiated_version,\n        },\n    )\n    assert response.status_code == 406\n    assert \"Not Acceptable\" in response.text\n\n\n# Client-specific fixtures\n@pytest.fixture\nasync def http_client(basic_server: None, basic_server_url: str):  # pragma: no cover\n    \"\"\"Create test client matching the SSE test pattern.\"\"\"\n    async with httpx.AsyncClient(base_url=basic_server_url) as client:\n        yield client\n\n\n@pytest.fixture\nasync def initialized_client_session(basic_server: None, basic_server_url: str):\n    \"\"\"Create initialized StreamableHTTP client session.\"\"\"\n    async with streamable_http_client(f\"{basic_server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            await session.initialize()\n            yield session\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_basic_connection(basic_server: None, basic_server_url: str):\n    \"\"\"Test basic client connection with initialization.\"\"\"\n    async with streamable_http_client(f\"{basic_server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            # Test initialization\n            result = await session.initialize()\n            assert isinstance(result, InitializeResult)\n            assert result.server_info.name == SERVER_NAME\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_resource_read(initialized_client_session: ClientSession):\n    \"\"\"Test client resource read functionality.\"\"\"\n    response = await initialized_client_session.read_resource(uri=\"foobar://test-resource\")\n    assert len(response.contents) == 1\n    assert response.contents[0].uri == \"foobar://test-resource\"\n    assert isinstance(response.contents[0], TextResourceContents)\n    assert response.contents[0].text == \"Read test-resource\"\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_tool_invocation(initialized_client_session: ClientSession):\n    \"\"\"Test client tool invocation.\"\"\"\n    # First list tools\n    tools = await initialized_client_session.list_tools()\n    assert len(tools.tools) == 10\n    assert tools.tools[0].name == \"test_tool\"\n\n    # Call the tool\n    result = await initialized_client_session.call_tool(\"test_tool\", {})\n    assert len(result.content) == 1\n    assert result.content[0].type == \"text\"\n    assert result.content[0].text == \"Called test_tool\"\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_error_handling(initialized_client_session: ClientSession):\n    \"\"\"Test error handling in client.\"\"\"\n    with pytest.raises(MCPError) as exc_info:\n        await initialized_client_session.read_resource(uri=\"unknown://test-error\")\n    assert exc_info.value.error.code == 0\n    assert \"Unknown resource: unknown://test-error\" in exc_info.value.error.message\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_session_persistence(basic_server: None, basic_server_url: str):\n    \"\"\"Test that session ID persists across requests.\"\"\"\n    async with streamable_http_client(f\"{basic_server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            # Initialize the session\n            result = await session.initialize()\n            assert isinstance(result, InitializeResult)\n\n            # Make multiple requests to verify session persistence\n            tools = await session.list_tools()\n            assert len(tools.tools) == 10\n\n            # Read a resource\n            resource = await session.read_resource(uri=\"foobar://test-persist\")\n            assert isinstance(resource.contents[0], TextResourceContents) is True\n            content = resource.contents[0]\n            assert isinstance(content, TextResourceContents)\n            assert content.text == \"Read test-persist\"\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_json_response(json_response_server: None, json_server_url: str):\n    \"\"\"Test client with JSON response mode.\"\"\"\n    async with streamable_http_client(f\"{json_server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            # Initialize the session\n            result = await session.initialize()\n            assert isinstance(result, InitializeResult)\n            assert result.server_info.name == SERVER_NAME\n\n            # Check tool listing\n            tools = await session.list_tools()\n            assert len(tools.tools) == 10\n\n            # Call a tool and verify JSON response handling\n            result = await session.call_tool(\"test_tool\", {})\n            assert len(result.content) == 1\n            assert result.content[0].type == \"text\"\n            assert result.content[0].text == \"Called test_tool\"\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_get_stream(basic_server: None, basic_server_url: str):\n    \"\"\"Test GET stream functionality for server-initiated messages.\"\"\"\n    notifications_received: list[types.ServerNotification] = []\n\n    # Define message handler to capture notifications\n    async def message_handler(  # pragma: no branch\n        message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,\n    ) -> None:\n        if isinstance(message, types.ServerNotification):  # pragma: no branch\n            notifications_received.append(message)\n\n    async with streamable_http_client(f\"{basic_server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:\n            # Initialize the session - this triggers the GET stream setup\n            result = await session.initialize()\n            assert isinstance(result, InitializeResult)\n\n            # Call the special tool that sends a notification\n            await session.call_tool(\"test_tool_with_standalone_notification\", {})\n\n            # Verify we received the notification\n            assert len(notifications_received) > 0\n\n            # Verify the notification is a ResourceUpdatedNotification\n            resource_update_found = False\n            for notif in notifications_received:\n                if isinstance(notif, types.ResourceUpdatedNotification):  # pragma: no branch\n                    assert str(notif.params.uri) == \"http://test_resource\"\n                    resource_update_found = True\n\n            assert resource_update_found, \"ResourceUpdatedNotification not received via GET stream\"\n\n\ndef create_session_id_capturing_client() -> tuple[httpx.AsyncClient, list[str]]:\n    \"\"\"Create an httpx client that captures the session ID from responses.\"\"\"\n    captured_ids: list[str] = []\n\n    async def capture_session_id(response: httpx.Response) -> None:\n        session_id = response.headers.get(MCP_SESSION_ID_HEADER)\n        if session_id:\n            captured_ids.append(session_id)\n\n    client = httpx.AsyncClient(\n        follow_redirects=True,\n        timeout=httpx.Timeout(MCP_DEFAULT_TIMEOUT, read=MCP_DEFAULT_SSE_READ_TIMEOUT),\n        event_hooks={\"response\": [capture_session_id]},\n    )\n    return client, captured_ids\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_session_termination(basic_server: None, basic_server_url: str):\n    \"\"\"Test client session termination functionality.\"\"\"\n    # Use httpx client with event hooks to capture session ID\n    httpx_client, captured_ids = create_session_id_capturing_client()\n\n    async with httpx_client:\n        async with streamable_http_client(f\"{basic_server_url}/mcp\", http_client=httpx_client) as (\n            read_stream,\n            write_stream,\n        ):\n            async with ClientSession(read_stream, write_stream) as session:  # pragma: no branch\n                # Initialize the session\n                result = await session.initialize()\n                assert isinstance(result, InitializeResult)\n                assert len(captured_ids) > 0\n                captured_session_id = captured_ids[0]\n                assert captured_session_id is not None\n                headers = {MCP_SESSION_ID_HEADER: captured_session_id}\n\n                # Make a request to confirm session is working\n                tools = await session.list_tools()\n                assert len(tools.tools) == 10\n\n    async with create_mcp_http_client(headers=headers) as httpx_client2:\n        async with streamable_http_client(f\"{basic_server_url}/mcp\", http_client=httpx_client2) as (\n            read_stream,\n            write_stream,\n        ):\n            async with ClientSession(read_stream, write_stream) as session:  # pragma: no branch\n                # Attempt to make a request after termination\n                with pytest.raises(MCPError, match=\"Session terminated\"):  # pragma: no branch\n                    await session.list_tools()\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_session_termination_204(\n    basic_server: None, basic_server_url: str, monkeypatch: pytest.MonkeyPatch\n):\n    \"\"\"Test client session termination functionality with a 204 response.\n\n    This test patches the httpx client to return a 204 response for DELETEs.\n    \"\"\"\n\n    # Save the original delete method to restore later\n    original_delete = httpx.AsyncClient.delete\n\n    # Mock the client's delete method to return a 204\n    async def mock_delete(self: httpx.AsyncClient, *args: Any, **kwargs: Any) -> httpx.Response:\n        # Call the original method to get the real response\n        response = await original_delete(self, *args, **kwargs)\n\n        # Create a new response with 204 status code but same headers\n        mocked_response = httpx.Response(\n            204,\n            headers=response.headers,\n            content=response.content,\n            request=response.request,\n        )\n        return mocked_response\n\n    # Apply the patch to the httpx client\n    monkeypatch.setattr(httpx.AsyncClient, \"delete\", mock_delete)\n\n    # Use httpx client with event hooks to capture session ID\n    httpx_client, captured_ids = create_session_id_capturing_client()\n\n    async with httpx_client:\n        async with streamable_http_client(f\"{basic_server_url}/mcp\", http_client=httpx_client) as (\n            read_stream,\n            write_stream,\n        ):\n            async with ClientSession(read_stream, write_stream) as session:  # pragma: no branch\n                # Initialize the session\n                result = await session.initialize()\n                assert isinstance(result, InitializeResult)\n                assert len(captured_ids) > 0\n                captured_session_id = captured_ids[0]\n                assert captured_session_id is not None\n                headers = {MCP_SESSION_ID_HEADER: captured_session_id}\n\n                # Make a request to confirm session is working\n                tools = await session.list_tools()\n                assert len(tools.tools) == 10\n\n    async with create_mcp_http_client(headers=headers) as httpx_client2:\n        async with streamable_http_client(f\"{basic_server_url}/mcp\", http_client=httpx_client2) as (\n            read_stream,\n            write_stream,\n        ):\n            async with ClientSession(read_stream, write_stream) as session:  # pragma: no branch\n                # Attempt to make a request after termination\n                with pytest.raises(MCPError, match=\"Session terminated\"):  # pragma: no branch\n                    await session.list_tools()\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_resumption(event_server: tuple[SimpleEventStore, str]):\n    \"\"\"Test client session resumption using sync primitives for reliable coordination.\"\"\"\n    _, server_url = event_server\n\n    # Variables to track the state\n    captured_resumption_token: str | None = None\n    captured_notifications: list[types.ServerNotification] = []\n    first_notification_received = False\n\n    async def message_handler(  # pragma: no branch\n        message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,\n    ) -> None:\n        if isinstance(message, types.ServerNotification):  # pragma: no branch\n            captured_notifications.append(message)\n            # Look for our first notification\n            if isinstance(message, types.LoggingMessageNotification):  # pragma: no branch\n                if message.params.data == \"First notification before lock\":\n                    nonlocal first_notification_received\n                    first_notification_received = True\n\n    async def on_resumption_token_update(token: str) -> None:\n        nonlocal captured_resumption_token\n        captured_resumption_token = token\n\n    # Use httpx client with event hooks to capture session ID\n    httpx_client, captured_ids = create_session_id_capturing_client()\n\n    # First, start the client session and begin the tool that waits on lock\n    async with httpx_client:\n        async with streamable_http_client(f\"{server_url}/mcp\", terminate_on_close=False, http_client=httpx_client) as (\n            read_stream,\n            write_stream,\n        ):\n            async with ClientSession(  # pragma: no branch\n                read_stream, write_stream, message_handler=message_handler\n            ) as session:\n                # Initialize the session\n                result = await session.initialize()\n                assert isinstance(result, InitializeResult)\n                assert len(captured_ids) > 0\n                captured_session_id = captured_ids[0]\n                assert captured_session_id is not None\n                # Build phase-2 headers now while both values are in scope\n                headers: dict[str, Any] = {\n                    MCP_SESSION_ID_HEADER: captured_session_id,\n                    MCP_PROTOCOL_VERSION_HEADER: result.protocol_version,\n                }\n\n                # Start the tool that will wait on lock in a task\n                async with anyio.create_task_group() as tg:  # pragma: no branch\n\n                    async def run_tool():\n                        metadata = ClientMessageMetadata(\n                            on_resumption_token_update=on_resumption_token_update,\n                        )\n                        await session.send_request(\n                            types.CallToolRequest(\n                                params=types.CallToolRequestParams(\n                                    name=\"wait_for_lock_with_notification\", arguments={}\n                                ),\n                            ),\n                            types.CallToolResult,\n                            metadata=metadata,\n                        )\n\n                    tg.start_soon(run_tool)\n\n                    # Wait for the first notification and resumption token\n                    while not first_notification_received or not captured_resumption_token:\n                        await anyio.sleep(0.1)\n\n                    # The while loop only exits after first_notification_received=True,\n                    # which is set by message_handler immediately after appending to\n                    # captured_notifications. The server tool is blocked on its lock,\n                    # so nothing else can arrive before we cancel.\n                    assert len(captured_notifications) == 1\n                    assert isinstance(captured_notifications[0], types.LoggingMessageNotification)\n                    assert captured_notifications[0].params.data == \"First notification before lock\"\n                    # Reset for phase 2 before cancelling\n                    captured_notifications.clear()\n\n                    # Kill the client session while tool is waiting on lock\n                    tg.cancel_scope.cancel()\n\n    async with create_mcp_http_client(headers=headers) as httpx_client2:\n        async with streamable_http_client(f\"{server_url}/mcp\", http_client=httpx_client2) as (\n            read_stream,\n            write_stream,\n        ):\n            async with ClientSession(\n                read_stream, write_stream, message_handler=message_handler\n            ) as session:  # pragma: no branch\n                result = await session.send_request(\n                    types.CallToolRequest(params=types.CallToolRequestParams(name=\"release_lock\", arguments={})),\n                    types.CallToolResult,\n                )\n                metadata = ClientMessageMetadata(\n                    resumption_token=captured_resumption_token,\n                )\n\n                result = await session.send_request(\n                    types.CallToolRequest(\n                        params=types.CallToolRequestParams(name=\"wait_for_lock_with_notification\", arguments={}),\n                    ),\n                    types.CallToolResult,\n                    metadata=metadata,\n                )\n                assert len(result.content) == 1\n                assert result.content[0].type == \"text\"\n                assert result.content[0].text == \"Completed\"\n\n                # We should have received the remaining notifications\n                assert len(captured_notifications) == 1\n                assert isinstance(captured_notifications[0], types.LoggingMessageNotification)\n                assert captured_notifications[0].params.data == \"Second notification after lock\"\n\n\n@pytest.mark.anyio\nasync def test_streamablehttp_server_sampling(basic_server: None, basic_server_url: str):\n    \"\"\"Test server-initiated sampling request through streamable HTTP transport.\"\"\"\n    # Variable to track if sampling callback was invoked\n    sampling_callback_invoked = False\n    captured_message_params = None\n\n    # Define sampling callback that returns a mock response\n    async def sampling_callback(\n        context: RequestContext[ClientSession],\n        params: types.CreateMessageRequestParams,\n    ) -> types.CreateMessageResult:\n        nonlocal sampling_callback_invoked, captured_message_params\n        sampling_callback_invoked = True\n        captured_message_params = params\n        msg_content = params.messages[0].content_as_list[0]\n        message_received = msg_content.text if msg_content.type == \"text\" else None\n\n        return types.CreateMessageResult(\n            role=\"assistant\",\n            content=types.TextContent(\n                type=\"text\",\n                text=f\"Received message from server: {message_received}\",\n            ),\n            model=\"test-model\",\n            stop_reason=\"endTurn\",\n        )\n\n    # Create client with sampling callback\n    async with streamable_http_client(f\"{basic_server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream, sampling_callback=sampling_callback) as session:\n            # Initialize the session\n            result = await session.initialize()\n            assert isinstance(result, InitializeResult)\n\n            # Call the tool that triggers server-side sampling\n            tool_result = await session.call_tool(\"test_sampling_tool\", {})\n\n            # Verify the tool result contains the expected content\n            assert len(tool_result.content) == 1\n            assert tool_result.content[0].type == \"text\"\n            assert \"Response from sampling: Received message from server\" in tool_result.content[0].text\n\n            # Verify sampling callback was invoked\n            assert sampling_callback_invoked\n            assert captured_message_params is not None\n            assert len(captured_message_params.messages) == 1\n            assert captured_message_params.messages[0].content.text == \"Server needs client sampling\"\n\n\n# Context-aware server implementation for testing request context propagation\nasync def _handle_context_list_tools(  # pragma: no cover\n    ctx: ServerRequestContext, params: PaginatedRequestParams | None\n) -> ListToolsResult:\n    return ListToolsResult(\n        tools=[\n            Tool(\n                name=\"echo_headers\",\n                description=\"Echo request headers from context\",\n                input_schema={\"type\": \"object\", \"properties\": {}},\n            ),\n            Tool(\n                name=\"echo_context\",\n                description=\"Echo request context with custom data\",\n                input_schema={\n                    \"type\": \"object\",\n                    \"properties\": {\n                        \"request_id\": {\"type\": \"string\"},\n                    },\n                    \"required\": [\"request_id\"],\n                },\n            ),\n        ]\n    )\n\n\nasync def _handle_context_call_tool(  # pragma: no cover\n    ctx: ServerRequestContext, params: CallToolRequestParams\n) -> CallToolResult:\n    name = params.name\n    args = params.arguments or {}\n\n    if name == \"echo_headers\":\n        headers_info: dict[str, Any] = {}\n        if ctx.request and isinstance(ctx.request, Request):\n            headers_info = dict(ctx.request.headers)\n        return CallToolResult(content=[TextContent(type=\"text\", text=json.dumps(headers_info))])\n\n    elif name == \"echo_context\":\n        context_data: dict[str, Any] = {\n            \"request_id\": args.get(\"request_id\"),\n            \"headers\": {},\n            \"method\": None,\n            \"path\": None,\n        }\n        if ctx.request and isinstance(ctx.request, Request):\n            request = ctx.request\n            context_data[\"headers\"] = dict(request.headers)\n            context_data[\"method\"] = request.method\n            context_data[\"path\"] = request.url.path\n        return CallToolResult(content=[TextContent(type=\"text\", text=json.dumps(context_data))])\n\n    return CallToolResult(content=[TextContent(type=\"text\", text=f\"Unknown tool: {name}\")])\n\n\n# Server runner for context-aware testing\ndef run_context_aware_server(port: int):  # pragma: no cover\n    \"\"\"Run the context-aware test server.\"\"\"\n    server = Server(\n        \"ContextAwareServer\",\n        on_list_tools=_handle_context_list_tools,\n        on_call_tool=_handle_context_call_tool,\n    )\n\n    session_manager = StreamableHTTPSessionManager(\n        app=server,\n        event_store=None,\n        json_response=False,\n    )\n\n    app = Starlette(\n        debug=True,\n        routes=[\n            Mount(\"/mcp\", app=session_manager.handle_request),\n        ],\n        lifespan=lambda app: session_manager.run(),\n    )\n\n    server_instance = uvicorn.Server(\n        config=uvicorn.Config(\n            app=app,\n            host=\"127.0.0.1\",\n            port=port,\n            log_level=\"error\",\n        )\n    )\n    server_instance.run()\n\n\n@pytest.fixture\ndef context_aware_server(basic_server_port: int) -> Generator[None, None, None]:\n    \"\"\"Start the context-aware server in a separate process.\"\"\"\n    proc = multiprocessing.Process(target=run_context_aware_server, args=(basic_server_port,), daemon=True)\n    proc.start()\n\n    # Wait for server to be running\n    wait_for_server(basic_server_port)\n\n    yield\n\n    proc.kill()\n    proc.join(timeout=2)\n    if proc.is_alive():  # pragma: no cover\n        print(\"Context-aware server process failed to terminate\")\n\n\n@pytest.mark.anyio\nasync def test_streamablehttp_request_context_propagation(context_aware_server: None, basic_server_url: str) -> None:\n    \"\"\"Test that request context is properly propagated through StreamableHTTP.\"\"\"\n    custom_headers = {\n        \"Authorization\": \"Bearer test-token\",\n        \"X-Custom-Header\": \"test-value\",\n        \"X-Trace-Id\": \"trace-123\",\n    }\n\n    async with create_mcp_http_client(headers=custom_headers) as httpx_client:\n        async with streamable_http_client(f\"{basic_server_url}/mcp\", http_client=httpx_client) as (\n            read_stream,\n            write_stream,\n        ):\n            async with ClientSession(read_stream, write_stream) as session:  # pragma: no branch\n                result = await session.initialize()\n                assert isinstance(result, InitializeResult)\n                assert result.server_info.name == \"ContextAwareServer\"\n\n                # Call the tool that echoes headers back\n                tool_result = await session.call_tool(\"echo_headers\", {})\n\n                # Parse the JSON response\n                assert len(tool_result.content) == 1\n                assert isinstance(tool_result.content[0], TextContent)\n                headers_data = json.loads(tool_result.content[0].text)\n\n                # Verify headers were propagated\n                assert headers_data.get(\"authorization\") == \"Bearer test-token\"\n                assert headers_data.get(\"x-custom-header\") == \"test-value\"\n                assert headers_data.get(\"x-trace-id\") == \"trace-123\"\n\n\n@pytest.mark.anyio\nasync def test_streamablehttp_request_context_isolation(context_aware_server: None, basic_server_url: str) -> None:\n    \"\"\"Test that request contexts are isolated between StreamableHTTP clients.\"\"\"\n    contexts: list[dict[str, Any]] = []\n\n    # Create multiple clients with different headers\n    for i in range(3):\n        headers = {\n            \"X-Request-Id\": f\"request-{i}\",\n            \"X-Custom-Value\": f\"value-{i}\",\n            \"Authorization\": f\"Bearer token-{i}\",\n        }\n\n        async with create_mcp_http_client(headers=headers) as httpx_client:\n            async with streamable_http_client(f\"{basic_server_url}/mcp\", http_client=httpx_client) as (\n                read_stream,\n                write_stream,\n            ):\n                async with ClientSession(read_stream, write_stream) as session:  # pragma: no branch\n                    await session.initialize()\n\n                    # Call the tool that echoes context\n                    tool_result = await session.call_tool(\"echo_context\", {\"request_id\": f\"request-{i}\"})\n\n                    assert len(tool_result.content) == 1\n                    assert isinstance(tool_result.content[0], TextContent)\n                    context_data = json.loads(tool_result.content[0].text)\n                    contexts.append(context_data)\n\n    # Verify each request had its own context\n    assert len(contexts) == 3\n    for i, ctx in enumerate(contexts):\n        assert ctx[\"request_id\"] == f\"request-{i}\"\n        assert ctx[\"headers\"].get(\"x-request-id\") == f\"request-{i}\"\n        assert ctx[\"headers\"].get(\"x-custom-value\") == f\"value-{i}\"\n        assert ctx[\"headers\"].get(\"authorization\") == f\"Bearer token-{i}\"\n\n\n@pytest.mark.anyio\nasync def test_client_includes_protocol_version_header_after_init(context_aware_server: None, basic_server_url: str):\n    \"\"\"Test that client includes mcp-protocol-version header after initialization.\"\"\"\n    async with streamable_http_client(f\"{basic_server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            # Initialize and get the negotiated version\n            init_result = await session.initialize()\n            negotiated_version = init_result.protocol_version\n\n            # Call a tool that echoes headers to verify the header is present\n            tool_result = await session.call_tool(\"echo_headers\", {})\n\n            assert len(tool_result.content) == 1\n            assert isinstance(tool_result.content[0], TextContent)\n            headers_data = json.loads(tool_result.content[0].text)\n\n            # Verify protocol version header is present\n            assert \"mcp-protocol-version\" in headers_data\n            assert headers_data[MCP_PROTOCOL_VERSION_HEADER] == negotiated_version\n\n\ndef test_server_validates_protocol_version_header(basic_server: None, basic_server_url: str):\n    \"\"\"Test that server returns 400 Bad Request version if header unsupported or invalid.\"\"\"\n    # First initialize a session to get a valid session ID\n    init_response = requests.post(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        },\n        json=INIT_REQUEST,\n    )\n    assert init_response.status_code == 200\n    session_id = init_response.headers.get(MCP_SESSION_ID_HEADER)\n\n    # Test request with invalid protocol version (should fail)\n    response = requests.post(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n            MCP_SESSION_ID_HEADER: session_id,\n            MCP_PROTOCOL_VERSION_HEADER: \"invalid-version\",\n        },\n        json={\"jsonrpc\": \"2.0\", \"method\": \"tools/list\", \"id\": \"test-2\"},\n    )\n    assert response.status_code == 400\n    assert MCP_PROTOCOL_VERSION_HEADER in response.text or \"protocol version\" in response.text.lower()\n\n    # Test request with unsupported protocol version (should fail)\n    response = requests.post(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n            MCP_SESSION_ID_HEADER: session_id,\n            MCP_PROTOCOL_VERSION_HEADER: \"1999-01-01\",  # Very old unsupported version\n        },\n        json={\"jsonrpc\": \"2.0\", \"method\": \"tools/list\", \"id\": \"test-3\"},\n    )\n    assert response.status_code == 400\n    assert MCP_PROTOCOL_VERSION_HEADER in response.text or \"protocol version\" in response.text.lower()\n\n    # Test request with valid protocol version (should succeed)\n    negotiated_version = extract_protocol_version_from_sse(init_response)\n\n    response = requests.post(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n            MCP_SESSION_ID_HEADER: session_id,\n            MCP_PROTOCOL_VERSION_HEADER: negotiated_version,\n        },\n        json={\"jsonrpc\": \"2.0\", \"method\": \"tools/list\", \"id\": \"test-4\"},\n    )\n    assert response.status_code == 200\n\n\ndef test_server_backwards_compatibility_no_protocol_version(basic_server: None, basic_server_url: str):\n    \"\"\"Test server accepts requests without protocol version header.\"\"\"\n    # First initialize a session to get a valid session ID\n    init_response = requests.post(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n        },\n        json=INIT_REQUEST,\n    )\n    assert init_response.status_code == 200\n    session_id = init_response.headers.get(MCP_SESSION_ID_HEADER)\n\n    # Test request without mcp-protocol-version header (backwards compatibility)\n    response = requests.post(\n        f\"{basic_server_url}/mcp\",\n        headers={\n            \"Accept\": \"application/json, text/event-stream\",\n            \"Content-Type\": \"application/json\",\n            MCP_SESSION_ID_HEADER: session_id,\n        },\n        json={\"jsonrpc\": \"2.0\", \"method\": \"tools/list\", \"id\": \"test-backwards-compat\"},\n        stream=True,\n    )\n    assert response.status_code == 200  # Should succeed for backwards compatibility\n    assert response.headers.get(\"Content-Type\") == \"text/event-stream\"\n\n\n@pytest.mark.anyio\nasync def test_client_crash_handled(basic_server: None, basic_server_url: str):\n    \"\"\"Test that cases where the client crashes are handled gracefully.\"\"\"\n\n    # Simulate bad client that crashes after init\n    async def bad_client():\n        \"\"\"Client that triggers ClosedResourceError\"\"\"\n        async with streamable_http_client(f\"{basic_server_url}/mcp\") as (read_stream, write_stream):\n            async with ClientSession(read_stream, write_stream) as session:\n                await session.initialize()\n                raise Exception(\"client crash\")\n\n    # Run bad client a few times to trigger the crash\n    for _ in range(3):\n        try:\n            await bad_client()\n        except Exception:\n            pass\n        await anyio.sleep(0.1)\n\n    # Try a good client, it should still be able to connect and list tools\n    async with streamable_http_client(f\"{basic_server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            result = await session.initialize()\n            assert isinstance(result, InitializeResult)\n            tools = await session.list_tools()\n            assert tools.tools\n\n\n@pytest.mark.anyio\nasync def test_handle_sse_event_skips_empty_data():\n    \"\"\"Test that _handle_sse_event skips empty SSE data (keep-alive pings).\"\"\"\n    transport = StreamableHTTPTransport(url=\"http://localhost:8000/mcp\")\n\n    # Create a mock SSE event with empty data (keep-alive ping)\n    mock_sse = ServerSentEvent(event=\"message\", data=\"\", id=None, retry=None)\n\n    # Create a mock stream writer\n    write_stream, read_stream = anyio.create_memory_object_stream[SessionMessage | Exception](1)\n\n    try:\n        # Call _handle_sse_event with empty data - should return False and not raise\n        result = await transport._handle_sse_event(mock_sse, write_stream)\n\n        # Should return False (not complete) for empty data\n        assert result is False\n\n        # Nothing should have been written to the stream\n        # Check buffer is empty (statistics().current_buffer_used returns buffer size)\n        assert write_stream.statistics().current_buffer_used == 0\n    finally:\n        await write_stream.aclose()\n        await read_stream.aclose()\n\n\n@pytest.mark.anyio\nasync def test_priming_event_not_sent_for_old_protocol_version():\n    \"\"\"Test that _maybe_send_priming_event skips for old protocol versions (backwards compat).\"\"\"\n    # Create a transport with an event store\n    transport = StreamableHTTPServerTransport(\n        \"/mcp\",\n        event_store=SimpleEventStore(),\n    )\n\n    # Create a mock stream writer\n    write_stream, read_stream = anyio.create_memory_object_stream[dict[str, Any]](1)\n\n    try:\n        # Call _maybe_send_priming_event with OLD protocol version - should NOT send\n        await transport._maybe_send_priming_event(\"test-request-id\", write_stream, \"2025-06-18\")\n\n        # Nothing should have been written to the stream\n        assert write_stream.statistics().current_buffer_used == 0\n\n        # Now test with NEW protocol version - should send\n        await transport._maybe_send_priming_event(\"test-request-id-2\", write_stream, \"2025-11-25\")\n\n        # Should have written a priming event\n        assert write_stream.statistics().current_buffer_used == 1\n    finally:\n        await write_stream.aclose()\n        await read_stream.aclose()\n\n\n@pytest.mark.anyio\nasync def test_priming_event_not_sent_without_event_store():\n    \"\"\"Test that _maybe_send_priming_event returns early when no event_store is configured.\"\"\"\n    # Create a transport WITHOUT an event store\n    transport = StreamableHTTPServerTransport(\"/mcp\")\n\n    # Create a mock stream writer\n    write_stream, read_stream = anyio.create_memory_object_stream[dict[str, Any]](1)\n\n    try:\n        # Call _maybe_send_priming_event - should return early without sending\n        await transport._maybe_send_priming_event(\"test-request-id\", write_stream, \"2025-11-25\")\n\n        # Nothing should have been written to the stream\n        assert write_stream.statistics().current_buffer_used == 0\n    finally:\n        await write_stream.aclose()\n        await read_stream.aclose()\n\n\n@pytest.mark.anyio\nasync def test_priming_event_includes_retry_interval():\n    \"\"\"Test that _maybe_send_priming_event includes retry field when retry_interval is set.\"\"\"\n    # Create a transport with an event store AND retry_interval\n    transport = StreamableHTTPServerTransport(\n        \"/mcp\",\n        event_store=SimpleEventStore(),\n        retry_interval=5000,\n    )\n\n    # Create a mock stream writer\n    write_stream, read_stream = anyio.create_memory_object_stream[dict[str, Any]](1)\n\n    try:\n        # Call _maybe_send_priming_event with new protocol version\n        await transport._maybe_send_priming_event(\"test-request-id\", write_stream, \"2025-11-25\")\n\n        # Should have written a priming event with retry field\n        assert write_stream.statistics().current_buffer_used == 1\n\n        # Read the event and verify it has retry field\n        event = await read_stream.receive()\n        assert \"retry\" in event\n        assert event[\"retry\"] == 5000\n    finally:\n        await write_stream.aclose()\n        await read_stream.aclose()\n\n\n@pytest.mark.anyio\nasync def test_close_sse_stream_callback_not_provided_for_old_protocol_version():\n    \"\"\"Test that close_sse_stream callbacks are NOT provided for old protocol versions.\"\"\"\n    # Create a transport with an event store\n    transport = StreamableHTTPServerTransport(\n        \"/mcp\",\n        event_store=SimpleEventStore(),\n    )\n\n    # Create a mock message and request\n    mock_message = JSONRPCRequest(jsonrpc=\"2.0\", id=\"test-1\", method=\"tools/list\")\n    mock_request = MagicMock()\n\n    # Call _create_session_message with OLD protocol version\n    session_msg = transport._create_session_message(mock_message, mock_request, \"test-request-id\", \"2025-06-18\")\n\n    # Callbacks should NOT be provided for old protocol version\n    assert session_msg.metadata is not None\n    assert isinstance(session_msg.metadata, ServerMessageMetadata)\n    assert session_msg.metadata.close_sse_stream is None\n    assert session_msg.metadata.close_standalone_sse_stream is None\n\n    # Now test with NEW protocol version - should provide callbacks\n    session_msg_new = transport._create_session_message(mock_message, mock_request, \"test-request-id-2\", \"2025-11-25\")\n\n    # Callbacks SHOULD be provided for new protocol version\n    assert session_msg_new.metadata is not None\n    assert isinstance(session_msg_new.metadata, ServerMessageMetadata)\n    assert session_msg_new.metadata.close_sse_stream is not None\n    assert session_msg_new.metadata.close_standalone_sse_stream is not None\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_receives_priming_event(\n    event_server: tuple[SimpleEventStore, str],\n) -> None:\n    \"\"\"Client should receive priming event (resumption token update) on POST SSE stream.\"\"\"\n    _, server_url = event_server\n\n    captured_resumption_tokens: list[str] = []\n\n    async def on_resumption_token_update(token: str) -> None:\n        captured_resumption_tokens.append(token)\n\n    async with streamable_http_client(f\"{server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            await session.initialize()\n\n            # Call tool with resumption token callback via send_request\n            metadata = ClientMessageMetadata(\n                on_resumption_token_update=on_resumption_token_update,\n            )\n            result = await session.send_request(\n                types.CallToolRequest(params=types.CallToolRequestParams(name=\"test_tool\", arguments={})),\n                types.CallToolResult,\n                metadata=metadata,\n            )\n            assert result is not None\n\n            # Should have received priming event token BEFORE response data\n            # Priming event = 1 token (empty data, id only)\n            # Response = 1 token (actual JSON-RPC response)\n            # Total = 2 tokens minimum\n            assert len(captured_resumption_tokens) >= 2, (\n                f\"Server must send priming event before response. \"\n                f\"Expected >= 2 tokens (priming + response), got {len(captured_resumption_tokens)}\"\n            )\n            assert captured_resumption_tokens[0] is not None\n\n\n@pytest.mark.anyio\nasync def test_server_close_sse_stream_via_context(\n    event_server: tuple[SimpleEventStore, str],\n) -> None:\n    \"\"\"Server tool can call ctx.close_sse_stream() to close connection.\"\"\"\n    _, server_url = event_server\n\n    async with streamable_http_client(f\"{server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            await session.initialize()\n\n            # Call tool that closes stream mid-operation\n            # This should NOT raise NotImplementedError when fully implemented\n            result = await session.call_tool(\"tool_with_stream_close\", {})\n\n            # Client should still receive complete response (via auto-reconnect)\n            assert result is not None\n            assert len(result.content) > 0\n            assert result.content[0].type == \"text\"\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"Done\"\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_auto_reconnects(\n    event_server: tuple[SimpleEventStore, str],\n) -> None:\n    \"\"\"Client should auto-reconnect with Last-Event-ID when server closes after priming event.\"\"\"\n    _, server_url = event_server\n    captured_notifications: list[str] = []\n\n    async def message_handler(\n        message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,\n    ) -> None:\n        if isinstance(message, Exception):  # pragma: no branch\n            return  # pragma: no cover\n        if isinstance(message, types.ServerNotification):  # pragma: no branch\n            if isinstance(message, types.LoggingMessageNotification):  # pragma: no branch\n                captured_notifications.append(str(message.params.data))\n\n    async with streamable_http_client(f\"{server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:\n            await session.initialize()\n\n            # Call tool that:\n            # 1. Sends notification\n            # 2. Closes SSE stream\n            # 3. Sends more notifications (stored in event_store)\n            # 4. Returns response\n            result = await session.call_tool(\"tool_with_stream_close\", {})\n\n            # Client should have auto-reconnected and received ALL notifications\n            assert len(captured_notifications) >= 2, (\n                \"Client should auto-reconnect and receive notifications sent both before and after stream close\"\n            )\n            assert result.content[0].type == \"text\"\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"Done\"\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_respects_retry_interval(\n    event_server: tuple[SimpleEventStore, str],\n) -> None:\n    \"\"\"Client MUST respect retry field, waiting specified ms before reconnecting.\"\"\"\n    _, server_url = event_server\n\n    async with streamable_http_client(f\"{server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            await session.initialize()\n\n            start_time = time.monotonic()\n            result = await session.call_tool(\"tool_with_stream_close\", {})\n            elapsed = time.monotonic() - start_time\n\n            # Verify result was received\n            assert result.content[0].type == \"text\"\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"Done\"\n\n            # The elapsed time should include at least the retry interval\n            # if reconnection occurred. This test may be flaky depending on\n            # implementation details, but demonstrates the expected behavior.\n            # Note: This assertion may need adjustment based on actual implementation\n            assert elapsed >= 0.4, f\"Client should wait ~500ms before reconnecting, but elapsed time was {elapsed:.3f}s\"\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_sse_polling_full_cycle(\n    event_server: tuple[SimpleEventStore, str],\n) -> None:\n    \"\"\"End-to-end test: server closes stream, client reconnects, receives all events.\"\"\"\n    _, server_url = event_server\n    all_notifications: list[str] = []\n\n    async def message_handler(\n        message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,\n    ) -> None:\n        if isinstance(message, Exception):  # pragma: no branch\n            return  # pragma: no cover\n        if isinstance(message, types.ServerNotification):  # pragma: no branch\n            if isinstance(message, types.LoggingMessageNotification):  # pragma: no branch\n                all_notifications.append(str(message.params.data))\n\n    async with streamable_http_client(f\"{server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:\n            await session.initialize()\n\n            # Call tool that simulates polling pattern:\n            # 1. Server sends priming event\n            # 2. Server sends \"Before close\" notification\n            # 3. Server closes stream (calls close_sse_stream)\n            # 4. (client reconnects automatically)\n            # 5. Server sends \"After close\" notification\n            # 6. Server sends final response\n            result = await session.call_tool(\"tool_with_stream_close\", {})\n\n            # Verify all notifications received in order\n            assert \"Before close\" in all_notifications, \"Should receive notification sent before stream close\"\n            assert \"After close\" in all_notifications, (\n                \"Should receive notification sent after stream close (via auto-reconnect)\"\n            )\n            assert result.content[0].type == \"text\"\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"Done\"\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_events_replayed_after_disconnect(\n    event_server: tuple[SimpleEventStore, str],\n) -> None:\n    \"\"\"Events sent while client is disconnected should be replayed on reconnect.\"\"\"\n    _, server_url = event_server\n    notification_data: list[str] = []\n\n    async def message_handler(\n        message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,\n    ) -> None:\n        if isinstance(message, Exception):  # pragma: no branch\n            return  # pragma: no cover\n        if isinstance(message, types.ServerNotification):  # pragma: no branch\n            if isinstance(message, types.LoggingMessageNotification):  # pragma: no branch\n                notification_data.append(str(message.params.data))\n\n    async with streamable_http_client(f\"{server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:\n            await session.initialize()\n\n            # Tool sends: notification1, close_stream, notification2, notification3, response\n            # Client should receive all notifications even though 2&3 were sent during disconnect\n            result = await session.call_tool(\"tool_with_multiple_notifications_and_close\", {})\n\n            assert \"notification1\" in notification_data, \"Should receive notification1 (sent before close)\"\n            assert \"notification2\" in notification_data, \"Should receive notification2 (sent after close, replayed)\"\n            assert \"notification3\" in notification_data, \"Should receive notification3 (sent after close, replayed)\"\n\n            # Verify order: notification1 should come before notification2 and notification3\n            idx1 = notification_data.index(\"notification1\")\n            idx2 = notification_data.index(\"notification2\")\n            idx3 = notification_data.index(\"notification3\")\n            assert idx1 < idx2 < idx3, \"Notifications should be received in order\"\n\n            assert result.content[0].type == \"text\"\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"All notifications sent\"\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_multiple_reconnections(\n    event_server: tuple[SimpleEventStore, str],\n):\n    \"\"\"Verify multiple close_sse_stream() calls each trigger a client reconnect.\n\n    Server uses retry_interval=500ms, tool sleeps 600ms after each close to ensure\n    client has time to reconnect before the next checkpoint.\n\n    With 3 checkpoints, we expect 8 resumption tokens:\n    - 1 priming (initial POST connection)\n    - 3 notifications (checkpoint_0, checkpoint_1, checkpoint_2)\n    - 3 priming (one per reconnect after each close)\n    - 1 response\n    \"\"\"\n    _, server_url = event_server\n    resumption_tokens: list[str] = []\n\n    async def on_resumption_token(token: str) -> None:\n        resumption_tokens.append(token)\n\n    async with streamable_http_client(f\"{server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream) as session:\n            await session.initialize()\n\n            # Use send_request with metadata to track resumption tokens\n            metadata = ClientMessageMetadata(on_resumption_token_update=on_resumption_token)\n            result = await session.send_request(\n                types.CallToolRequest(\n                    method=\"tools/call\",\n                    params=types.CallToolRequestParams(\n                        name=\"tool_with_multiple_stream_closes\",\n                        # retry_interval=500ms, so sleep 600ms to ensure reconnect completes\n                        arguments={\"checkpoints\": 3, \"sleep_time\": 0.6},\n                    ),\n                ),\n                types.CallToolResult,\n                metadata=metadata,\n            )\n\n            assert result.content[0].type == \"text\"\n            assert isinstance(result.content[0], TextContent)\n            assert \"Completed 3 checkpoints\" in result.content[0].text\n\n            # 4 priming + 3 notifications + 1 response = 8 tokens. All tokens are\n            # captured before send_request returns, so this is safe to check here.\n            assert len(resumption_tokens) == 8, (\n                f\"Expected 8 resumption tokens (4 priming + 3 notifs + 1 response), \"\n                f\"got {len(resumption_tokens)}: {resumption_tokens}\"\n            )\n\n\n@pytest.mark.anyio\nasync def test_standalone_get_stream_reconnection(event_server: tuple[SimpleEventStore, str]) -> None:\n    \"\"\"Test that standalone GET stream automatically reconnects after server closes it.\n\n    Verifies:\n    1. Client receives notification 1 via GET stream\n    2. Server closes GET stream\n    3. Client reconnects with Last-Event-ID\n    4. Client receives notification 2 on new connection\n\n    Note: Requires event_server fixture (with event store) because close_standalone_sse_stream\n    callback is only provided when event_store is configured and protocol version >= 2025-11-25.\n    \"\"\"\n    _, server_url = event_server\n    received_notifications: list[str] = []\n\n    async def message_handler(\n        message: RequestResponder[types.ServerRequest, types.ClientResult] | types.ServerNotification | Exception,\n    ) -> None:\n        if isinstance(message, Exception):\n            return  # pragma: no cover\n        if isinstance(message, types.ServerNotification):  # pragma: no branch\n            if isinstance(message, types.ResourceUpdatedNotification):  # pragma: no branch\n                received_notifications.append(str(message.params.uri))\n\n    async with streamable_http_client(f\"{server_url}/mcp\") as (read_stream, write_stream):\n        async with ClientSession(read_stream, write_stream, message_handler=message_handler) as session:\n            await session.initialize()\n\n            # Call tool that:\n            # 1. Sends notification_1 via GET stream\n            # 2. Closes standalone GET stream\n            # 3. Sends notification_2 (stored in event_store)\n            # 4. Returns response\n            result = await session.call_tool(\"tool_with_standalone_stream_close\", {})\n\n            # Verify the tool completed\n            assert result.content[0].type == \"text\"\n            assert isinstance(result.content[0], TextContent)\n            assert result.content[0].text == \"Standalone stream close test done\"\n\n            # Verify both notifications were received\n            assert \"http://notification_1\" in received_notifications, (\n                f\"Should receive notification 1 (sent before GET stream close), got: {received_notifications}\"\n            )\n            assert \"http://notification_2\" in received_notifications, (\n                f\"Should receive notification 2 after reconnect, got: {received_notifications}\"\n            )\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_does_not_mutate_provided_client(\n    basic_server: None, basic_server_url: str\n) -> None:\n    \"\"\"Test that streamable_http_client does not mutate the provided httpx client's headers.\"\"\"\n    # Create a client with custom headers\n    original_headers = {\n        \"X-Custom-Header\": \"custom-value\",\n        \"Authorization\": \"Bearer test-token\",\n    }\n\n    async with httpx.AsyncClient(headers=original_headers, follow_redirects=True) as custom_client:\n        # Use the client with streamable_http_client\n        async with streamable_http_client(f\"{basic_server_url}/mcp\", http_client=custom_client) as (\n            read_stream,\n            write_stream,\n        ):\n            async with ClientSession(read_stream, write_stream) as session:\n                result = await session.initialize()\n                assert isinstance(result, InitializeResult)\n\n        # Verify client headers were not mutated with MCP protocol headers\n        # If accept header exists, it should still be httpx default, not MCP's\n        if \"accept\" in custom_client.headers:  # pragma: no branch\n            assert custom_client.headers.get(\"accept\") == \"*/*\"\n        # MCP content-type should not have been added\n        assert custom_client.headers.get(\"content-type\") != \"application/json\"\n\n        # Verify custom headers are still present and unchanged\n        assert custom_client.headers.get(\"X-Custom-Header\") == \"custom-value\"\n        assert custom_client.headers.get(\"Authorization\") == \"Bearer test-token\"\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_mcp_headers_override_defaults(\n    context_aware_server: None, basic_server_url: str\n) -> None:\n    \"\"\"Test that MCP protocol headers override httpx.AsyncClient default headers.\"\"\"\n    # httpx.AsyncClient has default \"accept: */*\" header\n    # We need to verify that our MCP accept header overrides it in actual requests\n\n    async with httpx.AsyncClient(follow_redirects=True) as client:\n        # Verify client has default accept header\n        assert client.headers.get(\"accept\") == \"*/*\"\n\n        async with streamable_http_client(f\"{basic_server_url}/mcp\", http_client=client) as (read_stream, write_stream):\n            async with ClientSession(read_stream, write_stream) as session:  # pragma: no branch\n                await session.initialize()\n\n                # Use echo_headers tool to see what headers the server actually received\n                tool_result = await session.call_tool(\"echo_headers\", {})\n                assert len(tool_result.content) == 1\n                assert isinstance(tool_result.content[0], TextContent)\n                headers_data = json.loads(tool_result.content[0].text)\n\n                # Verify MCP protocol headers were sent (not httpx defaults)\n                assert \"accept\" in headers_data\n                assert \"application/json\" in headers_data[\"accept\"]\n                assert \"text/event-stream\" in headers_data[\"accept\"]\n\n                assert \"content-type\" in headers_data\n                assert headers_data[\"content-type\"] == \"application/json\"\n\n\n@pytest.mark.anyio\nasync def test_streamable_http_client_preserves_custom_with_mcp_headers(\n    context_aware_server: None, basic_server_url: str\n) -> None:\n    \"\"\"Test that both custom headers and MCP protocol headers are sent in requests.\"\"\"\n    custom_headers = {\n        \"X-Custom-Header\": \"custom-value\",\n        \"X-Request-Id\": \"req-123\",\n        \"Authorization\": \"Bearer test-token\",\n    }\n\n    async with httpx.AsyncClient(headers=custom_headers, follow_redirects=True) as client:\n        async with streamable_http_client(f\"{basic_server_url}/mcp\", http_client=client) as (read_stream, write_stream):\n            async with ClientSession(read_stream, write_stream) as session:  # pragma: no branch\n                await session.initialize()\n\n                # Use echo_headers tool to verify both custom and MCP headers are present\n                tool_result = await session.call_tool(\"echo_headers\", {})\n                assert len(tool_result.content) == 1\n                assert isinstance(tool_result.content[0], TextContent)\n                headers_data = json.loads(tool_result.content[0].text)\n\n                # Verify custom headers are present\n                assert headers_data.get(\"x-custom-header\") == \"custom-value\"\n                assert headers_data.get(\"x-request-id\") == \"req-123\"\n                assert headers_data.get(\"authorization\") == \"Bearer test-token\"\n\n                # Verify MCP protocol headers are also present\n                assert \"accept\" in headers_data\n                assert \"application/json\" in headers_data[\"accept\"]\n                assert \"text/event-stream\" in headers_data[\"accept\"]\n\n                assert \"content-type\" in headers_data\n                assert headers_data[\"content-type\"] == \"application/json\"\n"
  },
  {
    "path": "tests/shared/test_tool_name_validation.py",
    "content": "\"\"\"Tests for tool name validation utilities (SEP-986).\"\"\"\n\nimport logging\n\nimport pytest\n\nfrom mcp.shared.tool_name_validation import (\n    issue_tool_name_warning,\n    validate_and_warn_tool_name,\n    validate_tool_name,\n)\n\n# Tests for validate_tool_name function - valid names\n\n\n@pytest.mark.parametrize(\n    \"tool_name\",\n    [\n        \"getUser\",\n        \"get_user_profile\",\n        \"user-profile-update\",\n        \"admin.tools.list\",\n        \"DATA_EXPORT_v2.1\",\n        \"a\",\n        \"a\" * 128,\n    ],\n    ids=[\n        \"simple_alphanumeric\",\n        \"with_underscores\",\n        \"with_dashes\",\n        \"with_dots\",\n        \"mixed_characters\",\n        \"single_character\",\n        \"max_length_128\",\n    ],\n)\ndef test_validate_tool_name_accepts_valid_names(tool_name: str) -> None:\n    \"\"\"Valid tool names should pass validation with no warnings.\"\"\"\n    result = validate_tool_name(tool_name)\n    assert result.is_valid is True\n    assert result.warnings == []\n\n\n# Tests for validate_tool_name function - invalid names\n\n\ndef test_validate_tool_name_rejects_empty_name() -> None:\n    \"\"\"Empty names should be rejected.\"\"\"\n    result = validate_tool_name(\"\")\n    assert result.is_valid is False\n    assert \"Tool name cannot be empty\" in result.warnings\n\n\ndef test_validate_tool_name_rejects_name_exceeding_max_length() -> None:\n    \"\"\"Names exceeding 128 characters should be rejected.\"\"\"\n    result = validate_tool_name(\"a\" * 129)\n    assert result.is_valid is False\n    assert any(\"exceeds maximum length of 128 characters (current: 129)\" in w for w in result.warnings)\n\n\n@pytest.mark.parametrize(\n    \"tool_name,expected_char\",\n    [\n        (\"get user profile\", \"' '\"),\n        (\"get,user,profile\", \"','\"),\n        (\"user/profile/update\", \"'/'\"),\n        (\"user@domain.com\", \"'@'\"),\n    ],\n    ids=[\n        \"with_spaces\",\n        \"with_commas\",\n        \"with_slashes\",\n        \"with_at_symbol\",\n    ],\n)\ndef test_validate_tool_name_rejects_invalid_characters(tool_name: str, expected_char: str) -> None:\n    \"\"\"Names with invalid characters should be rejected.\"\"\"\n    result = validate_tool_name(tool_name)\n    assert result.is_valid is False\n    assert any(\"invalid characters\" in w and expected_char in w for w in result.warnings)\n\n\ndef test_validate_tool_name_rejects_multiple_invalid_chars() -> None:\n    \"\"\"Names with multiple invalid chars should list all of them.\"\"\"\n    result = validate_tool_name(\"user name@domain,com\")\n    assert result.is_valid is False\n    warning = next(w for w in result.warnings if \"invalid characters\" in w)\n    assert \"' '\" in warning\n    assert \"'@'\" in warning\n    assert \"','\" in warning\n\n\ndef test_validate_tool_name_rejects_unicode_characters() -> None:\n    \"\"\"Names with unicode characters should be rejected.\"\"\"\n    result = validate_tool_name(\"user-\\u00f1ame\")  # n with tilde\n    assert result.is_valid is False\n\n\n# Tests for validate_tool_name function - warnings for problematic patterns\n\n\ndef test_validate_tool_name_warns_on_leading_dash() -> None:\n    \"\"\"Names starting with dash should generate warning but be valid.\"\"\"\n    result = validate_tool_name(\"-get-user\")\n    assert result.is_valid is True\n    assert any(\"starts or ends with a dash\" in w for w in result.warnings)\n\n\ndef test_validate_tool_name_warns_on_trailing_dash() -> None:\n    \"\"\"Names ending with dash should generate warning but be valid.\"\"\"\n    result = validate_tool_name(\"get-user-\")\n    assert result.is_valid is True\n    assert any(\"starts or ends with a dash\" in w for w in result.warnings)\n\n\ndef test_validate_tool_name_warns_on_leading_dot() -> None:\n    \"\"\"Names starting with dot should generate warning but be valid.\"\"\"\n    result = validate_tool_name(\".get.user\")\n    assert result.is_valid is True\n    assert any(\"starts or ends with a dot\" in w for w in result.warnings)\n\n\ndef test_validate_tool_name_warns_on_trailing_dot() -> None:\n    \"\"\"Names ending with dot should generate warning but be valid.\"\"\"\n    result = validate_tool_name(\"get.user.\")\n    assert result.is_valid is True\n    assert any(\"starts or ends with a dot\" in w for w in result.warnings)\n\n\n# Tests for issue_tool_name_warning function\n\n\ndef test_issue_tool_name_warning_logs_warnings(caplog: pytest.LogCaptureFixture) -> None:\n    \"\"\"Warnings should be logged at WARNING level.\"\"\"\n    warnings = [\"Warning 1\", \"Warning 2\"]\n    with caplog.at_level(logging.WARNING):\n        issue_tool_name_warning(\"test-tool\", warnings)\n\n    assert 'Tool name validation warning for \"test-tool\"' in caplog.text\n    assert \"- Warning 1\" in caplog.text\n    assert \"- Warning 2\" in caplog.text\n    assert \"Tool registration will proceed\" in caplog.text\n    assert \"SEP-986\" in caplog.text\n\n\ndef test_issue_tool_name_warning_no_logging_for_empty_warnings(caplog: pytest.LogCaptureFixture) -> None:\n    \"\"\"Empty warnings list should not produce any log output.\"\"\"\n    with caplog.at_level(logging.WARNING):\n        issue_tool_name_warning(\"test-tool\", [])\n\n    assert caplog.text == \"\"\n\n\n# Tests for validate_and_warn_tool_name function\n\n\ndef test_validate_and_warn_tool_name_returns_true_for_valid_name() -> None:\n    \"\"\"Valid names should return True.\"\"\"\n    assert validate_and_warn_tool_name(\"valid-tool-name\") is True\n\n\ndef test_validate_and_warn_tool_name_returns_false_for_invalid_name() -> None:\n    \"\"\"Invalid names should return False.\"\"\"\n    assert validate_and_warn_tool_name(\"\") is False\n    assert validate_and_warn_tool_name(\"a\" * 129) is False\n    assert validate_and_warn_tool_name(\"invalid name\") is False\n\n\ndef test_validate_and_warn_tool_name_logs_warnings_for_invalid_name(caplog: pytest.LogCaptureFixture) -> None:\n    \"\"\"Invalid names should trigger warning logs.\"\"\"\n    with caplog.at_level(logging.WARNING):\n        validate_and_warn_tool_name(\"invalid name\")\n\n    assert \"Tool name validation warning\" in caplog.text\n\n\ndef test_validate_and_warn_tool_name_no_warnings_for_clean_valid_name(caplog: pytest.LogCaptureFixture) -> None:\n    \"\"\"Clean valid names should not produce any log output.\"\"\"\n    with caplog.at_level(logging.WARNING):\n        result = validate_and_warn_tool_name(\"clean-tool-name\")\n\n    assert result is True\n    assert caplog.text == \"\"\n\n\n# Tests for edge cases\n\n\n@pytest.mark.parametrize(\n    \"tool_name,is_valid,expected_warning_fragment\",\n    [\n        (\"...\", True, \"starts or ends with a dot\"),\n        (\"---\", True, \"starts or ends with a dash\"),\n        (\"///\", False, \"invalid characters\"),\n        (\"user@name123\", False, \"invalid characters\"),\n    ],\n    ids=[\n        \"only_dots\",\n        \"only_dashes\",\n        \"only_slashes\",\n        \"mixed_valid_invalid\",\n    ],\n)\ndef test_edge_cases(tool_name: str, is_valid: bool, expected_warning_fragment: str) -> None:\n    \"\"\"Various edge cases should be handled correctly.\"\"\"\n    result = validate_tool_name(tool_name)\n    assert result.is_valid is is_valid\n    assert any(expected_warning_fragment in w for w in result.warnings)\n"
  },
  {
    "path": "tests/shared/test_win32_utils.py",
    "content": "\"\"\"Windows-specific test utilities.\"\"\"\n\n\ndef escape_path_for_python(path: str) -> str:\n    \"\"\"Escape a file path for use in Python code strings.\n\n    Converts backslashes to forward slashes which work on all platforms\n    and don't need escaping in Python strings.\n    \"\"\"\n    return repr(path.replace(\"\\\\\", \"/\"))\n"
  },
  {
    "path": "tests/shared/test_ws.py",
    "content": "\"\"\"Smoke test for the WebSocket transport.\n\nRuns the full WS stack end-to-end over a real TCP connection, covering both\n``src/mcp/client/websocket.py`` and ``src/mcp/server/websocket.py``. MCP\nsemantics (error propagation, timeouts, etc.) are transport-agnostic and are\ncovered in ``tests/client/test_client.py`` and ``tests/issues/test_88_random_error.py``.\n\"\"\"\n\nfrom collections.abc import Generator\n\nimport pytest\nfrom starlette.applications import Starlette\nfrom starlette.routing import WebSocketRoute\nfrom starlette.websockets import WebSocket\n\nfrom mcp.client.session import ClientSession\nfrom mcp.client.websocket import websocket_client\nfrom mcp.server import Server\nfrom mcp.server.websocket import websocket_server\nfrom mcp.types import EmptyResult, InitializeResult\nfrom tests.test_helpers import run_uvicorn_in_thread\n\nSERVER_NAME = \"test_server_for_WS\"\n\n\ndef make_server_app() -> Starlette:\n    srv = Server(SERVER_NAME)\n\n    async def handle_ws(websocket: WebSocket) -> None:\n        async with websocket_server(websocket.scope, websocket.receive, websocket.send) as streams:\n            await srv.run(streams[0], streams[1], srv.create_initialization_options())\n\n    return Starlette(routes=[WebSocketRoute(\"/ws\", endpoint=handle_ws)])\n\n\n@pytest.fixture\ndef ws_server_url() -> Generator[str, None, None]:\n    with run_uvicorn_in_thread(make_server_app()) as base_url:\n        yield base_url.replace(\"http://\", \"ws://\") + \"/ws\"\n\n\n@pytest.mark.anyio\nasync def test_ws_client_basic_connection(ws_server_url: str) -> None:\n    async with websocket_client(ws_server_url) as streams:\n        async with ClientSession(*streams) as session:\n            result = await session.initialize()\n            assert isinstance(result, InitializeResult)\n            assert result.server_info.name == SERVER_NAME\n\n            ping_result = await session.send_ping()\n            assert isinstance(ping_result, EmptyResult)\n"
  },
  {
    "path": "tests/test_examples.py",
    "content": "\"\"\"Tests for example servers\"\"\"\n# TODO(Marcelo): The `examples` directory needs to be importable as a package.\n# pyright: reportMissingImports=false\n# pyright: reportUnknownVariableType=false\n# pyright: reportUnknownArgumentType=false\n# pyright: reportUnknownMemberType=false\n\nfrom pathlib import Path\n\nimport pytest\nfrom inline_snapshot import snapshot\nfrom pytest_examples import CodeExample, EvalExample, find_examples\n\nfrom mcp import Client\nfrom mcp.types import CallToolResult, TextContent, TextResourceContents\n\n\n@pytest.mark.anyio\nasync def test_simple_echo():\n    \"\"\"Test the simple echo server\"\"\"\n    from examples.mcpserver.simple_echo import mcp\n\n    async with Client(mcp) as client:\n        result = await client.call_tool(\"echo\", {\"text\": \"hello\"})\n        assert result == snapshot(\n            CallToolResult(content=[TextContent(text=\"hello\")], structured_content={\"result\": \"hello\"})\n        )\n\n\n@pytest.mark.anyio\nasync def test_complex_inputs():\n    \"\"\"Test the complex inputs server\"\"\"\n    from examples.mcpserver.complex_inputs import mcp\n\n    async with Client(mcp) as client:\n        tank = {\"shrimp\": [{\"name\": \"bob\"}, {\"name\": \"alice\"}]}\n        result = await client.call_tool(\"name_shrimp\", {\"tank\": tank, \"extra_names\": [\"charlie\"]})\n        assert result == snapshot(\n            CallToolResult(\n                content=[\n                    TextContent(text=\"bob\"),\n                    TextContent(text=\"alice\"),\n                    TextContent(text=\"charlie\"),\n                ],\n                structured_content={\"result\": [\"bob\", \"alice\", \"charlie\"]},\n            )\n        )\n\n\n@pytest.mark.anyio\nasync def test_direct_call_tool_result_return():\n    \"\"\"Test the CallToolResult echo server\"\"\"\n    from examples.mcpserver.direct_call_tool_result_return import mcp\n\n    async with Client(mcp) as client:\n        result = await client.call_tool(\"echo\", {\"text\": \"hello\"})\n        assert result == snapshot(\n            CallToolResult(\n                meta={\"some\": \"metadata\"},  # type: ignore[reportUnknownMemberType]\n                content=[TextContent(text=\"hello\")],\n                structured_content={\"text\": \"hello\"},\n            )\n        )\n\n\n@pytest.mark.anyio\nasync def test_desktop(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):\n    \"\"\"Test the desktop server\"\"\"\n    # Build a real Desktop directory under tmp_path rather than patching\n    # Path.iterdir — a class-level patch breaks jsonschema_specifications'\n    # import-time schema discovery when this test happens to be the first\n    # tool call in an xdist worker.\n    desktop = tmp_path / \"Desktop\"\n    desktop.mkdir()\n    (desktop / \"file1.txt\").touch()\n    (desktop / \"file2.txt\").touch()\n    monkeypatch.setattr(Path, \"home\", lambda: tmp_path)\n\n    from examples.mcpserver.desktop import mcp\n\n    async with Client(mcp) as client:\n        # Test the sum function\n        result = await client.call_tool(\"sum\", {\"a\": 1, \"b\": 2})\n        assert result == snapshot(CallToolResult(content=[TextContent(text=\"3\")], structured_content={\"result\": 3}))\n\n        # Test the desktop resource\n        result = await client.read_resource(\"dir://desktop\")\n        assert len(result.contents) == 1\n        content = result.contents[0]\n        assert isinstance(content, TextResourceContents)\n        assert isinstance(content.text, str)\n        assert \"file1.txt\" in content.text\n        assert \"file2.txt\" in content.text\n\n\n# TODO(v2): Change back to README.md when v2 is released\n@pytest.mark.parametrize(\"example\", find_examples(\"README.v2.md\"), ids=str)\ndef test_docs_examples(example: CodeExample, eval_example: EvalExample):\n    ruff_ignore: list[str] = [\"F841\", \"I001\", \"F821\"]  # F821: undefined names (snippets lack imports)\n\n    # Use project's actual line length of 120\n    eval_example.set_config(ruff_ignore=ruff_ignore, target_version=\"py310\", line_length=120)\n\n    # Use Ruff for both formatting and linting (skip Black)\n    if eval_example.update_examples:  # pragma: no cover\n        eval_example.format_ruff(example)\n    else:\n        eval_example.lint_ruff(example)\n"
  },
  {
    "path": "tests/test_helpers.py",
    "content": "\"\"\"Common test utilities for MCP server tests.\"\"\"\n\nimport socket\nimport threading\nimport time\nfrom collections.abc import Generator\nfrom contextlib import contextmanager\nfrom typing import Any\n\nimport uvicorn\n\n_SERVER_SHUTDOWN_TIMEOUT_S = 5.0\n\n\n@contextmanager\ndef run_uvicorn_in_thread(app: Any, **config_kwargs: Any) -> Generator[str, None, None]:\n    \"\"\"Run a uvicorn server in a background thread on an ephemeral port.\n\n    The socket is bound and put into listening state *before* the thread\n    starts, so the port is known immediately with no wait. The kernel's\n    listen queue buffers any connections that arrive before uvicorn's event\n    loop reaches ``accept()``, so callers can connect as soon as this\n    function yields — no polling, no sleeps, no startup race.\n\n    This also avoids the TOCTOU race of the old pick-a-port-then-rebind\n    pattern: the socket passed here is the one uvicorn serves on, with no\n    gap where another pytest-xdist worker could claim it.\n\n    Args:\n        app: ASGI application to serve.\n        **config_kwargs: Additional keyword arguments for :class:`uvicorn.Config`\n            (e.g. ``log_level``). ``host``/``port`` are ignored since the\n            socket is pre-bound.\n\n    Yields:\n        The base URL of the running server, e.g. ``http://127.0.0.1:54321``.\n    \"\"\"\n    host = \"127.0.0.1\"\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n    sock.bind((host, 0))\n    sock.listen()\n    port = sock.getsockname()[1]\n\n    config_kwargs.setdefault(\"log_level\", \"error\")\n    # Uvicorn's interface autodetection calls asyncio.iscoroutinefunction,\n    # which Python 3.14 deprecates. Under filterwarnings=error this crashes\n    # the server thread silently. Starlette is asgi3; skip the autodetect.\n    config_kwargs.setdefault(\"interface\", \"asgi3\")\n    server = uvicorn.Server(config=uvicorn.Config(app=app, **config_kwargs))\n\n    thread = threading.Thread(target=server.run, kwargs={\"sockets\": [sock]}, daemon=True)\n    thread.start()\n    try:\n        yield f\"http://{host}:{port}\"\n    finally:\n        server.should_exit = True\n        thread.join(timeout=_SERVER_SHUTDOWN_TIMEOUT_S)\n\n\ndef wait_for_server(port: int, timeout: float = 20.0) -> None:\n    \"\"\"Wait for server to be ready to accept connections.\n\n    Polls the server port until it accepts connections or timeout is reached.\n    This eliminates race conditions without arbitrary sleeps.\n\n    Args:\n        port: The port number to check\n        timeout: Maximum time to wait in seconds (default 5.0)\n\n    Raises:\n        TimeoutError: If server doesn't start within the timeout period\n    \"\"\"\n    start_time = time.time()\n    while time.time() - start_time < timeout:\n        try:\n            with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:\n                s.settimeout(0.1)\n                s.connect((\"127.0.0.1\", port))\n                # Server is ready\n                return\n        except (ConnectionRefusedError, OSError):\n            # Server not ready yet, retry quickly\n            time.sleep(0.01)\n    raise TimeoutError(f\"Server on port {port} did not start within {timeout} seconds\")  # pragma: no cover\n"
  },
  {
    "path": "tests/test_types.py",
    "content": "from typing import Any\n\nimport pytest\n\nfrom mcp.types import (\n    LATEST_PROTOCOL_VERSION,\n    ClientCapabilities,\n    CreateMessageRequestParams,\n    CreateMessageResult,\n    CreateMessageResultWithTools,\n    Implementation,\n    InitializeRequest,\n    InitializeRequestParams,\n    JSONRPCRequest,\n    ListToolsResult,\n    SamplingCapability,\n    SamplingMessage,\n    TextContent,\n    Tool,\n    ToolChoice,\n    ToolResultContent,\n    ToolUseContent,\n    client_request_adapter,\n    jsonrpc_message_adapter,\n)\n\n\n@pytest.mark.anyio\nasync def test_jsonrpc_request():\n    json_data = {\n        \"jsonrpc\": \"2.0\",\n        \"id\": 1,\n        \"method\": \"initialize\",\n        \"params\": {\n            \"protocolVersion\": LATEST_PROTOCOL_VERSION,\n            \"capabilities\": {\"batch\": None, \"sampling\": None},\n            \"clientInfo\": {\"name\": \"mcp\", \"version\": \"0.1.0\"},\n        },\n    }\n\n    request = jsonrpc_message_adapter.validate_python(json_data)\n    assert isinstance(request, JSONRPCRequest)\n    client_request_adapter.validate_python(request.model_dump(by_alias=True, exclude_none=True))\n\n    assert request.jsonrpc == \"2.0\"\n    assert request.id == 1\n    assert request.method == \"initialize\"\n    assert request.params is not None\n    assert request.params[\"protocolVersion\"] == LATEST_PROTOCOL_VERSION\n\n\n@pytest.mark.anyio\nasync def test_method_initialization():\n    \"\"\"Test that the method is automatically set on object creation.\n    Testing just for InitializeRequest to keep the test simple, but should be set for other types as well.\n    \"\"\"\n    initialize_request = InitializeRequest(\n        params=InitializeRequestParams(\n            protocol_version=LATEST_PROTOCOL_VERSION,\n            capabilities=ClientCapabilities(),\n            client_info=Implementation(\n                name=\"mcp\",\n                version=\"0.1.0\",\n            ),\n        )\n    )\n\n    assert initialize_request.method == \"initialize\", \"method should be set to 'initialize'\"\n    assert initialize_request.params is not None\n    assert initialize_request.params.protocol_version == LATEST_PROTOCOL_VERSION\n\n\n@pytest.mark.anyio\nasync def test_tool_use_content():\n    \"\"\"Test ToolUseContent type for SEP-1577.\"\"\"\n    tool_use_data = {\n        \"type\": \"tool_use\",\n        \"name\": \"get_weather\",\n        \"id\": \"call_abc123\",\n        \"input\": {\"location\": \"San Francisco\", \"unit\": \"celsius\"},\n    }\n\n    tool_use = ToolUseContent.model_validate(tool_use_data)\n    assert tool_use.type == \"tool_use\"\n    assert tool_use.name == \"get_weather\"\n    assert tool_use.id == \"call_abc123\"\n    assert tool_use.input == {\"location\": \"San Francisco\", \"unit\": \"celsius\"}\n\n    # Test serialization\n    serialized = tool_use.model_dump(by_alias=True, exclude_none=True)\n    assert serialized[\"type\"] == \"tool_use\"\n    assert serialized[\"name\"] == \"get_weather\"\n\n\n@pytest.mark.anyio\nasync def test_tool_result_content():\n    \"\"\"Test ToolResultContent type for SEP-1577.\"\"\"\n    tool_result_data = {\n        \"type\": \"tool_result\",\n        \"toolUseId\": \"call_abc123\",\n        \"content\": [{\"type\": \"text\", \"text\": \"It's 72°F in San Francisco\"}],\n        \"isError\": False,\n    }\n\n    tool_result = ToolResultContent.model_validate(tool_result_data)\n    assert tool_result.type == \"tool_result\"\n    assert tool_result.tool_use_id == \"call_abc123\"\n    assert len(tool_result.content) == 1\n    assert tool_result.is_error is False\n\n    # Test with empty content (should default to [])\n    minimal_result_data = {\"type\": \"tool_result\", \"toolUseId\": \"call_xyz\"}\n    minimal_result = ToolResultContent.model_validate(minimal_result_data)\n    assert minimal_result.content == []\n\n\n@pytest.mark.anyio\nasync def test_tool_choice():\n    \"\"\"Test ToolChoice type for SEP-1577.\"\"\"\n    # Test with mode\n    tool_choice_data = {\"mode\": \"required\"}\n    tool_choice = ToolChoice.model_validate(tool_choice_data)\n    assert tool_choice.mode == \"required\"\n\n    # Test with minimal data (all fields optional)\n    minimal_choice = ToolChoice.model_validate({})\n    assert minimal_choice.mode is None\n\n    # Test different modes\n    auto_choice = ToolChoice.model_validate({\"mode\": \"auto\"})\n    assert auto_choice.mode == \"auto\"\n\n    none_choice = ToolChoice.model_validate({\"mode\": \"none\"})\n    assert none_choice.mode == \"none\"\n\n\n@pytest.mark.anyio\nasync def test_sampling_message_with_user_role():\n    \"\"\"Test SamplingMessage with user role for SEP-1577.\"\"\"\n    # Test with single content\n    user_msg_data = {\"role\": \"user\", \"content\": {\"type\": \"text\", \"text\": \"Hello\"}}\n    user_msg = SamplingMessage.model_validate(user_msg_data)\n    assert user_msg.role == \"user\"\n    assert isinstance(user_msg.content, TextContent)\n\n    # Test with array of content including tool result\n    multi_content_data: dict[str, Any] = {\n        \"role\": \"user\",\n        \"content\": [\n            {\"type\": \"text\", \"text\": \"Here's the result:\"},\n            {\"type\": \"tool_result\", \"toolUseId\": \"call_123\", \"content\": []},\n        ],\n    }\n    multi_msg = SamplingMessage.model_validate(multi_content_data)\n    assert multi_msg.role == \"user\"\n    assert isinstance(multi_msg.content, list)\n    assert len(multi_msg.content) == 2\n\n\n@pytest.mark.anyio\nasync def test_sampling_message_with_assistant_role():\n    \"\"\"Test SamplingMessage with assistant role for SEP-1577.\"\"\"\n    # Test with tool use content\n    assistant_msg_data = {\n        \"role\": \"assistant\",\n        \"content\": {\n            \"type\": \"tool_use\",\n            \"name\": \"search\",\n            \"id\": \"call_456\",\n            \"input\": {\"query\": \"MCP protocol\"},\n        },\n    }\n    assistant_msg = SamplingMessage.model_validate(assistant_msg_data)\n    assert assistant_msg.role == \"assistant\"\n    assert isinstance(assistant_msg.content, ToolUseContent)\n\n    # Test with array of mixed content\n    multi_content_data: dict[str, Any] = {\n        \"role\": \"assistant\",\n        \"content\": [\n            {\"type\": \"text\", \"text\": \"Let me search for that...\"},\n            {\"type\": \"tool_use\", \"name\": \"search\", \"id\": \"call_789\", \"input\": {}},\n        ],\n    }\n    multi_msg = SamplingMessage.model_validate(multi_content_data)\n    assert isinstance(multi_msg.content, list)\n    assert len(multi_msg.content) == 2\n\n\n@pytest.mark.anyio\nasync def test_sampling_message_backward_compatibility():\n    \"\"\"Test that SamplingMessage maintains backward compatibility.\"\"\"\n    # Old-style message (single content, no tools)\n    old_style_data = {\"role\": \"user\", \"content\": {\"type\": \"text\", \"text\": \"Hello\"}}\n    old_msg = SamplingMessage.model_validate(old_style_data)\n    assert old_msg.role == \"user\"\n    assert isinstance(old_msg.content, TextContent)\n\n    # New-style message with tool content\n    new_style_data: dict[str, Any] = {\n        \"role\": \"assistant\",\n        \"content\": {\"type\": \"tool_use\", \"name\": \"test\", \"id\": \"call_1\", \"input\": {}},\n    }\n    new_msg = SamplingMessage.model_validate(new_style_data)\n    assert new_msg.role == \"assistant\"\n    assert isinstance(new_msg.content, ToolUseContent)\n\n    # Array content\n    array_style_data: dict[str, Any] = {\n        \"role\": \"user\",\n        \"content\": [{\"type\": \"text\", \"text\": \"Result:\"}, {\"type\": \"tool_result\", \"toolUseId\": \"call_1\", \"content\": []}],\n    }\n    array_msg = SamplingMessage.model_validate(array_style_data)\n    assert isinstance(array_msg.content, list)\n\n\n@pytest.mark.anyio\nasync def test_create_message_request_params_with_tools():\n    \"\"\"Test CreateMessageRequestParams with tools for SEP-1577.\"\"\"\n    tool = Tool(\n        name=\"get_weather\",\n        description=\"Get weather information\",\n        input_schema={\"type\": \"object\", \"properties\": {\"location\": {\"type\": \"string\"}}},\n    )\n\n    params = CreateMessageRequestParams(\n        messages=[SamplingMessage(role=\"user\", content=TextContent(type=\"text\", text=\"What's the weather?\"))],\n        max_tokens=1000,\n        tools=[tool],\n        tool_choice=ToolChoice(mode=\"auto\"),\n    )\n\n    assert params.tools is not None\n    assert len(params.tools) == 1\n    assert params.tools[0].name == \"get_weather\"\n    assert params.tool_choice is not None\n    assert params.tool_choice.mode == \"auto\"\n\n\n@pytest.mark.anyio\nasync def test_create_message_result_with_tool_use():\n    \"\"\"Test CreateMessageResultWithTools with tool use content for SEP-1577.\"\"\"\n    result_data = {\n        \"role\": \"assistant\",\n        \"content\": {\"type\": \"tool_use\", \"name\": \"search\", \"id\": \"call_123\", \"input\": {\"query\": \"test\"}},\n        \"model\": \"claude-3\",\n        \"stopReason\": \"toolUse\",\n    }\n\n    # Tool use content uses CreateMessageResultWithTools\n    result = CreateMessageResultWithTools.model_validate(result_data)\n    assert result.role == \"assistant\"\n    assert isinstance(result.content, ToolUseContent)\n    assert result.stop_reason == \"toolUse\"\n    assert result.model == \"claude-3\"\n\n    # Test content_as_list with single content (covers else branch)\n    content_list = result.content_as_list\n    assert len(content_list) == 1\n    assert content_list[0] == result.content\n\n\n@pytest.mark.anyio\nasync def test_create_message_result_basic():\n    \"\"\"Test CreateMessageResult with basic text content (backwards compatible).\"\"\"\n    result_data = {\n        \"role\": \"assistant\",\n        \"content\": {\"type\": \"text\", \"text\": \"Hello!\"},\n        \"model\": \"claude-3\",\n        \"stopReason\": \"endTurn\",\n    }\n\n    # Basic content uses CreateMessageResult (single content, no arrays)\n    result = CreateMessageResult.model_validate(result_data)\n    assert result.role == \"assistant\"\n    assert isinstance(result.content, TextContent)\n    assert result.content.text == \"Hello!\"\n    assert result.stop_reason == \"endTurn\"\n    assert result.model == \"claude-3\"\n\n\n@pytest.mark.anyio\nasync def test_client_capabilities_with_sampling_tools():\n    \"\"\"Test ClientCapabilities with nested sampling capabilities for SEP-1577.\"\"\"\n    # New structured format\n    capabilities_data: dict[str, Any] = {\n        \"sampling\": {\"tools\": {}},\n    }\n    capabilities = ClientCapabilities.model_validate(capabilities_data)\n    assert capabilities.sampling is not None\n    assert isinstance(capabilities.sampling, SamplingCapability)\n    assert capabilities.sampling.tools is not None\n\n    # With both context and tools\n    full_capabilities_data: dict[str, Any] = {\"sampling\": {\"context\": {}, \"tools\": {}}}\n    full_caps = ClientCapabilities.model_validate(full_capabilities_data)\n    assert isinstance(full_caps.sampling, SamplingCapability)\n    assert full_caps.sampling.context is not None\n    assert full_caps.sampling.tools is not None\n\n\ndef test_tool_preserves_json_schema_2020_12_fields():\n    \"\"\"Verify that JSON Schema 2020-12 keywords are preserved in Tool.inputSchema.\n\n    SEP-1613 establishes JSON Schema 2020-12 as the default dialect for MCP.\n    This test ensures the SDK doesn't strip $schema, $defs, or additionalProperties.\n    \"\"\"\n    input_schema = {\n        \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n        \"type\": \"object\",\n        \"$defs\": {\n            \"address\": {\n                \"type\": \"object\",\n                \"properties\": {\"street\": {\"type\": \"string\"}, \"city\": {\"type\": \"string\"}},\n            }\n        },\n        \"properties\": {\n            \"name\": {\"type\": \"string\"},\n            \"address\": {\"$ref\": \"#/$defs/address\"},\n        },\n        \"additionalProperties\": False,\n    }\n\n    tool = Tool(name=\"test_tool\", description=\"A test tool\", input_schema=input_schema)\n\n    # Verify fields are preserved in the model\n    assert tool.input_schema[\"$schema\"] == \"https://json-schema.org/draft/2020-12/schema\"\n    assert \"$defs\" in tool.input_schema\n    assert \"address\" in tool.input_schema[\"$defs\"]\n    assert tool.input_schema[\"additionalProperties\"] is False\n\n    # Verify fields survive serialization round-trip\n    serialized = tool.model_dump(mode=\"json\", by_alias=True)\n    assert serialized[\"inputSchema\"][\"$schema\"] == \"https://json-schema.org/draft/2020-12/schema\"\n    assert \"$defs\" in serialized[\"inputSchema\"]\n    assert serialized[\"inputSchema\"][\"additionalProperties\"] is False\n\n\ndef test_list_tools_result_preserves_json_schema_2020_12_fields():\n    \"\"\"Verify JSON Schema 2020-12 fields survive ListToolsResult deserialization.\"\"\"\n    raw_response = {\n        \"tools\": [\n            {\n                \"name\": \"json_schema_tool\",\n                \"description\": \"Tool with JSON Schema 2020-12 features\",\n                \"inputSchema\": {\n                    \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n                    \"type\": \"object\",\n                    \"$defs\": {\"item\": {\"type\": \"string\"}},\n                    \"properties\": {\"items\": {\"type\": \"array\", \"items\": {\"$ref\": \"#/$defs/item\"}}},\n                    \"additionalProperties\": False,\n                },\n            }\n        ]\n    }\n\n    result = ListToolsResult.model_validate(raw_response)\n    tool = result.tools[0]\n\n    assert tool.input_schema[\"$schema\"] == \"https://json-schema.org/draft/2020-12/schema\"\n    assert \"$defs\" in tool.input_schema\n    assert tool.input_schema[\"additionalProperties\"] is False\n"
  }
]