Loop Safety¶
When multiple services expose and consume MCP tools from each other, there is a risk of infinite recursion (Service A calls Service B, which calls Service A, etc.). The framework provides two mechanisms to prevent this: denyToolPrefix for import-time filtering and callStack propagation for runtime loop detection.
Import-Time Prevention: denyToolPrefix¶
The denyToolPrefix option in McpRemoteServerConfig prevents importing tools whose remote name starts with a given prefix. This is typically used to prevent a service from re-importing its own tools when they appear on a remote server.
import { registerMcpRemoteTools } from "@modernpath/agent-framework";
// Service A exports tools prefixed with "serviceA."
// When importing from Service B, deny any tools that start with "serviceA."
await registerMcpRemoteTools(registry, {
name: "serviceB",
url: "https://service-b.example.com/mcp",
denyToolPrefix: "serviceA.",
});
If a remote tool's name matches the denyToolPrefix, it is skipped during import and reported in the skipped array of the result:
const result = await registerMcpRemoteTools(registry, config);
console.log(result.skipped);
// [{ tool: "serviceA.processIncident", reason: "Denied by denyToolPrefix (serviceA.)" }]
Runtime Prevention: callStack¶
Every MCP tools/call request carries a callStack array in the _meta parameter. This tracks the chain of tool invocations across services, enabling runtime loop detection.
How It Works¶
- Provider side: When the MCP provider receives a
tools/callrequest, it reads the incomingcallStackfrom_metaand appends the current tool name before executing - Consumer side: When a proxy tool calls a remote server, it reads the existing
callStackfrom its metadata, checks if the current tool name already appears, and throws if a loop is detected - The
callStackis propagated through the entire chain of calls
Call Flow Example¶
sequenceDiagram
participant A as Service A
participant B as Service B
participant C as Service C
A->>B: tools/call "getAnalysis"<br/>_meta: { callStack: ["serviceA.run"] }
B->>C: tools/call "getData"<br/>_meta: { callStack: ["serviceA.run", "getAnalysis"] }
C->>A: tools/call "serviceA.run"<br/>_meta: { callStack: ["serviceA.run", "getAnalysis", "getData"] }
Note over A: Loop detected!<br/>"serviceA.run" already in callStack
A-->>C: Error: MCP loop detected Runtime Detection Code¶
The framework checks the call stack before every remote tool invocation:
const stack = getCallStack(callMeta);
if (stack.includes(localName)) {
throw new Error(`MCP loop detected: ${localName} already in callStack`);
}
Best Practices¶
1. Always Set denyToolPrefix¶
When Service A both exposes and consumes MCP tools, set denyToolPrefix to your own tool prefix:
// Service A exposes tools prefixed with "myService."
// When consuming from any remote server, deny re-importing our own tools
await registerMcpRemoteTools(registry, {
name: "partnerService",
url: "https://partner.example.com/mcp",
denyToolPrefix: "myService.",
});
2. Use Specific allowTools¶
Prefer explicit allowlists over open import to minimize the attack surface for loops:
await registerMcpRemoteTools(registry, {
name: "partnerService",
url: "https://partner.example.com/mcp",
allowTools: ["getData", "getStatus"], // Only import what you need
});
3. Set originService for Audit Trail¶
Use originService in McpProviderConfig to identify the calling service in the metadata chain:
const handler = createMcpStatelessHandlerFromToolRegistry(registry, {
originService: "incident-resolver-backend",
});
Internal Functions¶
These utility functions are used internally but are available for custom implementations:
import { getCallStack, withCallStack } from "@modernpath/agent-framework";
// Extract callStack from metadata
const stack: string[] = getCallStack(meta);
// ["serviceA.run", "getAnalysis"]
// Append a new entry to the callStack
const updatedMeta = withCallStack(meta, "newToolName");
// { ...meta, callStack: ["serviceA.run", "getAnalysis", "newToolName"] }
Related Pages¶
- Consuming Remote Tools -- where
denyToolPrefixis configured - Exposing Tools -- where
callStackis propagated