Skip to content

Commit f0d85cb

Browse files
authored
fix(mcp): fix caret misalignment and tool schema contract validation (#5566)
* fix(mcp): fix caret misalignment in Add MCP Server modal fields The Server URL and Header fields render a transparent input under a formatted overlay div for env-var highlighting. The overlay used font-medium/font-sans but the real input didn't, so glyph widths diverged and the native caret drifted from the visible text as you typed. * fix(mcp): loosen tool schema contract to accept valid JSON Schema shapes discoverMcpToolsContract's property schema rejected legal JSON Schema that real MCP servers can return: array-form `items` (tuple validation) and non-primitive `enum` values. Any server exercising either shape failed contract validation client-side and blanked the entire MCP tools list. * fix(mcp): only render dropdown UI for primitive-valued enums The MCP dynamic-args dropdown stringifies enum members for its labels/values. Now that the tool schema contract accepts non-primitive enum members (object/array), routing those through the dropdown would collapse distinct values to "[object Object]" and submit that string as the tool argument. Gate the dropdown on primitive-only enums; non-primitive enums fall through to the existing type-based branching (the JSON long-input editor for object/array types), which round-trips arbitrary JSON correctly. * fix(mcp): route non-primitive enums to the JSON editor regardless of type isPrimitiveEnum() correctly excluded object/array enum members from the dropdown, but the fallback only reached the long-input JSON editor when paramSchema.type was 'array'. An object-typed (or untyped) param with a non-primitive enum fell through to the default short-input, which stringifies via toString() and drops the enum-membership guarantee entirely. Any non-primitive enum now routes straight to long-input, independent of the declared type. * chore(mcp): fold inline comment into the existing TSDoc block * fix(mcp): serialize non-string values before displaying in the long-input editor The long-input JSON editor received value={value || ''} unconditionally, so an argument already holding a parsed object/array (loaded from the block's JSON arguments field) rendered as "[object Object]" or a comma-joined list instead of valid JSON, and saving would overwrite the real value with that mangled text. Serialize non-string values with JSON.stringify before display; onChange still stores the raw text the user edits, unchanged. * fix(mcp): parse JSON-typed long-input edits back into real values The long-input editor's onChange always stored the raw typed text, so a param whose schema requires an object/array/non-primitive-enum value (e.g. entering {"mode":"strict"}) was persisted as a string, not the actual JSON value — the MCP tool call could receive the wrong type. requiresJsonValue() identifies these schemas; onChange now parses the edited text back into the real value once it's valid JSON, falling back to the raw string mid-edit so the controlled textarea keeps reflecting in-progress keystrokes.
1 parent 4952ddb commit f0d85cb

4 files changed

Lines changed: 51 additions & 8 deletions

File tree

apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ function FormattedInput({
157157
onChange={onChange}
158158
onScroll={handleScroll}
159159
onInput={handleScroll}
160-
inputClassName='text-transparent caret-[var(--text-primary)]'
160+
inputClassName='font-medium font-sans text-transparent caret-[var(--text-primary)]'
161161
/>
162162
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden px-2 py-1.5 font-medium font-sans text-sm'>
163163
<div className='whitespace-nowrap' style={{ transform: `translateX(-${scrollLeft}px)` }}>

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/mcp-dynamic-args/mcp-dynamic-args.tsx

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,33 @@ import { formatParameterLabel } from '@/tools/params'
1515

1616
const logger = createLogger('McpDynamicArgs')
1717

18+
/**
19+
* The dropdown UI renders each enum member as a string label/value, so it can only
20+
* represent JSON Schema enums whose members are primitives — a non-primitive member
21+
* (object/array) would collapse to "[object Object]" and lose its identity. Callers
22+
* route a non-primitive enum to the JSON editor (`long-input`) instead.
23+
*/
24+
function isPrimitiveEnum(
25+
enumValues: unknown
26+
): enumValues is Array<string | number | boolean | null> {
27+
return (
28+
Array.isArray(enumValues) &&
29+
enumValues.every((value) => value === null || typeof value !== 'object')
30+
)
31+
}
32+
33+
/**
34+
* True when the schema's actual value must be a JSON object/array (a plain
35+
* object/array type, or a non-primitive enum member) rather than a string.
36+
*/
37+
function requiresJsonValue(paramSchema: any): boolean {
38+
return (
39+
paramSchema.type === 'object' ||
40+
paramSchema.type === 'array' ||
41+
(Array.isArray(paramSchema.enum) && !isPrimitiveEnum(paramSchema.enum))
42+
)
43+
}
44+
1845
interface McpDynamicArgsProps {
1946
blockId: string
2047
subBlockId: string
@@ -116,7 +143,9 @@ export function McpDynamicArgs({
116143
)
117144

118145
const getInputType = (paramSchema: any) => {
119-
if (paramSchema.enum) return 'dropdown'
146+
if (Array.isArray(paramSchema.enum)) {
147+
return isPrimitiveEnum(paramSchema.enum) ? 'dropdown' : 'long-input'
148+
}
120149
if (paramSchema.type === 'boolean') return 'switch'
121150
if (paramSchema.type === 'number' || paramSchema.type === 'integer') {
122151
if (paramSchema.minimum !== undefined && paramSchema.maximum !== undefined) {
@@ -241,6 +270,8 @@ export function McpDynamicArgs({
241270

242271
case 'long-input': {
243272
const config = createParamConfig(paramName, paramSchema, 'long-input')
273+
const displayValue =
274+
typeof value === 'string' || value == null ? value || '' : JSON.stringify(value)
244275
return (
245276
<LongInput
246277
key={`${paramName}-long`}
@@ -249,8 +280,18 @@ export function McpDynamicArgs({
249280
config={config}
250281
placeholder={config.placeholder}
251282
rows={4}
252-
value={value || ''}
253-
onChange={(newValue) => updateParameter(paramName, newValue)}
283+
value={displayValue}
284+
onChange={(newValue) => {
285+
if (!requiresJsonValue(paramSchema)) {
286+
updateParameter(paramName, newValue)
287+
return
288+
}
289+
try {
290+
updateParameter(paramName, JSON.parse(newValue))
291+
} catch {
292+
updateParameter(paramName, newValue)
293+
}
294+
}}
254295
isPreview={isPreview}
255296
disabled={disabled}
256297
workflowSearchValuePath={[paramName]}

apps/sim/lib/api/contracts/mcp.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,12 @@ export const mcpToolSchemaPropertySchema: z.ZodType<McpToolSchemaProperty> = z.l
5050
.object({
5151
type: z.union([z.string(), z.array(z.string())]).optional(),
5252
description: z.string().optional(),
53-
items: mcpToolSchemaPropertySchema.optional(),
53+
items: z
54+
.union([mcpToolSchemaPropertySchema, z.array(mcpToolSchemaPropertySchema)])
55+
.optional(),
5456
properties: z.record(z.string(), mcpToolSchemaPropertySchema).optional(),
5557
required: z.array(z.string()).optional(),
56-
enum: z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(),
58+
enum: z.array(z.unknown()).optional(),
5759
default: z.unknown().optional(),
5860
})
5961
.passthrough()

apps/sim/lib/mcp/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,10 @@ export interface McpSecurityPolicy {
6868
export interface McpToolSchemaProperty {
6969
type?: string | string[]
7070
description?: string
71-
items?: McpToolSchemaProperty
71+
items?: McpToolSchemaProperty | McpToolSchemaProperty[]
7272
properties?: Record<string, McpToolSchemaProperty>
7373
required?: string[]
74-
enum?: Array<string | number | boolean | null>
74+
enum?: unknown[]
7575
default?: unknown
7676
[key: string]: unknown
7777
}

0 commit comments

Comments
 (0)