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

# Connecting Clients

export const McpJsonGenerator = ({variant = "windsurf"}) => {
  const [name, setName] = useState("");
  const [url, setUrl] = useState("");
  const [token, setToken] = useState("");
  const [showToken, setShowToken] = useState(false);
  const [copied, setCopied] = useState(false);
  const trimmedName = name.trim();
  const trimmedUrl = url.trim();
  const trimmedToken = token.trim();
  const serverKey = trimmedName || "xano";
  const urlValue = trimmedUrl || "<url>";
  let serverConfig;
  let config;
  if (variant === "vscode") {
    serverConfig = {
      type: "http",
      url: urlValue
    };
    if (trimmedToken) {
      serverConfig.headers = {
        Authorization: "Bearer " + trimmedToken
      };
    }
    config = {
      servers: {
        [serverKey]: serverConfig
      }
    };
  } else if (variant === "warp") {
    serverConfig = {
      url: urlValue
    };
    if (trimmedToken) {
      serverConfig.headers = {
        Authorization: "Bearer " + trimmedToken
      };
    }
    config = {
      mcpServers: {
        [serverKey]: serverConfig
      }
    };
  } else {
    serverConfig = {
      serverUrl: urlValue
    };
    if (trimmedToken) {
      serverConfig.headers = {
        Authorization: "Bearer " + trimmedToken
      };
    }
    config = {
      mcpServers: {
        [serverKey]: serverConfig
      }
    };
  }
  const jsonString = JSON.stringify(config, null, 2);
  const handleCopy = () => {
    navigator.clipboard.writeText(jsonString);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };
  const inputClass = "w-full px-2 py-1.5 border rounded-md text-xs bg-white dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100";
  return <div className="not-prose border rounded-lg bg-gray-50 dark:bg-gray-800 transition-colors overflow-hidden">
      <div className="p-5">
        <div className="flex items-center justify-between mb-4">
          <p className="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
            Connection Details
          </p>
          {(trimmedName || trimmedUrl || trimmedToken) && <button onClick={() => {
    setName("");
    setUrl("");
    setToken("");
    setShowToken(false);
  }} className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
              Clear all
            </button>}
        </div>

        <div className="space-y-4">
          <div>
            <label className="block text-sm font-medium mb-1">Server name</label>
            <input type="text" placeholder="e.g. xano" value={name} onChange={e => setName(e.target.value)} className={inputClass} />
            <p className="mt-1 text-xs text-gray-400 dark:text-gray-500">
              The key used in your MCP config file.
            </p>
          </div>

          <div>
            <label className="block text-sm font-medium mb-1">Connection URL</label>
            <input type="text" placeholder="https://xxxx.n7d.xano.io/x2/mcp/meta/mcp/streaming" value={url} onChange={e => setUrl(e.target.value)} className={inputClass} />
          </div>

          <div>
            <label className="block text-sm font-medium mb-1">
              Auth token
              <span className="ml-1 font-normal text-gray-400 dark:text-gray-500">(optional)</span>
            </label>
            <div className="relative">
              <input type={showToken ? "text" : "password"} placeholder="eyJ..." value={token} onChange={e => setToken(e.target.value)} className={inputClass + " pr-16"} />
              {trimmedToken && <button onClick={() => setShowToken(!showToken)} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
                  {showToken ? "Hide" : "Show"}
                </button>}
            </div>
          </div>
        </div>
      </div>

      <div className="border-t border-gray-200 dark:border-gray-700" />

      <div className="p-5">
        <p className="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500 mb-3">
          Generated JSON
        </p>
        <pre className="p-3 bg-gray-900 text-green-300 text-xs rounded-md overflow-auto whitespace-pre-wrap break-all">
          {jsonString}
        </pre>
        <button onClick={handleCopy} className="mt-2 px-3 py-1.5 border border-gray-300 dark:border-gray-600 text-xs text-gray-500 dark:text-gray-400 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
          {copied ? "Copied!" : "Copy JSON"}
        </button>
      </div>
    </div>;
};

export const CursorInstallLink = () => {
  const [name, setName] = useState("");
  const [url, setUrl] = useState("");
  const [token, setToken] = useState("");
  const [showToken, setShowToken] = useState(false);
  const [copied, setCopied] = useState(false);
  const [expanded, setExpanded] = useState(false);
  const trimmedUrl = url.trim();
  const trimmedToken = token.trim();
  const trimmedName = name.trim();
  const isUrlValid = trimmedUrl.length > 0 && trimmedUrl.startsWith("https://") && trimmedUrl.includes("x2/mcp");
  const hasSSE = trimmedUrl.replace(/\/+$/, "").endsWith("/sse");
  const isTokenValid = !trimmedToken || trimmedToken.startsWith("eyJ");
  const isNameProvided = trimmedName.length > 0;
  const hasAnyInput = trimmedName || trimmedUrl || trimmedToken;
  let mcpUrl = "";
  let webInstallUrl = "";
  let deepLinkUrl = "";
  if (isUrlValid) {
    let normalized = trimmedUrl.replace(/\/+$/, "");
    if (normalized.endsWith("/sse")) {
      normalized = normalized.slice(0, -4) + "/streaming";
    }
    if (!normalized.endsWith("/streaming")) {
      normalized = normalized + "/streaming";
    }
    mcpUrl = normalized;
    const serverConfig = {
      url: mcpUrl
    };
    if (trimmedToken) {
      serverConfig.headers = {
        Authorization: "Bearer " + trimmedToken
      };
    }
    const base64Config = btoa(JSON.stringify(serverConfig));
    const encodedConfig = encodeURIComponent(base64Config);
    const encodedName = encodeURIComponent(trimmedName || "xano-mcp");
    webInstallUrl = `https://cursor.com/en/install-mcp?name=${encodedName}&config=${encodedConfig}`;
    deepLinkUrl = `cursor://anysphere.cursor-deeplink/mcp/install?name=${encodedName}&config=${encodedConfig}`;
  }
  const showOutput = isUrlValid && isTokenValid && isNameProvided && mcpUrl;
  const handleCopy = () => {
    navigator.clipboard.writeText(deepLinkUrl);
    setCopied(true);
    setTimeout(() => setCopied(false), 2000);
  };
  const handleClear = () => {
    setName("");
    setUrl("");
    setToken("");
    setShowToken(false);
    setCopied(false);
    setExpanded(false);
  };
  const inputClass = "w-full px-2 py-1.5 border rounded-md text-xs bg-white dark:bg-gray-700 dark:border-gray-600 dark:text-gray-100";
  return <div className="not-prose border rounded-lg bg-gray-50 dark:bg-gray-800 transition-colors overflow-hidden">

      {}
      <div className="p-5">
        <div className="flex items-center justify-between mb-4">
          <p className="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
            Connection Details
          </p>
          {hasAnyInput && <button onClick={handleClear} className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
              Clear all
            </button>}
        </div>

        <div className="space-y-4">
          {}
          <div>
            <label className="block text-sm font-medium mb-1">Server name</label>
            <input type="text" placeholder="e.g. xano-meta" value={name} onChange={e => setName(e.target.value)} className={inputClass} />
            <p className="mt-1 text-xs text-gray-400 dark:text-gray-500">
              Shows up in Cursor's MCP server list.
            </p>
          </div>

          {}
          <div>
            <label className="block text-sm font-medium mb-1">MCP server URL</label>
            <input type="text" placeholder="https://xxxx.n7d.xano.io/x2/mcp/meta/mcp/streaming" value={url} onChange={e => setUrl(e.target.value)} className={inputClass} />
            {trimmedUrl && !trimmedUrl.startsWith("https://") ? <p className="mt-1 text-xs text-red-500">URL must start with https://</p> : trimmedUrl && !trimmedUrl.includes("x2/mcp") ? <p className="mt-1 text-xs text-red-500">URL must contain x2/mcp</p> : hasSSE ? <p className="mt-1 text-xs text-amber-500">
                /sse will be replaced with /streaming for Cursor's streamable HTTP transport.
              </p> : <p className="mt-1 text-xs text-gray-400 dark:text-gray-500">
                The <code className="text-xs">/mcp/...</code> endpoint Cursor will connect to.
              </p>}
          </div>

          {}
          <div>
            <label className="block text-sm font-medium mb-1">
              Auth token
              <span className="ml-1 font-normal text-gray-400 dark:text-gray-500">(optional)</span>
            </label>
            <div className="relative">
              <input type={showToken ? "text" : "password"} placeholder="eyJ..." value={token} onChange={e => setToken(e.target.value)} className={inputClass + " pr-16"} />
              {trimmedToken && <button onClick={() => setShowToken(!showToken)} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
                  {showToken ? "Hide" : "Show"}
                </button>}
            </div>
            {trimmedToken && !isTokenValid ? <p className="mt-1 text-xs text-red-500">Token should be a valid JWE (starts with eyJ)</p> : trimmedToken && isTokenValid ? <p className="mt-1 text-xs text-green-500">Valid token format</p> : <p className="mt-1 text-xs text-gray-400 dark:text-gray-500">
                Sent as an Authorization header. Rotate if the link is shared.
              </p>}
          </div>
        </div>
      </div>

      {}
      {showOutput && <>
          <div className="border-t border-gray-200 dark:border-gray-700" />

          {}
          <div className="p-5">
            <p className="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500 mb-4">
              Install
            </p>

            <div className="flex items-center gap-3">
              {}
              <a href={webInstallUrl} target="_blank" rel="noopener noreferrer">
                <img src="https://cursor.com/deeplink/mcp-install-dark.svg" alt="Install MCP Server in Cursor" style={{
    height: "32px"
  }} />
              </a>

              {}
              <button onClick={handleCopy} className="px-3 py-1.5 border border-gray-300 dark:border-gray-600 text-xs text-gray-500 dark:text-gray-400 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
                {copied ? "Copied!" : "Copy install link"}
              </button>

              {}
              <button onClick={() => setExpanded(!expanded)} className="px-3 py-1.5 border border-gray-300 dark:border-gray-600 text-xs text-gray-500 dark:text-gray-400 rounded-md hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors">
                {expanded ? "Hide link preview" : "Show link preview"}
              </button>
            </div>

            <div>
              {expanded && <pre className="mt-2 p-3 bg-gray-900 text-green-300 text-xs rounded-md overflow-auto whitespace-pre-wrap break-all">
                  {deepLinkUrl}
                </pre>}
            </div>
          </div>
        </>}
    </div>;
};

## Before We Begin

Gather the following information, which you'll need regardless of client.

<Steps>
  <Step title="Name">
    This is just a name you want to give your MCP server. Make sure it is unique to other MCP servers you're using, and is human readable so you can easily keep track of what each server does.
  </Step>

  <Step title="URL">
    **For the Xano MCP Server**

    > Each instance has its own unique connection URL. The URL can be found inside of your Instance Settings panel from the instance selection screen by clicking the  icon next to the instance you want to connect to.

    > When creating a URL, you'll need to choose the scope of tools the URL provides. Each client will have its own limits on how many tools can be available at once, so make sure to consult your client's documentation for more information.

    **For an MCP Server you built in Xano**

    <span id="ssedep" />

    > Click the  button next to the MCP server you want in the  >  screen.

    <Note>
      SSE as a connection method has entered deprecation as of [release 2.5](/updates) and will be fully sunset in the upcoming release 2.6. Per the official MCP SDK, you should be using streaming exclusively going forward.
    </Note>
  </Step>

  <Step title="Token">
    **For the Xano MCP Server**

    > The Xano MCP Server token can be found in the same place you retrieved your URL. See [Generating an Access Token](/xano-features/metadata-api#generate-an-access-token) for more information.

    **For an MCP Server you built in Xano**

    > The token for a custom MCP server is generated the same way your other auth tokens are generated, usually as part of your `signup` and `login` API logic.
  </Step>
</Steps>

<Note>
  Most clients authenticate to Xano MCP servers with a Bearer token sent in an `Authorization` header. Clients that can't send a header — like Claude Desktop and Claude Web — can still connect by placing a token in the URL and validating it server-side. See [Clients without header support](#clients-without-header-support) below.
</Note>

***

<Columns cols={4}>
  <Card title="Claude Code" icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/claude-ai-icon.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=b9de3a97860e4296c6d48664852a8e16" href="#claude-code" width="256" height="257" data-path="images/icons/claude-ai-icon.svg" />

  <Card title="Cursor" icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/Cursor_light.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=c42cd27f6fa1b802cadd5a3514d7ff46" href="#cursor" width="467" height="532" data-path="images/icons/Cursor_light.svg" />

  <Card title="Windsurf" icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/Windsurf_light.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=ad7fc1569a810d534939040c60c6cc78" href="#windsurf" width="1024" height="1024" data-path="images/icons/Windsurf_light.svg" />

  <Card title="Antigravity" icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/antigravity.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=d3a59a03aa32cd67b8d38b666814bd61" href="#antigravity" width="16" height="15" data-path="images/icons/antigravity.svg" />

  <Card title="VS Code" icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/vscode.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=bb6c91058fcbe6ee28fcda04e03de2e6" href="#vs-code" width="100" height="100" data-path="images/icons/vscode.svg" />

  <Card title="Raycast" icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/raycast.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=ce2c71c7938f28d0f8172df87a7d823e" href="#raycast" width="28" height="28" data-path="images/icons/raycast.svg" />

  <Card title="Warp" icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/warp.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=b3224840b675a000ceca342e8b72ec54" href="#warp" width="400" height="400" data-path="images/icons/warp.svg" />

  <Card title="Claude Desktop / Web" icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/claude-ai-icon.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=b9de3a97860e4296c6d48664852a8e16" href="#clients-without-header-support" width="256" height="257" data-path="images/icons/claude-ai-icon.svg" />
</Columns>

***

## <Icon icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/claude-ai-icon.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=b9de3a97860e4296c6d48664852a8e16" width="256" height="257" data-path="images/icons/claude-ai-icon.svg" /> Claude Code

In your terminal, run the following command, replacing `<name>`, `<url>`, and `<token>` with your server's details:

```bash theme={null}
claude mcp add --transport http <name> <url> --header "Authorization: Bearer <token>"
```

***

## <Icon icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/Cursor_light.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=c42cd27f6fa1b802cadd5a3514d7ff46" width="467" height="532" data-path="images/icons/Cursor_light.svg" /><Icon icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/Cursor_dark.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=1f452577be9e9670a609608506ab5895" width="467" height="532" data-path="images/icons/Cursor_dark.svg" /> Cursor

Cursor supports one-click MCP server installation via install links, with your token sent as an `Authorization` header. Use the generator below to create yours.

<CursorInstallLink />

***

## <Icon icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/Windsurf_light.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=ad7fc1569a810d534939040c60c6cc78" width="1024" height="1024" data-path="images/icons/Windsurf_light.svg" /><Icon icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/Windsurf_dark.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=2980a14b21d7c580ffac4b5493d2102c" width="1024" height="1024" data-path="images/icons/Windsurf_dark.svg" /> Windsurf

<Steps>
  <Step title="Access your Windsurf settings">
    Head to Windsurf Settings > Cascade and click **MCP Marketplace**.

    Click the <Icon icon="gear" /> icon to access your `mcp_config.json` file directly.
  </Step>

  <Step title="Add an entry for your MCP server">
    Use the generator below to get the correct JSON.

    <McpJsonGenerator variant="windsurf" />
  </Step>
</Steps>

***

## <Icon icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/antigravity.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=d3a59a03aa32cd67b8d38b666814bd61" width="16" height="15" data-path="images/icons/antigravity.svg" /> Antigravity

<Steps>
  <Step title="Access your Antigravity MCP settings">
    1. Open the MCP store via the "..." dropdown at the top of the editor's agent panel.
    2. Click on "Manage MCP Servers"
    3. Click on "View raw config"
    4. Modify the mcp\_config.json with your custom MCP server configuration.
  </Step>

  <Step title="Add an entry for your MCP server">
    Use the generator below to get the correct JSON.

    <McpJsonGenerator variant="windsurf" />
  </Step>
</Steps>

***

## <Icon icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/vscode.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=bb6c91058fcbe6ee28fcda04e03de2e6" width="100" height="100" data-path="images/icons/vscode.svg" /> VS Code

These instructions may work for other VS Code-based IDEs, but we recommend consulting that client's official documentation for more specific instructions.

**Per-project**: Create or open `.vscode/mcp.json` in your workspace and add the configuration generated below.

**Across multiple projects**:

1. Run the **MCP: Open User Configuration** command, which opens the `mcp.json` file for your user profile. Add the configuration generated below.

<McpJsonGenerator variant="vscode" />

***

## <Icon icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/raycast.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=ce2c71c7938f28d0f8172df87a7d823e" width="28" height="28" data-path="images/icons/raycast.svg" /> Raycast

Raycast connects to remote MCP servers through its built-in **Install MCP Server** form, which supports custom HTTP headers. Native MCP support requires Raycast 1.98 or later.

1. Run the **Install MCP Server** command, or run **Manage MCP Servers** and choose **Install New Server**.
2. Set **Transport** to **HTTP** and enter your MCP server's **URL**.
3. Under **HTTP Headers**, add a header with the key `Authorization` and the value `Bearer <token>`. Leave the OAuth fields empty — Xano MCP servers authenticate with a static bearer token, not OAuth.
4. Press **⌘ + Enter** to install. Raycast connects to the server and loads its tools, which you can then @-mention in AI Chat.

***

## <Icon icon="https://mintcdn.com/xano-997cb9ee/aZQYcxhIvSDTNEim/images/icons/warp.svg?fit=max&auto=format&n=aZQYcxhIvSDTNEim&q=85&s=b3224840b675a000ceca342e8b72ec54" width="400" height="400" data-path="images/icons/warp.svg" /> Warp

1. Access **Warp Drive** and click **MCP Servers** in your Personal settings.
2. Click **+ Add** and add the configuration generated below.

<McpJsonGenerator variant="warp" />

***

## Clients without header support

Some MCP clients — including **Claude Desktop** and **Claude Web** — can only authenticate by putting credentials in the connection URL. Their **Add custom connector** flow accepts a URL but has no field for an `Authorization` header, so Xano's native, header-based MCP authentication won't work for them.

You can still connect these clients securely: put a token **in the URL** and validate it yourself with an MCP server trigger.

<Steps>
  <Step title="Get your server's streaming connection URL">
    Retrieve the streaming connection URL for your MCP server (see [Before We Begin](#before-we-begin)). It looks like this, with `mcp` appearing twice:

    `https://your-instance.xano.io/x2/mcp/{server-id}/mcp/streaming`
  </Step>

  <Step title="Put your token in the URL">
    Replace the **second** `mcp` with your token:

    `https://your-instance.xano.io/x2/mcp/{server-id}/{token}/streaming`

    This is the value you paste into the client's **Add custom connector** URL field. Server-side, the token is exposed as the [`token` variable](/ai-tools/mcp-builder#mcp-variables).
  </Step>

  <Step title="Do not enable native authentication on your tools in Xano">
    Native MCP authentication expects an `Authorization` header, which these clients can't send. Leave **Authentication** turned **off** on the  tools — you can enforce access with middleware or a trigger instead, so an unauthenticated client can't reach your tools without a valid URL token.
  </Step>

  <Step title="Validate the token with an MCP server trigger or middleware">
    Add an [MCP server trigger](/building/logic/triggers/mcp-servers) that runs on connection and rejects anyone whose URL token isn't valid. The token arrives on the trigger's special `toolset` object as `toolset.token`. If the trigger throws an `accessdenied` error, the connection is refused and no tools run.

    On a valid token, the trigger must return the `toolset` and `tools` objects — that's what hands the available tools back to the client — so pass them through unchanged once the check passes.

    You can also use middleware for more precise control over the user experience, but triggers are a better option if you have a mix of tools that do and do not require auth.
  </Step>
</Steps>

<Note>
  Please note that if a tool is not using native authentication, it will appear in the list of tools when connected to the MCP server (unless modified via a trigger). We recommend if at all possible, use headers instead.
</Note>

### Validate a single rotating token

The simplest setup checks the URL token against one environment variable. It's easy to rotate and is shared by everyone who has the URL — a good fit for a system-wide secret.

```xs theme={null}
mcp_server_trigger "validate_url_token" {
  mcp_server = "demo"
  actions = {connection: true}
  active = true
  description = "Reject MCP connections that don't carry a valid token in the URL"

  // Predefined MCP server trigger input — `toolset.token` is the URL token
  input {
    object toolset {
      schema {
        int id
        text name
        text instructions
        text token
      }
    }
    object[] tools {
      schema {
        int id
        text name
        text instructions
      }
    }
  }

  stack {
    var $token { value = $input.toolset.token }

    precondition ($token == $env.MCP_URL_TOKEN) {
      error_type = "accessdenied"
      error = "Invalid or missing MCP token"
    }
  }

  response = { toolset: $input.toolset, tools: $input.tools }
  history = false
}
```

### Validate per-user tokens

To attribute connections to individual users — and revoke them independently — store tokens in a table and look them up on connection.

<Expandable title="Per-user token validation (table + trigger)">
  First, a table that maps each token to a user:

  ```xs theme={null}
  table "mcp_token" {
    auth = false
    schema {
      int id
      text token { description = "The token value embedded in the MCP connection URL" }
      int user_id { description = "Maps this token to a user in your own user table" }
      text label? { description = "Human-readable note, e.g. who this token belongs to" }
      bool active?=true
      timestamp created_at?=now
    }
    index = [
      {type: "primary", field: [{name: "id"}]}
      {type: "btree|unique", field: [{name: "token"}]}
    ]
  }
  ```

  Then a trigger that looks the URL token up and rejects anything that isn't an active row:

  ```xs theme={null}
  mcp_server_trigger "validate_url_token" {
    mcp_server = "demo"
    actions = {connection: true}
    active = true
    description = "Reject MCP connections whose URL token isn't in the mcp_token table"

    input {
      object toolset {
        schema {
          int id
          text name
          text instructions
          text token
        }
      }
      object[] tools {
        schema {
          int id
          text name
          text instructions
        }
      }
    }

    stack {
      var $token { value = $input.toolset.token }

      db.get "mcp_token" {
        field_name = "token"
        field_value = $token
      } as $match

      precondition ($match != null && $match.active == true) {
        error_type = "accessdenied"
        error = "Invalid or missing MCP token"
      }
    }

    response = { toolset: $input.toolset, tools: $input.tools }
    history = false
  }
  ```
</Expandable>
