diff --git a/docs/toolhive/guides-mcp/notion-remote.mdx b/docs/toolhive/guides-mcp/notion-remote.mdx
index 4e1e0821..d418ce2c 100644
--- a/docs/toolhive/guides-mcp/notion-remote.mdx
+++ b/docs/toolhive/guides-mcp/notion-remote.mdx
@@ -14,8 +14,10 @@ agents to search, read, create, and manage pages, databases, and other content
in your Notion workspace.
This is a remote MCP server that uses the **Auto-Discovered** authorization
-method, meaning ToolHive handles OAuth authentication automatically with minimal
-configuration required.
+method, meaning ToolHive discovers Notion's OAuth endpoints automatically. On
+the CLI and UI, this requires minimal configuration. On Kubernetes, it takes a
+one-time OAuth client registration and an embedded authorization server setup,
+described below.
## Metadata
@@ -79,50 +81,155 @@ thv restart notion-remote
-:::note
+The ToolHive operator deploys a proxy in front of Notion's hosted remote MCP
+server using an `MCPRemoteProxy` resource with an
+[embedded authorization server](../concepts/embedded-auth-server.mdx).
-The ToolHive Kubernetes Operator does not currently support remote MCP servers
-using dynamic OAuth authentication. Instead, you can run the
-[local Notion MCP server](https://github.com/makenotion/notion-mcp-server) in
-Kubernetes using a static integration key.
+The examples below use `https://notion-mcp.example.com` as a placeholder for
+wherever you'll expose the proxy outside the cluster. Set up an Ingress or
+Gateway route pointing at the proxy Service first (see
+[Connect clients to MCP servers](../guides-k8s/connect-clients.mdx#connect-from-outside-the-cluster))
+and substitute your real hostname everywhere this placeholder appears. Notion
+redirects the user's browser to this URL during authentication, so it must be
+genuinely reachable over HTTPS, not just resolvable inside the cluster.
-:::
+Notion's remote MCP server (`mcp.notion.com`) only supports Dynamic Client
+Registration (RFC 7591) for third-party OAuth clients; there's no
+dashboard-based app registration for it, unlike Notion's classic API
+integrations. Once your hostname is live, register once to mint a persistent
+client ID and secret:
+
+```bash
+curl -s -X POST https://mcp.notion.com/register \
+ -H "Content-Type: application/json" \
+ -d '{
+ "client_name": "my-toolhive-proxy",
+ "redirect_uris": ["https://notion-mcp.example.com/oauth/callback"],
+ "grant_types": ["authorization_code", "refresh_token"],
+ "response_types": ["code"],
+ "token_endpoint_auth_method": "client_secret_post"
+ }'
+```
-Create an integration in Notion to obtain an authentication token by following
-the instructions in the MCP server's
-[README](https://github.com/makenotion/notion-mcp-server?tab=readme-ov-file#installation).
+Save the returned `client_id` and `client_secret`; they don't expire. The
+`redirect_uris` value must exactly match the callback URL you configure below.
-Create a Kubernetes secret containing your Notion integration token
-("`ntn_****`"):
+Generate a signing key and an HMAC key for the embedded authorization server,
+and store all three credentials as Secrets:
```bash
-kubectl -n toolhive-system create secret generic notion-token --from-literal=token=
+openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out signing-key.pem
+openssl rand -base64 32 > hmac-key.txt
+
+kubectl create secret generic notion-auth-signing-key -n toolhive-system \
+ --from-file=signing-key=signing-key.pem
+kubectl create secret generic notion-auth-hmac-key -n toolhive-system \
+ --from-literal=hmac-key="$(cat hmac-key.txt)"
+kubectl create secret generic notion-client-secret -n toolhive-system \
+ --from-literal=client-secret=
```
-Create a Kubernetes manifest to deploy the Notion MCP server using your secret:
+Create the embedded authorization server configuration, the OIDC config that
+validates its own issued tokens, and the remote proxy:
-```yaml title="notion.yaml"
+```yaml title="notion-proxy.yaml"
apiVersion: toolhive.stacklok.dev/v1beta1
-kind: MCPServer
+kind: MCPExternalAuthConfig
+metadata:
+ name: notion-embedded-auth
+ namespace: toolhive-system
+spec:
+ type: embeddedAuthServer
+ embeddedAuthServer:
+ issuer: 'https://notion-mcp.example.com'
+ signingKeySecretRefs:
+ - name: notion-auth-signing-key
+ key: signing-key
+ hmacSecretRefs:
+ - name: notion-auth-hmac-key
+ key: hmac-key
+ upstreamProviders:
+ - name: notion
+ type: oauth2
+ oauth2Config:
+ authorizationEndpoint: 'https://mcp.notion.com/authorize'
+ tokenEndpoint: 'https://mcp.notion.com/token'
+ clientId: ''
+ clientSecretRef:
+ name: notion-client-secret
+ key: client-secret
+ redirectUri: 'https://notion-mcp.example.com/oauth/callback'
+---
+apiVersion: toolhive.stacklok.dev/v1beta1
+kind: MCPOIDCConfig
+metadata:
+ name: notion-embedded-oidc
+ namespace: toolhive-system
+spec:
+ type: inline
+ inline:
+ issuer: 'https://notion-mcp.example.com'
+---
+apiVersion: toolhive.stacklok.dev/v1beta1
+kind: MCPRemoteProxy
metadata:
name: notion
namespace: toolhive-system
spec:
- image: ghcr.io/stacklok/dockyard/npx/notion:2.2.1
- transport: stdio
+ remoteUrl: https://mcp.notion.com/mcp
+ transport: streamable-http
proxyPort: 8080
- secrets:
- - name: notion-token
- key: token
- targetEnvName: NOTION_TOKEN
+ authServerRef:
+ kind: MCPExternalAuthConfig
+ name: notion-embedded-auth
+ oidcConfigRef:
+ name: notion-embedded-oidc
+ audience: 'https://notion-mcp.example.com/mcp'
+ resourceUrl: 'https://notion-mcp.example.com/mcp'
+ audit:
+ enabled: true
```
+:::note[Keep `issuer` path-free]
+
+Set `issuer` on both the `MCPExternalAuthConfig` and `MCPOIDCConfig` to your
+bare host, with no path. The embedded authorization server's OAuth endpoints
+(`/oauth/register`, `/oauth/authorize`, `/oauth/token`, `/oauth/callback`) are
+always served at the host root, regardless of any path in `issuer`. Adding a
+path here (for example, matching your MCP resource path) makes discovery
+advertise endpoints that nothing actually serves, and every authentication
+attempt fails with a generic "authorization header required" error.
+
+Also set `redirectUri` on the upstream provider explicitly to
+`/oauth/callback`, as shown above, instead of omitting it.
+[Default callback URL for upstream providers](../guides-k8s/auth-k8s.mdx#default-callback-url-for-upstream-providers)
+documents the default as `{resourceUrl}/oauth/callback`, which works when
+`resourceUrl` is a bare host. It breaks here because `resourceUrl` includes the
+`/mcp` path: the callback route is only ever served at the host root, so the
+computed default would land on a path nothing serves. Setting `redirectUri`
+explicitly avoids relying on that default.
+
+:::
+
Apply the manifest to your Kubernetes cluster:
```bash
-kubectl apply -f notion.yaml
+kubectl apply -f notion-proxy.yaml
+```
+
+Check the proxy status:
+
+```bash
+kubectl get mcpremoteproxy notion -n toolhive-system
```
+Connect your MCP client to the proxy's URL. Each client authenticates once
+through the OAuth flow, redirecting through Notion, then reuses the token issued
+by the embedded authorization server. For production deployments, configure
+Redis-backed session storage so tokens survive pod restarts. See
+[Horizontal scaling](../guides-k8s/run-mcp-k8s.mdx#horizontal-scaling) and
+[Redis session storage](../guides-k8s/redis-session-storage.mdx).
+
@@ -158,8 +265,12 @@ Here are some sample prompts you can use to interact with the Notion MCP server:
- **Be cautious with updates**: The MCP server can modify and delete content in
your Notion workspace. Always review changes before applying them to important
pages or databases.
-- **Handle authentication errors**: If you see authentication errors, restart
- the server to trigger a new OAuth flow: `thv restart notion-remote`
+- **Handle authentication errors**: On the CLI or UI, restart the server to
+ trigger a new OAuth flow: `thv restart notion-remote`. On Kubernetes, a proxy
+ pod restart invalidates all sessions unless you've configured Redis-backed
+ session storage, so every client must re-authenticate; with Redis-backed
+ storage, only re-authenticate a client whose underlying Notion token was
+ revoked or can't be refreshed.
## Troubleshooting
@@ -168,20 +279,42 @@ Here are some sample prompts you can use to interact with the Notion MCP server:
If OAuth authentication fails or times out:
+**CLI or UI:**
+
1. Ensure the callback port is not blocked by a firewall
2. Check that your browser allows pop-ups from ToolHive
3. Try restarting the server with `thv restart notion-remote`
+**Kubernetes:** Your client authenticates against the embedded authorization
+server, which then completes a separate OAuth exchange with Notion using your
+registered client credentials. Check the proxy pod logs for errors from either
+leg of that flow:
+
+```bash
+kubectl logs -n toolhive-system -l app.kubernetes.io/instance=notion | grep -i oauth
+```
+
+The most common causes are a path segment in `issuer` or a `redirectUri` that
+doesn't exactly match what you registered with Notion. See the note above.
+
Server shows "Running" but tools don't work
-The server may have lost authentication. Restart the server to re-authenticate:
+The server may have lost authentication.
+
+**CLI or UI:** Restart the server to re-authenticate:
```bash
thv restart notion-remote
```
+**Kubernetes:** Check whether the proxy pod restarted recently
+(`kubectl get pods -n toolhive-system`). Without Redis-backed session storage, a
+restart invalidates every session and all clients must re-authenticate. With
+Redis-backed storage, this instead means the affected client's underlying Notion
+token was revoked or couldn't be refreshed; re-authenticate from that client.
+
Can't find content in search results