mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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
This commit is contained in:
@@ -97,6 +97,7 @@ Understanding these core principles will help you build better workflows:
|
||||
3. **Smart Data Flow**: Outputs flow automatically to connected blocks
|
||||
4. **Error Handling**: Failed blocks stop their execution path but don't affect independent paths
|
||||
5. **State Persistence**: All block outputs and execution details are preserved for debugging
|
||||
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
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { NextRequest } from 'next/server'
|
||||
import { getHighestPrioritySubscription } from '@/lib/billing/core/plan'
|
||||
import { getExecutionTimeout } from '@/lib/core/execution-limits'
|
||||
import type { SubscriptionPlan } from '@/lib/core/rate-limiter/types'
|
||||
import { SIM_VIA_HEADER } from '@/lib/execution/call-chain'
|
||||
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import { mcpService } from '@/lib/mcp/service'
|
||||
import type { McpTool, McpToolCall, McpToolResult } from '@/lib/mcp/types'
|
||||
@@ -178,8 +179,14 @@ export const POST = withMcpAuth('read')(
|
||||
'sync'
|
||||
)
|
||||
|
||||
const simViaHeader = request.headers.get(SIM_VIA_HEADER)
|
||||
const extraHeaders: Record<string, string> = {}
|
||||
if (simViaHeader) {
|
||||
extraHeaders[SIM_VIA_HEADER] = simViaHeader
|
||||
}
|
||||
|
||||
const result = await Promise.race([
|
||||
mcpService.executeTool(userId, serverId, toolCall, workspaceId),
|
||||
mcpService.executeTool(userId, serverId, toolCall, workspaceId, extraHeaders),
|
||||
new Promise<never>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Tool execution timeout')), executionTimeout)
|
||||
),
|
||||
|
||||
@@ -158,7 +158,6 @@ export const DEFAULTS = {
|
||||
MAX_LOOP_ITERATIONS: 1000,
|
||||
MAX_FOREACH_ITEMS: 1000,
|
||||
MAX_PARALLEL_BRANCHES: 20,
|
||||
MAX_WORKFLOW_DEPTH: 10,
|
||||
MAX_SSE_CHILD_DEPTH: 3,
|
||||
EXECUTION_TIME: 0,
|
||||
TOKENS: {
|
||||
|
||||
@@ -123,7 +123,6 @@ describe('AgentBlockHandler', () => {
|
||||
let handler: AgentBlockHandler
|
||||
let mockBlock: SerializedBlock
|
||||
let mockContext: ExecutionContext
|
||||
let originalPromiseAll: any
|
||||
|
||||
beforeEach(() => {
|
||||
handler = new AgentBlockHandler()
|
||||
@@ -135,8 +134,6 @@ describe('AgentBlockHandler', () => {
|
||||
configurable: true,
|
||||
})
|
||||
|
||||
originalPromiseAll = Promise.all
|
||||
|
||||
mockBlock = {
|
||||
id: 'test-agent-block',
|
||||
metadata: { id: BlockType.AGENT, name: 'Test Agent' },
|
||||
@@ -209,8 +206,6 @@ describe('AgentBlockHandler', () => {
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
Promise.all = originalPromiseAll
|
||||
|
||||
try {
|
||||
Object.defineProperty(global, 'window', {
|
||||
value: undefined,
|
||||
@@ -271,38 +266,7 @@ describe('AgentBlockHandler', () => {
|
||||
expect(result).toEqual(expectedOutput)
|
||||
})
|
||||
|
||||
it('should preserve executeFunction for custom tools with different usageControl settings', async () => {
|
||||
let capturedTools: any[] = []
|
||||
|
||||
Promise.all = vi.fn().mockImplementation((promises: Promise<any>[]) => {
|
||||
const result = originalPromiseAll.call(Promise, promises)
|
||||
|
||||
result.then((tools: any[]) => {
|
||||
if (tools?.length) {
|
||||
capturedTools = tools.filter((t) => t !== null)
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
mockExecuteProviderRequest.mockResolvedValueOnce({
|
||||
content: 'Using tools to respond',
|
||||
model: 'mock-model',
|
||||
tokens: { input: 10, output: 20, total: 30 },
|
||||
toolCalls: [
|
||||
{
|
||||
name: 'auto_tool',
|
||||
arguments: { input: 'test input for auto tool' },
|
||||
},
|
||||
{
|
||||
name: 'force_tool',
|
||||
arguments: { input: 'test input for force tool' },
|
||||
},
|
||||
],
|
||||
timing: { total: 100 },
|
||||
})
|
||||
|
||||
it('should preserve usageControl for custom tools and filter out "none"', async () => {
|
||||
const inputs = {
|
||||
model: 'gpt-4o',
|
||||
userPrompt: 'Test custom tools with different usageControl settings',
|
||||
@@ -372,13 +336,14 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
expect(Promise.all).toHaveBeenCalled()
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const tools = providerCall[1].tools
|
||||
|
||||
expect(capturedTools.length).toBe(2)
|
||||
expect(tools.length).toBe(2)
|
||||
|
||||
const autoTool = capturedTools.find((t) => t.name === 'auto_tool')
|
||||
const forceTool = capturedTools.find((t) => t.name === 'force_tool')
|
||||
const noneTool = capturedTools.find((t) => t.name === 'none_tool')
|
||||
const autoTool = tools.find((t: any) => t.name === 'auto_tool')
|
||||
const forceTool = tools.find((t: any) => t.name === 'force_tool')
|
||||
const noneTool = tools.find((t: any) => t.name === 'none_tool')
|
||||
|
||||
expect(autoTool).toBeDefined()
|
||||
expect(forceTool).toBeDefined()
|
||||
@@ -386,37 +351,6 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
expect(autoTool.usageControl).toBe('auto')
|
||||
expect(forceTool.usageControl).toBe('force')
|
||||
|
||||
expect(typeof autoTool.executeFunction).toBe('function')
|
||||
expect(typeof forceTool.executeFunction).toBe('function')
|
||||
|
||||
await autoTool.executeFunction({ input: 'test input' })
|
||||
expect(mockExecuteTool).toHaveBeenCalledWith(
|
||||
'function_execute',
|
||||
expect.objectContaining({
|
||||
code: 'return { result: "auto tool executed", input }',
|
||||
input: 'test input',
|
||||
}),
|
||||
false, // skipPostProcess
|
||||
expect.any(Object) // execution context
|
||||
)
|
||||
|
||||
await forceTool.executeFunction({ input: 'another test' })
|
||||
expect(mockExecuteTool).toHaveBeenNthCalledWith(
|
||||
2, // Check the 2nd call
|
||||
'function_execute',
|
||||
expect.objectContaining({
|
||||
code: 'return { result: "force tool executed", input }',
|
||||
input: 'another test',
|
||||
}),
|
||||
false, // skipPostProcess
|
||||
expect.any(Object) // execution context
|
||||
)
|
||||
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const requestBody = providerCall[1]
|
||||
|
||||
expect(requestBody.tools.length).toBe(2)
|
||||
})
|
||||
|
||||
it('should filter out tools with usageControl set to "none"', async () => {
|
||||
@@ -1763,6 +1697,52 @@ describe('AgentBlockHandler', () => {
|
||||
expect(providerCallArgs[1].tools[0].name).toBe('search_files')
|
||||
})
|
||||
|
||||
it('should pass callChain to executeProviderRequest for MCP cycle detection', async () => {
|
||||
mockFetch.mockImplementation(() =>
|
||||
Promise.resolve({ ok: true, json: () => Promise.resolve({}) })
|
||||
)
|
||||
|
||||
const inputs = {
|
||||
model: 'gpt-4o',
|
||||
userPrompt: 'Search for files',
|
||||
apiKey: 'test-api-key',
|
||||
tools: [
|
||||
{
|
||||
type: 'mcp',
|
||||
title: 'search_files',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string', description: 'Search query' },
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
params: {
|
||||
serverId: 'mcp-search-server',
|
||||
toolName: 'search_files',
|
||||
serverName: 'search',
|
||||
},
|
||||
usageControl: 'auto' as const,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const contextWithCallChain = {
|
||||
...mockContext,
|
||||
workspaceId: 'test-workspace-123',
|
||||
workflowId: 'test-workflow-456',
|
||||
callChain: ['wf-parent', 'test-workflow-456'],
|
||||
}
|
||||
|
||||
mockGetProviderFromModel.mockReturnValue('openai')
|
||||
|
||||
await handler.execute(contextWithCallChain, mockBlock, inputs)
|
||||
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
const providerCallArgs = mockExecuteProviderRequest.mock.calls[0][1]
|
||||
expect(providerCallArgs.callChain).toEqual(['wf-parent', 'test-workflow-456'])
|
||||
})
|
||||
|
||||
it('should handle multiple MCP tools from the same server efficiently', async () => {
|
||||
const fetchCalls: any[] = []
|
||||
|
||||
@@ -2139,21 +2119,10 @@ describe('AgentBlockHandler', () => {
|
||||
expect(tools.length).toBe(0)
|
||||
})
|
||||
|
||||
it('should use DB code for executeFunction when customToolId resolves', async () => {
|
||||
it('should use DB schema when customToolId resolves', async () => {
|
||||
const toolId = 'custom-tool-123'
|
||||
mockFetchForCustomTool(toolId)
|
||||
|
||||
let capturedTools: any[] = []
|
||||
Promise.all = vi.fn().mockImplementation((promises: Promise<any>[]) => {
|
||||
const result = originalPromiseAll.call(Promise, promises)
|
||||
result.then((tools: any[]) => {
|
||||
if (tools?.length) {
|
||||
capturedTools = tools.filter((t) => t !== null)
|
||||
}
|
||||
})
|
||||
return result
|
||||
})
|
||||
|
||||
const inputs = {
|
||||
model: 'gpt-4o',
|
||||
userPrompt: 'Format a report',
|
||||
@@ -2174,19 +2143,12 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
await handler.execute(mockContext, mockBlock, inputs)
|
||||
|
||||
expect(capturedTools.length).toBe(1)
|
||||
expect(typeof capturedTools[0].executeFunction).toBe('function')
|
||||
expect(mockExecuteProviderRequest).toHaveBeenCalled()
|
||||
const providerCall = mockExecuteProviderRequest.mock.calls[0]
|
||||
const tools = providerCall[1].tools
|
||||
|
||||
await capturedTools[0].executeFunction({ title: 'Q1', format: 'pdf' })
|
||||
|
||||
expect(mockExecuteTool).toHaveBeenCalledWith(
|
||||
'function_execute',
|
||||
expect.objectContaining({
|
||||
code: dbCode,
|
||||
}),
|
||||
false,
|
||||
expect.any(Object)
|
||||
)
|
||||
expect(tools.length).toBe(1)
|
||||
expect(tools[0].name).toBe('formatReport')
|
||||
})
|
||||
|
||||
it('should not fetch from DB when no customToolId is present', async () => {
|
||||
|
||||
@@ -33,7 +33,6 @@ import { stringifyJSON } from '@/executor/utils/json'
|
||||
import { executeProviderRequest } from '@/providers'
|
||||
import { getProviderFromModel, transformBlockTool } from '@/providers/utils'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import { executeTool } from '@/tools'
|
||||
import { getTool, getToolAsync } from '@/tools/utils'
|
||||
|
||||
const logger = createLogger('AgentBlockHandler')
|
||||
@@ -276,14 +275,12 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
const userProvidedParams = tool.params || {}
|
||||
|
||||
let schema = tool.schema
|
||||
let code = tool.code
|
||||
let title = tool.title
|
||||
|
||||
if (tool.customToolId) {
|
||||
const resolved = await this.fetchCustomToolById(ctx, tool.customToolId)
|
||||
if (resolved) {
|
||||
schema = resolved.schema
|
||||
code = resolved.code
|
||||
title = resolved.title
|
||||
} else if (!schema) {
|
||||
logger.error(`Custom tool not found: ${tool.customToolId}`)
|
||||
@@ -296,7 +293,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
return null
|
||||
}
|
||||
|
||||
const { filterSchemaForLLM, mergeToolParameters } = await import('@/tools/params')
|
||||
const { filterSchemaForLLM } = await import('@/tools/params')
|
||||
|
||||
const filteredSchema = filterSchemaForLLM(schema.function.parameters, userProvidedParams)
|
||||
|
||||
@@ -313,43 +310,6 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
usageControl: tool.usageControl || 'auto',
|
||||
}
|
||||
|
||||
if (code) {
|
||||
base.executeFunction = async (callParams: Record<string, any>) => {
|
||||
const mergedParams = mergeToolParameters(userProvidedParams, callParams)
|
||||
|
||||
const { blockData, blockNameMapping, blockOutputSchemas } = collectBlockData(ctx)
|
||||
|
||||
const result = await executeTool(
|
||||
'function_execute',
|
||||
{
|
||||
code,
|
||||
...mergedParams,
|
||||
timeout: tool.timeout ?? AGENT.DEFAULT_FUNCTION_TIMEOUT,
|
||||
envVars: ctx.environmentVariables || {},
|
||||
workflowVariables: ctx.workflowVariables || {},
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
blockOutputSchemas,
|
||||
isCustomTool: true,
|
||||
_context: {
|
||||
workflowId: ctx.workflowId,
|
||||
workspaceId: ctx.workspaceId,
|
||||
userId: ctx.userId,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
enforceCredentialAccess: ctx.enforceCredentialAccess,
|
||||
},
|
||||
},
|
||||
false,
|
||||
ctx
|
||||
)
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'Function execution failed')
|
||||
}
|
||||
return result.output
|
||||
}
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
@@ -359,7 +319,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
private async fetchCustomToolById(
|
||||
ctx: ExecutionContext,
|
||||
customToolId: string
|
||||
): Promise<{ schema: any; code: string; title: string } | null> {
|
||||
): Promise<{ schema: any; title: string } | null> {
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const { getCustomTool } = await import('@/hooks/queries/custom-tools')
|
||||
@@ -367,7 +327,6 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
if (tool) {
|
||||
return {
|
||||
schema: tool.schema,
|
||||
code: tool.code || '',
|
||||
title: tool.title,
|
||||
}
|
||||
}
|
||||
@@ -416,7 +375,6 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
|
||||
return {
|
||||
schema: tool.schema,
|
||||
code: tool.code || '',
|
||||
title: tool.title,
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -498,47 +456,6 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
parameters: filteredSchema,
|
||||
params: userProvidedParams,
|
||||
usageControl: tool.usageControl || 'auto',
|
||||
executeFunction: async (callParams: Record<string, any>) => {
|
||||
const headers = await buildAuthHeaders()
|
||||
const execParams: Record<string, string> = {}
|
||||
if (ctx.userId) execParams.userId = ctx.userId
|
||||
const execUrl = buildAPIUrl('/api/mcp/tools/execute', execParams)
|
||||
|
||||
const execResponse = await fetch(execUrl.toString(), {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: stringifyJSON({
|
||||
serverId,
|
||||
toolName,
|
||||
arguments: callParams,
|
||||
workspaceId: ctx.workspaceId,
|
||||
workflowId: ctx.workflowId,
|
||||
toolSchema: tool.schema,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!execResponse.ok) {
|
||||
throw new Error(
|
||||
`MCP tool execution failed: ${execResponse.status} ${execResponse.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const result = await execResponse.json()
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'MCP tool execution failed')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: result.data.output || {},
|
||||
metadata: {
|
||||
source: 'mcp',
|
||||
serverId,
|
||||
serverName: serverName || serverId,
|
||||
toolName,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -684,47 +601,6 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
parameters: filteredSchema,
|
||||
params: userProvidedParams,
|
||||
usageControl: tool.usageControl || 'auto',
|
||||
executeFunction: async (callParams: Record<string, any>) => {
|
||||
const headers = await buildAuthHeaders()
|
||||
const discoverExecParams: Record<string, string> = {}
|
||||
if (ctx.userId) discoverExecParams.userId = ctx.userId
|
||||
const execUrl = buildAPIUrl('/api/mcp/tools/execute', discoverExecParams)
|
||||
|
||||
const execResponse = await fetch(execUrl.toString(), {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: stringifyJSON({
|
||||
serverId,
|
||||
toolName,
|
||||
arguments: callParams,
|
||||
workspaceId: ctx.workspaceId,
|
||||
workflowId: ctx.workflowId,
|
||||
toolSchema: mcpTool.inputSchema,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!execResponse.ok) {
|
||||
throw new Error(
|
||||
`MCP tool execution failed: ${execResponse.status} ${execResponse.statusText}`
|
||||
)
|
||||
}
|
||||
|
||||
const result = await execResponse.json()
|
||||
if (!result.success) {
|
||||
throw new Error(result.error || 'MCP tool execution failed')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: result.data.output || {},
|
||||
metadata: {
|
||||
source: 'mcp',
|
||||
serverId,
|
||||
serverName: mcpTool.serverName,
|
||||
toolName,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1082,6 +958,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
blockData,
|
||||
blockNameMapping,
|
||||
isDeployedContext: ctx.isDeployedContext,
|
||||
callChain: ctx.callChain,
|
||||
reasoningEffort: providerRequest.reasoningEffort,
|
||||
verbosity: providerRequest.verbosity,
|
||||
thinkingLevel: providerRequest.thinkingLevel,
|
||||
|
||||
@@ -108,18 +108,16 @@ describe('WorkflowBlockHandler', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('should enforce maximum depth limit', async () => {
|
||||
it('should enforce maximum call chain depth limit', async () => {
|
||||
const inputs = { workflowId: 'child-workflow-id' }
|
||||
|
||||
// Create a deeply nested context (simulate 11 levels deep to exceed the limit of 10)
|
||||
const deepContext = {
|
||||
...mockContext,
|
||||
workflowId:
|
||||
'level1_sub_level2_sub_level3_sub_level4_sub_level5_sub_level6_sub_level7_sub_level8_sub_level9_sub_level10_sub_level11',
|
||||
callChain: Array.from({ length: 25 }, (_, i) => `wf-${i}`),
|
||||
}
|
||||
|
||||
await expect(handler.execute(deepContext, mockBlock, inputs)).rejects.toThrow(
|
||||
'"child-workflow-id" failed: Maximum workflow nesting depth of 10 exceeded'
|
||||
'Maximum workflow call chain depth (25) exceeded'
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -98,13 +98,17 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
// workflow block execution, preventing cross-iteration child mixing in loop contexts.
|
||||
const instanceId = crypto.randomUUID()
|
||||
|
||||
const childCallChain = buildNextCallChain(ctx.callChain || [], workflowId)
|
||||
const depthError = validateCallChain(childCallChain)
|
||||
if (depthError) {
|
||||
throw new ChildWorkflowError({
|
||||
message: depthError,
|
||||
childWorkflowName,
|
||||
})
|
||||
}
|
||||
|
||||
let childWorkflowSnapshotId: string | undefined
|
||||
try {
|
||||
const currentDepth = (ctx.workflowId?.split('_sub_').length || 1) - 1
|
||||
if (currentDepth >= DEFAULTS.MAX_WORKFLOW_DEPTH) {
|
||||
throw new Error(`Maximum workflow nesting depth of ${DEFAULTS.MAX_WORKFLOW_DEPTH} exceeded`)
|
||||
}
|
||||
|
||||
if (ctx.isDeployedContext) {
|
||||
const hasActiveDeployment = await this.checkChildDeployment(workflowId)
|
||||
if (!hasActiveDeployment) {
|
||||
@@ -126,7 +130,7 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
childWorkflowName = workflowMetadata?.name || childWorkflow.name || 'Unknown Workflow'
|
||||
|
||||
logger.info(
|
||||
`Executing child workflow: ${childWorkflowName} (${workflowId}) at depth ${currentDepth}`
|
||||
`Executing child workflow: ${childWorkflowName} (${workflowId}), call chain depth ${ctx.callChain?.length || 0}`
|
||||
)
|
||||
|
||||
let childWorkflowInput: Record<string, any> = {}
|
||||
@@ -168,15 +172,6 @@ export class WorkflowBlockHandler implements BlockHandler {
|
||||
ctx.onChildWorkflowInstanceReady?.(effectiveBlockId, instanceId, iterationContext)
|
||||
}
|
||||
|
||||
const childCallChain = buildNextCallChain(ctx.callChain || [], workflowId)
|
||||
const depthError = validateCallChain(childCallChain)
|
||||
if (depthError) {
|
||||
throw new ChildWorkflowError({
|
||||
message: depthError,
|
||||
childWorkflowName,
|
||||
})
|
||||
}
|
||||
|
||||
const subExecutor = new Executor({
|
||||
workflow: childWorkflow.serializedState,
|
||||
workflowInput: childWorkflowInput,
|
||||
|
||||
@@ -19,8 +19,8 @@ describe('call-chain', () => {
|
||||
})
|
||||
|
||||
describe('MAX_CALL_CHAIN_DEPTH', () => {
|
||||
it('equals 10', () => {
|
||||
expect(MAX_CALL_CHAIN_DEPTH).toBe(10)
|
||||
it('equals 25', () => {
|
||||
expect(MAX_CALL_CHAIN_DEPTH).toBe(25)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
export const SIM_VIA_HEADER = 'X-Sim-Via'
|
||||
export const MAX_CALL_CHAIN_DEPTH = 10
|
||||
export const MAX_CALL_CHAIN_DEPTH = 25
|
||||
|
||||
/**
|
||||
* Parses the `X-Sim-Via` header value into an ordered list of workflow IDs.
|
||||
|
||||
@@ -170,7 +170,8 @@ class McpService {
|
||||
userId: string,
|
||||
serverId: string,
|
||||
toolCall: McpToolCall,
|
||||
workspaceId: string
|
||||
workspaceId: string,
|
||||
extraHeaders?: Record<string, string>
|
||||
): Promise<McpToolResult> {
|
||||
const requestId = generateRequestId()
|
||||
const maxRetries = 2
|
||||
@@ -187,6 +188,9 @@ class McpService {
|
||||
}
|
||||
|
||||
const resolvedConfig = await this.resolveConfigEnvVars(config, userId, workspaceId)
|
||||
if (extraHeaders && Object.keys(extraHeaders).length > 0) {
|
||||
resolvedConfig.headers = { ...resolvedConfig.headers, ...extraHeaders }
|
||||
}
|
||||
const client = await this.createClient(resolvedConfig)
|
||||
|
||||
try {
|
||||
|
||||
@@ -174,6 +174,7 @@ export interface ProviderRequest {
|
||||
verbosity?: string
|
||||
thinkingLevel?: string
|
||||
isDeployedContext?: boolean
|
||||
callChain?: string[]
|
||||
/** Previous interaction ID for multi-turn Interactions API requests (deep research follow-ups) */
|
||||
previousInteractionId?: string
|
||||
abortSignal?: AbortSignal
|
||||
|
||||
@@ -1110,6 +1110,7 @@ export function prepareToolExecution(
|
||||
blockData?: Record<string, any>
|
||||
blockNameMapping?: Record<string, string>
|
||||
isDeployedContext?: boolean
|
||||
callChain?: string[]
|
||||
}
|
||||
): {
|
||||
toolParams: Record<string, any>
|
||||
@@ -1137,6 +1138,7 @@ export function prepareToolExecution(
|
||||
...(request.isDeployedContext !== undefined
|
||||
? { isDeployedContext: request.isDeployedContext }
|
||||
: {}),
|
||||
...(request.callChain ? { callChain: request.callChain } : {}),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
||||
+27
-1
@@ -578,6 +578,25 @@ function isErrorResponse(
|
||||
return { isError: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a fully resolved URL points back to this Sim instance.
|
||||
* Used to propagate cycle-detection headers on API blocks that target
|
||||
* the platform's own workflow execution endpoints via absolute URL.
|
||||
*/
|
||||
function isSelfOriginUrl(url: string): boolean {
|
||||
try {
|
||||
const targetOrigin = new URL(url).origin
|
||||
const publicOrigin = new URL(getBaseUrl()).origin
|
||||
if (targetOrigin === publicOrigin) return true
|
||||
|
||||
const internalOrigin = new URL(getInternalApiBaseUrl()).origin
|
||||
if (targetOrigin === internalOrigin) return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Add internal authentication token to headers if running on server
|
||||
* @param headers - Headers object to modify
|
||||
@@ -737,7 +756,8 @@ async function executeToolRequest(
|
||||
const headers = new Headers(requestParams.headers)
|
||||
await addInternalAuthIfNeeded(headers, isInternalRoute, requestId, toolId)
|
||||
|
||||
if (isInternalRoute) {
|
||||
const shouldPropagateCallChain = isInternalRoute || isSelfOriginUrl(fullUrl)
|
||||
if (shouldPropagateCallChain) {
|
||||
const callChain = params._context?.callChain as string[] | undefined
|
||||
if (callChain && callChain.length > 0) {
|
||||
headers.set(SIM_VIA_HEADER, serializeCallChain(callChain))
|
||||
@@ -1123,6 +1143,12 @@ async function executeMcpTool(
|
||||
const workspaceId = params._context?.workspaceId || executionContext?.workspaceId
|
||||
const workflowId = params._context?.workflowId || executionContext?.workflowId
|
||||
const userId = params._context?.userId || executionContext?.userId
|
||||
const callChain =
|
||||
(params._context?.callChain as string[] | undefined) || executionContext?.callChain
|
||||
|
||||
if (callChain && callChain.length > 0) {
|
||||
headers[SIM_VIA_HEADER] = serializeCallChain(callChain)
|
||||
}
|
||||
|
||||
if (!workspaceId) {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user