import type { McpScope } from '@n8n/api-types'; import { MCP_INSTANCE_SCOPES } from '@n8n/api-types'; import { MCP_GET_USER_PREFERENCES_TOOL_NAME, MCP_SAVE_USER_PREFERENCE_TOOL_NAME, MCP_UNDO_USER_PREFERENCE_TOOL_NAME, MCP_UPDATE_USER_PREFERENCE_TOOL_NAME, } from './mcp.constants'; /** * Maps each grantable OAuth scope to the MCP tools it unlocks. A tool is * available if ANY granted scope covers it, so support tools (node search, * SDK reference, validation) can ride on both read and write scopes. * * Keep in sync with the tools registered in `McpService.getServer` — the * drift-guard test in `__tests__/mcp-scopes.test.ts` fails when a registered * tool is missing here. */ export const TOOLS_BY_SCOPE: Record = { 'workflow:read': [ 'search_workflows', 'get_workflow_details', 'get_workflow_history', 'get_workflow_version', 'get_workflow_versions_diff', // Read-only builder support tools 'search_nodes', 'get_node_types', 'get_workflow_best_practices', 'get_workflow_sdk_reference', 'validate_workflow', 'validate_node_config', // Instance-context reads. Both describe workflows, so `workflow:read` is the bar; the // activity tools drop credential entries for a grant that lacks `credential:read`. 'get_instance_context', 'get_instance_activity', 'expand_instance_activity', 'get_node_usage', ], 'workflow:write': [ 'create_workflow_from_code', 'update_workflow', 'archive_workflow', 'restore_workflow_version', 'publish_workflow', 'unpublish_workflow', 'move_workflows_to_folder', // Builder support tools, so a write-only grant can still build 'search_nodes', 'get_node_types', 'get_workflow_best_practices', 'get_workflow_sdk_reference', 'validate_workflow', 'validate_node_config', ], 'workflow:execute': ['execute_workflow', 'test_workflow', 'prepare_workflow_pin_data'], 'execution:read': ['get_workflow_execution', 'search_workflow_executions'], 'agent:read': [ 'search_agents', 'get_agent', 'list_agent_versions', 'discover_agent_assets', 'validate_agent', 'get_agent_builder_reference', ], // The read tools ride along on a write-only grant: mutate_agent's // configHash handshake starts at get_agent, and building needs search // (sub-agents), asset discovery, validation, and the reference. 'agent:write': [ 'create_agent', 'mutate_agent', 'revert_agent', 'delete_agent', 'verify_agent_mcp_server', 'search_agents', 'get_agent', 'list_agent_versions', 'discover_agent_assets', 'validate_agent', 'get_agent_builder_reference', 'update_agent_integration', 'publish_agent', 'unpublish_agent', ], 'agent:execute': ['call_agent'], 'skill:read': ['search_skills', 'get_skill'], 'skill:write': ['create_skill', 'update_skill', 'delete_skill', 'search_skills', 'get_skill'], // explore_node_resources queries external services with stored credentials, // so it must sit behind the credential scope rather than a workflow one. 'credential:read': ['list_credentials', 'list_n8n_gateway_services', 'explore_node_resources'], 'dataTable:read': ['search_data_tables', 'get_data_table_rows'], // Writing requires finding tables, so search rides along. 'dataTable:write': [ 'search_data_tables', 'create_data_table', 'rename_data_table', 'add_data_table_column', 'delete_data_table_column', 'rename_data_table_column', 'add_data_table_rows', ], 'project:read': ['search_projects', 'search_folders'], // Creating or moving folders requires finding projects and folders first, // so the search tools ride along on a write-only grant. 'project:write': ['create_folder', 'update_folder', 'search_projects', 'search_folders'], 'tag:read': ['list_workflow_tags'], // Installing a package changes what the instance can run, so it gets its own // scope rather than riding along on a workflow write grant. The tool is also // gated on the user's `communityPackage:install` global scope at // registration, so granting this to a member still exposes nothing. 'communityPackage:install': ['install_community_node'], // `ai_preference` is a first-class resource, so reading it rides on a normal scope rather // than an MCP-only string. Not builder-gated: preferences apply to Agents, data tables and // folders too, none of which need the builder. 'aiPreference:read': [MCP_GET_USER_PREFERENCES_TOOL_NAME], // The read tool does not ride along: a client that may write is expected to read first, and // the consent screen shows that as two scopes. Undo is here rather than on a delete scope // because it only removes what this surface wrote. 'aiPreference:write': [ MCP_SAVE_USER_PREFERENCE_TOOL_NAME, MCP_UPDATE_USER_PREFERENCE_TOOL_NAME, MCP_UNDO_USER_PREFERENCE_TOOL_NAME, ], }; /** * Tools that operate on folders and therefore require the `feat:folders` * license, matching the gate on the REST and public API folder endpoints. * Only registered (and advertised on the consent screen) when the instance * is licensed for folders. */ export const FOLDER_FEATURE_TOOLS: ReadonlySet = new Set([ 'search_folders', 'create_folder', 'update_folder', 'move_workflows_to_folder', ]); /** * Tools only registered when the workflow builder is enabled * (`N8N_MCP_BUILDER_ENABLED`). Keep in sync with `registerBuilderTools` in * `mcp.service.ts` — covered by the same drift-guard test as TOOLS_BY_SCOPE. */ export const BUILDER_TOOLS: ReadonlySet = new Set([ 'search_nodes', 'get_node_types', 'get_workflow_best_practices', 'get_workflow_sdk_reference', 'validate_workflow', 'validate_node_config', 'create_workflow_from_code', 'update_workflow', 'archive_workflow', 'restore_workflow_version', 'explore_node_resources', 'search_projects', 'search_folders', 'install_community_node', ...FOLDER_FEATURE_TOOLS, ]); /** * Tools that additionally require the `community-packages` module to be active, * verified packages to be enabled, and the caller to hold the * `communityPackage:install` global scope. Excluded from the "every mapped tool * is registered" drift guard for the same reason as {@link AGENT_TOOLS}: none of * those conditions hold in a bare service under test. The gate itself is covered * by `isCommunityNodeInstallAvailable` in mcp-tool-availability.test.ts. */ export const COMMUNITY_PACKAGE_TOOLS: ReadonlySet = new Set(['install_community_node']); /** Context tools that also require the instance-ai module. */ export const ACTIVITY_LOG_TOOLS: ReadonlySet = new Set([ 'get_instance_context', 'get_instance_activity', 'expand_instance_activity', ]); /** Tools that require the shared instance activity gate. */ export const INSTANCE_CONTEXT_TOOLS: ReadonlySet = new Set([ 'get_instance_context', 'get_instance_activity', 'expand_instance_activity', 'get_node_usage', ]); export const AGENT_TOOLS: ReadonlySet = new Set([ ...TOOLS_BY_SCOPE['agent:read'], ...TOOLS_BY_SCOPE['agent:write'], ...TOOLS_BY_SCOPE['agent:execute'], ]); export const SKILL_TOOLS: ReadonlySet = new Set([ ...TOOLS_BY_SCOPE['skill:read'], ...TOOLS_BY_SCOPE['skill:write'], ]); function isMcpScope(scope: string): scope is McpScope { return (MCP_INSTANCE_SCOPES as readonly string[]).includes(scope); } /** * Resolves the set of tool names a token with the given granted scopes may * list and call. `undefined` means the credential predates scoping or is not * scope-bearing (e.g. an API key) and grants access to all tools. */ export function getAllowedToolNames(grantedScopes: string[] | undefined): Set | undefined { if (grantedScopes === undefined) return undefined; const allowed = new Set(); for (const scope of grantedScopes) { if (!isMcpScope(scope)) continue; for (const toolName of TOOLS_BY_SCOPE[scope]) { allowed.add(toolName); } } return allowed; }