Skip to content

Improve HTTP transport for DNS allowlists and stateless mode#327

Open
almirmcunhajr wants to merge 5 commits into
makenotion:mainfrom
almirmcunhajr:main
Open

Improve HTTP transport for DNS allowlists and stateless mode#327
almirmcunhajr wants to merge 5 commits into
makenotion:mainfrom
almirmcunhajr:main

Conversation

@almirmcunhajr

Copy link
Copy Markdown

Description

This PR brings two related HTTP transport improvements to the local Notion MCP server:

  1. support for --allowed-hosts, so unauthenticated HTTP deployments can explicitly permit additional hosts in the DNS rebinding allowlist
  2. a stateless HTTP mode for the Streamable HTTP transport, so the server can handle each POST /mcp request independently without persisting MCP sessions

Together, these changes make HTTP deployments more flexible for reverse proxies, tunnels, and stateless gateway setups while keeping the default stateful behavior unchanged.

Changes in this PR:

  • add --allowed-hosts <hosts> CLI parsing as a comma-separated, additive list
  • include those hosts in DNS rebinding allowedHosts and allowedOrigins
  • add --stateless-http CLI parsing and ENABLE_STATELESS_HTTP environment variable support
  • create a fresh Streamable HTTP transport per POST /mcp request when stateless mode is enabled
  • require a per-request Notion token on every request in --stateless-http --enable-token-passthrough mode
  • keep the existing authenticated and stateful HTTP behavior unchanged by default
  • refactor the POST /mcp handler by extracting the stateful and stateless flows into dedicated functions
  • update token resolution helpers, CLI help text, tests, and README examples/documentation

How was this change tested?

  • Automated test (unit, integration, etc.)
  • Manual test (provide reproducible testing steps below)

Reproducible manual testing steps

  1. Build the project:

    npm run build
  2. Export a valid Notion token:

    export TEST_NOTION_TOKEN="ntn_..."
  3. Start the server locally without --allowed-hosts and without stateless mode:

    node bin/cli.mjs \
      --transport http \
      --port 4321 \
      --unsafe-disable-auth \
      --enable-token-passthrough
  4. In another terminal, attempt to initialize an MCP session through app.local, forcing it to resolve locally:

    curl --resolve app.local:4321:127.0.0.1 \
      -D /tmp/notion-mcp-negative-headers.txt \
      -o /tmp/notion-mcp-negative-init.json \
      -H "Origin: http://app.local:4321" \
      -H "Notion-Token: $TEST_NOTION_TOKEN" \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -d '{
        "jsonrpc":"2.0",
        "id":1,
        "method":"initialize",
        "params":{
          "protocolVersion":"2025-03-26",
          "capabilities":{},
          "clientInfo":{"name":"local-curl","version":"1.0.0"}
        }
      }' \
      http://app.local:4321/mcp

    Expected result: the request is rejected because app.local is not in the DNS rebinding allowlist.

  5. Stop the server and restart it with the new host allowlist support:

    node bin/cli.mjs \
      --transport http \
      --port 4321 \
      --unsafe-disable-auth \
      --enable-token-passthrough \
      --allowed-hosts app.local
  6. Initialize an MCP session again through app.local:

    curl --resolve app.local:4321:127.0.0.1 \
      -D /tmp/notion-mcp-headers.txt \
      -o /tmp/notion-mcp-init.json \
      -H "Origin: http://app.local:4321" \
      -H "Notion-Token: $TEST_NOTION_TOKEN" \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -d '{
        "jsonrpc":"2.0",
        "id":1,
        "method":"initialize",
        "params":{
          "protocolVersion":"2025-03-26",
          "capabilities":{},
          "clientInfo":{"name":"local-curl","version":"1.0.0"}
        }
      }' \
      http://app.local:4321/mcp

    Expected result: initialization succeeds and returns an MCP session id.

  7. Stop the server and restart it in stateless mode with token passthrough enabled:

    node bin/cli.mjs \
      --transport http \
      --port 4321 \
      --unsafe-disable-auth \
      --enable-token-passthrough \
      --stateless-http
  8. Call initialize without sending a Notion token:

    curl -i \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -d '{
        "jsonrpc":"2.0",
        "id":1,
        "method":"initialize",
        "params":{
          "protocolVersion":"2025-03-26",
          "capabilities":{},
          "clientInfo":{"name":"local-curl","version":"1.0.0"}
        }
      }' \
      http://127.0.0.1:4321/mcp

    Expected result: the request is rejected with 401 because stateless passthrough requires a Notion token on every request.

  9. Call initialize again, this time with a Notion token:

    curl -i \
      -H "Authorization: Bearer $TEST_NOTION_TOKEN" \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -d '{
        "jsonrpc":"2.0",
        "id":1,
        "method":"initialize",
        "params":{
          "protocolVersion":"2025-03-26",
          "capabilities":{},
          "clientInfo":{"name":"local-curl","version":"1.0.0"}
        }
      }' \
      http://127.0.0.1:4321/mcp

    Expected result: initialization succeeds and no mcp-session-id header is returned.

  10. Call a Notion-backed tool in a separate request, again sending the token:

curl -i \
  -H "Authorization: Bearer $TEST_NOTION_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc":"2.0",
    "id":2,
    "method":"tools/call",
    "params":{
      "name":"API-get-self",
      "arguments":{}
    }
  }' \
  http://127.0.0.1:4321/mcp

Expected result: the tool call succeeds without any MCP session id.

  1. Verify that non-POST MCP methods are rejected in stateless mode:
curl -i http://127.0.0.1:4321/mcp

Expected result: 405 Method not allowed.

Expected result:

  • without --allowed-hosts app.local, initialization through app.local is rejected
  • with --allowed-hosts app.local, initialization through app.local succeeds
  • without a Notion token, stateless passthrough requests are rejected with 401
  • with a Notion token on each request, stateless initialize and tools/call succeed without using an MCP session id
  • GET /mcp and DELETE /mcp are rejected in stateless mode

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant