feat(core): Rename the MCP n8n Connect services tool to Gateway credits (no-changelog) (#37272)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Precious Amesinlola 2026-08-31 07:13:28 +00:00 committed by GitHub
parent c96e7c0da4
commit db49eaec07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 100 additions and 90 deletions

View File

@ -954,7 +954,7 @@ describe('create-workflow-from-code MCP tool', () => {
},
{
nodeName: 'OpenAI',
credentialName: 'n8n credits',
credentialName: 'Gateway credits',
credentialType: 'openAiApi',
source: 'aiGateway',
},

View File

@ -397,12 +397,12 @@ describe('autoPopulateNodeCredentials', () => {
);
expect(node.credentials).toEqual({
slackApi: { id: null, name: 'n8n credits', __aiGatewayManaged: true },
slackApi: { id: null, name: 'Gateway credits', __aiGatewayManaged: true },
});
expect(result.assignments).toEqual([
{
nodeName: 'Test Node',
credentialName: 'n8n credits',
credentialName: 'Gateway credits',
credentialType: 'slackApi',
source: 'aiGateway',
},
@ -473,13 +473,13 @@ describe('autoPopulateNodeCredentials', () => {
);
expect(node.credentials).toEqual({
serviceApiKey: { id: null, name: 'n8n credits', __aiGatewayManaged: true },
serviceApiKey: { id: null, name: 'Gateway credits', __aiGatewayManaged: true },
});
expect(node.parameters.authentication).toBe('apiKey');
expect(result.assignments).toEqual([
{
nodeName: 'Test Node',
credentialName: 'n8n credits',
credentialName: 'Gateway credits',
credentialType: 'serviceApiKey',
source: 'aiGateway',
},
@ -551,7 +551,7 @@ describe('autoPopulateNodeCredentials', () => {
);
expect(node.credentials).toEqual({
serviceApiKey: { id: null, name: 'n8n credits', __aiGatewayManaged: true },
serviceApiKey: { id: null, name: 'Gateway credits', __aiGatewayManaged: true },
});
expect(node.parameters.authentication).toBe('apiKey');
});
@ -616,7 +616,7 @@ describe('autoPopulateNodeCredentials', () => {
);
expect(node.credentials).toEqual({
serviceApiKey: { id: null, name: 'n8n credits', __aiGatewayManaged: true },
serviceApiKey: { id: null, name: 'Gateway credits', __aiGatewayManaged: true },
});
expect(node.parameters.authentication).toBe('apiKeyLegacy');
});
@ -807,7 +807,7 @@ describe('autoPopulateNodeCredentials', () => {
// Canonicalized to the sentinel and kept — the explicit n8n Connect request
// wins over the owned credential, and no assignment is recorded for it.
expect(node.credentials).toEqual({
slackApi: { id: null, name: 'n8n credits', __aiGatewayManaged: true },
slackApi: { id: null, name: 'Gateway credits', __aiGatewayManaged: true },
});
expect(result.assignments).toEqual([]);
expect(result.outcomes).toEqual([]);

View File

@ -124,7 +124,7 @@ describe('get-workflow-details MCP tool', () => {
disabled: false,
parameters: {},
credentials: {
openAiApi: { id: null, name: 'n8n credits', __aiGatewayManaged: true },
openAiApi: { id: null, name: 'Gateway credits', __aiGatewayManaged: true },
},
},
{

View File

@ -50,7 +50,7 @@ describe('get-workflow-node-types MCP tool', () => {
expect(result.structuredContent).toEqual({ definitions: 'typescript definitions' });
});
test('adds n8nConnect block when gateway is available', async () => {
test('adds gatewayCredits block when gateway is available', async () => {
aiGatewayService.isAvailable.mockResolvedValue({
available: true,
config: {
@ -68,18 +68,18 @@ describe('get-workflow-node-types MCP tool', () => {
expect(result.structuredContent).toEqual({
definitions: 'typescript definitions',
n8nConnect: {
gatewayCredits: {
credentialTypes: ['openAiApi'],
nodes: ['@n8n/n8n-nodes-langchain.openAi'],
},
});
// Also mirrored into the unstructured content for text-only clients.
expect((result.content[0] as { text: string }).text).toBe(
'typescript definitions\n\nn8nConnect: {"credentialTypes":["openAiApi"],"nodes":["@n8n/n8n-nodes-langchain.openAi"]}',
'typescript definitions\n\ngatewayCredits: {"credentialTypes":["openAiApi"],"nodes":["@n8n/n8n-nodes-langchain.openAi"]}',
);
});
test('omits n8nConnect block when unavailable', async () => {
test('omits gatewayCredits block when unavailable', async () => {
const tool = createTool();
const result = await tool.handler(
{ nodeIds: [{ nodeId: 'n8n-nodes-base.slack' }] },

View File

@ -275,7 +275,7 @@ describe('list-credentials MCP tool', () => {
);
});
describe('n8nConnect block', () => {
describe('gatewayCredits block', () => {
async function callHandler(opts: { available?: boolean } = {}) {
const { credentialsService, telemetry } = createMocks([buildCredential()]);
const { aiGatewayService } = makeAiGatewayMocks(opts);
@ -298,21 +298,21 @@ describe('list-credentials MCP tool', () => {
return result.structuredContent as {
data: unknown[];
count: number;
n8nConnect?: { credentialTypes: string[]; nodes: string[] };
gatewayCredits?: { credentialTypes: string[]; nodes: string[] };
};
}
test('includes n8nConnect block when gateway is available', async () => {
test('includes gatewayCredits block when gateway is available', async () => {
const structured = await callHandler({ available: true });
expect(structured.n8nConnect).toEqual({
expect(structured.gatewayCredits).toEqual({
credentialTypes: ['openAiApi'],
nodes: ['@n8n/n8n-nodes-langchain.openAi'],
});
});
test('omits n8nConnect block when unavailable', async () => {
test('omits gatewayCredits block when unavailable', async () => {
const structured = await callHandler({ available: false });
expect(structured.n8nConnect).toBeUndefined();
expect(structured.gatewayCredits).toBeUndefined();
});
});
});

View File

@ -6,7 +6,7 @@ import { mock } from 'vitest-mock-extended';
import type { AiGatewayService } from '@/services/ai-gateway.service';
import { Telemetry } from '@/telemetry';
import { createListN8nConnectServicesTool } from '../tools/list-n8n-connect-services.tool';
import { createListN8nGatewayServicesTool } from '../tools/list-n8n-gateway-services.tool';
const user = Object.assign(new User(), { id: 'user-1' });
@ -39,11 +39,11 @@ function makeMocks(opts: { available?: boolean; config?: AiGatewayConfigDto } =
return { aiGatewayService, telemetry };
}
describe('list_n8n_connect_services MCP tool', () => {
test('registers under the name list_n8n_connect_services', () => {
describe('list_n8n_gateway_services MCP tool', () => {
test('registers under the name list_n8n_gateway_services', () => {
const { aiGatewayService, telemetry } = makeMocks();
const tool = createListN8nConnectServicesTool(user, aiGatewayService, telemetry);
expect(tool.name).toBe('list_n8n_connect_services');
const tool = createListN8nGatewayServicesTool(user, aiGatewayService, telemetry);
expect(tool.name).toBe('list_n8n_gateway_services');
expect(tool.config.annotations).toMatchObject({
readOnlyHint: true,
destructiveHint: false,
@ -54,7 +54,7 @@ describe('list_n8n_connect_services MCP tool', () => {
test('returns full coverage payload when available', async () => {
const { aiGatewayService, telemetry } = makeMocks({ available: true });
const tool = createListN8nConnectServicesTool(user, aiGatewayService, telemetry);
const tool = createListN8nGatewayServicesTool(user, aiGatewayService, telemetry);
const result = await tool.handler({}, {} as never);
expect(result.structuredContent).toEqual({
available: true,
@ -70,20 +70,20 @@ describe('list_n8n_connect_services MCP tool', () => {
test('returns { available: false } when unavailable', async () => {
const { aiGatewayService, telemetry } = makeMocks({ available: false });
const tool = createListN8nConnectServicesTool(user, aiGatewayService, telemetry);
const tool = createListN8nGatewayServicesTool(user, aiGatewayService, telemetry);
const result = await tool.handler({}, {} as never);
expect(result.structuredContent).toEqual({ available: false });
});
test('emits USER_CALLED_MCP_TOOL_EVENT with tool_name and success', async () => {
const { aiGatewayService, telemetry } = makeMocks();
const tool = createListN8nConnectServicesTool(user, aiGatewayService, telemetry);
const tool = createListN8nGatewayServicesTool(user, aiGatewayService, telemetry);
await tool.handler({}, {} as never);
expect(telemetry.track).toHaveBeenCalledWith(
'User called mcp tool',
expect.objectContaining({
user_id: 'user-1',
tool_name: 'list_n8n_connect_services',
tool_name: 'list_n8n_gateway_services',
results: { success: true, data: { available: true } },
}),
);

View File

@ -4,7 +4,7 @@ describe('getMcpInstructions', () => {
test('returns intro-only string when builder is disabled', () => {
const instructions = getMcpInstructions({ isBuilderEnabled: false });
expect(instructions).toContain('official MCP server for n8n');
expect(instructions).not.toContain('n8nConnect');
expect(instructions).not.toContain('gatewayCredits');
});
test('includes n8n credits hint when builder is enabled and n8n Connect is available', () => {
@ -12,10 +12,10 @@ describe('getMcpInstructions', () => {
isBuilderEnabled: true,
isN8nConnectAvailable: true,
});
expect(instructions).toContain('nodes covered by n8n credits');
expect(instructions).toContain('n8nConnect.nodes');
expect(instructions).toContain('n8n credits');
expect(instructions).toContain('list_n8n_connect_services');
expect(instructions).toContain('nodes covered by Gateway credits');
expect(instructions).toContain('gatewayCredits.nodes');
expect(instructions).toContain('Gateway credits');
expect(instructions).toContain('list_n8n_gateway_services');
});
test('omits n8n credits hint when n8n Connect is not available', () => {
@ -24,14 +24,14 @@ describe('getMcpInstructions', () => {
isN8nConnectAvailable: false,
});
expect(instructions).toContain('official MCP server for n8n');
expect(instructions).not.toContain('n8n credits');
expect(instructions).not.toContain('n8nConnect');
expect(instructions).not.toContain('list_n8n_connect_services');
expect(instructions).not.toContain('Gateway credits');
expect(instructions).not.toContain('gatewayCredits');
expect(instructions).not.toContain('list_n8n_gateway_services');
});
test('omits n8n credits hint by default', () => {
const instructions = getMcpInstructions({ isBuilderEnabled: true });
expect(instructions).not.toContain('n8n credits');
expect(instructions).not.toContain('Gateway credits');
});
describe('node groups pointer', () => {

View File

@ -82,6 +82,10 @@ describe('getAllowedToolNames', () => {
it('grants only call_agent with agent:execute', () => {
expect(getAllowedToolNames(['agent:execute'])).toEqual(new Set(['call_agent']));
});
it('exposes the renamed list_n8n_gateway_services tool via credential:read', () => {
expect(getAllowedToolNames(['credential:read'])).toContain('list_n8n_gateway_services');
});
});
describe('McpService scope enforcement', () => {

View File

@ -118,8 +118,8 @@ describe('search-workflow-nodes MCP tool', () => {
);
});
describe('n8nConnect block', () => {
test('includes n8nConnect block when gateway is available', async () => {
describe('gatewayCredits block', () => {
test('includes gatewayCredits block when gateway is available', async () => {
aiGatewayService.isAvailable.mockResolvedValue({
available: true,
config: {
@ -134,18 +134,18 @@ describe('search-workflow-nodes MCP tool', () => {
expect(result.structuredContent).toEqual({
results: 'search-result',
n8nConnect: {
gatewayCredits: {
credentialTypes: ['openAiApi'],
nodes: ['@n8n/n8n-nodes-langchain.openAi'],
},
});
// Also mirrored into the unstructured content for text-only clients.
expect((result.content[0] as { text: string }).text).toBe(
'search-result\n\nn8nConnect: {"credentialTypes":["openAiApi"],"nodes":["@n8n/n8n-nodes-langchain.openAi"]}',
'search-result\n\ngatewayCredits: {"credentialTypes":["openAiApi"],"nodes":["@n8n/n8n-nodes-langchain.openAi"]}',
);
});
test('omits n8nConnect block when unavailable', async () => {
test('omits gatewayCredits block when unavailable', async () => {
const tool = createTool();
const result = await tool.handler({ queries: ['openai'] }, {} as never);
expect(result.structuredContent).toEqual({ results: 'search-result' });

View File

@ -1958,7 +1958,7 @@ describe('update-workflow MCP tool', () => {
},
{
nodeName: 'D',
credentialName: 'n8n credits',
credentialName: 'Gateway credits',
credentialType: 'openAiApi',
source: 'aiGateway',
},
@ -1983,7 +1983,7 @@ describe('update-workflow MCP tool', () => {
{ nodeName: 'C', credentialName: 'My Slack', credentialType: 'slackApi', source: 'user' },
{
nodeName: 'D',
credentialName: 'n8n credits',
credentialName: 'Gateway credits',
credentialType: 'openAiApi',
source: 'aiGateway',
},

View File

@ -73,7 +73,7 @@ export const TOOLS_BY_SCOPE: Record<McpScope, readonly string[]> = {
'agent:execute': ['call_agent'],
// 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_connect_services', 'explore_node_resources'],
'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': [

View File

@ -50,7 +50,7 @@ export const MISSING_PROTOCOL_VERSION_ERROR_MESSAGE =
/**
* Tool name constants
*/
export const LIST_N8N_CONNECT_SERVICES_TOOL_NAME = 'list_n8n_connect_services';
export const LIST_N8N_GATEWAY_SERVICES_TOOL_NAME = 'list_n8n_gateway_services';
export const MCP_CALL_AGENT_TOOL_NAME = 'call_agent';
export const MCP_CREATE_AGENT_TOOL_NAME = 'create_agent';

View File

@ -77,7 +77,7 @@ import { createGetWorkflowHistoryTool } from './tools/get-workflow-history.tool'
import { createGetWorkflowVersionTool } from './tools/get-workflow-version.tool';
import { createGetWorkflowVersionsDiffTool } from './tools/get-workflow-versions-diff.tool';
import { createListCredentialsTool } from './tools/list-credentials.tool';
import { createListN8nConnectServicesTool } from './tools/list-n8n-connect-services.tool';
import { createListN8nGatewayServicesTool } from './tools/list-n8n-gateway-services.tool';
import { createListTagsTool } from './tools/list-tags.tool';
import { createMoveWorkflowsToFolderTool } from './tools/move-workflows-to-folder.tool';
import { createPrepareTestPinDataTool } from './tools/prepare-workflow-pin-data.tool';
@ -560,13 +560,13 @@ export class McpService {
this.aiGatewayService,
);
const listN8nConnectServicesTool = createListN8nConnectServicesTool(
const listN8nGatewayServicesTool = createListN8nGatewayServicesTool(
user,
this.aiGatewayService,
this.telemetry,
);
registerIfAllowed(listCredentialsTool);
registerIfAllowed(listN8nConnectServicesTool);
registerIfAllowed(listN8nGatewayServicesTool);
if (!this.globalConfig.tags.disabled) {
const listTagsTool = createListTagsTool(user, this.tagService, this.telemetry);

View File

@ -7,7 +7,7 @@ import type { AiGatewayService } from '@/services/ai-gateway.service';
import type { Telemetry } from '@/telemetry';
import { toN8nConnectCoverage } from '../mcp-ai-gateway.helper';
import { LIST_N8N_CONNECT_SERVICES_TOOL_NAME, USER_CALLED_MCP_TOOL_EVENT } from '../mcp.constants';
import { LIST_N8N_GATEWAY_SERVICES_TOOL_NAME, USER_CALLED_MCP_TOOL_EVENT } from '../mcp.constants';
import type {
N8nConnectCoverage,
ToolDefinition,
@ -51,14 +51,14 @@ const n8nConnectSchema = z
.object({
credentialTypes: z
.array(z.string())
.describe('Credential type names that n8n credits can provide (e.g. "openAiApi").'),
.describe('Credential type names that Gateway credits can provide (e.g. "openAiApi").'),
nodes: z
.array(z.string())
.describe('Node types covered by n8n credits (e.g. "@n8n/n8n-nodes-langchain.openAi").'),
.describe('Node types covered by Gateway credits (e.g. "@n8n/n8n-nodes-langchain.openAi").'),
})
.optional()
.describe(
`Present when n8n credits is available for this instance. Omitted otherwise. Candidate coverage only — actual eligibility for a managed credential also depends on the node action, minimum type version, and hidden properties; call ${LIST_N8N_CONNECT_SERVICES_TOOL_NAME} for the authoritative contract.`,
`Present when Gateway credits are available for this instance. Omitted otherwise. Candidate coverage only — actual eligibility for a managed credential also depends on the node action, minimum type version, and hidden properties; call ${LIST_N8N_GATEWAY_SERVICES_TOOL_NAME} for the authoritative contract.`,
);
const outputSchema = {
@ -80,7 +80,7 @@ const outputSchema = {
)
.describe('List of credentials accessible to the current user'),
count: z.number().int().min(0).describe('Number of credentials returned'),
n8nConnect: n8nConnectSchema,
gatewayCredits: n8nConnectSchema,
error: z.string().optional().describe('Error message when the tool failed'),
} satisfies z.ZodRawShape;
@ -105,7 +105,7 @@ export type ListCredentialsItem = {
export type ListCredentialsResult = {
data: ListCredentialsItem[];
count: number;
n8nConnect?: N8nConnectCoverage;
gatewayCredits?: N8nConnectCoverage;
error?: string;
};
@ -152,7 +152,7 @@ export const createListCredentialsTool = (
});
const coverage = toN8nConnectCoverage(await aiGatewayService.isAvailable());
if (coverage) payload.n8nConnect = coverage;
if (coverage) payload.gatewayCredits = coverage;
telemetryPayload.results = {
success: true,

View File

@ -4,7 +4,7 @@ import z from 'zod';
import type { AiGatewayService } from '@/services/ai-gateway.service';
import type { Telemetry } from '@/telemetry';
import { LIST_N8N_CONNECT_SERVICES_TOOL_NAME, USER_CALLED_MCP_TOOL_EVENT } from '../mcp.constants';
import { LIST_N8N_GATEWAY_SERVICES_TOOL_NAME, USER_CALLED_MCP_TOOL_EVENT } from '../mcp.constants';
import type { ToolDefinition, UserCalledMCPToolEventPayload } from '../mcp.types';
const inputSchema = {} satisfies z.ZodRawShape;
@ -13,16 +13,16 @@ const outputSchema = {
available: z
.boolean()
.describe(
'True when n8n credits is available for this instance. When false, the remaining fields are omitted.',
'True when Gateway credits are available for this instance. When false, the remaining fields are omitted.',
),
credentialTypes: z
.array(z.string())
.optional()
.describe('Credential type names Connect can provide (e.g. "openAiApi").'),
.describe('Credential type names Gateway credits can provide (e.g. "openAiApi").'),
nodes: z
.array(z.string())
.optional()
.describe('Node types Connect covers (e.g. "@n8n/n8n-nodes-langchain.openAi").'),
.describe('Node types Gateway credits cover (e.g. "@n8n/n8n-nodes-langchain.openAi").'),
supportedActions: z
.record(z.record(z.array(z.string())))
.optional()
@ -32,35 +32,37 @@ const outputSchema = {
minNodeTypeVersion: z
.record(z.number())
.optional()
.describe('Minimum `typeVersion` per node type for Connect coverage.'),
.describe('Minimum `typeVersion` per node type for Gateway credits coverage.'),
hiddenNodeProperties: z
.record(z.array(z.string()))
.optional()
.describe('Per-node property names hidden from the user when Connect provides the credential.'),
.describe(
'Per-node property names hidden from the user when Gateway credits provide the credential.',
),
} satisfies z.ZodRawShape;
/**
* Returns the current n8n Connect coverage snapshot for the
* instance: which node types and credential types Connect can serve, plus
* Returns the current Gateway credits coverage snapshot for the
* instance: which node types and credential types Gateway credits can serve, plus
* per-node action allowlists, min versions, and hidden properties.
*
* Read-only. Omits all coverage fields when Connect is unavailable
* Read-only. Omits all coverage fields when Gateway credits are unavailable
* (unlicensed, misconfigured, or gateway down) — callers should key on
* `available: false` and fall back to user credentials.
*/
export const createListN8nConnectServicesTool = (
export const createListN8nGatewayServicesTool = (
user: User,
aiGatewayService: AiGatewayService,
telemetry: Telemetry,
): ToolDefinition<typeof inputSchema> => ({
name: LIST_N8N_CONNECT_SERVICES_TOOL_NAME,
name: LIST_N8N_GATEWAY_SERVICES_TOOL_NAME,
config: {
description:
'List n8n credits coverage: node and credential types the platform can provide managed credentials for, plus supported resource+operation combinations, minimum type versions, and hidden node properties. Use this to decide which nodes let the user skip credential setup.',
'List Gateway credits coverage: node and credential types the platform can provide managed credentials for, plus supported resource+operation combinations, minimum type versions, and hidden node properties. Use this to decide which nodes let the user skip credential setup.',
inputSchema,
outputSchema,
annotations: {
title: 'List services available with n8n credits',
title: 'List services available with Gateway credits',
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
@ -70,7 +72,7 @@ export const createListN8nConnectServicesTool = (
handler: async () => {
const telemetryPayload: UserCalledMCPToolEventPayload = {
user_id: user.id,
tool_name: LIST_N8N_CONNECT_SERVICES_TOOL_NAME,
tool_name: LIST_N8N_GATEWAY_SERVICES_TOOL_NAME,
};
const availability = await aiGatewayService.isAvailable();

View File

@ -128,7 +128,7 @@ const outputSchema = {
.enum(['user', 'aiGateway'])
.optional()
.describe(
'Where the credential came from: "user" for an existing user credential, "aiGateway" for a credential managed via n8n credits.',
'Where the credential came from: "user" for an existing user credential, "aiGateway" for a credential managed via Gateway credits.',
),
}),
)

View File

@ -20,7 +20,7 @@ import type { Telemetry } from '@/telemetry';
import { MCP_CREDENTIALS_AUTOASSIGN_EVENT } from '../../mcp.constants';
/** Display name written into AI Gateway-managed credential sentinels. User-facing brand. */
const AI_GATEWAY_CREDENTIAL_NAME = 'n8n credits';
const AI_GATEWAY_CREDENTIAL_NAME = 'Gateway credits';
/**
* Attach the managed (n8n-credits) sentinel to `credentialType` and, unless it's

View File

@ -8,7 +8,7 @@ import type { Telemetry } from '@/telemetry';
import { CODE_BUILDER_GET_NODE_TYPES_TOOL } from './constants';
import { toN8nConnectCoverage } from '../../mcp-ai-gateway.helper';
import {
LIST_N8N_CONNECT_SERVICES_TOOL_NAME,
LIST_N8N_GATEWAY_SERVICES_TOOL_NAME,
USER_CALLED_MCP_TOOL_EVENT,
} from '../../mcp.constants';
import type {
@ -36,18 +36,20 @@ const inputSchema = {
const outputSchema = {
definitions: z.string().describe('TypeScript type definitions for the requested nodes'),
n8nConnect: z
gatewayCredits: z
.object({
credentialTypes: z.array(z.string()).describe('Credential types n8n Connect can provide.'),
credentialTypes: z
.array(z.string())
.describe('Credential types Gateway credits can provide.'),
nodes: z
.array(z.string())
.describe(
'Node types n8n Connect may cover. Prefer these when the user has not specified an integration. Candidate coverage only — exact eligibility also depends on the node action, minimum type version, and hidden properties.',
'Node types Gateway credits may cover. Prefer these when the user has not specified an integration. Candidate coverage only — exact eligibility also depends on the node action, minimum type version, and hidden properties.',
),
})
.optional()
.describe(
`Present when n8n Connect is available. Candidate coverage — cross-reference against the returned node types, but call ${LIST_N8N_CONNECT_SERVICES_TOOL_NAME} for exact eligibility (supported actions, min versions, hidden properties).`,
`Present when Gateway credits are available. Candidate coverage — cross-reference against the returned node types, but call ${LIST_N8N_GATEWAY_SERVICES_TOOL_NAME} for exact eligibility (supported actions, min versions, hidden properties).`,
),
} satisfies z.ZodRawShape;
@ -95,12 +97,12 @@ export const createGetWorkflowNodeTypesTool = (
const structured: {
definitions: string;
n8nConnect?: N8nConnectCoverage;
gatewayCredits?: N8nConnectCoverage;
} = { definitions: result };
const coverage = toN8nConnectCoverage(availability);
if (coverage) structured.n8nConnect = coverage;
if (coverage) structured.gatewayCredits = coverage;
const text = coverage ? `${result}\n\nn8nConnect: ${JSON.stringify(coverage)}` : result;
const text = coverage ? `${result}\n\ngatewayCredits: ${JSON.stringify(coverage)}` : result;
return {
content: [{ type: 'text', text }],

View File

@ -18,7 +18,7 @@ import {
CODE_BUILDER_VALIDATE_TOOL,
CODE_BUILDER_VALIDATE_NODE_TOOL,
} from './constants';
import { LIST_N8N_CONNECT_SERVICES_TOOL_NAME } from '../../mcp.constants';
import { LIST_N8N_GATEWAY_SERVICES_TOOL_NAME } from '../../mcp.constants';
export type McpInstructionsOptions = {
/**
@ -64,7 +64,7 @@ Node groups: when a workflow has several distinct stages, organise it into named
const N8N_CONNECT_HINT = isN8nConnectAvailable
? `
Explore nodes covered by n8n credits when the user has not specified a particular integration. n8n credits let users consume LLMs and third-party services directly through n8n with usage-based billing, so they can skip credential setup. Discovery tools (${CODE_BUILDER_SEARCH_NODES_TOOL.toolName}, ${CODE_BUILDER_GET_NODE_TYPES_TOOL.toolName}) and list_credentials return an optional \`n8nConnect.nodes\` array when the instance has n8n credits available. Nodes in that array can attach a managed credential automatically — the workflow runs without the user configuring keys. If the user asked for a specific integration or none of the covered nodes fit, use the requested integration with regular credentials. Call ${LIST_N8N_CONNECT_SERVICES_TOOL_NAME} if you need details (per-node supported resource+operation combos, min type versions, hidden properties).`
Explore nodes covered by Gateway credits when the user has not specified a particular integration. Gateway credits let users consume LLMs and third-party services directly through n8n with usage-based billing, so they can skip credential setup. Discovery tools (${CODE_BUILDER_SEARCH_NODES_TOOL.toolName}, ${CODE_BUILDER_GET_NODE_TYPES_TOOL.toolName}) and list_credentials return an optional \`gatewayCredits.nodes\` array when the instance has Gateway credits available. Nodes in that array can attach a managed credential automatically — the workflow runs without the user configuring keys. If the user asked for a specific integration or none of the covered nodes fit, use the requested integration with regular credentials. Call ${LIST_N8N_GATEWAY_SERVICES_TOOL_NAME} if you need details (per-node supported resource+operation combos, min type versions, hidden properties).`
: '';
const ARTIFACT_ROUTING_INSTRUCTIONS = `Choose the artifact before choosing build tools. Treat an explicit artifact request as a routing instruction: if the user asks to build or create an "agent" or "assistant", default to a first-class n8n Agent; if they explicitly ask for a workflow or an AI Agent node, use the workflow tools.

View File

@ -8,7 +8,7 @@ import type { Telemetry } from '@/telemetry';
import { CODE_BUILDER_SEARCH_NODES_TOOL } from './constants';
import { toN8nConnectCoverage } from '../../mcp-ai-gateway.helper';
import {
LIST_N8N_CONNECT_SERVICES_TOOL_NAME,
LIST_N8N_GATEWAY_SERVICES_TOOL_NAME,
USER_CALLED_MCP_TOOL_EVENT,
} from '../../mcp.constants';
import type {
@ -36,18 +36,20 @@ const outputSchema = {
results: z
.string()
.describe('Search results with matching node IDs, discriminators, and related nodes'),
n8nConnect: z
gatewayCredits: z
.object({
credentialTypes: z.array(z.string()).describe('Credential types n8n Connect can provide.'),
credentialTypes: z
.array(z.string())
.describe('Credential types Gateway credits can provide.'),
nodes: z
.array(z.string())
.describe(
'Node types n8n Connect may cover. Prefer these when the user has not specified an integration. Candidate coverage only — exact eligibility also depends on the node action, minimum type version, and hidden properties.',
'Node types Gateway credits may cover. Prefer these when the user has not specified an integration. Candidate coverage only — exact eligibility also depends on the node action, minimum type version, and hidden properties.',
),
})
.optional()
.describe(
`Present when n8n Connect is available. Candidate coverage — cross-reference against the search results, but call ${LIST_N8N_CONNECT_SERVICES_TOOL_NAME} for exact eligibility (supported actions, min versions, hidden properties).`,
`Present when Gateway credits are available. Candidate coverage — cross-reference against the search results, but call ${LIST_N8N_GATEWAY_SERVICES_TOOL_NAME} for exact eligibility (supported actions, min versions, hidden properties).`,
),
} satisfies z.ZodRawShape;
@ -122,14 +124,14 @@ export const createSearchWorkflowNodesTool = (
const structured: {
results: string;
n8nConnect?: N8nConnectCoverage;
gatewayCredits?: N8nConnectCoverage;
} = {
results,
};
const coverage = toN8nConnectCoverage(availability);
if (coverage) structured.n8nConnect = coverage;
if (coverage) structured.gatewayCredits = coverage;
const text = coverage ? `${results}\n\nn8nConnect: ${JSON.stringify(coverage)}` : results;
const text = coverage ? `${results}\n\ngatewayCredits: ${JSON.stringify(coverage)}` : results;
return {
content: [{ type: 'text', text }],