Skip to content

Commit 22f096b

Browse files
authored
improvement(mcp): advertise server metadata, output schemas, and clearer tool docs (#9037)
1 parent 2676575 commit 22f096b

4 files changed

Lines changed: 123 additions & 42 deletions

File tree

‎apps/sim/lib/api/contracts/types.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { z } from 'zod'
22

3-
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
3+
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
44

55
export type ApiSchema = z.ZodType
66

‎apps/sim/lib/api/mcp/catalog.ts‎

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { omit, toRecord } from '@sim/utils/object'
22
import { z } from 'zod'
3-
import type { ApiSchema, HttpMethod } from '@/lib/api/contracts/types'
3+
import type { ApiSchema } from '@/lib/api/contracts/types'
44
import { V2_MCP_OPERATIONS, type V2McpOperationName } from '@/lib/api/mcp/generated/v2-operations'
55
import type { V2McpOperation } from '@/lib/api/mcp/types'
66
import { v2RouteOperation } from '@/lib/api/server/routes/v2-json-route'
@@ -20,30 +20,59 @@ export const TOOL_NAMES = { read: 'call_read_operation', write: 'call_write_oper
2020
type ToolKind = keyof typeof TOOL_NAMES
2121

2222
const REQUEST_SLOTS = ['params', 'query', 'body', 'headers'] as const
23-
type RequestSlot = (typeof REQUEST_SLOTS)[number]
24-
type JsonSchema = Record<string, unknown>
2523

26-
/** One catalog row: enough to choose an operation, not to call it. */
27-
interface McpOperationEntry {
28-
operation: V2McpOperationName
29-
method: HttpMethod
30-
path: string
31-
domain: string
32-
summary: string
33-
/** The tool that runs it: the read tool only for operations that need nothing beyond read access. */
34-
/** Refuses workspace API keys; call it with a personal key or an OAuth connection. */
35-
personalCredentialOnly?: true
36-
}
24+
const operationSummarySchema = z.object({
25+
operation: z
26+
.string()
27+
.describe('Operation name to pass to describe_operation and the call tools.'),
28+
method: z
29+
.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
30+
.describe('HTTP method of the Sim API route.'),
31+
path: z.string().describe('Sim API route path; bracketed segments are path parameters.'),
32+
domain: z.string().describe('API area, usable as the search_operations domain.'),
33+
summary: z.string(),
34+
tool: z
35+
.enum([TOOL_NAMES.read, TOOL_NAMES.write])
36+
.describe(
37+
'The tool that runs the operation: the read tool only for operations that need nothing beyond read access.'
38+
),
39+
personalCredentialOnly: z
40+
.literal(true)
41+
.optional()
42+
.describe(
43+
'Present when the operation refuses workspace API keys; call it with a personal API key or an OAuth connection.'
44+
),
45+
})
3746

38-
interface McpOperationSummary extends McpOperationEntry {
39-
/** The tool that runs it: the read tool only for operations that need nothing beyond read access. */
40-
tool: (typeof TOOL_NAMES)[ToolKind]
41-
}
47+
const jsonSchema = z.record(z.string(), z.unknown())
48+
49+
export const searchOperationsOutput = z.object({
50+
total: z.number().describe('How many operations match, which can exceed those returned.'),
51+
operations: z.array(operationSummarySchema).describe('Matching operations, best match first.'),
52+
})
4253

43-
/** Everything needed to call one operation: its documentation plus the JSON Schema of each request slot. */
44-
interface McpOperationDescription extends McpOperationSummary {
45-
description?: string
46-
input: Partial<Record<RequestSlot, JsonSchema>>
54+
/** What describe_operation returns: everything needed to call one operation. */
55+
export const describeOperationOutput = operationSummarySchema.extend({
56+
description: z.string().optional().describe('Longer documentation, when the operation has it.'),
57+
input: z
58+
.object({
59+
params: jsonSchema.optional(),
60+
query: jsonSchema.optional(),
61+
body: jsonSchema.optional(),
62+
headers: jsonSchema.optional(),
63+
})
64+
.describe(
65+
'JSON Schema of each request part the operation takes; an absent part takes no input.'
66+
),
67+
})
68+
69+
type McpOperationSummary = z.infer<typeof operationSummarySchema>
70+
type McpOperationDescription = z.infer<typeof describeOperationOutput>
71+
type JsonSchema = z.infer<typeof jsonSchema>
72+
73+
/** One catalog row: enough to choose an operation, not to call it. */
74+
type McpOperationEntry = Omit<McpOperationSummary, 'operation' | 'tool'> & {
75+
operation: V2McpOperationName
4776
}
4877

4978
const OPERATION_NAMES = Object.keys(V2_MCP_OPERATIONS) as V2McpOperationName[]
@@ -156,7 +185,7 @@ export async function searchOperations(options: {
156185
query?: string
157186
domain?: string
158187
limit: number
159-
}): Promise<{ total: number; operations: McpOperationSummary[] }> {
188+
}): Promise<z.infer<typeof searchOperationsOutput>> {
160189
const ranked = rankOperations(options.query, options.domain)
161190
return {
162191
total: ranked.length,

‎apps/sim/lib/api/mcp/server.ts‎

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,30 @@
11
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
22
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
33
import { createLogger } from '@sim/logger'
4+
import { SIM_DOCS_URL } from '@sim/utils/site'
45
import { z } from 'zod'
56
import {
67
describeOperation,
8+
describeOperationOutput,
79
OPERATION_DOMAINS,
810
resolveOperation,
911
searchOperations,
12+
searchOperationsOutput,
1013
TOOL_NAMES,
1114
} from '@/lib/api/mcp/catalog'
1215
import {
1316
dispatchMcpOperation,
1417
type McpDispatchContext,
1518
type McpOperationCall,
1619
} from '@/lib/api/mcp/dispatch'
17-
import { jsonToolResult, toolError } from '@/lib/mcp/tool-result'
20+
import { getBaseUrl } from '@/lib/core/utils/urls'
21+
import { structuredToolResult, toolError } from '@/lib/mcp/tool-result'
1822

1923
const logger = createLogger('SimMcpServer')
2024

25+
const DESCRIPTION =
26+
'Lets an agent work in Sim, the AI workspace for building, deploying, and managing AI agents: search the Sim API, then read or change workspaces, workflows and their runs, tables, knowledge bases, files, logs, credentials, and deployments.'
27+
2128
const INSTRUCTIONS = `Sim is the AI workspace where teams build, deploy, and manage AI agents. This server exposes the full Sim API: workspaces, workflows and their runs, tables, knowledge bases, files, logs, credentials, deployments, and more.
2229
2330
1. Find an operation with search_operations (keywords, optionally a domain).
@@ -43,8 +50,21 @@ const searchInput = z
4350
.describe(
4451
'Keywords matched against operation names, summaries, and paths, e.g. "table rows".'
4552
),
46-
domain: z.enum(OPERATION_DOMAINS).optional().describe('Only operations in this API area.'),
47-
limit: z.number().int().min(1).max(100).default(25),
53+
domain: z
54+
.enum(OPERATION_DOMAINS)
55+
.optional()
56+
.describe(
57+
'API area to search or list, such as tables, workflows, or knowledge. Omit to search every area.'
58+
),
59+
limit: z
60+
.number()
61+
.int()
62+
.min(1)
63+
.max(100)
64+
.default(25)
65+
.describe(
66+
'How many matching operations to return, best match first. When total in the result is larger, narrow the query or domain.'
67+
),
4868
})
4969
.strict()
5070

@@ -57,20 +77,28 @@ const callInput = z
5777
params: z
5878
.record(z.string(), z.string())
5979
.optional()
60-
.describe('Path parameters by name, e.g. { "tableId": "..." }.'),
80+
.describe(
81+
'Values for the bracketed segments of the operation’s path, by name, e.g. { "tableId": "..." }. IDs come from list operations such as listTables.'
82+
),
6183
query: z
6284
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
6385
.optional()
64-
.describe('Query-string parameters by name.'),
86+
.describe(
87+
'Query-string values by name, as the query schema from describe_operation lists them, e.g. { "limit": 50 } or the cursor from the previous page.'
88+
),
6589
headers: z
6690
.record(z.string(), z.string())
6791
.optional()
68-
.describe('Request headers the operation declares, such as upload-token.'),
92+
.describe(
93+
'Headers the operation’s describe_operation schema lists, such as upload-token. Sim sets authentication and content headers itself.'
94+
),
6995
body: z
7096
.record(z.string(), z.unknown())
7197
.meta({ additionalProperties: true })
7298
.optional()
73-
.describe('JSON request body, as describe_operation specifies.'),
99+
.describe(
100+
'JSON request body matching the body schema from describe_operation. Omit it when the operation takes no body.'
101+
),
74102
})
75103
.strict()
76104

@@ -84,7 +112,29 @@ const callInput = z
84112
* tools so a client can approve reads once and still confirm every change.
85113
*/
86114
export function createSimMcpServer(context: Omit<McpDispatchContext, 'signal'>): McpServer {
87-
const server = new McpServer({ name: 'Sim', version: '1.0.0' }, { instructions: INSTRUCTIONS })
115+
const baseUrl = getBaseUrl()
116+
const server = new McpServer(
117+
{
118+
name: 'Sim',
119+
title: 'Sim',
120+
version: '1.0.0',
121+
description: DESCRIPTION,
122+
websiteUrl: `${SIM_DOCS_URL}/mcp/overview`,
123+
icons: [
124+
{
125+
src: `${baseUrl}/brandbook/icon/green.svg`,
126+
mimeType: 'image/svg+xml',
127+
sizes: ['any'],
128+
},
129+
{
130+
src: `${baseUrl}/favicon/android-chrome-512x512.png`,
131+
mimeType: 'image/png',
132+
sizes: ['512x512'],
133+
},
134+
],
135+
},
136+
{ instructions: INSTRUCTIONS }
137+
)
88138

89139
async function call(
90140
tool: 'read' | 'write',
@@ -111,8 +161,9 @@ export function createSimMcpServer(context: Omit<McpDispatchContext, 'signal'>):
111161
{
112162
title: 'Search operations',
113163
description:
114-
'Find Sim API operations by keyword or domain. Returns each operation’s name, HTTP method, path, summary, and the tool that runs it. Call without a query to list a domain.',
164+
'Find Sim API operations by keyword or domain. Returns how many match and, best match first, each operation’s name, HTTP method, path, summary, and the tool that runs it. Call without a query to list a domain.',
115165
inputSchema: searchInput,
166+
outputSchema: searchOperationsOutput,
116167
annotations: {
117168
title: 'Search operations',
118169
readOnlyHint: true,
@@ -121,16 +172,17 @@ export function createSimMcpServer(context: Omit<McpDispatchContext, 'signal'>):
121172
openWorldHint: false,
122173
},
123174
},
124-
async (input) => jsonToolResult(await searchOperations(input))
175+
async (input) => structuredToolResult(await searchOperations(input))
125176
)
126177

127178
server.registerTool(
128179
'describe_operation',
129180
{
130181
title: 'Describe operation',
131182
description:
132-
'Get the JSON Schema of an operation’s path parameters, query, body, and headers. Read it before calling an operation for the first time.',
183+
'Get what is needed to call one operation: its HTTP method, path, summary, longer description when it has one, the tool that runs it, whether it refuses workspace API keys, and the JSON Schema of its path parameters, query, body, and headers. Read it before calling an operation for the first time.',
133184
inputSchema: describeInput,
185+
outputSchema: describeOperationOutput,
134186
annotations: {
135187
title: 'Describe operation',
136188
readOnlyHint: true,
@@ -143,7 +195,7 @@ export function createSimMcpServer(context: Omit<McpDispatchContext, 'signal'>):
143195
const resolved = await resolveOperation(operation, 'any')
144196
return 'error' in resolved
145197
? toolError(resolved.error)
146-
: jsonToolResult(await describeOperation(resolved.operation))
198+
: structuredToolResult(await describeOperation(resolved.operation))
147199
}
148200
)
149201

@@ -168,12 +220,12 @@ export function createSimMcpServer(context: Omit<McpDispatchContext, 'signal'>):
168220
server.registerTool(
169221
TOOL_NAMES.write,
170222
{
171-
title: 'Change Sim',
223+
title: 'Write to Sim',
172224
description:
173-
'Run a Sim API operation that creates, changes, runs, or deletes something, such as createTable, executeWorkflow, or deleteFile.',
225+
'Run a Sim API operation that needs write access: one that creates, changes, runs, or deletes something, or reaches out to another service, such as createTable, executeWorkflow, or deleteFile. Changes apply immediately, with no preview or undo. A workflow run executes its blocks, which can call external services and use credits, so confirm deletions and runs with the user first.',
174226
inputSchema: callInput,
175227
annotations: {
176-
title: 'Change Sim',
228+
title: 'Write to Sim',
177229
readOnlyHint: false,
178230
destructiveHint: true,
179231
idempotentHint: false,

‎apps/sim/lib/mcp/tool-result.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export function toolError(message: string): CallToolResult {
55
return { isError: true, content: [{ type: 'text', text: message }] }
66
}
77

8-
/** A tool result carrying a JSON value as text. */
9-
export function jsonToolResult(value: unknown): CallToolResult {
10-
return { content: [{ type: 'text', text: JSON.stringify(value) }] }
8+
/** A JSON object as text and as `structuredContent`, for a tool that declares an output schema. */
9+
export function structuredToolResult(value: Record<string, unknown>): CallToolResult {
10+
return { content: [{ type: 'text', text: JSON.stringify(value) }], structuredContent: value }
1111
}

0 commit comments

Comments
 (0)