Skip to content

Commit 8314b28

Browse files
icecrasher321waleedlatif1
authored andcommitted
fix(call-chain): x-sim-via propagation for API blocks and MCP tools (#3332)
* fix(call-chain): x-sim-via propagation for API blocks and MCP tools * addres bugbot comment
1 parent 67f8a68 commit 8314b28

File tree

13 files changed

+122
-250
lines changed

13 files changed

+122
-250
lines changed

apps/docs/content/docs/en/execution/basics.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ Understanding these core principles will help you build better workflows:
9797
3. **Smart Data Flow**: Outputs flow automatically to connected blocks
9898
4. **Error Handling**: Failed blocks stop their execution path but don't affect independent paths
9999
5. **State Persistence**: All block outputs and execution details are preserved for debugging
100+
6. **Cycle Protection**: Workflows that call other workflows (via Workflow blocks, MCP tools, or API blocks) are tracked with a call chain. If the chain exceeds 25 hops, execution is stopped to prevent infinite loops
100101

101102
## Next Steps
102103

apps/sim/app/api/mcp/tools/execute/route.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { NextRequest } from 'next/server'
33
import { getHighestPrioritySubscription } from '@/lib/billing/core/plan'
44
import { getExecutionTimeout } from '@/lib/core/execution-limits'
55
import type { SubscriptionPlan } from '@/lib/core/rate-limiter/types'
6+
import { SIM_VIA_HEADER } from '@/lib/execution/call-chain'
67
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
78
import { mcpService } from '@/lib/mcp/service'
89
import type { McpTool, McpToolCall, McpToolResult } from '@/lib/mcp/types'
@@ -178,8 +179,14 @@ export const POST = withMcpAuth('read')(
178179
'sync'
179180
)
180181

182+
const simViaHeader = request.headers.get(SIM_VIA_HEADER)
183+
const extraHeaders: Record<string, string> = {}
184+
if (simViaHeader) {
185+
extraHeaders[SIM_VIA_HEADER] = simViaHeader
186+
}
187+
181188
const result = await Promise.race([
182-
mcpService.executeTool(userId, serverId, toolCall, workspaceId),
189+
mcpService.executeTool(userId, serverId, toolCall, workspaceId, extraHeaders),
183190
new Promise<never>((_, reject) =>
184191
setTimeout(() => reject(new Error('Tool execution timeout')), executionTimeout)
185192
),

apps/sim/executor/constants.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,6 @@ export const DEFAULTS = {
158158
MAX_LOOP_ITERATIONS: 1000,
159159
MAX_FOREACH_ITEMS: 1000,
160160
MAX_PARALLEL_BRANCHES: 20,
161-
MAX_WORKFLOW_DEPTH: 10,
162161
MAX_SSE_CHILD_DEPTH: 3,
163162
EXECUTION_TIME: 0,
164163
TOKENS: {

apps/sim/executor/handlers/agent/agent-handler.test.ts

Lines changed: 59 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ describe('AgentBlockHandler', () => {
123123
let handler: AgentBlockHandler
124124
let mockBlock: SerializedBlock
125125
let mockContext: ExecutionContext
126-
let originalPromiseAll: any
127126

128127
beforeEach(() => {
129128
handler = new AgentBlockHandler()
@@ -135,8 +134,6 @@ describe('AgentBlockHandler', () => {
135134
configurable: true,
136135
})
137136

138-
originalPromiseAll = Promise.all
139-
140137
mockBlock = {
141138
id: 'test-agent-block',
142139
metadata: { id: BlockType.AGENT, name: 'Test Agent' },
@@ -209,8 +206,6 @@ describe('AgentBlockHandler', () => {
209206
})
210207

211208
afterEach(() => {
212-
Promise.all = originalPromiseAll
213-
214209
try {
215210
Object.defineProperty(global, 'window', {
216211
value: undefined,
@@ -271,38 +266,7 @@ describe('AgentBlockHandler', () => {
271266
expect(result).toEqual(expectedOutput)
272267
})
273268

274-
it('should preserve executeFunction for custom tools with different usageControl settings', async () => {
275-
let capturedTools: any[] = []
276-
277-
Promise.all = vi.fn().mockImplementation((promises: Promise<any>[]) => {
278-
const result = originalPromiseAll.call(Promise, promises)
279-
280-
result.then((tools: any[]) => {
281-
if (tools?.length) {
282-
capturedTools = tools.filter((t) => t !== null)
283-
}
284-
})
285-
286-
return result
287-
})
288-
289-
mockExecuteProviderRequest.mockResolvedValueOnce({
290-
content: 'Using tools to respond',
291-
model: 'mock-model',
292-
tokens: { input: 10, output: 20, total: 30 },
293-
toolCalls: [
294-
{
295-
name: 'auto_tool',
296-
arguments: { input: 'test input for auto tool' },
297-
},
298-
{
299-
name: 'force_tool',
300-
arguments: { input: 'test input for force tool' },
301-
},
302-
],
303-
timing: { total: 100 },
304-
})
305-
269+
it('should preserve usageControl for custom tools and filter out "none"', async () => {
306270
const inputs = {
307271
model: 'gpt-4o',
308272
userPrompt: 'Test custom tools with different usageControl settings',
@@ -372,51 +336,21 @@ describe('AgentBlockHandler', () => {
372336

373337
await handler.execute(mockContext, mockBlock, inputs)
374338

375-
expect(Promise.all).toHaveBeenCalled()
339+
const providerCall = mockExecuteProviderRequest.mock.calls[0]
340+
const tools = providerCall[1].tools
376341

377-
expect(capturedTools.length).toBe(2)
342+
expect(tools.length).toBe(2)
378343

379-
const autoTool = capturedTools.find((t) => t.name === 'auto_tool')
380-
const forceTool = capturedTools.find((t) => t.name === 'force_tool')
381-
const noneTool = capturedTools.find((t) => t.name === 'none_tool')
344+
const autoTool = tools.find((t: any) => t.name === 'auto_tool')
345+
const forceTool = tools.find((t: any) => t.name === 'force_tool')
346+
const noneTool = tools.find((t: any) => t.name === 'none_tool')
382347

383348
expect(autoTool).toBeDefined()
384349
expect(forceTool).toBeDefined()
385350
expect(noneTool).toBeUndefined()
386351

387352
expect(autoTool.usageControl).toBe('auto')
388353
expect(forceTool.usageControl).toBe('force')
389-
390-
expect(typeof autoTool.executeFunction).toBe('function')
391-
expect(typeof forceTool.executeFunction).toBe('function')
392-
393-
await autoTool.executeFunction({ input: 'test input' })
394-
expect(mockExecuteTool).toHaveBeenCalledWith(
395-
'function_execute',
396-
expect.objectContaining({
397-
code: 'return { result: "auto tool executed", input }',
398-
input: 'test input',
399-
}),
400-
false, // skipPostProcess
401-
expect.any(Object) // execution context
402-
)
403-
404-
await forceTool.executeFunction({ input: 'another test' })
405-
expect(mockExecuteTool).toHaveBeenNthCalledWith(
406-
2, // Check the 2nd call
407-
'function_execute',
408-
expect.objectContaining({
409-
code: 'return { result: "force tool executed", input }',
410-
input: 'another test',
411-
}),
412-
false, // skipPostProcess
413-
expect.any(Object) // execution context
414-
)
415-
416-
const providerCall = mockExecuteProviderRequest.mock.calls[0]
417-
const requestBody = providerCall[1]
418-
419-
expect(requestBody.tools.length).toBe(2)
420354
})
421355

422356
it('should filter out tools with usageControl set to "none"', async () => {
@@ -1763,6 +1697,52 @@ describe('AgentBlockHandler', () => {
17631697
expect(providerCallArgs[1].tools[0].name).toBe('search_files')
17641698
})
17651699

1700+
it('should pass callChain to executeProviderRequest for MCP cycle detection', async () => {
1701+
mockFetch.mockImplementation(() =>
1702+
Promise.resolve({ ok: true, json: () => Promise.resolve({}) })
1703+
)
1704+
1705+
const inputs = {
1706+
model: 'gpt-4o',
1707+
userPrompt: 'Search for files',
1708+
apiKey: 'test-api-key',
1709+
tools: [
1710+
{
1711+
type: 'mcp',
1712+
title: 'search_files',
1713+
schema: {
1714+
type: 'object',
1715+
properties: {
1716+
query: { type: 'string', description: 'Search query' },
1717+
},
1718+
required: ['query'],
1719+
},
1720+
params: {
1721+
serverId: 'mcp-search-server',
1722+
toolName: 'search_files',
1723+
serverName: 'search',
1724+
},
1725+
usageControl: 'auto' as const,
1726+
},
1727+
],
1728+
}
1729+
1730+
const contextWithCallChain = {
1731+
...mockContext,
1732+
workspaceId: 'test-workspace-123',
1733+
workflowId: 'test-workflow-456',
1734+
callChain: ['wf-parent', 'test-workflow-456'],
1735+
}
1736+
1737+
mockGetProviderFromModel.mockReturnValue('openai')
1738+
1739+
await handler.execute(contextWithCallChain, mockBlock, inputs)
1740+
1741+
expect(mockExecuteProviderRequest).toHaveBeenCalled()
1742+
const providerCallArgs = mockExecuteProviderRequest.mock.calls[0][1]
1743+
expect(providerCallArgs.callChain).toEqual(['wf-parent', 'test-workflow-456'])
1744+
})
1745+
17661746
it('should handle multiple MCP tools from the same server efficiently', async () => {
17671747
const fetchCalls: any[] = []
17681748

@@ -2139,21 +2119,10 @@ describe('AgentBlockHandler', () => {
21392119
expect(tools.length).toBe(0)
21402120
})
21412121

2142-
it('should use DB code for executeFunction when customToolId resolves', async () => {
2122+
it('should use DB schema when customToolId resolves', async () => {
21432123
const toolId = 'custom-tool-123'
21442124
mockFetchForCustomTool(toolId)
21452125

2146-
let capturedTools: any[] = []
2147-
Promise.all = vi.fn().mockImplementation((promises: Promise<any>[]) => {
2148-
const result = originalPromiseAll.call(Promise, promises)
2149-
result.then((tools: any[]) => {
2150-
if (tools?.length) {
2151-
capturedTools = tools.filter((t) => t !== null)
2152-
}
2153-
})
2154-
return result
2155-
})
2156-
21572126
const inputs = {
21582127
model: 'gpt-4o',
21592128
userPrompt: 'Format a report',
@@ -2174,19 +2143,12 @@ describe('AgentBlockHandler', () => {
21742143

21752144
await handler.execute(mockContext, mockBlock, inputs)
21762145

2177-
expect(capturedTools.length).toBe(1)
2178-
expect(typeof capturedTools[0].executeFunction).toBe('function')
2179-
2180-
await capturedTools[0].executeFunction({ title: 'Q1', format: 'pdf' })
2146+
expect(mockExecuteProviderRequest).toHaveBeenCalled()
2147+
const providerCall = mockExecuteProviderRequest.mock.calls[0]
2148+
const tools = providerCall[1].tools
21812149

2182-
expect(mockExecuteTool).toHaveBeenCalledWith(
2183-
'function_execute',
2184-
expect.objectContaining({
2185-
code: dbCode,
2186-
}),
2187-
false,
2188-
expect.any(Object)
2189-
)
2150+
expect(tools.length).toBe(1)
2151+
expect(tools[0].name).toBe('formatReport')
21902152
})
21912153

21922154
it('should not fetch from DB when no customToolId is present', async () => {

0 commit comments

Comments
 (0)