From 34080fe3d53cb773067ae383013b12f44110c561 Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Wed, 1 Jul 2026 12:59:15 -0400 Subject: [PATCH 1/2] Reform Notion MCP guide for Kubernetes remote proxy support The K8s tab claimed the operator doesn't support remote MCP servers with OAuth, which hasn't been true for a long time. Replace the static-integration-key workaround with a verified MCPRemoteProxy + embedded authorization server setup. Notion's remote MCP server only supports Dynamic Client Registration for third-party OAuth clients, so the guide walks through minting persistent credentials via DCR instead of a dashboard app. Callouts cover two gotchas found while verifying this end to end: the issuer field must stay path-free, and the upstream redirectUri default computed from resourceUrl breaks once resourceUrl has a path. --- docs/toolhive/guides-mcp/notion-remote.mdx | 183 ++++++++++++++++++--- 1 file changed, 156 insertions(+), 27 deletions(-) diff --git a/docs/toolhive/guides-mcp/notion-remote.mdx b/docs/toolhive/guides-mcp/notion-remote.mdx index 4e1e0821..e8ce464e 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,151 @@ 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: 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: MCPServer +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. Don't rely on the default computed +from `resourceUrl`: since `resourceUrl` includes the `/mcp` path here, the +default would land on the same unserved path. + +::: + 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 +261,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 +275,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 From 2e728cffc1aaad1fe77a024bdf5c7c915337b8b9 Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Wed, 1 Jul 2026 13:16:08 -0400 Subject: [PATCH 2/2] Address Copilot review comments on notion-remote.mdx Rephrase four spaced-hyphen sentence separators per the style guide. Clarify why this guide sets redirectUri explicitly instead of relying on the documented {resourceUrl}/oauth/callback default: that default only works for a bare-host resourceUrl, and breaks once resourceUrl has a path (as it does here), since the callback route is only ever served at the host root. Cross-references the relevant auth-k8s.mdx section instead of leaving the divergence unexplained. --- docs/toolhive/guides-mcp/notion-remote.mdx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/docs/toolhive/guides-mcp/notion-remote.mdx b/docs/toolhive/guides-mcp/notion-remote.mdx index e8ce464e..d418ce2c 100644 --- a/docs/toolhive/guides-mcp/notion-remote.mdx +++ b/docs/toolhive/guides-mcp/notion-remote.mdx @@ -86,7 +86,7 @@ server using an `MCPRemoteProxy` resource with an [embedded authorization server](../concepts/embedded-auth-server.mdx). 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 +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 @@ -111,7 +111,7 @@ curl -s -X POST https://mcp.notion.com/register \ }' ``` -Save the returned `client_id` and `client_secret` - they don't expire. The +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. Generate a signing key and an HMAC key for the embedded authorization server, @@ -201,9 +201,13 @@ 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. Don't rely on the default computed -from `resourceUrl`: since `resourceUrl` includes the `/mcp` path here, the -default would land on the same unserved path. +`/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. ::: @@ -222,7 +226,7 @@ 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 +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). @@ -291,7 +295,7 @@ kubectl logs -n toolhive-system -l app.kubernetes.io/instance=notion | grep -i o ``` 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. +doesn't exactly match what you registered with Notion. See the note above.