Skip to content
Merged
35 changes: 31 additions & 4 deletions apps/docs/content/docs/agents/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -117,16 +117,16 @@ Once MCP servers are configured, their tools become available within your agent
/>
</div>

4. Select individual tools, or choose **Use all N tools** to add every tool from that server
4. Select individual tools, or choose **Configure operations access** for a dynamic server attachment
5. The agent can now access these tools during execution

<Callout type="info">
If you haven't configured a server yet, click **Add MCP Server** at the top of the dropdown to open the setup modal without leaving the block.
</Callout>

## Standalone MCP Tool Block
## Standalone MCP Block

For more granular control, you can use the dedicated MCP Tool block to execute specific MCP tools:
Use the MCP block to discover operations or run one operation with explicit inputs:

<div className="flex justify-center">
<Image
Expand All @@ -138,7 +138,34 @@ For more granular control, you can use the dedicated MCP Tool block to execute s
/>
</div>

The MCP Tool block runs one configured tool with parameters you set explicitly, and its output is readable by later blocks like any other.
Choose an **Action**:

- **List operations** discovers authorized operation names, descriptions, and input schemas without executing provider operations. Filter by name or description, set a page size from 1 to 100, and pass `nextCursor` into the next request while `hasMore` is true. An authorized list can be empty.
- **Run operation** executes one exact operation name. Configured operations keep their generated argument fields. For an operation name resolved at runtime, supply a JSON arguments object; Sim validates it against the operation's discovered schema before execution.

**MCP Server** takes one shared-server ID or managed-connection ID. Sim resolves a managed connection's parent server internally and verifies workspace and credential access. Both actions accept the same ID; List operations returns that ID as `serverId`, alongside the discovered `operations` and pagination metadata.

The standalone block's Basic fields select a configured connection and discovered operation. Advanced fields accept literal IDs/names or upstream references. A runtime server reference requires JSON arguments. Listing hides the operation and argument fields.

### Operations access

The **MCP Server (Advanced)** Agent attachment takes a server/connection ID or upstream reference in a plain input. Its **Tool IDs** field accepts exact MCP tool names, such as `search_docs`, entered directly. These are the names returned by List operations, without a Sim server prefix. Neither field uses a server or operation catalog picker.

Agent attachments have three access modes:

| Mode | Behavior |
| --- | --- |
| **Only selected** | Allows only selected exact operation names. An empty selection allows nothing. Newly discovered names stay excluded. |
| **All except selected** | Denies selected exact names. An empty selection allows everything otherwise permitted. Denied names stay saved if they temporarily disappear. |
| **All permitted** | Allows every operation available to the authorized credential. |

New restricted configurations start with an empty explicit selection. Existing saved workflows retain their prior access through normalization, while current organization and credential authorization still apply.

Operation restrictions are saved in workflow state on the Agent attachment. Tool IDs are literal configuration, not upstream references or model arguments. An operation must be available to the resolved, authorized connection and permitted by the saved restriction. The standalone block runs its explicitly specified operation and has no separate access policy.

Discovery filters the tools exposed to the Agent. Execution checks the actual server, connection, and saved restriction again before calling the provider. Missing or forbidden operations, unverifiable schemas, malformed arguments, and incorrect connection scopes fail the call. An Agent attachment with no permitted operations fails clearly.

Policies match exact, case-sensitive MCP tool names on whichever authorized connection resolves at runtime. They do not inspect operation arguments: allowing a generic `execute_sql` operation does not limit which SQL it can execute.

## When to Use MCP Tool vs Agent

Expand Down
67 changes: 65 additions & 2 deletions apps/docs/openapi-v2-workflows.json
Original file line number Diff line number Diff line change
Expand Up @@ -8201,6 +8201,69 @@
"const": "mcp-server-advanced",
"description": "Server-wide MCP binding discriminator."
},
"operationPolicy": {
"oneOf": [
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"const": "all",
"description": "Allow all operations available to the authorized credential."
}
},
"required": ["mode"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"const": "allow",
"description": "Allow only the selected exact operations."
},
"operations": {
"maxItems": 1000,
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"description": "Exact MCP tool name on the resolved connection, without a Sim server prefix."
},
"description": "Allowed exact MCP tool names; an empty list grants no access."
}
},
"required": ["mode", "operations"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"mode": {
"type": "string",
"const": "deny",
"description": "Exclude the selected exact operations."
},
"operations": {
"maxItems": 1000,
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"description": "Exact MCP tool name on the resolved connection, without a Sim server prefix."
},
"description": "Denied exact MCP tool names; an empty list allows otherwise permitted tools."
}
},
"required": ["mode", "operations"],
"additionalProperties": false
}
],
"description": "Saved workflow operation restrictions that can only narrow authorized credential access."
},
"params": {
"type": "object",
"properties": {
Expand All @@ -8213,7 +8276,7 @@
},
"required": ["serverId"],
"additionalProperties": false,
"description": "Server identity for discovering and invoking every available MCP tool."
"description": "Executable server or connection identity for authorized operation discovery and execution."
},
"usageControl": {
"type": "string",
Expand All @@ -8226,7 +8289,7 @@
"description": "Forward-compatible MCP server metadata preserved by the workflow editor."
},
"title": "Agent MCP server (advanced)",
"description": "All tools available to the executing subject from one MCP server.",
"description": "Dynamically discovered operations permitted by the authorized credential and saved block policy.",
"examples": [
{
"type": "mcp-server-advanced",
Expand Down
59 changes: 53 additions & 6 deletions apps/sim/app/api/mothership/execute/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ const {
mockRunHeadlessCopilotLifecycle: vi.fn(),
}))

vi.mock('@/lib/auth/internal', () => ({
verifyInternalDelegationToken: vi.fn().mockResolvedValue({
workflowId: 'workflow-1',
executionId: 'execution-1',
mcpBlockId: 'block-1',
subjectUserId: 'user-1',
}),
}))
vi.mock('@/lib/internal/principals/executor', () => ({
createExecutorPrincipalFromExecutionContext: vi
.fn()
.mockResolvedValue({ workspaceId: 'workspace-1' }),
}))

vi.mock('@/lib/core/security/encryption', () => ({
decryptSecret: mockDecryptSecret,
}))
Expand Down Expand Up @@ -112,6 +126,8 @@ describe('buildExecuteResponsePayload', () => {

describe('mothership private trace provenance transport', () => {
const requestBody = {
workflowId: 'workflow-1',
executionId: 'execution-1',
messages: [{ role: 'user', content: 'hello' }],
workspaceId: 'workspace-1',
userId: 'user-1',
Expand Down Expand Up @@ -177,7 +193,11 @@ describe('mothership private trace provenance transport', () => {
createMockRequest(
'POST',
requestBody,
{ Authorization: 'Bearer internal', 'x-sim-billing-attribution': 'billing' },
{
'X-Sim-Mcp-Delegation': 'signed-block',
Authorization: 'Bearer internal',
'x-sim-billing-attribution': 'billing',
},
'http://localhost:3000/api/mothership/execute'
)
)
Expand All @@ -204,7 +224,11 @@ describe('mothership private trace provenance transport', () => {
createMockRequest(
'POST',
requestBody,
{ Authorization: 'Bearer internal', 'x-sim-billing-attribution': 'billing' },
{
'X-Sim-Mcp-Delegation': 'signed-block',
Authorization: 'Bearer internal',
'x-sim-billing-attribution': 'billing',
},
'http://localhost:3000/api/mothership/execute'
)
)
Expand All @@ -226,7 +250,11 @@ describe('mothership private trace provenance transport', () => {
messages: [{ role: 'user', content: 'secret-value __var_FOREIGN' }],
contexts: [{ kind: 'docs', label: 'Docs' }],
},
{ Authorization: 'Bearer internal', 'x-sim-billing-attribution': 'billing' },
{
'X-Sim-Mcp-Delegation': 'signed-block',
Authorization: 'Bearer internal',
'x-sim-billing-attribution': 'billing',
},
'http://localhost:3000/api/mothership/execute'
)
)
Expand Down Expand Up @@ -274,13 +302,22 @@ describe('mothership private trace provenance transport', () => {
},
],
},
{ Authorization: 'Bearer internal', 'x-sim-billing-attribution': 'billing' },
{
'X-Sim-Mcp-Delegation': 'signed-block',
Authorization: 'Bearer internal',
'x-sim-billing-attribution': 'billing',
},
'http://localhost:3000/api/mothership/execute'
)
)

expect(response.status).toBe(200)
expect(mockBuildTaggedMcpToolSchemas).toHaveBeenCalledWith('user-1', 'workspace-1', ['123'])
expect(mockBuildTaggedMcpToolSchemas).toHaveBeenCalledWith(
'user-1',
'workspace-1',
['123'],
expect.objectContaining({ mcpBlockId: 'block-1' })
)
expect(mockProcessContextsServer).toHaveBeenCalledWith(
[
{
Expand Down Expand Up @@ -324,7 +361,11 @@ describe('mothership private trace provenance transport', () => {
secretScope: 'selected',
mountedSecrets: ['API_KEY'],
},
{ Authorization: 'Bearer internal', 'x-sim-billing-attribution': 'billing' },
{
'X-Sim-Mcp-Delegation': 'signed-block',
Authorization: 'Bearer internal',
'x-sim-billing-attribution': 'billing',
},
'http://localhost:3000/api/mothership/execute'
)
)
Expand Down Expand Up @@ -355,6 +396,7 @@ describe('mothership private trace provenance transport', () => {
'POST',
requestBody,
{
'X-Sim-Mcp-Delegation': 'signed-block',
Authorization: 'Bearer internal',
'x-sim-billing-attribution': 'billing',
'x-sim-request-private-tool-metadata': 'resolved-secret-provenance-v1',
Expand Down Expand Up @@ -397,6 +439,7 @@ describe('mothership private trace provenance transport', () => {
'POST',
requestBody,
{
'X-Sim-Mcp-Delegation': 'signed-block',
Authorization: 'Bearer internal',
'x-sim-billing-attribution': 'billing',
'x-sim-request-private-tool-metadata': 'resolved-secret-provenance-v1',
Expand Down Expand Up @@ -432,6 +475,7 @@ describe('mothership private trace provenance transport', () => {
'POST',
requestBody,
{
'X-Sim-Mcp-Delegation': 'signed-block',
Authorization: 'Bearer internal',
'x-sim-billing-attribution': 'billing',
'x-sim-request-private-tool-metadata': 'resolved-secret-provenance-v1',
Expand Down Expand Up @@ -485,6 +529,7 @@ describe('mothership private trace provenance transport', () => {
contexts: [{ kind: 'mcp', label: 'Docs', serverId: 'server-1' }],
},
{
'X-Sim-Mcp-Delegation': 'signed-block',
Authorization: 'Bearer internal',
'x-sim-billing-attribution': 'billing',
'x-sim-request-private-tool-metadata': 'resolved-secret-provenance-v1',
Expand Down Expand Up @@ -524,6 +569,7 @@ describe('mothership private trace provenance transport', () => {
'POST',
requestBody,
{
'X-Sim-Mcp-Delegation': 'signed-block',
Authorization: 'Bearer internal',
'x-sim-billing-attribution': 'billing',
'x-sim-request-private-tool-metadata': 'resolved-secret-provenance-v1',
Expand Down Expand Up @@ -554,6 +600,7 @@ describe('mothership private trace provenance transport', () => {
'POST',
requestBody,
{
'X-Sim-Mcp-Delegation': 'signed-block',
Authorization: 'Bearer internal',
'x-sim-billing-attribution': 'billing',
'x-sim-request-private-tool-metadata': 'resolved-secret-provenance-v1',
Expand Down
32 changes: 30 additions & 2 deletions apps/sim/app/api/mothership/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { mothershipExecuteContract } from '@/lib/api/contracts/mothership-chats'
import { parseRequest } from '@/lib/api/server'
import { checkInternalAuth } from '@/lib/auth/hybrid'
import { verifyInternalDelegationToken } from '@/lib/auth/internal'
import { requireBillingAttributionHeader } from '@/lib/billing/core/billing-attribution'
import { buildIntegrationToolSchemas } from '@/lib/copilot/chat/payload'
import { processContextsServer } from '@/lib/copilot/chat/process-contents'
Expand Down Expand Up @@ -33,6 +34,8 @@ import {
RESOLVED_SECRET_PROVENANCE_METADATA_V1,
requestsPrivateToolMetadata,
} from '@/lib/execution/private-tool-metadata'
import { createExecutorPrincipalFromExecutionContext } from '@/lib/internal/principals/executor'
import { MCP_SERVER_DELEGATION_AUDIENCE } from '@/lib/mcp/application/authorization'
import {
assertActiveWorkspaceAccess,
isWorkspaceAccessDeniedError,
Expand Down Expand Up @@ -160,6 +163,29 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
secretScope,
mountedSecrets,
} = validation.data.body
const mcpDelegationToken = validation.data.headers['x-sim-mcp-delegation']
if (!mcpDelegationToken) throw new Error('Mothership requires signed workflow provenance')
const delegation = await verifyInternalDelegationToken(mcpDelegationToken)
if (!delegation.mcpBlockId) throw new Error('Mothership requires signed block provenance')
if (
workflowId !== (delegation.currentWorkflow?.workflowId ?? delegation.workflowId) ||
executionId !== delegation.executionId
)
throw new Error('Mothership workflow scope does not match signed provenance')
const mcpContext = {
userId: auth.userId,
workflowId: workflowId ?? delegation.workflowId,
workspaceId,
executionId,
executorDelegationOrigin: delegation,
mcpBlockId: delegation.mcpBlockId,
}
const mcpPrincipal = await createExecutorPrincipalFromExecutionContext({
context: mcpContext,
audience: MCP_SERVER_DELEGATION_AUDIENCE,
})
if (mcpPrincipal.workspaceId !== workspaceId)
throw new Error('MCP workspace scope does not match')
const secretMountPolicy = normalizeSecretMountPolicy({ secretScope, mountedSecrets })

/**
Expand Down Expand Up @@ -221,8 +247,8 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
const nonMcpAgentMentions = agentMentions?.filter((context) => context.kind !== 'mcp')
const userPermission = workspaceAccess.permission
const mothershipToolsPromise = Promise.allSettled([
buildSelectedMcpToolSchemas(userId, workspaceId, mcpTools ?? []),
buildTaggedMcpToolSchemas(userId, workspaceId, taggedMcpServerIds),
buildSelectedMcpToolSchemas(userId, workspaceId, mcpTools ?? [], mcpContext),
buildTaggedMcpToolSchemas(userId, workspaceId, taggedMcpServerIds, mcpContext),
]).then((results) => {
const groups = results.map((result) => {
if (result.status === 'rejected') throw result.reason
Expand Down Expand Up @@ -344,6 +370,8 @@ export const POST = withRouteHandler(async (req: NextRequest) => {
simRequestId: requestId,
goRoute: '/api/mothership/execute',
autoExecuteTools: true,
mcpBlockId: delegation.mcpBlockId,
executorDelegationOrigin: delegation,
interactive: false,
abortSignal: lifecycleAbortController.signal,
billingAttribution,
Expand Down
Loading
Loading