first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,327 @@
|
||||
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
||||
import type {
|
||||
IBinaryKeyData,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
NodeExecutionWithMetadata,
|
||||
} from 'n8n-workflow';
|
||||
import { jsonParse, NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
|
||||
import { ZodError } from 'zod';
|
||||
import { prettifyError } from 'zod/v4/core';
|
||||
|
||||
import * as listSearch from './listSearch';
|
||||
import * as resourceMapping from './resourceMapping';
|
||||
import { credentials, transportSelect } from '../shared/descriptions';
|
||||
import type { McpAuthenticationOption, McpServerTransport } from '../shared/types';
|
||||
import {
|
||||
getAuthHeaders,
|
||||
tryRefreshOAuth2Token,
|
||||
connectMcpClient,
|
||||
mapToNodeOperationError,
|
||||
} from '../shared/utils';
|
||||
|
||||
export class McpClient implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'MCP Client',
|
||||
description: 'Standalone MCP Client',
|
||||
name: 'mcpClient',
|
||||
icon: {
|
||||
light: 'file:../mcp.svg',
|
||||
dark: 'file:../mcp.dark.svg',
|
||||
},
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
defaults: {
|
||||
name: 'MCP Client',
|
||||
},
|
||||
credentials,
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
properties: [
|
||||
transportSelect({
|
||||
defaultOption: 'httpStreamable',
|
||||
}),
|
||||
{
|
||||
displayName: 'MCP Endpoint URL',
|
||||
name: 'endpointUrl',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: 'e.g. https://my-mcp-server.ai/mcp',
|
||||
required: true,
|
||||
description: 'The URL of the MCP server to connect to',
|
||||
},
|
||||
{
|
||||
displayName: 'Authentication',
|
||||
name: 'authentication',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Bearer Auth',
|
||||
value: 'bearerAuth',
|
||||
},
|
||||
{
|
||||
name: 'Header Auth',
|
||||
value: 'headerAuth',
|
||||
},
|
||||
{
|
||||
name: 'MCP OAuth2',
|
||||
value: 'mcpOAuth2Api',
|
||||
},
|
||||
{
|
||||
name: 'Multiple Headers Auth',
|
||||
value: 'multipleHeadersAuth',
|
||||
},
|
||||
{
|
||||
name: 'None',
|
||||
value: 'none',
|
||||
},
|
||||
],
|
||||
default: 'none',
|
||||
description: 'The way to authenticate with your endpoint',
|
||||
},
|
||||
{
|
||||
displayName: 'Credentials',
|
||||
name: 'credentials',
|
||||
type: 'credentials',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['headerAuth', 'bearerAuth', 'mcpOAuth2Api', 'multipleHeadersAuth'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Tool',
|
||||
name: 'tool',
|
||||
type: 'resourceLocator',
|
||||
default: { mode: 'list', value: '' },
|
||||
required: true,
|
||||
description: 'The tool to use',
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
typeOptions: {
|
||||
searchListMethod: 'getTools',
|
||||
searchable: true,
|
||||
skipCredentialsCheckInRLC: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Input Mode',
|
||||
name: 'inputMode',
|
||||
type: 'options',
|
||||
default: 'manual',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Manual',
|
||||
value: 'manual',
|
||||
description: 'Manually specify the input data for each tool parameter',
|
||||
},
|
||||
{
|
||||
name: 'JSON',
|
||||
value: 'json',
|
||||
description: 'Specify the input data as a JSON object',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Parameters',
|
||||
name: 'parameters',
|
||||
type: 'resourceMapper',
|
||||
default: {
|
||||
mappingMode: 'defineBelow',
|
||||
value: null,
|
||||
},
|
||||
noDataExpression: true,
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['tool.value'],
|
||||
resourceMapper: {
|
||||
resourceMapperMethod: 'getToolParameters',
|
||||
hideNoDataError: true,
|
||||
addAllFields: false,
|
||||
supportAutoMap: false,
|
||||
mode: 'add',
|
||||
fieldWords: {
|
||||
singular: 'parameter',
|
||||
plural: 'parameters',
|
||||
},
|
||||
},
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
inputMode: ['manual'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'JSON',
|
||||
name: 'jsonInput',
|
||||
type: 'json',
|
||||
typeOptions: {
|
||||
rows: 5,
|
||||
},
|
||||
default: '{\n "my_field_1": "value",\n "my_field_2": 1\n}\n',
|
||||
validateType: 'object',
|
||||
displayOptions: {
|
||||
show: {
|
||||
inputMode: ['json'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
placeholder: 'Add Option',
|
||||
description: 'Additional options to add',
|
||||
type: 'collection',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Convert to Binary',
|
||||
name: 'convertToBinary',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to convert images and audio to binary data. If false, images and audio will be returned as base64 encoded strings.',
|
||||
},
|
||||
{
|
||||
displayName: 'Timeout',
|
||||
name: 'timeout',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
minValue: 1,
|
||||
},
|
||||
default: 60000,
|
||||
description: 'Time in ms to wait for tool calls to finish',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
listSearch,
|
||||
resourceMapping,
|
||||
};
|
||||
|
||||
async execute(
|
||||
this: IExecuteFunctions,
|
||||
): Promise<INodeExecutionData[][] | NodeExecutionWithMetadata[][] | null> {
|
||||
const authentication = this.getNodeParameter('authentication', 0) as McpAuthenticationOption;
|
||||
const serverTransport = this.getNodeParameter('serverTransport', 0) as McpServerTransport;
|
||||
const endpointUrl = this.getNodeParameter('endpointUrl', 0) as string;
|
||||
const node = this.getNode();
|
||||
const { headers } = await getAuthHeaders(this, authentication);
|
||||
const client = await connectMcpClient({
|
||||
serverTransport,
|
||||
endpointUrl,
|
||||
headers,
|
||||
name: node.type,
|
||||
version: node.typeVersion,
|
||||
onUnauthorized: async (headers) => await tryRefreshOAuth2Token(this, authentication, headers),
|
||||
});
|
||||
if (!client.ok) {
|
||||
throw mapToNodeOperationError(node, client.error);
|
||||
}
|
||||
|
||||
const inputMode = this.getNodeParameter('inputMode', 0, 'manual') as 'manual' | 'json';
|
||||
const items = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
try {
|
||||
const tool = this.getNodeParameter('tool.value', itemIndex) as string;
|
||||
const options = this.getNodeParameter('options', itemIndex);
|
||||
let parameters: IDataObject = {};
|
||||
if (inputMode === 'manual') {
|
||||
parameters = this.getNodeParameter('parameters.value', itemIndex) as IDataObject;
|
||||
} else {
|
||||
parameters = this.getNodeParameter('jsonInput', itemIndex) as IDataObject;
|
||||
}
|
||||
|
||||
const result = (await client.result.callTool(
|
||||
{
|
||||
name: tool,
|
||||
arguments: parameters,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
timeout: options.timeout ? Number(options.timeout) : undefined,
|
||||
},
|
||||
)) as CallToolResult;
|
||||
|
||||
let binaryIndex = 0;
|
||||
const binary: IBinaryKeyData = {};
|
||||
const content: IDataObject[] = [];
|
||||
const convertToBinary = options.convertToBinary ?? true;
|
||||
for (const contentItem of result.content) {
|
||||
if (contentItem.type === 'text') {
|
||||
content.push({
|
||||
...contentItem,
|
||||
text: jsonParse(contentItem.text, { fallbackValue: contentItem.text }),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (convertToBinary && (contentItem.type === 'image' || contentItem.type === 'audio')) {
|
||||
binary[`data_${binaryIndex}`] = await this.helpers.prepareBinaryData(
|
||||
Buffer.from(contentItem.data, 'base64'),
|
||||
undefined,
|
||||
contentItem.mimeType,
|
||||
);
|
||||
binaryIndex++;
|
||||
continue;
|
||||
}
|
||||
|
||||
content.push(contentItem as IDataObject);
|
||||
}
|
||||
|
||||
returnData.push({
|
||||
json: {
|
||||
content: content.length > 0 ? content : undefined,
|
||||
},
|
||||
binary: Object.keys(binary).length > 0 ? binary : undefined,
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const errorMessage =
|
||||
e instanceof ZodError ? prettifyError(e) : e instanceof Error ? e.message : String(e);
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({
|
||||
json: {
|
||||
error: {
|
||||
message: errorMessage,
|
||||
issues: e instanceof ZodError ? e.issues : undefined,
|
||||
},
|
||||
},
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new NodeOperationError(node, errorMessage, {
|
||||
itemIndex,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user