@forestnpm/n8n-nodes-forest 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="Calque_1" data-name="Calque 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 35.93 36">
3
+ <defs>
4
+ <style>
5
+ .cls-1 {
6
+ fill: #183225;
7
+ }
8
+
9
+ .cls-1, .cls-2 {
10
+ stroke-width: 0px;
11
+ }
12
+
13
+ .cls-2 {
14
+ fill: #d5ee84;
15
+ }
16
+ </style>
17
+ </defs>
18
+ <g id="Isotype_only" data-name="Isotype only">
19
+ <rect class="cls-2" x="-.04" y="0" width="36" height="36" rx="6.75" ry="6.75"/>
20
+ <path class="cls-1" d="M17.96,9.86l7.82,7.16,2.31-2.12-10.13-9.28L7.84,14.91l2.31,2.11,7.81-7.16Z"/>
21
+ <path class="cls-1" d="M7.84,22.15l2.31,2.11,6.18-5.66v12.34h3.27v-12.32l6.18,5.65,2.31-2.11-10.13-9.28-10.12,9.28Z"/>
22
+ </g>
23
+ </svg>
@@ -0,0 +1,2 @@
1
+ import type { ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow';
2
+ export declare function getTools(this: ILoadOptionsFunctions, filter?: string, paginationToken?: string): Promise<INodeListSearchResult>;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getTools = getTools;
4
+ const utils_1 = require("./utils");
5
+ async function getTools(filter, paginationToken) {
6
+ const authentication = this.getNodeParameter('authentication');
7
+ const { headers, endpointUrl } = await (0, utils_1.getAuthHeadersAndEndpoint)(this, authentication);
8
+ // Return empty results if credentials are not configured yet
9
+ if (!endpointUrl) {
10
+ return { results: [] };
11
+ }
12
+ const node = this.getNode();
13
+ const client = await (0, utils_1.connectMcpClient)({
14
+ endpointUrl,
15
+ headers,
16
+ name: node.type,
17
+ version: node.typeVersion,
18
+ });
19
+ // Throw error if connection fails (e.g., auth error, network error)
20
+ if (!client.ok) {
21
+ throw (0, utils_1.mapToNodeOperationError)(node, client.error);
22
+ }
23
+ try {
24
+ const result = await client.result.listTools({ cursor: paginationToken });
25
+ const tools = filter
26
+ ? result.tools.filter((tool) => tool.name.toLowerCase().includes(filter.toLowerCase()))
27
+ : result.tools;
28
+ return {
29
+ results: tools.map((tool) => ({
30
+ name: tool.name,
31
+ value: tool.name,
32
+ description: tool.description,
33
+ })),
34
+ paginationToken: result.nextCursor,
35
+ };
36
+ }
37
+ finally {
38
+ await client.result.close();
39
+ }
40
+ }
@@ -0,0 +1,2 @@
1
+ import type { ILoadOptionsFunctions, ResourceMapperFields } from 'n8n-workflow';
2
+ export declare function getToolParameters(this: ILoadOptionsFunctions): Promise<ResourceMapperFields>;
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getToolParameters = getToolParameters;
4
+ const utils_1 = require("./utils");
5
+ function jsonSchemaTypeToFieldType(type) {
6
+ const primaryType = Array.isArray(type) ? type[0] : type;
7
+ switch (primaryType) {
8
+ case 'string':
9
+ return 'string';
10
+ case 'number':
11
+ case 'integer':
12
+ return 'number';
13
+ case 'boolean':
14
+ return 'boolean';
15
+ case 'array':
16
+ return 'array';
17
+ case 'object':
18
+ return 'object';
19
+ default:
20
+ return 'string';
21
+ }
22
+ }
23
+ function convertJsonSchemaToResourceMapperFields(schema, requiredFields = []) {
24
+ const fields = [];
25
+ if (schema.type !== 'object' || !schema.properties) {
26
+ return fields;
27
+ }
28
+ for (const [key, value] of Object.entries(schema.properties)) {
29
+ if (typeof value === 'boolean')
30
+ continue;
31
+ const propertySchema = value;
32
+ const displayName = propertySchema.description ? `${key} - ${propertySchema.description}` : key;
33
+ const fieldType = jsonSchemaTypeToFieldType(propertySchema.type || 'string');
34
+ const field = {
35
+ id: key,
36
+ displayName,
37
+ type: fieldType,
38
+ required: requiredFields.includes(key),
39
+ defaultMatch: false,
40
+ canBeUsedToMatch: false,
41
+ display: true,
42
+ };
43
+ // Set default value for object and array types
44
+ if (fieldType === 'object') {
45
+ field.defaultValue = '{}';
46
+ }
47
+ else if (fieldType === 'array') {
48
+ field.defaultValue = '[]';
49
+ }
50
+ if (propertySchema.enum &&
51
+ Array.isArray(propertySchema.enum) &&
52
+ propertySchema.enum.length > 0) {
53
+ field.type = 'options';
54
+ field.options = propertySchema.enum.map((value) => ({
55
+ name: String(value),
56
+ value,
57
+ }));
58
+ }
59
+ fields.push(field);
60
+ }
61
+ return fields;
62
+ }
63
+ async function getToolParameters() {
64
+ const toolId = this.getNodeParameter('tool.value');
65
+ // Return empty fields if no tool is selected yet
66
+ if (!toolId) {
67
+ return { fields: [] };
68
+ }
69
+ const authentication = this.getNodeParameter('authentication');
70
+ const node = this.getNode();
71
+ const { headers, endpointUrl } = await (0, utils_1.getAuthHeadersAndEndpoint)(this, authentication);
72
+ if (!endpointUrl) {
73
+ return { fields: [] };
74
+ }
75
+ const client = await (0, utils_1.connectMcpClient)({
76
+ endpointUrl,
77
+ headers,
78
+ name: node.type,
79
+ version: node.typeVersion,
80
+ });
81
+ // Return empty fields if connection fails (e.g., auth error, network error)
82
+ if (!client.ok) {
83
+ return { fields: [] };
84
+ }
85
+ try {
86
+ const tools = await (0, utils_1.getAllTools)(client.result);
87
+ const tool = tools.find((t) => t.name === toolId);
88
+ // Return empty fields if tool not found
89
+ if (!tool) {
90
+ return { fields: [] };
91
+ }
92
+ const schema = tool.inputSchema;
93
+ const requiredFields = Array.isArray(schema.required) ? schema.required : [];
94
+ const fields = convertJsonSchemaToResourceMapperFields(schema, requiredFields);
95
+ return { fields };
96
+ }
97
+ finally {
98
+ await client.result.close();
99
+ }
100
+ }
@@ -0,0 +1,7 @@
1
+ import type { JSONSchema7 } from 'json-schema';
2
+ export type McpTool = {
3
+ name: string;
4
+ description?: string;
5
+ inputSchema: JSONSchema7;
6
+ };
7
+ export type McpAuthenticationOption = 'bearerAuth' | 'mcpOAuth2Api';
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,38 @@
1
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
+ import type { IExecuteFunctions, ILoadOptionsFunctions, INode } from 'n8n-workflow';
3
+ import { NodeOperationError } from 'n8n-workflow';
4
+ import type { McpAuthenticationOption, McpTool } from './types';
5
+ export type Result<T, E> = {
6
+ ok: true;
7
+ result: T;
8
+ } | {
9
+ ok: false;
10
+ error: E;
11
+ };
12
+ export declare function cleanParameters<T>(obj: T): T;
13
+ export declare function getAllTools(client: Client, cursor?: string): Promise<McpTool[]>;
14
+ type ConnectMcpClientError = {
15
+ type: 'invalid_url';
16
+ error: Error;
17
+ } | {
18
+ type: 'connection';
19
+ error: Error;
20
+ } | {
21
+ type: 'auth';
22
+ error: Error;
23
+ } | {
24
+ type: 'not_found';
25
+ error: Error;
26
+ };
27
+ export declare function mapToNodeOperationError(node: INode, error: ConnectMcpClientError): NodeOperationError;
28
+ export declare function connectMcpClient({ headers, endpointUrl, name, version, }: {
29
+ endpointUrl: string;
30
+ headers?: Record<string, string>;
31
+ name: string;
32
+ version: number;
33
+ }): Promise<Result<Client, ConnectMcpClientError>>;
34
+ export declare function getAuthHeadersAndEndpoint(ctx: Pick<IExecuteFunctions | ILoadOptionsFunctions, 'getCredentials'>, authentication: McpAuthenticationOption): Promise<{
35
+ headers?: Record<string, string>;
36
+ endpointUrl?: string;
37
+ }>;
38
+ export {};
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cleanParameters = cleanParameters;
4
+ exports.getAllTools = getAllTools;
5
+ exports.mapToNodeOperationError = mapToNodeOperationError;
6
+ exports.connectMcpClient = connectMcpClient;
7
+ exports.getAuthHeadersAndEndpoint = getAuthHeadersAndEndpoint;
8
+ const index_js_1 = require("@modelcontextprotocol/sdk/client/index.js");
9
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
10
+ const n8n_workflow_1 = require("n8n-workflow");
11
+ function cleanParameters(obj) {
12
+ if (obj === null || obj === undefined) {
13
+ return obj;
14
+ }
15
+ if (Array.isArray(obj)) {
16
+ const cleanedArray = obj
17
+ .map((item) => cleanParameters(item))
18
+ .filter((item) => item !== undefined);
19
+ return cleanedArray;
20
+ }
21
+ if (typeof obj === 'object') {
22
+ const cleaned = {};
23
+ for (const [key, value] of Object.entries(obj)) {
24
+ const cleanedValue = cleanParameters(value);
25
+ // Skip undefined values and empty objects
26
+ if (cleanedValue !== undefined) {
27
+ if (typeof cleanedValue === 'object' &&
28
+ cleanedValue !== null &&
29
+ !Array.isArray(cleanedValue) &&
30
+ Object.keys(cleanedValue).length === 0) {
31
+ // Skip empty objects
32
+ continue;
33
+ }
34
+ cleaned[key] = cleanedValue;
35
+ }
36
+ }
37
+ return cleaned;
38
+ }
39
+ return obj;
40
+ }
41
+ function createResultOk(result) {
42
+ return { ok: true, result };
43
+ }
44
+ function createResultError(error) {
45
+ return { ok: false, error };
46
+ }
47
+ async function getAllTools(client, cursor) {
48
+ const { tools, nextCursor } = await client.listTools({ cursor });
49
+ if (nextCursor) {
50
+ return tools.concat(await getAllTools(client, nextCursor));
51
+ }
52
+ return tools;
53
+ }
54
+ function normalizeAndValidateUrl(input) {
55
+ const withProtocol = !/^https?:\/\//i.test(input) ? `https://${input}` : input;
56
+ try {
57
+ return createResultOk(new URL(withProtocol));
58
+ }
59
+ catch (error) {
60
+ return createResultError(error);
61
+ }
62
+ }
63
+ function errorHasCode(error, code) {
64
+ return (!!error &&
65
+ typeof error === 'object' &&
66
+ (('code' in error && Number(error.code) === code) ||
67
+ ('message' in error &&
68
+ typeof error.message === 'string' &&
69
+ error.message.includes(code.toString()))));
70
+ }
71
+ function isUnauthorizedError(error) {
72
+ return errorHasCode(error, 401);
73
+ }
74
+ function isForbiddenError(error) {
75
+ return errorHasCode(error, 403);
76
+ }
77
+ function isNotFoundError(error) {
78
+ return errorHasCode(error, 404);
79
+ }
80
+ function mapToNodeOperationError(node, error) {
81
+ switch (error.type) {
82
+ case 'invalid_url':
83
+ return new n8n_workflow_1.NodeOperationError(node, error.error, {
84
+ message: 'Could not connect to your Forest MCP server. The provided URL is invalid.',
85
+ });
86
+ case 'auth':
87
+ return new n8n_workflow_1.NodeOperationError(node, error.error, {
88
+ message: 'Could not connect to your Forest MCP server. Authentication failed.',
89
+ });
90
+ case 'not_found':
91
+ return new n8n_workflow_1.NodeOperationError(node, error.error, {
92
+ message: 'MCP server not found. The MCP server has not been setup on your Forest agent. See https://docs.forestadmin.com/developer-guide-agents-nodejs/agent-customization/ai/mcp-server',
93
+ });
94
+ case 'connection':
95
+ default:
96
+ return new n8n_workflow_1.NodeOperationError(node, error.error, {
97
+ message: 'Could not connect to your Forest MCP server',
98
+ });
99
+ }
100
+ }
101
+ async function connectMcpClient({ headers, endpointUrl, name, version, }) {
102
+ const endpoint = normalizeAndValidateUrl(endpointUrl);
103
+ if (!endpoint.ok) {
104
+ return createResultError({ type: 'invalid_url', error: endpoint.error });
105
+ }
106
+ const client = new index_js_1.Client({ name, version: version.toString() }, { capabilities: {} });
107
+ try {
108
+ const transport = new streamableHttp_js_1.StreamableHTTPClientTransport(endpoint.result, {
109
+ requestInit: { headers },
110
+ });
111
+ await client.connect(transport);
112
+ return createResultOk(client);
113
+ }
114
+ catch (error) {
115
+ if (isUnauthorizedError(error) || isForbiddenError(error)) {
116
+ return createResultError({ type: 'auth', error: error });
117
+ }
118
+ else if (isNotFoundError(error)) {
119
+ return createResultError({ type: 'not_found', error: error });
120
+ }
121
+ else {
122
+ return createResultError({ type: 'connection', error: error });
123
+ }
124
+ }
125
+ }
126
+ async function getAuthHeadersAndEndpoint(ctx, authentication) {
127
+ var _a, _b, _c, _d, _e;
128
+ switch (authentication) {
129
+ case 'bearerAuth': {
130
+ let result = null;
131
+ try {
132
+ result = await ctx.getCredentials('forestMcpApi');
133
+ }
134
+ catch {
135
+ // Credentials not configured or not accessible
136
+ return {};
137
+ }
138
+ if (!result)
139
+ return {};
140
+ const serverUrl = ((_a = result.serverUrl) === null || _a === void 0 ? void 0 : _a.trim()) || '';
141
+ const endpointUrl = serverUrl.endsWith('/mcp') ? serverUrl : `${serverUrl}/mcp`;
142
+ const token = ((_b = result.token) === null || _b === void 0 ? void 0 : _b.trim()) || '';
143
+ if (!token) {
144
+ return {};
145
+ }
146
+ return {
147
+ headers: { Authorization: `Bearer ${token}` },
148
+ endpointUrl,
149
+ };
150
+ }
151
+ case 'mcpOAuth2Api':
152
+ default: {
153
+ let result = null;
154
+ try {
155
+ // n8n automatically refreshes the token if expired when getCredentials is called
156
+ result = await ctx.getCredentials('forestMcpOAuth2Api');
157
+ }
158
+ catch (error) {
159
+ // This can happen if:
160
+ // - Credentials not configured
161
+ // - Token refresh failed (refresh_token expired or invalid)
162
+ // - OAuth server is unreachable
163
+ console.error('Failed to get OAuth2 credentials:', error);
164
+ return {};
165
+ }
166
+ if (!result)
167
+ return {};
168
+ const serverUrl = ((_c = result.serverUrl) === null || _c === void 0 ? void 0 : _c.trim()) || '';
169
+ const endpointUrl = serverUrl.endsWith('/mcp') ? serverUrl : `${serverUrl}/mcp`;
170
+ // Check if oauthTokenData exists and has an access_token
171
+ const accessToken = ((_e = (_d = result.oauthTokenData) === null || _d === void 0 ? void 0 : _d.access_token) === null || _e === void 0 ? void 0 : _e.trim()) || '';
172
+ if (!accessToken) {
173
+ // OAuth2 credentials exist but token data is not available yet
174
+ // This can happen if:
175
+ // - User hasn't completed the OAuth flow
176
+ // - Token refresh failed and n8n cleared the token data
177
+ return {};
178
+ }
179
+ return {
180
+ headers: { Authorization: `Bearer ${accessToken}` },
181
+ endpointUrl,
182
+ };
183
+ }
184
+ }
185
+ }
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@forestnpm/n8n-nodes-forest",
3
+ "version": "1.0.0",
4
+ "description": "n8n community node for Forest",
5
+ "keywords": [
6
+ "n8n-community-node-package"
7
+ ],
8
+ "license": "MIT",
9
+ "homepage": "",
10
+ "author": {
11
+ "name": "Pierre Merlet",
12
+ "email": "pierre.merlet@forestadmin.com"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/ForestAdmin/n8n-nodes-forest.git"
17
+ },
18
+ "engines": {
19
+ "node": ">=18.10",
20
+ "pnpm": ">=9.1"
21
+ },
22
+ "main": "index.js",
23
+ "scripts": {
24
+ "preinstall": "npx only-allow pnpm",
25
+ "build": "node esbuild.config.mjs && gulp build:icons",
26
+ "dev": "tsc --watch",
27
+ "format": "prettier nodes credentials --write",
28
+ "lint": "eslint nodes credentials package.json",
29
+ "lintfix": "eslint nodes credentials package.json --fix",
30
+ "prepublishOnly": "pnpm build && pnpm lint -c .eslintrc.prepublish.js nodes credentials package.json"
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "n8n": {
36
+ "n8nNodesApiVersion": 1,
37
+ "credentials": [
38
+ "dist/credentials/ForestMcpApi.credentials.js",
39
+ "dist/credentials/ForestMcpOAuth2Api.credentials.js"
40
+ ],
41
+ "nodes": [
42
+ "dist/nodes/Forest/Forest.node.js"
43
+ ]
44
+ },
45
+ "devDependencies": {
46
+ "@modelcontextprotocol/sdk": "^1.12.1",
47
+ "@n8n/eslint-plugin-community-nodes": "^0.7.0",
48
+ "@types/json-schema": "^7.0.15",
49
+ "@types/node": "^22.0.0",
50
+ "@typescript-eslint/parser": "^7.15.0",
51
+ "esbuild": "^0.27.3",
52
+ "eslint": "^8.56.0",
53
+ "eslint-plugin-n8n-nodes-base": "^1.16.1",
54
+ "gulp": "^4.0.2",
55
+ "jsonc-eslint-parser": "^2.4.2",
56
+ "n8n-workflow": "*",
57
+ "prettier": "^3.3.2",
58
+ "typescript": "^5.5.3",
59
+ "zod": "^3.24.0"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
63
+ },
64
+ "peerDependencies": {
65
+ "n8n-workflow": "*"
66
+ },
67
+ "packageManager": "pnpm@9.15.9"
68
+ }