# Quickstart: MCP

In this quickstart, you'll put a [Model Context Protocol](https://modelcontextprotocol.io) server behind [Catalyst Cloud](https://docs.diagrid.io/operate/hosting/catalyst-cloud) and secure it from both sides. You will learn how to:

- Reach an MCP server through Catalyst's MCP proxy endpoint in order to dynamically provide access policy rights to the tools. This is achieved by giving the MCP server a secure identity.
- Give Catalyst the credential it needs to authenticate itself to your MCP server, without that credential ever entering the caller's code.
- Grant a caller (application or agent) access to one tool but not another, and dynamically change that policy at runtime with no redeploy.
- See why a newly registered MCP server is closed by default, and how the two security gates fail independently.

Authentication and authorization are separate gates here, and this quickstart runs them in that order on purpose: nothing works at all until Catalyst can reach your server, and even then only approved callers and tools get through.

```mermaid
---
title: MCP client and MCP server connected through Catalyst
---
flowchart LR
  CLIENT(MCP Client App)
  subgraph Catalyst
    APPID(ID: mcp-client)
    POLICY(MCP proxy endpoint
    + access policy)
    SERVERID(ID: mcp-server)
  end
  SERVER(MCP Server
  add,
  get_account_balance)

  CLIENT<-->APPID
  APPID<-->POLICY
  POLICY<-->SERVERID
  SERVERID<--"HTTP + shared secret"-->SERVER
```

The **mcp-client** never talks to the MCP server directly — it only calls Catalyst's proxy endpoint. **Catalyst** enforces the per-tool access policy before proxying anything, then authenticates itself to **mcp-server** with a shared-secret header on every request. The MCP server exposes two tools: `add`, which is harmless, and `get_account_balance`, which stands in for the kind of sensitive tool you'd want restricted to specific callers.

## 1. Prerequisites

Before you proceed, ensure you have the following prerequisites installed.

- [Diagrid Catalyst account](https://catalyst.diagrid.io/)
- [Diagrid CLI](https://docs.diagrid.io/getting-started/install-cli)
- [Git](https://git-scm.com/downloads)
- [Python 3.12+](https://www.python.org/downloads/) & [uv](https://docs.astral.sh/uv/#installation)

## 2. Log in to Catalyst

Authenticate to Diagrid Catalyst using the following command:

```bash
diagrid login
```

This command opens a new browser window where you'll be shown a confirmation code that should match the code in your terminal.
Confirm the code, and if you're not logged into Catalyst, you'll be redirected to login.

Confirm your user details are correct using the following command:

```bash
diagrid whoami
```

The expected output contains the name of the organization, your user name, and the Catalyst API endpoint.

## 3. Clone Quickstart Code

Clone the quickstart code from GitHub:

```bash
git clone https://github.com/diagridio/catalyst-quickstarts
```

Navigate to the quickstart directory:

**macOS/Linux**

```bash
cd catalyst-quickstarts/mcp-auth/python
```

**Windows**

```powershell
cd catalyst-quickstarts\mcp-auth\python
```

## 4. Install Dependencies

The client and the server share one `uv`-managed project at the quickstart root, so a single sync installs dependencies for both:

```bash
uv sync
```

## 5. Create the project, app, and MCP server

Create the Catalyst project and set it as your default, so the commands that follow don't need an explicit `--project` flag:

```bash
diagrid project create mcp-auth --use
```

Create the App identity for the caller. `mcp-client` has no MCP-specific behavior of its own — it's a plain caller — so a generic Catalyst App is all it needs:

```bash
diagrid app create mcp-client --wait
```

Now register the MCP server:

```bash
diagrid apply -f resources/mcp-server.yaml
```

The server doesn't get a plain `diagrid app create` like the client did. It needs an identity *and* its MCP-specific behavior — the proxy endpoint and the access policy — and applying the `MCPServer` resource is what grants both at once. Creating it as a plain App first would collide with that.

This registers a connection pointing at `http://localhost:8000/mcp`, with its access policy starting deny-all and no upstream credential configured yet. Nothing is listening on `localhost:8000` until the next step either, so the server isn't reachable yet.

:::note
`resources/mcp-server.yaml` should have no `headers` block at this point. You'll add one in step 7.2. If you're re-running this quickstart, see the note in step 7.1.
:::

## 6. Run the application with Catalyst Cloud

The `diagrid dev run` command launches both services locally, opens the tunnel connectivity they need, and wires Catalyst's endpoint and API token into the client's process — no manual token or endpoint copying.

```bash
diagrid dev run -f mcp-auth-quickstart.yaml --project mcp-auth --approve --skip-managed-kv --skip-managed-pubsub --skip-default-resiliency
```

This quickstart needs no state store, pub/sub broker, or default resiliency policy, so the `--skip-*` flags keep the project to just what MCP requires.

`mcp-auth-quickstart.yaml` also points `resourcesPath` at `./resources`, so `dev run` re-applies the `mcp-server` resource you registered in step 5. That's harmless, since applying is idempotent, and it recognizes the App identity is already managed by the `MCPServer` resource.

Because `mcp-server` declares an `appPort`, it gets a secure tunnel from Catalyst Cloud to `localhost:8000` — reachable from your hosted project with no public endpoint or inbound firewall rule. `mcp-client` declares no port, since it only calls out to Catalyst and never receives inbound requests, so no tunnel is opened for it.

:::tip
Wait until you see application logs for both apps in the terminal before continuing. Leave `diagrid dev run` running in this terminal and use a second terminal for the steps below.
:::

Catalyst starts periodically probing the MCP server's reachability as soon as its tunnel is up, independent of anything the client does. This terminal may already show rejected requests before you trigger anything yourself.

## 7. Exercise the two security gates

### 7.1 See it fail closed

A freshly registered MCP server is closed in two ways at once: it requires a shared-secret header that Catalyst hasn't been given, and its access policy denies every caller and every tool by default.

In a second terminal, trigger the client to open a fresh MCP session and return the list of available tools:

**macOS/Linux**

```bash
curl -s -X POST http://localhost:5001/run | python -m json.tool
```

**Windows**

```powershell
Invoke-RestMethod -Method Post -Uri 'http://localhost:5001/run' | ConvertTo-Json -Depth 6
```

Both problems look identical from here — `Session terminated`, with no detail:

```json
{
    "tools": [],
    "add_result": null,
    "balance_result": null,
    "errors": [
        { "step": "list_tools", "error": "Session terminated" },
        { "tool": "add", "error": "Session terminated" },
        { "tool": "get_account_balance", "error": "Session terminated" }
    ]
}
```

The real reason is in the `diagrid dev run` terminal, in the client's own log lines (labeled `== APP - mcp-client ==`):

```
INFO:httpx:HTTP Request: POST https://.../v1.0/diagrid/mcp/mcp-server "HTTP/1.1 404 Not Found"
```

That `404` is Catalyst's [access policy](https://docs.diagrid.io/develop/mcp/mcp-access-policies) turning the caller away before the request ever reaches the MCP server. Read it as "this caller matches no rule" rather than "the server doesn't exist" — this quickstart has no case where the server is missing.

You may also see the server's own lines (`== APP - mcp-server ==`) showing `401 Unauthorized`. That's Catalyst's background credential check, not your call: while the access policy denies you, your request never reaches the server at all.

Keep the applications running.

### 7.2 Authenticate Catalyst to the MCP server

`mcp_server/main.py` requires every request to carry an `x-mcp-shared-secret` header — the server's own defense, independent of Catalyst. Catalyst has to be given that credential before it can authenticate itself to your server on the caller's behalf.

Add a `headers` entry to `resources/mcp-server.yaml`:

```yaml
apiVersion: dapr.io/v1alpha1
kind: MCPServer
metadata:
  name: mcp-server
spec:
  endpoint:
    streamableHTTP:
      url: http://localhost:8000/mcp
      headers:
        - name: x-mcp-shared-secret
          value: local-dev-shared-secret
```

Apply the change from your second terminal. Everything you started in step 6 keeps running — nothing needs restarting:

```bash
diagrid apply -f resources/mcp-server.yaml
```

Catalyst has been retrying its own connection to the server in the background ever since the tunnel came up, so you don't need to trigger anything to see this succeed. Check the `diagrid dev run` terminal:

```
INFO:     ... "POST /mcp HTTP/1.1" 200 OK
Processing request of type ListToolsRequest
```

Catalyst now authenticates successfully and the request reaches your tool code. Trigger the client again:

**macOS/Linux**

```bash
curl -s -X POST http://localhost:5001/run | python -m json.tool
```

**Windows**

```powershell
Invoke-RestMethod -Method Post -Uri 'http://localhost:5001/run' | ConvertTo-Json -Depth 6
```

The response is unchanged — still `Session terminated` for everything. Fixing the upstream credential doesn't unlock the caller, because the access policy is a separate gate and it's still deny-all. Authentication is fixed; authorization is next.

:::tip
This quickstart uses the simplest credential option, a static header stored in Catalyst's secret store. Catalyst also supports an OAuth2 client-credentials flow and a secretless SPIFFE JWT it mints per request — see [Authentication for MCP servers](https://docs.diagrid.io/develop/mcp/mcp-authentication).
:::

### 7.3 Authorize the "add" tool only

A Catalyst MCP access policy is an allow-list of which caller App IDs may use which tools. Like the credential, you change it at runtime.

Grant `mcp-client` access to just the `add` tool:

```bash
diagrid mcpserver access grant mcp-server --caller mcp-client --allow-tools add --wait
```

:::note
`--wait` waits for the control-plane update to finish. Data-plane enforcement can lag a couple of seconds behind that, so if the very next call still looks denied, retry once.
:::

Trigger the client again:

**macOS/Linux**

```bash
curl -s -X POST http://localhost:5001/run | python -m json.tool
```

**Windows**

```powershell
Invoke-RestMethod -Method Post -Uri 'http://localhost:5001/run' | ConvertTo-Json -Depth 6
```

Now `add` is discoverable and succeeds, while `get_account_balance` is rejected with a clean `403`. The session itself no longer terminates, because the caller matches *some* rule:

```json
{
    "tools": [
        { "name": "add", "description": "Add two numbers together." }
    ],
    "add_result": "5",
    "balance_result": null,
    "errors": [
        {
            "tool": "get_account_balance",
            "error": "Client error '403 Forbidden' for url '.../v1.0/diagrid/mcp/mcp-server'",
            "status_code": 403,
            "reason": "ACCESS_DENIED"
        }
    ]
}
```

Notice that `tools` lists only `add`. Catalyst filters `tools/list` down to what the caller is granted, so an unauthorized tool isn't just un-callable — it's invisible.

### 7.4 Authorize all tools

Open the server up with a wildcard grant:

```bash
diagrid mcpserver access grant mcp-server --caller "*" --allow-tools "*" --wait
```

Trigger the client one more time:

**macOS/Linux**

```bash
curl -s -X POST http://localhost:5001/run | python -m json.tool
```

**Windows**

```powershell
Invoke-RestMethod -Method Post -Uri 'http://localhost:5001/run' | ConvertTo-Json -Depth 6
```

Both tools are now discoverable and both succeed:

```json
{
    "tools": [
        { "name": "add", "description": "Add two numbers together." },
        { "name": "get_account_balance", "description": "Look up the balance for an account. Treated as a sensitive operation." }
    ],
    "add_result": "5",
    "balance_result": "Account acct-42 balance: $1,204.53",
    "errors": []
}
```

You can preview a policy verdict at any time without calling the server, which is the fastest way to tell an authorization problem from an authentication one:

```bash
diagrid mcpserver access test mcp-server --caller mcp-client --tool get_account_balance
```

### 7.5 View in the Catalyst web console

Open the [Catalyst Cloud web console](https://catalyst.diagrid.io/), select the `mcp-auth` project, and go to its MCP Servers page. The entry for `mcp-server` shows the registered endpoint, the configured upstream credential, and the access policy rules you granted in the previous steps.

## 8. Clean Up

Press CTRL+C in the terminal that runs `diagrid dev run` to stop both applications and disconnect from Catalyst Cloud.

To delete the entire project and all provisioned resources:

```bash
diagrid project delete mcp-auth
```

## Summary

In this quickstart you:

- Registered an MCP server with Catalyst and reached it through Catalyst's MCP proxy endpoint rather than directly.
- Saw a newly registered server fail closed on both of its independent gates.
- Configured the credential Catalyst presents to your MCP server, keeping it out of the caller's code entirely.
- Granted a caller one tool but not another, then widened the policy — both at runtime, with no redeploy and no restart.

Catalyst held the upstream credential and enforced the access policy in front of your server, so the caller never handled a secret and never had a chance to invoke a tool it wasn't granted.

## Next steps

- Read [Build with MCP on Catalyst](https://docs.diagrid.io/develop/mcp) for the full picture of how Catalyst fronts MCP servers.
- Explore the other credential options in [Authentication for MCP servers](https://docs.diagrid.io/develop/mcp/mcp-authentication), including OAuth2 and secretless SPIFFE JWT.
- Learn the full rule model in [Control tool access for MCP servers](https://docs.diagrid.io/develop/mcp/mcp-access-policies).
- Register a server from the console, CLI, or YAML with [Add an MCP server](https://docs.diagrid.io/develop/mcp/mcpserver-getting-started), and browse the built-in [MCP servers reference](https://docs.diagrid.io/references/mcp-servers).
- Point a standard MCP client or agent framework at Catalyst with [Connect an MCP client](https://docs.diagrid.io/develop/mcp/connect).
