diff --git a/docs/toolhive/guides-mcp/notion-remote.mdx b/docs/toolhive/guides-mcp/notion-remote.mdx index 4e1e0821..e2326493 100644 --- a/docs/toolhive/guides-mcp/notion-remote.mdx +++ b/docs/toolhive/guides-mcp/notion-remote.mdx @@ -14,8 +14,9 @@ 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 an +embedded authorization server setup, described below. ## Metadata @@ -79,50 +80,137 @@ 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. -::: - -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). +Notion's remote MCP server (`mcp.notion.com`) uses Dynamic Client Registration +(RFC 7591) for third-party OAuth clients. The embedded authorization server's +`dcrConfig` field handles this automatically: it registers a client with Notion +at startup using Notion's discovery document and caches the returned +credentials, so there's no manual registration step and no client secret to +store. -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 them 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)" ``` -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: 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' + redirectUri: 'https://notion-mcp.example.com/oauth/callback' + dcrConfig: + discoveryUrl: 'https://mcp.notion.com/.well-known/oauth-authorization-server' +--- apiVersion: toolhive.stacklok.dev/v1beta1 -kind: MCPServer +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 a 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, so a path in `issuer` breaks discovery and every +authentication attempt fails with a generic "authorization header required" +error. + +For the same reason, set `redirectUri` on the upstream provider explicitly to +`/oauth/callback`, as shown above. Because `resourceUrl` includes the +`/mcp` path, the +[default computed from it](../guides-k8s/embedded-auth-server-k8s.mdx#default-callback-url-for-upstream-providers) +would land on a path nothing serves. + +::: + 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. This matters more +than usual here: the DCR-issued client credentials live in the same store as +sessions, so without Redis-backed storage, a pod restart also registers a +brand-new OAuth client with Notion instead of reusing the previous one, leaving +the old registration orphaned. See +[Horizontal scaling](../guides-k8s/run-mcp-k8s.mdx#horizontal-scaling) and +[Redis session storage](../guides-k8s/redis-session-storage.mdx). + @@ -158,8 +246,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 +260,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 +credentials obtained automatically via Dynamic Client Registration. 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 match your externally reachable hostname. 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