Skip to content

Commit 92ce843

Browse files
authored
fix(mcp): answer an unpublishable workflow input on tool updates as validation, and match bare-name alternatives titles (#9367)
* fix(mcp): answer an unpublishable workflow input on tool updates as validation, and match bare-name alternatives titles Updating a workflow MCP tool's parameter descriptions regenerated its schema without the validation translation the create and add paths use, so a reserved workflow input name returned an internal error instead of a validation failure naming the field. Both paths now share one helper. The comparison library links scored a title naming a competitor's alternatives only through its mention aliases, which leave out a bare common-word name such as Make. The bare name now counts directly before "Alternatives", so a "Make Alternatives" title ranks first without reading "Make" elsewhere in a title as the competitor. * test(library): type the comparison fixture's tags so the cast to ContentMeta type-checks
1 parent 420dc7a commit 92ce843

4 files changed

Lines changed: 71 additions & 11 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, expect, it } from 'vitest'
2+
import type { ContentMeta } from '@/lib/content/schema'
3+
import { getComparisonsForPost } from '@/app/(landing)/comparisons/library-links'
4+
5+
function post(title: string, description: string, tags: string[] = []): ContentMeta {
6+
return { slug: 'post', title, description, tags } as ContentMeta
7+
}
8+
9+
describe('getComparisonsForPost', () => {
10+
it('ranks a bare common-word competitor first when the title names its alternatives', () => {
11+
const [first] = getComparisonsForPost(
12+
post('Best Make Alternatives for AI Agents', 'How Zapier and n8n compare for AI agents.'),
13+
new Set()
14+
)
15+
expect(first?.name).toBe('Make')
16+
})
17+
18+
it('does not read the bare common word elsewhere in a title as that competitor', () => {
19+
const comparisons = getComparisonsForPost(
20+
post('How to Make and Ship AI Agent Alternatives', 'A guide to building AI agents.'),
21+
new Set()
22+
)
23+
expect(comparisons.map(({ name }) => name)).not.toContain('Make')
24+
})
25+
})

‎apps/sim/app/(landing)/comparisons/library-links.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ const LINKS_TO_COMPARISON_SCORE = 2
1818
const MENTION_SCORE = 1
1919

2020
interface MentionPatterns {
21-
/** Matches a title naming the competitor's alternatives ("n8n Alternatives", "LangChain and LangGraph Alternatives"). */
21+
/**
22+
* Matches a title naming the competitor's alternatives ("n8n Alternatives", "LangChain and LangGraph
23+
* Alternatives"). A bare common-word name ("Make") counts only directly before "Alternatives".
24+
*/
2225
alternatives: RegExp
2326
/** Matches Title Case titles and tags, where a bare common-word name ("Make") is ambiguous. */
2427
subject: RegExp
@@ -38,7 +41,9 @@ function wordPattern(phrases: string[]): RegExp {
3841
function buildMentionPatterns(competitor: CompetitorProfile): MentionPatterns {
3942
const aliases = competitor.mentions ?? [competitor.name]
4043
return {
41-
alternatives: new RegExp(`\\b${phraseAlternation(aliases)}(?: and [^,:]+?)? Alternatives\\b`),
44+
alternatives: new RegExp(
45+
`\\b(?:${phraseAlternation(aliases)}(?: and [^,:]+?)?|${escapeRegExp(competitor.name)}) Alternatives\\b`
46+
),
4247
subject: wordPattern(aliases),
4348
mention: wordPattern([competitor.name, ...aliases]),
4449
}

‎apps/sim/lib/mcp/orchestration/workflow-mcp-lifecycle.test.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,33 @@ describe('workflow MCP lifecycle orchestration', () => {
169169
expect(dbChainMockFns.insert).not.toHaveBeenCalled()
170170
})
171171

172+
it('answers an unpublishable workflow input on a tool update as a validation failure', async () => {
173+
const message = 'Workflow input name "__proto__" is reserved. Rename this input field.'
174+
vi.mocked(generateParameterSchemaForWorkflow).mockRejectedValueOnce(
175+
new OrchestrationError('validation', message)
176+
)
177+
dbChainMockFns.limit.mockResolvedValueOnce([{ id: 'server-1' }]).mockResolvedValueOnce([
178+
{
179+
id: 'tool-1',
180+
workflowId: 'wf-1',
181+
toolName: 'tool_a',
182+
toolDescription: null,
183+
parameterSchemaBytes: 10,
184+
},
185+
])
186+
187+
const result = await performUpdateWorkflowMcpTool({
188+
workspaceId: 'workspace-1',
189+
userId: 'user-1',
190+
serverId: 'server-1',
191+
toolId: 'tool-1',
192+
parameterDescriptionOverrides: { input: 'The request to answer' },
193+
})
194+
195+
expect(result).toEqual({ success: false, error: message, errorCode: 'validation' })
196+
expect(dbChainMockFns.update).not.toHaveBeenCalled()
197+
})
198+
172199
it('allows updating tool metadata when an unchanged stored schema exceeds the new cap', async () => {
173200
dbChainMockFns.limit.mockResolvedValueOnce([{ id: 'server-1' }]).mockResolvedValueOnce([
174201
{

‎apps/sim/lib/mcp/orchestration/workflow-mcp-lifecycle.ts‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,16 @@ function validateServerToolMetadataBudgetForUpdate(
221221
return null
222222
}
223223

224+
/** A workflow's tool input schema, with an input the schema cannot publish answered as a validation failure. */
225+
async function generateWorkflowMcpBaseSchema(workflowId: string): Promise<Record<string, unknown>> {
226+
return generateParameterSchemaForWorkflow(workflowId).catch((error: unknown) => {
227+
if (error instanceof OrchestrationError && error.code === 'validation') {
228+
throw new WorkflowMcpExpectedError(error.message, 'validation')
229+
}
230+
throw error
231+
})
232+
}
233+
224234
async function prepareWorkflowMcpTool(params: {
225235
workflowRecord: WorkflowMcpToolWorkflowRecord
226236
toolName?: string
@@ -233,14 +243,7 @@ async function prepareWorkflowMcpTool(params: {
233243
const toolDescription =
234244
params.toolDescription !== undefined ? params.toolDescription?.trim() || null : null
235245

236-
const baseSchema = await generateParameterSchemaForWorkflow(workflowRecord.id).catch(
237-
(error: unknown) => {
238-
if (error instanceof OrchestrationError && error.code === 'validation') {
239-
throw new WorkflowMcpExpectedError(error.message, 'validation')
240-
}
241-
throw error
242-
}
243-
)
246+
const baseSchema = await generateWorkflowMcpBaseSchema(workflowRecord.id)
244247
const requestedOverrides =
245248
params.parameterDescriptionOverrides ??
246249
(params.legacyParameterSchema
@@ -932,7 +935,7 @@ export async function performUpdateWorkflowMcpTool(
932935
.limit(1)
933936
if (!toolRow) return { success: false, error: 'Tool not found', errorCode: 'not_found' }
934937

935-
const baseSchema = await generateParameterSchemaForWorkflow(toolRow.workflowId)
938+
const baseSchema = await generateWorkflowMcpBaseSchema(toolRow.workflowId)
936939
const overrides = pruneOverridesToSchema(
937940
params.parameterDescriptionOverrides ??
938941
(params.parameterSchema

0 commit comments

Comments
 (0)