Elicitation

URL-mode elicitation with consent approval. Experimental (tracks draft SEP-2643).

URL Elicitation — Consent Approval Flow (UC1)

Experimental — Tracks SEP-2643 (Structured Authorization Denials), currently a draft. Wire format may change as the SEP evolves.

A scripted MCP host walking through the UC1 consent approval flow: a tool call gets denied with a JSON-RPC -32042 (URLElicitationRequired) carrying a consent URL; the host opens the URL in a browser; the user approves; the server pushes a notifications/elicitation/complete notification over SSE; the host auto-retries with the authorizationContextId and gets the result.

Quick Start

# Terminal 1 — start the MCP server
just serve

# Terminal 2 — run the scripted walkthrough
just demo

The walkthrough opens a browser to http://localhost:8080/approve?ctx=.... Click Approve there; the demo auto-retries and completes.

See WALKTHROUGH.md for the full sequence diagram and step-by-step description (regenerate via just readme).

What it demonstrates

  • The -32042 URLElicitationRequired denial shape — error.code + error.data.authorization.authorizationContextId + error.data.elicitations[].url.
  • The GET SSE notification stream carrying notifications/elicitation/complete from server to host while the user interacts with the consent URL out-of-band.
  • The auto-retry pattern: host parses the denial, opens the URL, waits for the SSE notification, then re-issues tools/call with _meta.authorizationContextId set to the captured value.
  • The server-side consent middleware that intercepts tools/call for protected tools, mints a context, denies with the URL, persists approval state, and lets the retry through once approved.
  • CORS configuration for browser-based MCP hosts (MCPJam) — Mcp-Session-Id in both Allow-Headers and Expose-Headers, DELETE in allowed methods.

Where to look in the code

Notes

  • CORS is applied via server.WithHandlerWrap(cors) so it covers /mcp plus the /approve route registered through server.WithMux. Browser-based MCP hosts (MCPJam) need Mcp-Session-Id in both Allow- and Expose-Headers; the canonical CORS configuration lives in this example’s serve() block.
  • The walkthrough’s “Approve” button click is a real human-in-the-loop step; CI runs would need either a headless browser or a synthetic POST /approve?ctx=....

Next steps


Walkthrough

URL Elicitation — Consent Approval Flow (UC1)

EXPERIMENTAL — Tracks SEP-2643 (Structured Authorization Denials), currently a draft. A scripted MCP host walking through the UC1 consent approval flow. Wire format may change as the SEP evolves.

What you’ll learn

  • Connect to the MCP server and initialize session — Connect with a notification callback listening for notifications/elicitation/complete. The GET SSE stream receives server-pushed notifications.
  • Call access_protected_resource — denied with consent URL — The consent middleware intercepts the call and returns -32042 (URLElicitationRequired) with a URL the user must visit to approve access.
  • Open consent URL → wait for approval notification → auto-retry — The host opens the consent URL and waits for the server to send a notifications/elicitation/complete notification via the SSE stream. When it arrives, the host automatically retries with the authorizationContextId.

Flow

sequenceDiagram
    participant Host as MCP Host (this client)
    participant Server as MCP Server (just serve)
    participant Browser as User Browser

    Note over Host,Browser: Step 1: Connect to the MCP server and initialize session
    Host->>Server: POST /mcp — initialize
    Server-->>Host: serverInfo + Mcp-Session-Id
    Host->>Server: GET /mcp — open SSE stream for notifications

    Note over Host,Browser: Step 2: Call access_protected_resource — denied with consent URL
    Host->>Server: tools/call: access_protected_resource
    Server-->>Host: error -32042 + consent URL + authzContextId

    Note over Host,Browser: Step 3: Open consent URL → wait for approval notification → auto-retry
    Host->>Browser: open consent URL
    Browser->>Server: POST /approve?ctx=...
    Server-->>Host: notifications/elicitation/complete (via SSE)
    Host->>Server: tools/call + _meta.authorizationContextId (auto-retry)
    Server-->>Host: Access granted to resource

Steps

Setup

Before running this demo, start the MCP server in a separate terminal:

Terminal 1:  just serve        # start the MCP server on :8080
Terminal 2:  just run          # run this demo

Step 1: Connect to the MCP server and initialize session

Connect with a notification callback listening for notifications/elicitation/complete. The GET SSE stream receives server-pushed notifications.

Reproduce on the wire

# Initialize an MCP session and capture the session id returned in the headers.
SID=$(curl -s -X POST http://localhost:8080/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":"i","method":"initialize","params":{"protocolVersion":"2025-11-25","clientInfo":{"name":"x","version":"1"},"capabilities":{}}}' \
  -D - -o /dev/null | grep -i 'mcp-session-id' | awk '{print $2}' | tr -d '\r\n')
curl -s -X POST http://localhost:8080/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' >/dev/null
echo "SID=$SID"

# In a SECOND terminal, open the SSE notification stream so the
# notifications/elicitation/complete event in step 3 arrives there:
#
#   curl -N http://localhost:8080/mcp \
#        -H "Mcp-Session-Id: $SID" \
#        -H 'Accept: text/event-stream'
#
# Keep that terminal open. The notification will appear there once the user
# clicks Approve in step 3.

The consent middleware intercepts the call and returns -32042 (URLElicitationRequired) with a URL the user must visit to approve access.

Reproduce on the wire

# Call the protected tool. Response is JSON-RPC error -32042 carrying the
# consent URL and the authorizationContextId you must echo on retry.
RESP=$(curl -s -X POST http://localhost:8080/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","id":"c","method":"tools/call","params":{"name":"access_protected_resource","arguments":{"resourceId":"my-doc"}}}')
echo "$RESP" | jq '.error'

# Capture the context id and the consent URL into shell vars for step 3.
export CTX=$(echo "$RESP" | jq -r .error.data.authorization.authorizationContextId)
export APPROVE_URL=$(echo "$RESP" | jq -r '.error.data.elicitations[0].url')
echo "CTX=$CTX"
echo "APPROVE_URL=$APPROVE_URL"

The host opens the consent URL and waits for the server to send a notifications/elicitation/complete notification via the SSE stream. When it arrives, the host automatically retries with the authorizationContextId.

Reproduce on the wire

# Approve the context. You can either click Approve in a browser at
# $APPROVE_URL, or POST to it directly:
curl -s -X POST "$APPROVE_URL" >/dev/null

# The SECOND terminal (running the SSE stream from step 1) should now print
# something like:
#   event: message
#   data: {"jsonrpc":"2.0","method":"notifications/elicitation/complete","params":{"elicitationId":"..."}}

# Retry tools/call with the context id echoed under the SEP-2643 _meta key.
# The server matches it against the approved context and lets the call through.
curl -s -X POST http://localhost:8080/mcp \
  -H 'Content-Type: application/json' -H 'Accept: application/json' \
  -H "Mcp-Session-Id: $SID" \
  -d "{\"jsonrpc\":\"2.0\",\"id\":\"r\",\"method\":\"tools/call\",\"params\":{\"name\":\"access_protected_resource\",\"arguments\":{\"resourceId\":\"my-doc\"},\"_meta\":{\"io.modelcontextprotocol/authorization-context-id\":\"$CTX\"}}}" \
  | jq .

Run it

go run ./examples/elicitation/

Pass --non-interactive to skip pauses:

go run ./examples/elicitation/ --non-interactive