\"MCP Proxy — Tool Interception and Plugin Tool Exposure\"
Overview
MCP Proxy intercepts tool calls from AI agents and routes them through an AWF-controlled local MCP server. This enables:
- Tool call observability: Every tool invocation by an agent produces an OpenTelemetry span and structured log line for auditing and monitoring
- Built-in tool re-exposure: AWF’s 6 core tools (
Read,Write,Edit,Bash,Glob,Grep) are re-exposed through the MCP protocol, giving you introspection into all file and shell operations - Plugin tool exposure: Extend the agent’s available tools with custom operations from installed AWF plugins without modifying the plugin interface
The proxy is configured per-step via the mcp_proxy: block in your workflow YAML.
Why Use MCP Proxy?
Observability: Without the proxy, agent tool calls are opaque — you see only the final output. With the proxy, you get:
- OTel spans per tool call (child of the step span)
- Structured
zaplogs with tool name, source, duration, and errors - Integration with your telemetry backend (Jaeger, Grafana Tempo, Honeycomb)
Extensibility: Expose gRPC plugin operations as MCP tools alongside the built-ins:
mcp_proxy:
enable: true
plugin_tools:
- plugin: kubernetes
expose: [kubectl_apply, kubectl_get]Security & Control: Choose between full interception (only MCP tools available) or additive mode (native built-ins + MCP tools):
mcp_proxy:
enable: true
intercept_builtins: true # Full control: agent uses ONLY MCP tools
# vs.
intercept_builtins: false # Additive: native built-ins + MCP toolsConfiguration
Basic Syntax
Add the mcp_proxy: block to any agent step:
analyze:
type: agent
provider: claude
prompt: "Analyze this file: {{.inputs.file}}"
mcp_proxy:
enable: true
intercept_builtins: true
options:
model: claude-sonnet-4-20250514
on_success: doneSchema
| Field | Type | Default | Description |
|---|---|---|---|
enable | boolean | false | Activate the proxy on this step |
intercept_builtins | boolean | true | If true, agent sees ONLY MCP tools (built-ins + plugins). If false, agent sees native built-ins + MCP tools. |
plugin_tools | array | [] | List of plugins and operations to expose as MCP tools |
plugin_tools[].plugin | string | — | Plugin name (must exist in .awf/plugins.yaml) |
plugin_tools[].expose | array | — | Operations from that plugin to expose as MCP tools |
Examples
Example 1: Full Interception (Built-ins Only)
Pure observability — every Read, Write, Edit, Bash, Glob, Grep call flows through the proxy.
refactor:
type: agent
provider: claude
prompt: "Refactor src/main.go for clarity"
mcp_proxy:
enable: true
# intercept_builtins: true is the default
options:
model: claude-sonnet-4-20250514
on_success: doneAgent sees: Read, Write, Edit, Bash, Glob, Grep.
All calls are logged and traced.
Example 2: Full Interception + Plugin Tools
Agent sees built-ins plus custom plugin operations.
deploy:
type: agent
provider: claude
prompt: "Apply the Kubernetes manifest to staging"
mcp_proxy:
enable: true
plugin_tools:
- plugin: kubernetes
expose: [kubectl_apply, kubectl_get, kubectl_describe]
options:
model: claude-sonnet-4-20250514
timeout: 300
on_success: verifyAgent sees: Read, Write, Edit, Bash, Glob, Grep, kubernetes_kubectl_apply, kubernetes_kubectl_get, kubernetes_kubectl_describe.
Plugin tool names are prefixed with <plugin>_ to avoid collisions.
Naming convention. Built-in tools intentionally use PascalCase (
Read,Write,Edit,Bash,Glob,Grep) to align with the names Anthropic-class agents (Claude Code, OpenCode) emit in theirtool_useevents. Plugin tools use snake_case with a<plugin>_<operation>prefix. This is the only deliberate exception to the snake_case convention documented in ADR 017, and it keeps the proxy a drop-in replacement for the agent’s native tools.
Example 3: Additive Mode (Native Built-ins + Plugin Tools)
Keep the agent’s native tools and add plugin operations alongside.
notify:
type: agent
provider: claude
prompt: "Send a deployment notification"
mcp_proxy:
enable: true
intercept_builtins: false
plugin_tools:
- plugin: notify
expose: [send_slack, send_webhook]
options:
model: claude-sonnet-4-20250514
on_success: doneAgent sees: all native built-ins (without MCP tracing) + notify_send_slack, notify_send_webhook (with tracing).
Built-in Tools
When mcp_proxy.enable: true, the following tools are available to the agent (unless intercept_builtins: false):
Read
Read a file and return its contents.
# Agent call
tool: Read
args:
path: "/etc/config.yaml"
# Returns
content: "..."Write
Write contents to a file. Creates parent directories if needed.
tool: Write
args:
path: "/tmp/output.txt"
content: "File content here"
# Returns
path: "/tmp/output.txt"
success: trueEdit
Edit a file using regex-based find-and-replace.
tool: Edit
args:
path: "/src/main.go"
old_string: "// TODO: fix this"
new_string: "// FIXED in v2.0"
# Returns
path: "/src/main.go"
success: trueBash
Execute a shell command.
tool: Bash
args:
command: "ls -la /home"
working_dir: "/tmp"
# Returns
stdout: "..."
stderr: ""
exit_code: 0Glob
Find files matching a pattern.
tool: Glob
args:
pattern: "**/*.go"
directory: "."
# Returns
matches: ["main.go", "cmd/cli.go", "internal/pkg.go"]Grep
Search for text in files.
tool: Grep
args:
pattern: "TODO"
path: "./src"
context_lines: 2
# Returns
matches:
- file: "src/main.go"
line: 42
text: "// TODO: implement this"Plugin Tools
When you expose plugin operations via plugin_tools:, they become available as MCP tools with the naming pattern <plugin>_<operation>.
Exposure
List the operations you want to expose:
plugin_tools:
- plugin: github
expose:
- create_issue
- add_comment_to_pr
- list_pull_requestsTool Schema
Plugin tools are automatically converted from the plugin’s OperationSchema to MCP’s InputSchema (JSON Schema). For example:
Plugin operation schema:
OperationSchema{
Name: "create_issue",
Inputs: InputSchema{
Type: "object",
Required: []string{"title", "body"},
Properties: map[string]any{
"title": {Type: "string"},
"body": {Type: "string"},
"labels": {Type: "array"},
},
},
}Becomes MCP tool schema:
{
"name": "github_create_issue",
"description": "Create a GitHub issue",
"inputSchema": {
"type": "object",
"required": ["title", "body"],
"properties": {
"title": {"type": "string"},
"body": {"type": "string"},
"labels": {"type": "array"}
}
}
}Supported Providers
MCP Proxy works with all AWF agent providers:
| Provider | Mechanism | Interception Mode | Notes |
|---|---|---|---|
| claude | --mcp-config flag | Full control (intercept_builtins:true enforced) | MCP-only isolation guaranteed |
| gemini | gemini mcp add temporary registration | Full control | MCP-only isolation guaranteed |
| codex | -c 'mcp_servers.awf-proxy' | Coexistence (⚠️ see below) | Native tools remain accessible; startup warning emitted |
| opencode | Workspace opencode.json entry | Coexistence (⚠️ see below) | Native tools remain accessible; startup warning emitted |
| github_copilot | --additional-mcp-config @<file> (+ --disable-builtin-mcps in intercept mode) | Coexistence (⚠️ see below) | Native tools remain accessible; startup warning emitted |
| mistral_vibe | Temporary VIBE_HOME/config.toml with [[mcp_servers]] | Provider allowlist | Non-interactive tool workflows require agent_profile: auto-approve and trust: true; Vibe prefixes MCP tools as <server>_<tool> |
| openai_compatible | HTTP tools[] field | Full control | MCP tools injected in Chat Completions request |
Codex, OpenCode & Copilot Coexistence Warning
Codex, OpenCode and GitHub Copilot CLIs cannot fully disable their native built-in tools — they lack a --tools "" equivalent. When you use mcp_proxy.enable: true on these providers, AWF:
- Injects the proxy MCP server
- Emits a startup
WARNlog message:WARN: mcp_proxy on provider=codex runs in coexistence mode. Built-in tools cannot be disabled and may bypass the proxy. Use 'claude' or 'openai-compatible' for guaranteed MCP-only isolation. - Adds system prompt mitigation (“Use only MCP tools, never built-in tools”)
If you need strict MCP-only isolation, use claude or openai_compatible instead.
Mistral Vibe Tool Names and Permissions
Mistral Vibe publishes MCP tools with the server alias prefixed to the remote tool name. AWF registers the server as awf-proxy, so a plugin tool exposed by AWF as awf-plugin-time_time is visible to Vibe as:
awf-proxy_awf-plugin-time_timeFor non-interactive workflows that expect Vibe to call tools, set:
options:
agent_profile: auto-approve
trust: trueWithout those options, Vibe may discover the MCP tool but refuse execution with Tool execution not permitted.
Validation
awf validate checks the mcp_proxy: block for configuration errors:
| Error Code | Condition | Example |
|---|---|---|
USER.MCP_PROXY.UNKNOWN_KEY | Unknown key in the block (typo, future schema) | intercept_builtins_future: true |
USER.MCP_PROXY.UNKNOWN_PLUGIN | Plugin does not exist in .awf/plugins.yaml | plugin: nonexistent_plugin |
USER.MCP_PROXY.UNKNOWN_OPERATION | Operation not found in the plugin | expose: [invalid_op] |
USER.MCP_PROXY.NAME_COLLISION | Two tools resolve to the same name | Two plugins with create_issue operation |
USER.MCP_PROXY.EMPTY_PROXY | enable: true + intercept_builtins: false + no plugins | Dead config with no effect |
Example validation output:
$ awf validate my-workflow.yaml
Error: USER.MCP_PROXY.UNKNOWN_PLUGIN
Step: deploy
Details: plugin 'k8s' not found in .awf/plugins.yamlObservability
OpenTelemetry Spans
Each tool call produces a child span of the step span:
workflow.execution
└─ step: deploy
└─ tool.call: kubernetes_kubectl_apply
├─ Duration: 2.341s
├─ tool.name: kubernetes_kubectl_apply
├─ tool.source: plugin:kubernetes
└─ [Error]: command timeoutAttributes available for export to your telemetry backend:
tool.name— Name of the tooltool.source—builtinorplugin:<name>tool.duration_ms— Duration in milliseconds- Error information (if the call failed)
Structured Logging
Each tool call produces a zap log entry:
{
"level": "info",
"message": "tool call",
"tool": "Read",
"source": "builtin",
"duration_ms": 12,
"timestamp": "2026-05-23T10:30:45Z"
}If the call fails:
{
"level": "error",
"message": "tool call",
"tool": "Bash",
"source": "builtin",
"duration_ms": 456,
"error": "command exited with code 127: command not found",
"timestamp": "2026-05-23T10:30:46Z"
}Performance Considerations
- Per-step overhead: Each step with
mcp_proxy.enable: truespawns a subprocess (~10 MB memory) for stdio providers (Claude, Gemini, Codex, OpenCode). The subprocess is cleaned up when the step completes. - Tool call latency: MCP tool calls add ~50ms overhead (process communication) compared to direct agent calls. This is negligible for most workflows.
- Tracing cost: OTel spans and structured logging have zero cost when no telemetry exporter is configured (default behavior).
Troubleshooting
“mcp_proxy on provider=codex runs in coexistence mode” warning
This is expected behavior for Codex and OpenCode. They cannot disable native tools via CLI flags. Options:
- Accept the warning and understand that native tools may be called (though the system prompt discourages this)
- Switch to Claude or OpenAI Compatible for guaranteed MCP-only isolation
- Use
intercept_builtins: falseto intentionally add plugin tools alongside native ones
“NAME_COLLISION detected at step startup”
Two tools (built-ins or plugins) resolved to the same name. Examples:
- Two plugins both have a
create_issueoperation (both become<plugin>_create_issue) - A plugin has a
readoperation (collides with built-inRead)
Fix: Rename one of the operations in the plugin, or remove one from expose:.
“UNKNOWN_OPERATION in plugin”
The plugin does not expose the operation you’re trying to expose. Verify:
- Run
awf plugin list <name>to see available operations - Check the plugin’s documentation
- Correct the operation name in
expose:
Tool call takes longer than expected
Check your telemetry backend or logs for the tool call duration. Possible causes:
- The underlying command is slow (not proxy-related)
- Network latency (for HTTP providers)
- High system load
Proxy subprocess crashes or hangs
AWF automatically detects subprocess failure and reports it as a structured error. If it hangs:
- Press
Ctrl+Cto interrupt (the proxy subprocess will be forcefully terminated after a 5-second grace period) - Check logs for error details
- Report the issue with the full AWF log output
Examples
Example: Code Review with Full Observability
name: code-review-with-tracing
version: "1.0.0"
inputs:
- name: file
type: string
required: true
states:
initial: analyze
analyze:
type: agent
provider: claude
prompt: |
Review this code for bugs, security issues, and style:
{{.inputs.file}}
mcp_proxy:
enable: true
# All file reads and shell commands flow through MCP,
# producing OTel spans and structured logs
options:
model: claude-sonnet-4-20250514
on_success: report
report:
type: step
command: echo "Code review complete. Check logs for observability data."
on_success: done
done:
type: terminal
status: successRun with tracing enabled:
awf run code-review-with-tracing \
--input file=src/main.go \
--otel-exporter otlp \
--otel-service-name my-workflowExample: K8s Deployment with Plugin Tools
name: deploy-to-k8s
version: "1.0.0"
inputs:
- name: manifest
type: string
required: true
- name: namespace
type: string
default: default
states:
initial: validate
validate:
type: step
command: kubectl --version
on_success: deploy
deploy:
type: agent
provider: claude
prompt: |
Apply this Kubernetes manifest to {{.inputs.namespace}}:
{{.inputs.manifest}}
First validate with kubectl apply --dry-run, then apply for real.
mcp_proxy:
enable: true
plugin_tools:
- plugin: kubernetes
expose: [kubectl_apply, kubectl_get, kubectl_describe]
options:
model: claude-sonnet-4-20250514
timeout: 300
on_success: verify
verify:
type: step
command: kubectl get all -n {{.inputs.namespace}}
on_success: done
done:
type: terminal
status: successExample: Additive Mode (Native + Plugin)
notify_deployment:
type: agent
provider: claude
prompt: "Send a deployment summary to Slack"
mcp_proxy:
enable: true
intercept_builtins: false
# Agent uses native Read/Write/Bash (not traced)
# + notify_send_slack (traced)
plugin_tools:
- plugin: notify
expose: [send_slack, send_email]
options:
model: claude-haiku-4-5
on_success: doneSee Also
- Agent Steps — Full agent step reference
- OpenTelemetry Tracing — Configure telemetry export
- Plugins — Install and manage plugins
- Error Codes — USER.MCP_PROXY.* codes