Skip to content

Commit 4b28dff

Browse files
authored
feat(mcp): Sim MCP server for the full Sim API (#7985)
* feat(mcp): Sim MCP server for the full Sim API * fix(mcp): pick the read or write tool from the operation's declared scope; CORS on the MCP host * fix(mcp): match the MCP host by full authority; keep legacy API keys off the OAuth token prefix
1 parent c822ec2 commit 4b28dff

62 files changed

Lines changed: 5070 additions & 239 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/docs/app/[[...slug]]/page.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,10 @@ export default async function Page(props: { params: Promise<{ slug?: string[] }>
125125
// width so the lesson hero/video gets the room (chapters live in-page instead).
126126
const isAcademy = slug?.[0] === 'academy'
127127
const isCli = slug?.[0] === 'cli'
128+
const isMcp = slug?.[0] === 'mcp'
128129

129130
const rawNeighbours = findNeighbour(source.pageTree, page.url)
130-
// Academy, API Reference, and CLI are self-contained sections; keep prev/next
131+
// Academy, API Reference, CLI, and MCP are self-contained sections; keep prev/next
131132
// inside the section instead of spilling into the main documentation tree.
132133
// Match both the section's pages (`/<slug>/...`) and its index (`/<slug>`).
133134
const sectionSlug = isApiReference
@@ -136,7 +137,9 @@ export default async function Page(props: { params: Promise<{ slug?: string[] }>
136137
? 'academy'
137138
: isCli
138139
? 'cli'
139-
: null
140+
: isMcp
141+
? 'mcp'
142+
: null
140143
const inSection = (url?: string) =>
141144
url != null && (url.includes(`/${sectionSlug}/`) || url.endsWith(`/${sectionSlug}`))
142145
const neighbours = sectionSlug

apps/docs/components/docs-layout/docs-sidebar.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export function DocsSidebar() {
103103
['Docs', '/introduction'],
104104
['API Reference', '/api-reference/getting-started'],
105105
['CLI', '/cli'],
106+
['MCP', '/mcp'],
106107
['Academy', '/academy'],
107108
].map(([label, href]) => (
108109
<ChipLink key={href} href={href} onNavigate={() => setOpen(false)}>

apps/docs/components/navbar/navbar.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,20 @@ import { ThemeToggle } from '@/components/ui/theme-toggle'
99
import { cn } from '@/lib/utils'
1010

1111
/**
12-
* Sections that own a tab, in reading order: the main docs, then the two
12+
* Sections that own a tab, in reading order: the main docs, then the three
1313
* reference surfaces, then Academy. `Documentation` matches by exclusion, so
1414
* every section listed here is one it must not claim.
1515
*/
16-
const SECTION_TABS = ['api-reference', 'academy', 'cli'] as const
16+
const SECTION_TABS = ['api-reference', 'academy', 'cli', 'mcp'] as const
1717

1818
/**
19-
* Whether a pathname is inside a section, matched by whole path segment.
19+
* Whether a pathname is inside a section, matched on its first path segment.
2020
*
21-
* A substring test is wrong: `/integrations/clickup` and
22-
* `/integrations/clickhouse` both contain `/cli`, which lit the CLI tab and
23-
* unlit Documentation on two existing integration pages.
21+
* A substring or suffix test is wrong: `/integrations/clickup` contains `/cli`,
22+
* and `/agents/mcp` ends with `/mcp`, and both belong to Documentation.
2423
*/
2524
function isInSection(pathname: string, section: string): boolean {
26-
return (
27-
pathname === `/${section}` ||
28-
pathname.endsWith(`/${section}`) ||
29-
pathname.includes(`/${section}/`)
30-
)
25+
return pathname === `/${section}` || pathname.startsWith(`/${section}/`)
3126
}
3227

3328
const NAV_TABS = [
@@ -49,6 +44,12 @@ const NAV_TABS = [
4944
match: (p: string) => isInSection(p, 'cli'),
5045
external: false,
5146
},
47+
{
48+
label: 'MCP',
49+
href: '/mcp',
50+
match: (p: string) => isInSection(p, 'mcp'),
51+
external: false,
52+
},
5253
{
5354
label: 'Academy',
5455
href: '/academy',
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
---
2+
title: Authentication
3+
description: Sign in with OAuth, or connect with an API key
4+
---
5+
6+
import { Callout } from 'fumadocs-ui/components/callout'
7+
8+
## OAuth
9+
10+
Most apps sign in with OAuth. The first time you connect, your app opens Sim in
11+
the browser, you sign in, and you approve its access. The app then holds a
12+
token that renews itself; you do not copy any secret.
13+
14+
The approval screen names the app and what it can do:
15+
16+
| Access | Scope | Allows |
17+
| --- | --- | --- |
18+
| Read-only | `api:read` | Reading workspaces, workflows, runs, tables, files, knowledge bases, and logs |
19+
| Full | `api:write` | Everything above, plus creating, changing, running, deploying, and deleting |
20+
21+
Most apps request full access. To connect an app for reads only, configure it
22+
to request the `api:read` scope; changes then fail with an insufficient-scope
23+
error.
24+
25+
Tokens are issued for the Sim MCP server itself. An app cannot take one to
26+
another service and use it there.
27+
28+
### Revoke access
29+
30+
Open **Settings → General → Authorized apps** in Sim, find the app, and revoke
31+
it. The app's next request fails, and you can reconnect at any time. Revoking
32+
does not undo changes the app already made.
33+
34+
## API keys
35+
36+
Apps that cannot sign in through a browser, such as CI jobs and headless
37+
agents, can send a Sim [API key](/api-reference/authentication) in the
38+
`X-API-Key` header, or as `Authorization: Bearer <key>`.
39+
40+
```bash
41+
claude mcp add --transport http sim https://mcp.sim.ai/mcp \
42+
--header "X-API-Key: $SIM_API_KEY"
43+
```
44+
45+
```json title="~/.cursor/mcp.json"
46+
{
47+
"mcpServers": {
48+
"sim": {
49+
"url": "https://mcp.sim.ai/mcp",
50+
"headers": { "X-API-Key": "${env:SIM_API_KEY}" }
51+
}
52+
}
53+
}
54+
```
55+
56+
A personal key acts as you in every workspace you can access. A workspace key
57+
reaches only its own workspace, and a few account-level operations refuse it;
58+
`search_operations` marks them `personalCredentialOnly`.
59+
60+
<Callout type="warn">
61+
An API key does not expire until you revoke it. Prefer OAuth for any app that
62+
can open a browser, and store keys in your app's secret or environment
63+
settings rather than in a shared config file.
64+
</Callout>
65+
66+
## Organization policy
67+
68+
The server follows your organization's access policy. If an administrator turns
69+
off **OAuth apps** or **personal API keys** for your permission group, requests
70+
with that credential are refused in the affected workspaces.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
---
2+
title: Sim MCP
3+
description: Build, run, and manage everything in your Sim workspace from Claude, Codex, Cursor, and other MCP apps
4+
---
5+
6+
import { Callout } from 'fumadocs-ui/components/callout'
7+
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
8+
9+
The Sim MCP server gives an AI app the whole Sim API through the
10+
[Model Context Protocol](https://modelcontextprotocol.io). Your agent can list
11+
workspaces, run and deploy workflows, query and edit tables, manage files and
12+
knowledge bases, read run logs, and more. It covers the same operations as the
13+
[API](/api-reference/getting-started) and the [CLI](/cli).
14+
15+
| Deployment | Server URL |
16+
| --- | --- |
17+
| Sim Cloud | `https://mcp.sim.ai/mcp` |
18+
| Self-hosted | `https://<your-sim-host>/api/mcp`, or your [`SIM_MCP_URL`](/platform/self-hosting/environment-variables) |
19+
20+
The server uses the Streamable HTTP transport. Sign in with OAuth, the default
21+
in every app below, or send an [API key](/mcp/authentication#api-keys).
22+
23+
## Connect an app
24+
25+
<Tabs items={['Claude Code', 'Claude', 'Codex', 'Cursor', 'VS Code']}>
26+
<Tab value="Claude Code">
27+
```bash
28+
claude mcp add --transport http sim https://mcp.sim.ai/mcp
29+
```
30+
31+
Open `/mcp` in Claude Code, select **sim**, and sign in to Sim in the
32+
browser.
33+
</Tab>
34+
<Tab value="Claude">
35+
Add `https://mcp.sim.ai/mcp` as a custom connector under **Settings →
36+
Connectors**, then connect it and sign in to Sim. For Team or Enterprise,
37+
an owner first adds it under **Organization settings → Connectors**. See
38+
[Claude's custom connector instructions](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp).
39+
</Tab>
40+
<Tab value="Codex">
41+
```bash
42+
codex mcp add sim --url https://mcp.sim.ai/mcp
43+
```
44+
45+
Complete the browser sign-in. To sign in again later, run
46+
`codex mcp login sim`.
47+
</Tab>
48+
<Tab value="Cursor">
49+
Add `sim` to `mcpServers` in `~/.cursor/mcp.json`, then enable it in
50+
Cursor and sign in to Sim:
51+
52+
```json
53+
{
54+
"mcpServers": {
55+
"sim": { "url": "https://mcp.sim.ai/mcp" }
56+
}
57+
}
58+
```
59+
</Tab>
60+
<Tab value="VS Code">
61+
Add `sim` to `.vscode/mcp.json`, then start it and sign in to Sim:
62+
63+
```json
64+
{
65+
"servers": {
66+
"sim": { "type": "http", "url": "https://mcp.sim.ai/mcp" }
67+
}
68+
}
69+
```
70+
</Tab>
71+
</Tabs>
72+
73+
Any other app that supports remote MCP servers with OAuth works the same way:
74+
give it the server URL and choose **Streamable HTTP** if asked.
75+
76+
<Callout type="info">
77+
Claude's hosted connectors call your server from Claude's infrastructure, so a
78+
self-hosted Sim must be reachable from the internet. A `localhost` URL works
79+
only with apps that run on your machine, such as Claude Code, Codex, Cursor,
80+
and VS Code.
81+
</Callout>
82+
83+
## Try it
84+
85+
Ask your app:
86+
87+
- "List my Sim workspaces and the tables in each."
88+
- "Run the `lead-scoring` workflow with this input and show me the result."
89+
- "Find failed runs from the last day and explain what went wrong."
90+
- "Create a table of our open support tickets and add these rows."
91+
92+
The agent finds the right operation, reads its inputs, and calls it. See
93+
[Tools](/mcp/tools) for how that works.
94+
95+
## What your agent can do
96+
97+
The server acts as you. It sees the workspaces you can see, with your role in
98+
each, and every call is authorized, rate limited, and logged exactly like the
99+
same request to the API. Reads leave your resources unchanged, and apps can ask
100+
you to confirm each change. See [Authentication](/mcp/authentication) to limit
101+
an app to reads.
102+
103+
## Other Sim MCP surfaces
104+
105+
This server is for operating Sim. Two other MCP features do different jobs:
106+
107+
- [Search MCP](/search/mcp) searches your organization's indexed sources.
108+
- [MCP deployment](/workflows/deployment/mcp) exposes your own workflows as
109+
tools, and [MCP tools](/agents/mcp) connect external servers to Sim agents.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"title": "MCP",
3+
"root": true,
4+
"pages": ["---Sim MCP---", "index", "authentication", "tools"]
5+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
title: Tools
3+
description: How an agent finds, reads, and calls Sim operations through four tools
4+
---
5+
6+
The Sim API has more than 200 operations. Instead of one tool per operation,
7+
which would crowd your app's tool list and your agent's context, the server
8+
exposes four tools. The agent searches for an operation, reads its inputs, and
9+
calls it.
10+
11+
| Tool | Does |
12+
| --- | --- |
13+
| `search_operations` | Finds operations by keyword (`"table rows"`) or by area (`tables`, `workflows`, `knowledge`, …). Returns each operation's name, method, path, summary, and the tool that runs it. |
14+
| `describe_operation` | Returns an operation's description and the JSON Schema of its path parameters, query, body, and headers. |
15+
| `call_read_operation` | Runs an operation that only needs read access, such as `listWorkspaces`, `queryRows`, or `getWorkflowRun`. |
16+
| `call_write_operation` | Runs an operation that needs write access: one that creates, changes, runs, or deletes something, or reaches out to another service, such as `createTable`, `executeWorkflow`, or `listMcpServerTools`. |
17+
18+
Reads and writes are separate tools so your app can approve reads once and still
19+
ask you before each change.
20+
21+
## Calling an operation
22+
23+
Operation names match the [CLI](/cli/reference) and the
24+
[API reference](/api-reference/getting-started). A call names the operation and
25+
fills the parts of the request it needs:
26+
27+
```json
28+
{
29+
"operation": "listTableRows",
30+
"params": { "tableId": "tbl_8f2c" },
31+
"query": { "workspaceId": "ws_91ab", "limit": 50 }
32+
}
33+
```
34+
35+
| Field | Holds |
36+
| --- | --- |
37+
| `params` | Path parameters, such as `tableId` or `workflowId` |
38+
| `query` | Query-string parameters; most operations need `workspaceId` |
39+
| `body` | The JSON request body (write operations only) |
40+
| `headers` | Headers the operation declares, such as `upload-token` |
41+
42+
The result is the same JSON the API returns, usually `{ "data": … }`. List
43+
operations page with `limit` and `cursor`. A failed call returns the API's error,
44+
such as `{ "error": { "code": "NOT_FOUND", "message": "…" } }`, so the agent can
45+
correct its request.
46+
47+
## Limits
48+
49+
- **Same rules as the API.** Permissions, rate limits, and request validation
50+
are the API's own; nothing is looser through MCP.
51+
- **1 MiB per result.** Page through larger lists with `limit` and `cursor`.
52+
- **No streaming.** Run a workflow without `stream: true` to wait for its
53+
result, or with `async: true` and poll `getWorkflowRun`.
54+
- **No file bytes.** Downloads, knowledge base exports, and multipart document
55+
uploads are not available over MCP; use the [CLI](/cli/files) or the API.

apps/docs/content/docs/platform/self-hosting/environment-variables.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { Callout } from 'fumadocs-ui/components/callout'
3434
| `TRUSTED_ORIGINS` | Comma-separated additional origins to trust for auth (apex + `www`, alias domains) |
3535
| `AUTH_TRUSTED_PROXIES` | Comma-separated reverse-proxy IPs/CIDRs so the client IP cannot be forged through `X-Forwarded-For` |
3636
| `INTERNAL_API_BASE_URL` | Internal URL for server-side self-calls, e.g. `http://sim-app.simstudio.svc.cluster.local:3000`. Optional — falls back to the public base URL. Deliberately ignored inside the Trigger.dev worker runtime, where a cluster-internal address resolves to the worker itself |
37+
| `SIM_MCP_URL` | Public URL of the [Sim MCP server](/mcp) when you serve it on its own host, e.g. `https://mcp.example.com/mcp`. Point that host at the app; Sim serves only the MCP endpoint and its OAuth metadata there, and stops serving `/api/mcp` on the app host so clients use one URL. Optional — defaults to `<NEXT_PUBLIC_APP_URL>/api/mcp` |
3738
| `DATABASE_REPLICA_URL` | Read-replica connection string for log listing, audit logs, and dashboard aggregations. Falls back to the primary when unset |
3839

3940
## AI Providers

apps/docs/lib/integration-navigation.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ describe('docs section navigation', () => {
5858
}
5959
})
6060

61-
it('keeps root-tab overview pages in the CLI and Academy navigation', () => {
62-
for (const root of ['cli', 'academy']) {
61+
it('keeps root-tab overview pages in the CLI, MCP, and Academy navigation', () => {
62+
for (const root of ['cli', 'mcp', 'academy']) {
6363
const folder = folders(source.pageTree.fallback?.children ?? []).find(
6464
(node) => node.$ref === `${root}/meta.json`
6565
)

apps/sim/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ BETTER_AUTH_URL=http://localhost:3000
2020
NEXT_PUBLIC_APP_URL=http://localhost:3000
2121
# NEXT_PUBLIC_STATUS_NOTICE_PREVIEW=true # Force the sidebar service-status notice into its critical preview state for testing
2222
# INTERNAL_API_BASE_URL=http://sim-app.default.svc.cluster.local:3000 # Optional: internal URL for server-side /api self-calls; defaults to NEXT_PUBLIC_APP_URL
23+
# SIM_MCP_URL=https://mcp.example.com/mcp # Optional: dedicated host for the Sim MCP server; defaults to NEXT_PUBLIC_APP_URL/api/mcp
2324
# TRUSTED_ORIGINS=https://www.example.com,https://app.example.com # Optional: comma-separated additional public origins to trust for auth (apex+www, alias domains). Merged into Better Auth trustedOrigins.
2425
# AUTH_TRUSTED_PROXIES=10.0.0.0/24,192.0.2.10 # Optional: reverse-proxy IPs/CIDRs in front of the app. Better Auth walks x-forwarded-for right to left, skips these hops, and uses the first untrusted address as the client IP (prevents forwarded-header spoofing). Use your proxies' actual addresses, not broad private ranges that also cover clients.
2526

0 commit comments

Comments
 (0)