Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions apps/docs/app/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,10 @@ export default async function Page(props: { params: Promise<{ slug?: string[] }>
// width so the lesson hero/video gets the room (chapters live in-page instead).
const isAcademy = slug?.[0] === 'academy'
const isCli = slug?.[0] === 'cli'
const isMcp = slug?.[0] === 'mcp'

const rawNeighbours = findNeighbour(source.pageTree, page.url)
// Academy, API Reference, and CLI are self-contained sections; keep prev/next
// Academy, API Reference, CLI, and MCP are self-contained sections; keep prev/next
// inside the section instead of spilling into the main documentation tree.
// Match both the section's pages (`/<slug>/...`) and its index (`/<slug>`).
const sectionSlug = isApiReference
Expand All @@ -136,7 +137,9 @@ export default async function Page(props: { params: Promise<{ slug?: string[] }>
? 'academy'
: isCli
? 'cli'
: null
: isMcp
? 'mcp'
: null
const inSection = (url?: string) =>
url != null && (url.includes(`/${sectionSlug}/`) || url.endsWith(`/${sectionSlug}`))
const neighbours = sectionSlug
Expand Down
1 change: 1 addition & 0 deletions apps/docs/components/docs-layout/docs-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export function DocsSidebar() {
['Docs', '/introduction'],
['API Reference', '/api-reference/getting-started'],
['CLI', '/cli'],
['MCP', '/mcp'],
['Academy', '/academy'],
].map(([label, href]) => (
<ChipLink key={href} href={href} onNavigate={() => setOpen(false)}>
Expand Down
23 changes: 12 additions & 11 deletions apps/docs/components/navbar/navbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,20 @@ import { ThemeToggle } from '@/components/ui/theme-toggle'
import { cn } from '@/lib/utils'

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

/**
* Whether a pathname is inside a section, matched by whole path segment.
* Whether a pathname is inside a section, matched on its first path segment.
*
* A substring test is wrong: `/integrations/clickup` and
* `/integrations/clickhouse` both contain `/cli`, which lit the CLI tab and
* unlit Documentation on two existing integration pages.
* A substring or suffix test is wrong: `/integrations/clickup` contains `/cli`,
* and `/agents/mcp` ends with `/mcp`, and both belong to Documentation.
*/
function isInSection(pathname: string, section: string): boolean {
return (
pathname === `/${section}` ||
pathname.endsWith(`/${section}`) ||
pathname.includes(`/${section}/`)
)
return pathname === `/${section}` || pathname.startsWith(`/${section}/`)
}

const NAV_TABS = [
Expand All @@ -49,6 +44,12 @@ const NAV_TABS = [
match: (p: string) => isInSection(p, 'cli'),
external: false,
},
{
label: 'MCP',
href: '/mcp',
match: (p: string) => isInSection(p, 'mcp'),
external: false,
},
{
label: 'Academy',
href: '/academy',
Expand Down
70 changes: 70 additions & 0 deletions apps/docs/content/docs/mcp/authentication.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: Authentication
description: Sign in with OAuth, or connect with an API key
---

import { Callout } from 'fumadocs-ui/components/callout'

## OAuth

Most apps sign in with OAuth. The first time you connect, your app opens Sim in
the browser, you sign in, and you approve its access. The app then holds a
token that renews itself; you do not copy any secret.

The approval screen names the app and what it can do:

| Access | Scope | Allows |
| --- | --- | --- |
| Read-only | `api:read` | Reading workspaces, workflows, runs, tables, files, knowledge bases, and logs |
| Full | `api:write` | Everything above, plus creating, changing, running, deploying, and deleting |

Most apps request full access. To connect an app for reads only, configure it
to request the `api:read` scope; changes then fail with an insufficient-scope
error.

Tokens are issued for the Sim MCP server itself. An app cannot take one to
another service and use it there.

### Revoke access

Open **Settings → General → Authorized apps** in Sim, find the app, and revoke
it. The app's next request fails, and you can reconnect at any time. Revoking
does not undo changes the app already made.

## API keys

Apps that cannot sign in through a browser, such as CI jobs and headless
agents, can send a Sim [API key](/api-reference/authentication) in the
`X-API-Key` header, or as `Authorization: Bearer <key>`.

```bash
claude mcp add --transport http sim https://mcp.sim.ai/mcp \
--header "X-API-Key: $SIM_API_KEY"
```

```json title="~/.cursor/mcp.json"
{
"mcpServers": {
"sim": {
"url": "https://mcp.sim.ai/mcp",
"headers": { "X-API-Key": "${env:SIM_API_KEY}" }
}
}
}
```

A personal key acts as you in every workspace you can access. A workspace key
reaches only its own workspace, and a few account-level operations refuse it;
`search_operations` marks them `personalCredentialOnly`.

<Callout type="warn">
An API key does not expire until you revoke it. Prefer OAuth for any app that
can open a browser, and store keys in your app's secret or environment
settings rather than in a shared config file.
</Callout>

## Organization policy

The server follows your organization's access policy. If an administrator turns
off **OAuth apps** or **personal API keys** for your permission group, requests
with that credential are refused in the affected workspaces.
109 changes: 109 additions & 0 deletions apps/docs/content/docs/mcp/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
---
title: Sim MCP
description: Build, run, and manage everything in your Sim workspace from Claude, Codex, Cursor, and other MCP apps
---

import { Callout } from 'fumadocs-ui/components/callout'
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'

The Sim MCP server gives an AI app the whole Sim API through the
[Model Context Protocol](https://modelcontextprotocol.io). Your agent can list
workspaces, run and deploy workflows, query and edit tables, manage files and
knowledge bases, read run logs, and more. It covers the same operations as the
[API](/api-reference/getting-started) and the [CLI](/cli).

| Deployment | Server URL |
| --- | --- |
| Sim Cloud | `https://mcp.sim.ai/mcp` |
| Self-hosted | `https://<your-sim-host>/api/mcp`, or your [`SIM_MCP_URL`](/platform/self-hosting/environment-variables) |

The server uses the Streamable HTTP transport. Sign in with OAuth, the default
in every app below, or send an [API key](/mcp/authentication#api-keys).

## Connect an app

<Tabs items={['Claude Code', 'Claude', 'Codex', 'Cursor', 'VS Code']}>
<Tab value="Claude Code">
```bash
claude mcp add --transport http sim https://mcp.sim.ai/mcp
```

Open `/mcp` in Claude Code, select **sim**, and sign in to Sim in the
browser.
</Tab>
<Tab value="Claude">
Add `https://mcp.sim.ai/mcp` as a custom connector under **Settings →
Connectors**, then connect it and sign in to Sim. For Team or Enterprise,
an owner first adds it under **Organization settings → Connectors**. See
[Claude's custom connector instructions](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp).
</Tab>
<Tab value="Codex">
```bash
codex mcp add sim --url https://mcp.sim.ai/mcp
```

Complete the browser sign-in. To sign in again later, run
`codex mcp login sim`.
</Tab>
<Tab value="Cursor">
Add `sim` to `mcpServers` in `~/.cursor/mcp.json`, then enable it in
Cursor and sign in to Sim:

```json
{
"mcpServers": {
"sim": { "url": "https://mcp.sim.ai/mcp" }
}
}
```
</Tab>
<Tab value="VS Code">
Add `sim` to `.vscode/mcp.json`, then start it and sign in to Sim:

```json
{
"servers": {
"sim": { "type": "http", "url": "https://mcp.sim.ai/mcp" }
}
}
```
</Tab>
</Tabs>

Any other app that supports remote MCP servers with OAuth works the same way:
give it the server URL and choose **Streamable HTTP** if asked.

<Callout type="info">
Claude's hosted connectors call your server from Claude's infrastructure, so a
self-hosted Sim must be reachable from the internet. A `localhost` URL works
only with apps that run on your machine, such as Claude Code, Codex, Cursor,
and VS Code.
</Callout>

## Try it

Ask your app:

- "List my Sim workspaces and the tables in each."
- "Run the `lead-scoring` workflow with this input and show me the result."
- "Find failed runs from the last day and explain what went wrong."
- "Create a table of our open support tickets and add these rows."

The agent finds the right operation, reads its inputs, and calls it. See
[Tools](/mcp/tools) for how that works.

## What your agent can do

The server acts as you. It sees the workspaces you can see, with your role in
each, and every call is authorized, rate limited, and logged exactly like the
same request to the API. Reads leave your resources unchanged, and apps can ask
you to confirm each change. See [Authentication](/mcp/authentication) to limit
an app to reads.

## Other Sim MCP surfaces

This server is for operating Sim. Two other MCP features do different jobs:

- [Search MCP](/search/mcp) searches your organization's indexed sources.
- [MCP deployment](/workflows/deployment/mcp) exposes your own workflows as
tools, and [MCP tools](/agents/mcp) connect external servers to Sim agents.
5 changes: 5 additions & 0 deletions apps/docs/content/docs/mcp/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"title": "MCP",
"root": true,
"pages": ["---Sim MCP---", "index", "authentication", "tools"]
}
55 changes: 55 additions & 0 deletions apps/docs/content/docs/mcp/tools.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
title: Tools
description: How an agent finds, reads, and calls Sim operations through four tools
---

The Sim API has more than 200 operations. Instead of one tool per operation,
which would crowd your app's tool list and your agent's context, the server
exposes four tools. The agent searches for an operation, reads its inputs, and
calls it.

| Tool | Does |
| --- | --- |
| `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. |
| `describe_operation` | Returns an operation's description and the JSON Schema of its path parameters, query, body, and headers. |
| `call_read_operation` | Runs an operation that only needs read access, such as `listWorkspaces`, `queryRows`, or `getWorkflowRun`. |
| `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`. |

Reads and writes are separate tools so your app can approve reads once and still
ask you before each change.

## Calling an operation

Operation names match the [CLI](/cli/reference) and the
[API reference](/api-reference/getting-started). A call names the operation and
fills the parts of the request it needs:

```json
{
"operation": "listTableRows",
"params": { "tableId": "tbl_8f2c" },
"query": { "workspaceId": "ws_91ab", "limit": 50 }
}
```

| Field | Holds |
| --- | --- |
| `params` | Path parameters, such as `tableId` or `workflowId` |
| `query` | Query-string parameters; most operations need `workspaceId` |
| `body` | The JSON request body (write operations only) |
| `headers` | Headers the operation declares, such as `upload-token` |

The result is the same JSON the API returns, usually `{ "data": … }`. List
operations page with `limit` and `cursor`. A failed call returns the API's error,
such as `{ "error": { "code": "NOT_FOUND", "message": "…" } }`, so the agent can
correct its request.

## Limits

- **Same rules as the API.** Permissions, rate limits, and request validation
are the API's own; nothing is looser through MCP.
- **1 MiB per result.** Page through larger lists with `limit` and `cursor`.
- **No streaming.** Run a workflow without `stream: true` to wait for its
result, or with `async: true` and poll `getWorkflowRun`.
- **No file bytes.** Downloads, knowledge base exports, and multipart document
uploads are not available over MCP; use the [CLI](/cli/files) or the API.
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { Callout } from 'fumadocs-ui/components/callout'
| `TRUSTED_ORIGINS` | Comma-separated additional origins to trust for auth (apex + `www`, alias domains) |
| `AUTH_TRUSTED_PROXIES` | Comma-separated reverse-proxy IPs/CIDRs so the client IP cannot be forged through `X-Forwarded-For` |
| `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 |
| `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` |
| `DATABASE_REPLICA_URL` | Read-replica connection string for log listing, audit logs, and dashboard aggregations. Falls back to the primary when unset |

## AI Providers
Expand Down
4 changes: 2 additions & 2 deletions apps/docs/lib/integration-navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ describe('docs section navigation', () => {
}
})

it('keeps root-tab overview pages in the CLI and Academy navigation', () => {
for (const root of ['cli', 'academy']) {
it('keeps root-tab overview pages in the CLI, MCP, and Academy navigation', () => {
for (const root of ['cli', 'mcp', 'academy']) {
const folder = folders(source.pageTree.fallback?.children ?? []).find(
(node) => node.$ref === `${root}/meta.json`
)
Expand Down
1 change: 1 addition & 0 deletions apps/sim/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ BETTER_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_APP_URL=http://localhost:3000
# NEXT_PUBLIC_STATUS_NOTICE_PREVIEW=true # Force the sidebar service-status notice into its critical preview state for testing
# 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
# SIM_MCP_URL=https://mcp.example.com/mcp # Optional: dedicated host for the Sim MCP server; defaults to NEXT_PUBLIC_APP_URL/api/mcp
# 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.
# 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.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/** @vitest-environment node */
import { resetEnvFlagsMock, setEnvFlags } from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterAll, describe, expect, it, vi } from 'vitest'

vi.mock('@/lib/core/utils/urls', () => ({ getBaseUrl: () => 'https://sim.test' }))

import { GET } from '@/app/.well-known/oauth-protected-resource/api/mcp/route'

afterAll(resetEnvFlagsMock)

describe('Sim MCP protected-resource metadata', () => {
it('names the Sim MCP server as a Sim API resource', async () => {
setEnvFlags({ isAuthDisabled: false })
const response = await GET(new NextRequest('https://sim.test/'), undefined)
expect(await response.json()).toEqual({
resource: 'https://sim.test/api/mcp',
resource_name: 'Sim',
authorization_servers: ['https://sim.test/api/auth'],
scopes_supported: ['api:read', 'api:write'],
bearer_methods_supported: ['header'],
})
expect(response.headers.get('access-control-allow-origin')).toBe('*')
})

it('does not advertise disabled OAuth', async () => {
setEnvFlags({ isAuthDisabled: true })
const response = await GET(new NextRequest('https://sim.test/'), undefined)
expect(response.status).toBe(404)
})
})
10 changes: 10 additions & 0 deletions apps/sim/app/.well-known/oauth-protected-resource/api/mcp/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { NextResponse } from 'next/server'
import { simMcpResourceMetadata } from '@/lib/api/mcp/oauth-metadata'
import { isAuthDisabled } from '@/lib/core/config/env-flags'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'

/** RFC 9728 metadata for the Sim MCP server; `proxy.ts` also serves it on the dedicated MCP host. */
export const GET = withRouteHandler(async () => {
if (isAuthDisabled) return new NextResponse(null, { status: 404 })
return simMcpResourceMetadata()
})
Loading
Loading