# MCP
URL: https://synquic.tech/docs/mcp.md

Let AI clients like Claude drive your Synq server as tools, over the Model Context Protocol. The endpoint on your panel's host, OAuth or token sign-in, what agents can do, and how to limit and review them.

import { Step, Steps } from 'fumadocs-ui/components/steps';

**MCP (Model Context Protocol)** is a standard way for AI clients (Claude, Cursor, VS Code Copilot and others) to
call an app's features as *tools*. Point one of those clients at Synq and it can list your projects, kick off
deployments, check logs, add domains, look at a server's disk usage, check that panel backups run, and more, using
the exact same permissions you'd have yourself.

The important part: it's not a second, looser door into Synq. **Every tool call re-runs the full auth and permission
stack**, so an agent can only ever see and do what its credential is allowed to, nothing more.

<Callout title="Where this lives in the dashboard">
Everything below is set up under **Settings**, **MCP**. That tab shows the exact endpoint for *your* install, a
per-client "add" recipe (Claude Code, Codex, Cursor, VS Code, Claude Desktop, Windsurf, Zed), and the list of clients
you've already connected.
</Callout>

## The endpoint

The endpoint lives on your **panel's** host, the address in `SYNQ_PUBLIC_URL`:

```text
https://app.<domain>/api/mcp
```

`app.<domain>` stands for the host you open the panel on, for example `panel.example.com`. A few things worth
knowing up front:

- It's a stateless **Streamable-HTTP JSON-RPC 2.0** endpoint: request in, response out.
- It's **`POST`-only**. An authenticated `GET` returns `405` (there's no server-to-client stream to open); an
  unauthenticated `GET` returns `401` with the pointer OAuth clients use to find the sign-in.
- **No batching**: send one JSON-RPC message per request, not an array.
- The OAuth discovery documents are on the same host: `/.well-known/oauth-authorization-server` and
  `/.well-known/oauth-protected-resource/api/mcp`.

You rarely type this URL by hand. **Settings**, **MCP** shows it pre-filled under **Endpoint**, with a copy button.

## Connect a client

There are two ways to authenticate, and most clients pick one for you. Use the tab that matches your client.

<Tabs items={['OAuth (recommended)', 'Static token']}>

<Tab value="OAuth (recommended)">

Synq is a standards-compliant **OAuth 2.1** MCP server, so capable clients need nothing but the URL.

<Steps>

<Step>
### Paste the endpoint into your client

Give the client your endpoint, `https://app.<domain>/api/mcp`. In Claude Code that's one command:

```bash
claude mcp add --transport http synq https://app.<domain>/api/mcp
```

Codex registers the server and authorizes it as two commands:

```bash
codex mcp add synq --url https://app.<domain>/api/mcp
codex mcp login synq
```

Other clients take it through their own UI; **Settings**, **MCP** has the exact snippet for each one (**Add to your
client**).
</Step>

<Step>
### Approve it in the browser

For most clients this happens on the first connect: a browser window opens by itself. With Codex it's the explicit
`codex mcp login synq` above: the server is registered without it, but every call comes back unauthorized until you
run it.

Either way, that browser page is where you **choose what the client may do**: read-only or full control, and either
all resources or a specific set of projects, servers and repositories. No credential to copy or paste.
</Step>

<Step>
### You're connected

The client can now call tools within exactly the scope you approved. The connection shows up under **Settings**,
**MCP**, **Connected clients**, where you can change or disconnect it at any time.
</Step>

</Steps>

<Callout title="The scope you pick is enforced, not advisory" type="info">
Your choices are stored as the client's grant, the same grant model as a scoped [personal access
token](/docs/api/tokens). The client can never exceed what you granted, or what you can access yourself.
</Callout>

<Callout title="Codex: local surfaces share one config; hosted tasks don't" type="warn">
The Codex **CLI, desktop app, and IDE extension** all read the same local MCP configuration, so adding Synq once
covers all three. Restart any of them that were already running: they read the config at startup.

A **hosted** Codex task is a different environment. It doesn't inherit your machine's MCP config, and it can only
reach `https://app.<domain>/api/mcp` if that environment has network access to your server. For a private or
LAN-only install, it won't: the endpoint has to be reachable from wherever the agent actually runs. A Synq install on
a public hostname (the panel at `https://app.<domain>`) is reachable; a `localhost` or private-network endpoint is
not. See [Installation](/docs/getting-started/installation) for putting an install on a public domain.
</Callout>

</Tab>

<Tab value="Static token">

For clients without OAuth support, or when you want a fixed, narrowly-scoped credential in a config file,
authenticate with a **personal access token** as a bearer credential. There's no separate MCP credential.

Create the token under **Settings**, **API tokens** (see [Authentication](/docs/security/auth#create-one)), then
drop the endpoint and token into your client's config:

```json
{
  "mcpServers": {
    "synq": {
      "url": "https://app.<domain>/api/mcp",
      "headers": { "Authorization": "Bearer synq_pat_…" }
    }
  }
}
```

For Claude Code, add the header on the command:

```bash
claude mcp add --transport http synq https://app.<domain>/api/mcp \
  --header "Authorization: Bearer synq_pat_…"
```

<Callout title="Give an agent a narrow token" type="warn">
Mint a **read-only** or **scoped** token for agents rather than handing over your full access. A read-only token
limits the agent to read tools; a scoped token confines it to exactly the projects, servers and repositories you
grant, even below your own role.
</Callout>

</Tab>

</Tabs>

## Every call is re-checked

This is the heart of the security model, and it's why MCP access is as safe as your token:

- Authenticating once resolves your **capability**, but that's used only to **filter `tools/list`**, so a client
  isn't shown tools its token could never use.
- The real gate runs on **every `tools/call`**: the endpoint builds an internal request, forwards your bearer, and
  dispatches it through the same routes, validation, auth middleware and per-resource permission check as a normal
  HTTP call.

The practical upshot: a read-only token can only reach read tools, and a scoped token stays boxed into its granted
resources. A scoped token isn't even *shown* the org-wide list and create tools: `get_projects` (list every project)
simply won't appear in its `tools/list`; it sees only the per-resource tools for the resource types it was granted.
And if a `tools/call` touches something outside that scope, it comes back `404`: the scope doing its job. For the
full protocol details (JSON-RPC methods, negotiation, error codes), see the [MCP API reference](/docs/api/mcp).

The **organization boundary** applies to MCP exactly as it does to a browser session: because every call runs
through the same per-resource permission check, an MCP client acts only within organizations you belong to and can
never reach another org's projects, servers or secrets. A resource outside your membership comes back `404`, just as
it would in the dashboard. See [Isolation](/docs/security/isolation).

Instance-wide operations (panel backups, a Docker cleanup, the certificate contact email, system jobs) additionally
need an **instance administrator's** credential, as they do in the dashboard.

## Which tools map to which API

Tools aren't a separate feature set: each one **maps onto a permission-tagged REST route**, and the tool name is
built from that route's method and path, without the `/api` prefix. So `get_projects` is `GET /api/projects`,
`post_domains` is `POST /api/domains`, and `patch_projects_by_id` is `PATCH /api/projects/:id`. Call `tools/list` to
see the exact set your token can use.

Not every route is a tool: a route becomes one only when it **opts in**, so the set is a curated slice of the API
rather than the whole surface. Each exposed tool also carries two hints a client can surface: `readOnlyHint` (the
tool only reads) and `destructiveHint` (it deletes or tears something down), so an agent UI can flag a destructive
call before it runs.

| Tool name (examples) | REST route | Reference |
|---|---|---|
| `get_projects`, `patch_projects_by_id` | `/api/projects` | [Projects API](/docs/api/projects) |
| `get_deployments`, `post_deployments_build_access` | `/api/deployments` | [Deployments API](/docs/api/deployments) |
| `get_projects_by_id_services`, `post_projects_by_id_services` | `/api/projects/:id/services` | [Services API](/docs/api/services) |
| `get_domains`, `post_domains`, `get_domains_acme`, `put_domains_acme_email` | `/api/domains` | [Domains API](/docs/api/domains) |
| `get_github_repos` | `/api/github` | [GitHub API](/docs/api/github) |
| `get_analytics` | `/api/analytics` | [Analytics API](/docs/api/analytics) |
| `get_notifications_channels` | `/api/notifications` | [Notifications API](/docs/api/notifications) |
| `get_settings`, `patch_settings_build_mode` | `/api/settings` | [Settings API](/docs/api/settings) |
| `get_projects_by_projectId_backup_policies`, `post_backup_policies_by_policyId_run` | `/api/backups` | [Backups API](/docs/api/backups) |
| `get_system_servers_by_id_docker_disk_usage`, `post_system_servers_by_id_docker_cleanup_preview`, `get_system_servers_by_id_cpu_reservations` | `/api/system/servers/:id/...` | [Servers API](/docs/api/servers) |
| `get_system_panel_backup`, `post_system_panel_backup_run`, `get_system_docker_cleanup_schedule` | `/api/system/...` | [System API](/docs/api/system) |
| `get_jobs`, `post_jobs_by_key_run` | `/api/jobs` | [Jobs guide](/docs/guides/jobs) |
| `get_apps_catalog`, `post_apps` | `/api/apps` | [App catalog](/docs/api/apps) |
| `get_updates`, `post_updates_scan` | `/api/updates` | [Updating Synq](/docs/guides/updating) |

<Callout title="Credentials and auth are never exposed" type="info">
The `tokens`, `auth` and `mcp` modules are hard-blocked from ever becoming tools: a full-access MCP token can't mint
itself a fresh credential or escape its own scope. Manage tokens and connected clients from the dashboard or the
[Tokens API](/docs/api/tokens), not from an agent.
</Callout>

## Guided flows

Some tasks are a chain of calls in a specific order: deploying a repo, uploading a folder, installing a catalog app.
The server ships a small set of **prompts** (`prompts/list` / `prompts/get`) that spell those chains out step by
step, referencing the exact tools to call. Compatible clients surface them as slash-commands or starters, so an agent
can pick a flow and follow it rather than reverse-engineering the order from the flat tool list. See the
[MCP API reference](/docs/api/mcp#guided-flows-prompts) for the full catalog.

## Verify from the terminal

To confirm a token works and see what it can do, ask for the tool list:

```bash
curl -s https://app.<domain>/api/mcp \
  -H "Authorization: Bearer synq_pat_…" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

This returns exactly the tools that token is allowed to call.

## Manage connected clients

Every OAuth client you've approved is listed under **Settings**, **MCP**, **Connected clients**, tagged
**Read-only** or **Full control**, with **All resources** or how many resources it can reach, when it was authorized,
when it was last used and how many calls it made.

- **Edit access** changes what the client can reach. Changes apply on the agent's next request; it doesn't need to
  reconnect. Removing every restriction asks for a second confirmation.
- **Activity** opens the [audit log](/docs/guides/audit-log) filtered to that client, so you see exactly what it did.
  Everything an agent changes is recorded with the source **AI assistant**.
- **Disconnect** revokes the client's issued tokens immediately and drops its consent, so it has to re-authorize
  next time.

The same list and controls are available programmatically via the [Tokens API](/docs/api/tokens).

## After the panel moves

OAuth tokens are issued for one endpoint address. If you move the panel to another host (see
[Domains for the panel and your apps](/docs/guides/server-domains#move-the-panel-to-another-host)), the old
tokens are refused at the new address: remove Synq from each OAuth client, add `https://<new host>/api/mcp`, and
approve it again. Clients that use a static token only need the new URL.

## Troubleshooting

<Callout title="401: Missing or invalid access token" type="error">
There's no bearer credential, or it's expired or revoked. The response includes a `WWW-Authenticate` header so OAuth
clients can start discovery; if you're using a static token, mint a fresh one under **Settings**, **API tokens** and
update your client config.
</Callout>

<Callout title="401: Access token was issued for a different resource" type="error">
The client reached a different address than the one it was authorized for, typically after the panel moved or when
the client uses the website's host. Point the client at `https://app.<domain>/api/mcp` and authorize it again.
</Callout>

<Callout title="405 on a GET request" type="warn">
Expected. This is a request/response server with no server-to-client stream, so `GET /api/mcp` isn't supported.
Always `POST` your JSON-RPC message.
</Callout>

<Callout title="A tool call returns 404 for a project, server, or repo" type="info">
`tools/list` worked but a specific `tools/call` reports `404`: that's the token's scope working as intended. It
wasn't granted that resource. Re-authorize the client (or mint a token) with the resource included, or use **Edit
access** on the connected client.
</Callout>

## Related guides

<Cards>
  <Card title="MCP API reference" href="/docs/api/mcp" description="The JSON-RPC methods, protocol negotiation, and error codes behind /api/mcp." />
  <Card title="Tokens API" href="/docs/api/tokens" description="Mint scoped or read-only personal access tokens and manage connected MCP clients." />
  <Card title="Audit log" href="/docs/guides/audit-log" description="Review what each agent did." />
</Cards>
