Skip to content

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

  1. Provider side: When the MCP provider receives a tools/call request, it reads the incoming callStack from _meta and appends the current tool name before executing
  2. Consumer side: When a proxy tool calls a remote server, it reads the existing callStack from its metadata, checks if the current tool name already appears, and throws if a loop is detected
  3. The callStack is 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"] }