SecurityCommercialFreeActiveMachine-verified· intermediate · ~15 min setup

n8n as an MCP server: prove each exposed workflow is one narrow tool behind its own token

Expose n8n workflows to an agent through MCP Server Triggers without handing it your whole toolbox, by proving each agent-callable trigger is one narrow workflow behind its own bearer token with no wildcard-HTTP or delete-capable node, so a leaked token or a prompt injection is bounded.

by Shilpa Mitra· verified today· v1.0.0

Run this workflow

CI-verified, 2/2 fixtures passing.

Build this with your agent

One copy-paste hands Claude Code, Codex, or Cursor the full recipe, steps included, nothing to fetch.

Intended Use

Anyone exposing n8n workflows to an agent via the MCP Server Trigger who wants to bound the blast radius before a token leaks. CI audits a set of trigger configs and asserts each agent-callable trigger exposes exactly one workflow behind a dedicated (not shared) bearer token with no wildcard-HTTP or delete/drop/truncate node, and that the whole-toolbox anti-pattern is caught with named findings. Pure Python stdlib, no n8n, no server, no network, no key. The confused-deputy runtime behaviour, per-node auth, and the real deploy are fenced.

Not for

  • Bolting your whole toolbox onto one MCP Server Trigger, its single bearer token gates the entire endpoint not individual tools, so one leaked token makes every wired node callable; expose one narrow workflow per trigger with its own token
  • Attaching a loaded-gun node to an agent-callable trigger, a wildcard HTTP Request node or a delete-capable database node is a gun you just handed a language model; keep agent-exposed tools read-only or tightly scoped
  • Forgetting the agent runs as YOU, tools execute with n8n's stored credentials (your Postgres user, your Slack token), so a prompt injection reaching the agent runs with your access, not the attacker's, this is a confused-deputy risk to design against
  • Trusting a community MCP node as official, packages like czlonkowski/n8n-mcp and n8n-nodes-mcp are independent projects that run third-party code inside your instance with your instance's access; read the source and pin a version
  • Calling n8n open source or dating MCP to the July 29 2026 release, n8n is fair-code / source-available under the Sustainable Use License (not OSI), and the MCP nodes shipped in late 2025 while July 29 added an AI workflow builder and admin-managed credentials

The Stack

Tested Against

docs.n8n.io MCP Server Trigger node (2026-08)github.com/n8n-io/n8n (2026-08)python@3.9-3.14

Side effects & data flow

Network
none, local only
Writes
no filesystem writes
Credentials
none required

Prerequisites

  • Python 3 for the audit (no packages)
  • For a real deployment: n8n with the MCP Server Trigger node, one bearer token per exposed workflow

Steps

  1. 1

    Audit the trigger configs: one narrow workflow, own token, no loaded gun

    Model your MCP Server Triggers as {token, workflows, nodes}. CI asserts every agent-callable trigger holds exactly one workflow, no two triggers share a bearer token, and no wired node is a wildcard HTTP Request or a delete/drop/truncate database node. It reports the worst-case blast radius (the number of tools reachable if a single token leaks) and proves the whole-toolbox anti-pattern is caught. A clean config bounds a leaked token to one narrow workflow's tools; the anti-pattern exposes everything.

    python3 - <<'EOF'
    import re
    good = [
        {"id": "order-lookup", "token": "tok_orders", "workflows": ["order-lookup"], "nodes": ["postgres_select_readonly", "http_get_status_fixedhost"]},
        {"id": "post-status", "token": "tok_status", "workflows": ["status-post"], "nodes": ["slack_post_to_ops_channel"]},
    ]
    bad = [
        {"id": "toolbox", "token": "tok_shared", "workflows": ["orders", "billing", "admin"], "nodes": ["http_request_wildcard", "postgres_delete", "slack_post_any"]},
        {"id": "reports", "token": "tok_shared", "workflows": ["reports"], "nodes": ["postgres_select_readonly"]},
    ]
    danger = [r"wildcard", r"_any$", r"\bany\b", r"delete", r"drop", r"truncate", r"exec", r"_write$"]
    def loaded_gun(node):
        return any(re.search(p, node, re.I) for p in danger)
    def audit(triggers):
        findings = []
        tokens = [t["token"] for t in triggers]
        for t in triggers:
            if tokens.count(t["token"]) > 1:
                findings.append(t["id"] + ": bearer token shared across triggers (one leaked token exposes all of them)")
            if len(t["workflows"]) != 1:
                findings.append(t["id"] + ": exposes " + str(len(t["workflows"])) + " workflows on one endpoint (expose one narrow workflow)")
            guns = [n for n in t["nodes"] if loaded_gun(n)]
            if guns:
                findings.append(t["id"] + ": loaded-gun node(s) agent-callable: " + ", ".join(guns))
        blast = max(sum(len(t["nodes"]) for t in triggers if t["token"] == tok) for tok in set(tokens))
        return findings, blast
    
    good_findings, good_blast = audit(good)
    bad_findings, bad_blast = audit(bad)
    assert not good_findings, good_findings
    assert bad_findings, "the whole-toolbox anti-pattern must be caught"
    print("n8n MCP exposure OK: each agent-callable Server Trigger exposes ONE narrow workflow behind its OWN bearer token with no loaded-gun node, so a leaked token is bounded to " + str(good_blast) + " tool(s); the anti-pattern (whole toolbox on one shared token, delete-capable node attached) is caught with " + str(len(bad_findings)) + " finding(s). The confused-deputy runtime, per-node auth, and the real deploy are fenced")
    EOF
  2. 2

    Do it in real n8n (the parts CI cannot do for you)

    Drop one MCP Server Trigger per narrow workflow, set a distinct Bearer/header token on each, and wire in only the nodes that workflow needs (prefer read-only). Point your agent at the URL, and if it is Claude Desktop bridge stdio to HTTP with mcp-remote. Then watch the execution log while the agent uses it, you will learn your real exposure in ten minutes of watching what it calls. Behind nginx disable proxy buffering on the MCP endpoint, and in queue mode route all /mcp traffic to one dedicated replica. The runtime behaviour and what the agent actually decides to call are the fenced parts.

Eval, 2 fixtures

Last passed: verified today
  • exposure-okcontainstimeout 30s · max $0

    Expected: n8n MCP exposure OK: each agent-callable Server Trigger exposes ONE narrow workflow behind its OWN bearer token with no loaded-gun node, so a leaked token is bounded to 2 tool(s); the anti-pattern (whole toolbox on one shared token, delete-capable node attached) is caught with 4 finding(s). The confused-deputy runtime, per-node auth, and the real deploy are fenced

  • clean-exitexit_codetimeout 30s · max $0

    Expected: 0

Results

Since late 2025 n8n can act as an MCP server: drop an MCP Server Trigger on a canvas and every tool node wired into it (HTTP Request, Postgres, Slack) becomes a tool an external agent can call. The catch nobody demos is that the agent runs those tools with n8n's stored credentials, not its own, so a prompt injection that reaches the agent is a confused-deputy problem, and the trigger's single bearer token gates the whole endpoint rather than individual tools. The fix is least privilege: one narrow, ideally read-only workflow per trigger, each behind its own token, and no loaded-gun node (a wildcard HTTP Request or a delete-capable database node) attached to anything an agent can call. This recipe turns that into an audit.

Did this work for you?

Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.

Related workflows

Liked this workflow?

Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).