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