n8n-mcp icon indicating copy to clipboard operation
n8n-mcp copied to clipboard

Per-operation tool filtering for read-only deployments (DISABLED_TOOL_OPERATIONS)

Open czlonkowski opened this issue 3 months ago • 1 comments

Background

Several users running n8n-mcp in enterprise / governance-sensitive contexts want a true read-only mode but are blocked by tools that bundle read and write operations under a single tool name. The existing DISABLED_TOOLS env var only operates at the tool level, so disabling a destructive branch means losing the read branch as well.

Discussed in #564 (by @mahmoudnaif, with a strong governance writeup from @christiansousadev).

The problem

Two tools mix read and destructive operations under one name:

  • n8n_workflow_versions

    • Read: list, get
    • Write/destructive: rollback, delete, prune, truncate
  • n8n_executions

    • Read: list, get
    • Destructive: delete

Today the only way to block the destructive branches via n8n-mcp is to disable the entire tool, which also kills the valuable read paths. Tool annotations (destructiveHint: true) exist but are metadata only — nothing in the dispatch layer enforces them.

The robust workaround today is n8n-side API key scoping (RBAC), which is the right pattern for production but does not give clients a way to express the same intent at the MCP layer.

Proposed feature

Add an env var that filters operations within a tool at dispatch time, before the handler runs:

DISABLED_TOOL_OPERATIONS=n8n_workflow_versions:delete,rollback,prune,truncate;n8n_executions:delete

Format sketch (open to bikeshedding):

  • Semicolon-separated list of <tool_name>:<comma_separated_operations>
  • Operation names match the existing action / mode enum values in each tools schema
  • Disabled operations should also be advertised as such in tools_documentation output so the LLM does not waste a turn trying them

Acceptance criteria

  • [ ] New env var parsed at startup with the same safety limits as DISABLED_TOOLS (size cap, count cap, validation against known tool/operation names)
  • [ ] Dispatch layer in src/mcp/server.ts checks the disabled-operation set before calling the handler and returns a clear error (something like Operation 'delete' on tool 'n8n_executions' is disabled by server policy)
  • [ ] tools_documentation reflects the disabled operations so they are visible to clients
  • [ ] Unit tests covering: parser edge cases, dispatch enforcement for both n8n_workflow_versions and n8n_executions, interaction with existing DISABLED_TOOLS
  • [ ] README / docs/ section explaining the read-only deployment recipe (this env var + n8n API key RBAC)

Out of scope

  • Full RBAC inside n8n-mcp — that lives in the n8n API
  • Per-user / per-session policy — this is a server-level config knob

Notes

  • The implementation is straightforward: a Set lookup in the dispatch path. Cheap at runtime.
  • Happy to review a PR. Tagging @christiansousadev and @mahmoudnaif from the discussion in case either of you wants to take a swing at it.

Concieved by Romuald Członkowski — https://www.aiadvisors.pl/en

czlonkowski avatar Apr 08 '26 19:04 czlonkowski

🎯 Agentic Issue Triage

This is a well-scoped feature request for sub-tool-level operation filtering via a DISABLED_TOOL_OPERATIONS environment variable. The motivation is clear: two MCP tools (n8n_workflow_versions, n8n_executions) bundle read and destructive operations under a single name, and the existing DISABLED_TOOLS mechanism only operates at the whole-tool level, forcing an all-or-nothing choice.

The implementation path is straightforward — the existing getDisabledTools() pattern in src/mcp/server.ts is the direct template for the new feature.

📍 Relevant code locations
File Relevance
src/mcp/server.ts getDisabledTools() / disabledToolsCache — model to follow; dispatch check at callTool handler
src/mcp/tools.ts Tool definitions listing action/mode enum values per tool — needed to build the allowlist for validation
src/mcp/tools-documentation.ts tools_documentation output — needs to advertise disabled operations

The two tools in scope:

  • n8n_workflow_versions — destructive ops: rollback, delete, prune, truncate
  • n8n_executions — destructive op: delete
🔧 Implementation sketch

1. Parser (mirrors getDisabledTools()):

// src/mcp/server.ts
private disabledToolOperationsCache: Map<string, Set<string>> | null = null;

private getDisabledToolOperations(): Map<string, Set<string>> {
  if (this.disabledToolOperationsCache !== null) return this.disabledToolOperationsCache;
  const env = process.env.DISABLED_TOOL_OPERATIONS || '';
  const map = new Map<string, Set<string>>();
  if (!env) { this.disabledToolOperationsCache = map; return map; }
  // guard: size cap
  const raw = env.length > 10000 ? env.substring(0, 10000) : env;
  for (const segment of raw.split(';').slice(0, 200)) {
    const [tool, ops] = segment.split(':');
    if (tool && ops) map.set(tool.trim(), new Set(ops.split(',').map(o => o.trim())));
  }
  this.disabledToolOperationsCache = map;
  return map;
}

2. Dispatch check (slot in after existing DISABLED_TOOLS check, before handler call):

const disabledOps = this.getDisabledToolOperations().get(name);
if (disabledOps) {
  const operation = args?.action ?? args?.mode;
  if (operation && disabledOps.has(operation)) {
    return { isError: true, content: [{ type: 'text',
      text: `Operation '${operation}' on tool '${name}' is disabled by server policy.` }] };
  }
}

3. tools_documentation: when building the documentation string for a tool, append a note listing disabled operations if the tool appears in the map. This can be a short suffix like (disabled operations: delete, rollback).

4. Validation at startup: cross-check parsed tool/operation names against the known schema to warn on typos.

🧪 Testing checklist
  • [ ] Parser: empty env → empty map
  • [ ] Parser: size/count cap enforcement (mirror existing DISABLED_TOOLS tests)
  • [ ] Parser: malformed segments (no :, empty ops list) handled gracefully
  • [ ] Dispatch: n8n_executions delete blocked, list and get still pass through
  • [ ] Dispatch: n8n_workflow_versions rollback / delete / prune / truncate blocked, list / get pass
  • [ ] Dispatch: interaction with DISABLED_TOOLS (tool-level disable takes precedence and tool is filtered before operation check)
  • [ ] tools_documentation output includes disabled-ops annotation for affected tools
📚 Suggested docs / README section

A short "Read-only deployment recipe" section in README.md or docs/ explaining:

  1. Set DISABLED_TOOL_OPERATIONS=n8n_workflow_versions:delete,rollback,prune,truncate;n8n_executions:delete to block all destructive ops at the MCP layer.
  2. Also scope the n8n API key to read-only permissions (RBAC) for defence in depth — the MCP-layer block is a UX guardrail, the API key is the enforcement point.
  3. Optionally combine with DISABLED_TOOLS to remove write-only tools entirely (e.g. n8n_delete_workflow).
💡 Notes for reviewers
  • The args?.action ?? args?.mode pattern should cover both affected tools; verify that n8n_workflow_versions uses action and n8n_executions uses action (or mode) in their respective schemas in src/mcp/tools.ts.
  • Consider whether tools/list should also omit the enum values for disabled operations from the JSON Schema returned to clients (not just tools_documentation). That would prevent schema-aware clients from offering those options.
  • The disabledToolOperationsCache should be invalidated (set to null) under the same conditions as disabledToolsCache, if any hot-reload path exists.

Generated by Agentic Triage for issue #714 · ● 238.6K ·

To install this agentic workflow, run

gh aw add githubnext/agentics/workflows/issue-triage.md@13377ddf7e35c2b6e47aa58f45acb228fba902c8

github-actions[bot] avatar Apr 28 '26 08:04 github-actions[bot]