> ## Documentation Index
> Fetch the complete documentation index at: https://docs.formal.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Encrypt MCP OAuth Tokens

> Encrypt Notion MCP OAuth tokens on the Formal Endpoint or Connector

export const G = ({term, anchor, children}) => {
  const href = anchor ? `/docs/glossary/index#${anchor}` : `/docs/glossary/index`;
  return <a href={href} className="glossary-link" style={{
    textDecoration: "underline",
    textDecorationLine: "underline",
    textDecorationColor: "#6b7280",
    textDecorationThickness: "1px",
    textUnderlineOffset: "2px",
    color: "inherit",
    transition: "text-decoration-color 0.2s ease",
    borderBottom: "none"
  }} onMouseEnter={e => e.target.style.textDecorationColor = "#fff"} onMouseLeave={e => e.target.style.textDecorationColor = "#6b7280"}>
  {children || term}
</a>;
};

> **Outcome:** You can encrypt OAuth tokens so clients only see token ciphertexts instead of plaintext Bearer tokens. This example focuses on Notion MCP OAuth, but this approach should extend to any OAuth DCR flow.\
> **Prerequisites:** The Formal Endpoint [transparent proxy](/docs/guides/client-apps/desktop-app#transparent-proxy) on macOS. Use Secure Enclave encryption on the Endpoint, or forward traffic to a <G anchor="connector">Connector</G> with [token encryption](/docs/guides/core-concepts/connectors/token_encryption).

## Overview

[Notion MCP](https://developers.notion.com/docs/mcp) (`https://mcp.notion.com/mcp`) issues OAuth tokens from the same host (`POST /token`). Clients then send `Authorization: Bearer <token>` on MCP calls.

Formal can encrypt those tokens in two places:

| Where encryption runs | Key material         | When to use                                                 |
| --------------------- | -------------------- | ----------------------------------------------------------- |
| **Formal Endpoint**   | macOS Secure Enclave | Encrypt on the device; no traffic through Connector         |
| **Connector**         | Cloud KMS KEK        | Encrypt off the device; route MCP traffic through Connector |

In both cases:

1. An **encrypt** <G anchor="policy">policy</G> encrypts `access_token` and `refresh_token` on the OAuth token response.
2. The MCP client stores `formalsealed:v1:...`.
3. A **decrypt** policy decrypts `Authorization` before Notion MCP sees the request.

The MCP client keeps using `https://mcp.notion.com`. You do not rewrite the MCP URL.

## Policies

These policies apply whether encryption runs on the Endpoint or the Connector.

### Encrypt OAuth tokens

<Tabs>
  <Tab title="Web Console">
    1. Navigate to [Policies](https://app.formal.ai/policies)
    2. Click **Create Policy**
    3. Set name to `notion-mcp-encrypt-oauth-tokens` and add a description
    4. Paste the Rego below into the editor
    5. Click **Create Policy** to save

    ```rego theme={null}
    package formal.v2

    import future.keywords.if

    response := {
      "action": "encrypt",
      "targets": [
        {"kind": "json", "name": "access_token"},
        {"kind": "json", "name": "refresh_token"}
      ],
      "reason": "Encrypt Notion OAuth tokens before they leave the Connector"
    } if {
      input.resource.hostname == "mcp.notion.com"
      input.http.method == "POST"
      input.http.path == "/token"
    }
    ```
  </Tab>

  <Tab title="Terraform">
    ```hcl theme={null}
    resource "formal_policy" "notion_mcp_encrypt_oauth_tokens" {
      name        = "notion-mcp-encrypt-oauth-tokens"
      description = "Encrypt Notion MCP OAuth tokens on the token response"
      status      = "active"

      module = <<-EOT
        package formal.v2

        import future.keywords.if

        response := {
          "action": "encrypt",
          "targets": [
            {"kind": "json", "name": "access_token"},
            {"kind": "json", "name": "refresh_token"}
          ],
          "reason": "Encrypt Notion OAuth tokens before they leave the Connector"
        } if {
          input.resource.hostname == "mcp.notion.com"
          input.http.method == "POST"
          input.http.path == "/token"
        }
      EOT
    }
    ```
  </Tab>
</Tabs>

### Decrypt Authorization on MCP requests

<Tabs>
  <Tab title="Web Console">
    1. Navigate to [Policies](https://app.formal.ai/policies)
    2. Click **Create Policy**
    3. Set name to `notion-mcp-decrypt-authorization` and add a description
    4. Paste the Rego below into the editor
    5. Click **Create Policy** to save

    ```rego theme={null}
    package formal.v2

    import future.keywords.if

    request := {
      "action": "decrypt",
      "targets": [
        {"kind": "header", "name": "Authorization"}
      ],
      "reason": "Decrypt Notion tokens for the upstream MCP server"
    } if {
      input.resource.hostname == "mcp.notion.com"
    }
    ```
  </Tab>

  <Tab title="Terraform">
    ```hcl theme={null}
    resource "formal_policy" "notion_mcp_decrypt_authorization" {
      name        = "notion-mcp-decrypt-authorization"
      description = "Decrypt Notion tokens on MCP Authorization headers"
      status      = "active"

      module = <<-EOT
        package formal.v2

        import future.keywords.if

        request := {
          "action": "decrypt",
          "targets": [
            {"kind": "header", "name": "Authorization"}
          ],
          "reason": "Decrypt Notion tokens for the upstream MCP server"
        } if {
          input.resource.hostname == "mcp.notion.com"
        }
      EOT
    }
    ```
  </Tab>
</Tabs>

Decrypt supports embedded encrypted tokens, so `Bearer formalsealed:v1:...` becomes `Bearer <plaintext>` for Notion. Values without an encrypted token pass through unchanged.

<Warning>
  Encrypt is fail-closed. If encryption fails, Formal blocks the response. If decryption fails, Formal forwards the request unchanged.
</Warning>

## Encrypt on the Formal Endpoint (Secure Enclave)

On macOS, the Formal Endpoint can encrypt and decrypt tokens with the device Secure Enclave. Traffic stays on the Endpoint. You do not set **Forward to Connector**.

Create a network rule that matches Notion MCP. No Connector listener or KMS key is required for this path.

<Tabs>
  <Tab title="Web Console">
    1. Navigate to [Network Rules](https://app.formal.ai/network-rules) (or use Network Rules in the Desktop App)
    2. Create a rule named `notion-mcp-endpoint`
    3. Match hostname `mcp.notion.com` on the pre-TLS condition
    4. Leave **Forward to Connector** unset / false
    5. Save the rule
  </Tab>

  <Tab title="Terraform">
    ```hcl theme={null}
    resource "formal_network_rule" "notion_mcp_endpoint" {
      name        = "notion-mcp-endpoint"
      description = "Intercept Notion MCP for Secure Enclave token encryption"
      status      = "active"

      cel_expression = <<-EOT
        {
          "condition": {
            "pre_tls": hostname == "mcp.notion.com",
            "post_tls": true
          },
          "outputs": {}
        }
      EOT
    }
    ```
  </Tab>
</Tabs>

**Verify:**

1. Enable the transparent proxy: `formal transparent-proxy enable`
2. Confirm the network rule is **Active**
3. Complete Notion MCP OAuth. Confirm `access_token` starts with `formalsealed:v1:`

<Note>
  Endpoint `encrypt` is macOS-only. See [Encrypt & Decrypt](/docs/guides/policies/enforcement#encrypt--decrypt-actions).
</Note>

## Encrypt on the Connector

To encrypt tokens on the Connector, forward Notion MCP through a Connector and attach a [token encryption key](/docs/guides/core-concepts/connectors/token_encryption) (`formal_connector_token_encryption_key`). Without that KEK, Connector `encrypt` fails closed and blocks the OAuth response.

Set up the following:

1. An MCP resource for `mcp.notion.com`
2. A Connector listener and listener rule so the resource is reachable
3. A **token encryption key** on the Connector (cloud KMS KEK)
4. A network rule with `forward_to_connector` set to `true`

<Tabs>
  <Tab title="Web Console">
    1) Create an MCP resource for hostname `mcp.notion.com` on port `443`
    2) On your Connector, add a listener on port `443` and a listener rule that points at that resource
    3) Attach a [token encryption key](/docs/guides/core-concepts/connectors/token_encryption) to the Connector
    4) Create a network rule named `notion-mcp-connector` that matches `mcp.notion.com` and sets **Forward to Connector**
  </Tab>

  <Tab title="Terraform">
    ```hcl theme={null}
    resource "formal_resource" "notion_mcp" {
      name       = "notion-mcp"
      technology = "mcp"
      hostname   = "mcp.notion.com"
      port       = 443
    }

    resource "formal_connector_listener" "notion_mcp_listener" {
      connector_id = formal_connector.example.id
      name         = "notion-mcp-listener"
      port         = 443
    }

    resource "formal_connector_listener_rule" "notion_mcp" {
      connector_listener_id = formal_connector_listener.notion_mcp_listener.id
      type                  = "resource"
      rule                  = formal_resource.notion_mcp.id
    }

    resource "formal_connector_token_encryption_key" "example" {
      connector_id = formal_connector.example.id
      key_provider = "aws-kms"
      key_id       = aws_kms_key.formal_token_kek.arn
    }

    resource "formal_network_rule" "notion_mcp_connector" {
      name        = "notion-mcp-connector"
      description = "Forward Notion MCP to the Connector for token encryption"
      status      = "active"

      cel_expression = <<-EOT
        {
          "condition": {
            "pre_tls": hostname == "mcp.notion.com",
            "post_tls": true
          },
          "outputs": {
            "resource_name": "notion-mcp",
            "forward_to_connector": true
          }
        }
      EOT
    }
    ```
  </Tab>
</Tabs>

See [Token Encryption](/docs/guides/core-concepts/connectors/token_encryption) for GCP and Azure KEK setup and IAM requirements.

<Warning>
  **Forward to Connector** must be `true`. Without it, the Endpoint does not send Notion traffic to the Connector for KMS-backed encrypt and decrypt.
</Warning>

**Verify:**

1. Notion MCP appears under the Connector's **Reachable Resources**
2. The Connector has a token encryption key configured
3. The network rule is **Active** with **Forward to Connector**
4. Complete Notion MCP OAuth. Confirm encrypted `formalsealed:v1:` tokens and successful MCP tool calls

## Add Notion MCP and complete OAuth

After policies and routing are in place, add Notion MCP as usual. Formal does not change Dynamic Client Registration (DCR) or the OAuth browser flow.

1. Add the Notion MCP server at `https://mcp.notion.com/mcp`. For Claude Code:

```bash theme={null}
claude mcp add -t http notion https://mcp.notion.com/mcp
```

You can also add it from the Formal console registry or another MCP client config.

2. Complete DCR and OAuth when the client prompts you.

Network rules keep intercepting `mcp.notion.com`, so Claude Code still uses the real Notion URL. Encrypted tokens are what the client persists after `/token`.

## Verify end to end

1. Keep the MCP client pointed at `https://mcp.notion.com/mcp`.
2. Finish OAuth through Claude Code (or your MCP client).
3. Call a Notion MCP tool. The call should succeed.
4. Confirm encrypt on `POST /token` and decrypt on later MCP requests in Formal logs.

### Inspect encrypted tokens in Claude Code (macOS)

Claude Code stores MCP OAuth credentials in the macOS Keychain. After OAuth completes, read that entry and confirm Notion tokens are encrypted:

```bash theme={null}
security find-generic-password -s "Claude Code-credentials" -w | jq .
```

Look under the Notion server entry (key shape like `notion|<id>`). Both `accessToken` and `refreshToken` should start with `formalsealed:v1:`:

```json theme={null}
{
  "mcpOAuth": {
    "notion|<CLIENT_OR_SERVER_ID>": {
      "serverName": "notion",
      "serverUrl": "https://mcp.notion.com/mcp",
      "accessToken": "formalsealed:v1:...",
      "refreshToken": "formalsealed:v1:...",
      "discoveryState": {
        "authorizationServerUrl": "https://mcp.notion.com",
        "resourceMetadataUrl": "https://mcp.notion.com/.well-known/oauth-protected-resource/mcp",
        "oauthMetadataFound": true
      },
      "scope": "default"
    }
  }
}
```

**Verify:**

```bash theme={null}
security find-generic-password -s "Claude Code-credentials" -w \
  | jq -r '.. | objects | select(.serverUrl? == "https://mcp.notion.com/mcp") | .accessToken' \
  | head -n 1
# Expected: a value that starts with formalsealed:v1:
```

If `accessToken` is a raw Notion token instead of `formalsealed:v1:...`, encrypt did not run on `POST /token` (check the network rule, policies, and Connector token encryption key if you use the Connector path).

<Note>
  Keychain entry names can vary by Claude Code version. If the command finds no password, search Keychain Access for Claude credentials or re-run OAuth after confirming the transparent proxy is enabled.
</Note>

## Troubleshooting

<AccordionGroup>
  <Accordion title="Token response is not encrypted">
    **Possible causes:**

    * Transparent proxy or network rule is inactive
    * Connector path missing `forward_to_connector` or KEK
    * Policy path does not match `POST /token`

    **Fix:**

    1. Run `formal transparent-proxy status`
    2. Set the network rule to **Active**
    3. For Connector encryption, confirm the token encryption key, listener rule, and `forward_to_connector`
  </Accordion>

  <Accordion title="MCP calls return 401 Unauthorized">
    **Possible causes:**

    * Encrypt ran, but decrypt did not match `mcp.notion.com`
    * Encrypted token came from a different Endpoint or Connector KEK
    * Client truncated the `formalsealed:v1:` value

    **Fix:**

    1. Confirm the decrypt policy matches `input.resource.hostname == "mcp.notion.com"`
    2. Re-authenticate after rotating keys
    3. Keep the full encrypted string in `Authorization: Bearer ...`
  </Accordion>
</AccordionGroup>

## Next Steps

<CardGroup cols={2}>
  <Card title="MCP Resources" icon="robot" href="/docs/guides/core-concepts/resources/mcp">
    Proxy and govern MCP traffic
  </Card>

  <Card title="Token Encryption" icon="key" href="/docs/guides/core-concepts/connectors/token_encryption">
    Configure the Connector KMS KEK
  </Card>

  <Card title="Encrypt & Decrypt" icon="gavel" href="/docs/guides/policies/enforcement#encrypt--decrypt-actions">
    Full action reference
  </Card>

  <Card title="Desktop App" icon="desktop" href="/docs/guides/client-apps/desktop-app#transparent-proxy">
    Enable the transparent proxy
  </Card>
</CardGroup>
