n8n-mcp-community 0.1.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +116 -0
  2. package/CODE_OF_CONDUCT.md +51 -0
  3. package/CONTRIBUTING.md +69 -0
  4. package/LICENSE +21 -0
  5. package/README.md +271 -0
  6. package/ROADMAP.md +41 -0
  7. package/SECURITY.md +53 -0
  8. package/SUPPORT.md +19 -0
  9. package/THIRD_PARTY_NOTICES.md +48 -0
  10. package/dist/config.js +76 -0
  11. package/dist/content/node-docs.js +47 -0
  12. package/dist/content/official-urls.js +11 -0
  13. package/dist/doctor.js +88 -0
  14. package/dist/index.js +119 -0
  15. package/dist/introspect/collector.js +642 -0
  16. package/dist/introspect/contracts.js +309 -0
  17. package/dist/introspect/engine.js +184 -0
  18. package/dist/introspect/http-client.js +142 -0
  19. package/dist/introspect/index.js +5 -0
  20. package/dist/introspect/order.js +3 -0
  21. package/dist/introspect/render.js +25 -0
  22. package/dist/introspect/rule-ids.js +25 -0
  23. package/dist/introspect/rules.js +905 -0
  24. package/dist/introspect/sanitize.js +66 -0
  25. package/dist/n8n/client.js +170 -0
  26. package/dist/prompts.js +69 -0
  27. package/dist/resources.js +42 -0
  28. package/dist/security/operation-policy.js +20 -0
  29. package/dist/security/redaction.js +253 -0
  30. package/dist/server.js +16 -0
  31. package/dist/tools/common.js +54 -0
  32. package/dist/tools/credentials.js +259 -0
  33. package/dist/tools/definition.js +169 -0
  34. package/dist/tools/endpoint-contracts.js +53 -0
  35. package/dist/tools/executions.js +180 -0
  36. package/dist/tools/registry.js +24 -0
  37. package/dist/tools/schemas.js +203 -0
  38. package/dist/tools/tags.js +73 -0
  39. package/dist/tools/users.js +130 -0
  40. package/dist/tools/utilities.js +347 -0
  41. package/dist/tools/workflows.js +771 -0
  42. package/dist/version.js +1 -0
  43. package/docs/README.md +21 -0
  44. package/docs/architecture.md +122 -0
  45. package/docs/assets/demo.gif +0 -0
  46. package/docs/clients.md +95 -0
  47. package/docs/compatibility.md +112 -0
  48. package/docs/configuration.md +87 -0
  49. package/docs/demo-transcript.md +31 -0
  50. package/docs/examples.md +86 -0
  51. package/docs/faq.md +90 -0
  52. package/docs/getting-started.md +93 -0
  53. package/docs/installation.md +120 -0
  54. package/docs/provenance.md +152 -0
  55. package/docs/security-model.md +170 -0
  56. package/docs/tools.md +937 -0
  57. package/docs/troubleshooting.md +185 -0
  58. package/package.json +126 -0
  59. package/server.json +51 -0
@@ -0,0 +1,48 @@
1
+ # Third-party notices
2
+
3
+ Project-authored code is licensed under MIT. Dependencies retain their own
4
+ licenses; `package-lock.json` is the exact dependency record and
5
+ `npm run licenses:check` validates every installed package path.
6
+
7
+ The npm runtime tarball does not bundle dependencies. The following packages
8
+ are development-only transitive dependencies of ESLint or the MCPB build CLI.
9
+ Their published npm tarballs declare an SPDX license but omit a top-level
10
+ LICENSE or NOTICE file. They are exact-pinned in the lockfile, are not present
11
+ in the runtime package or MCPB, and are accepted only at the listed version,
12
+ license, repository, and integrity hash:
13
+
14
+ | Package | License | Upstream repository |
15
+ | ----------------------- | ------------ | ----------------------------------------------------------------------------------------------- |
16
+ | `@humanfs/types@0.15.0` | Apache-2.0 | [humanwhocodes/humanfs](https://github.com/humanwhocodes/humanfs) |
17
+ | `esrecurse@4.3.0` | BSD-2-Clause | [estools/esrecurse](https://github.com/estools/esrecurse) |
18
+ | `flora-colossus@2.0.0` | MIT | [MarshallOfSound/flora-colossus](https://github.com/MarshallOfSound/flora-colossus/tree/v2.0.0) |
19
+ | `imurmurhash@0.1.4` | MIT | [jensyt/imurmurhash-js](https://github.com/jensyt/imurmurhash-js) |
20
+ | `keyv@4.5.4` | MIT | [jaredwray/keyv](https://github.com/jaredwray/keyv) |
21
+ | `natural-compare@1.4.0` | MIT | [litejs/natural-compare-lite](https://github.com/litejs/natural-compare-lite) |
22
+
23
+ Their exact SHA-512 integrity values are enforced in
24
+ `scripts/verify-dependency-licenses.mjs`. Any version, license, integrity, role,
25
+ or notice-file change fails the gate and requires a new review. A package with a
26
+ missing notice file can never use this exception when it is a runtime dependency
27
+ or bundled into a release artifact.
28
+
29
+ Complete license texts are available from the linked upstream repositories and
30
+ the SPDX identifiers above. The final MCPB must independently preserve the
31
+ license and notice files for every dependency it actually bundles.
32
+
33
+ `minimatch@10.2.5` is another development-only ESLint dependency and is licensed
34
+ under the permissive Blue Oak Model License 1.0.0. Its installed package includes
35
+ the license file; the official license text and required notice link are
36
+ <https://blueoakcouncil.org/license/1.0.0>. It is not bundled in the npm runtime
37
+ tarball.
38
+
39
+ Two development-only MCPB build dependencies offer alternative licenses. This
40
+ project selects the permissive alternative shown below. The gate pins the exact
41
+ version, declared expression, selected license, repository, and integrity hash;
42
+ both installed packages include the selected license text and neither is bundled
43
+ in the npm runtime tarball or MCPB.
44
+
45
+ | Package | Declared expression | Selected license |
46
+ | ------------------ | ------------------------- | ---------------- |
47
+ | `node-forge@1.4.0` | `BSD-3-Clause OR GPL-2.0` | BSD-3-Clause |
48
+ | `type-fest@0.21.3` | `MIT OR CC0-1.0` | MIT |
package/dist/config.js ADDED
@@ -0,0 +1,76 @@
1
+ export class ConfigurationError extends Error {
2
+ code = "configuration_error";
3
+ reason;
4
+ /** Name of the offending environment variable. Never its value. */
5
+ setting;
6
+ constructor(reason, setting, message) {
7
+ super(message);
8
+ this.name = "ConfigurationError";
9
+ this.reason = reason;
10
+ this.setting = setting;
11
+ }
12
+ }
13
+ function parseBoolean(value, name) {
14
+ if (value === undefined || value === "0")
15
+ return false;
16
+ if (value === "1")
17
+ return true;
18
+ throw new ConfigurationError("insecure_http_flag_invalid", name, `${name} must be 0 or 1.`);
19
+ }
20
+ function parseMode(value) {
21
+ const mode = value ?? "read-only";
22
+ if (mode === "read-only" || mode === "write" || mode === "unsafe")
23
+ return mode;
24
+ throw new ConfigurationError("mode_invalid", "N8N_MCP_MODE", "N8N_MCP_MODE must be read-only, write, or unsafe.");
25
+ }
26
+ export function readStartupConfig(env = process.env) {
27
+ return {
28
+ mode: parseMode(env.N8N_MCP_MODE),
29
+ allowInsecureHttp: parseBoolean(env.N8N_ALLOW_INSECURE_HTTP, "N8N_ALLOW_INSECURE_HTTP"),
30
+ };
31
+ }
32
+ function isLoopback(hostname) {
33
+ const normalized = hostname.toLowerCase();
34
+ return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "[::1]";
35
+ }
36
+ function isLegalHeaderValue(value) {
37
+ // Reject any value the fetch/undici layer would refuse as an HTTP header value
38
+ // (control characters, CR, LF, or non-Latin-1 code points) so an invalid API key
39
+ // fails at config load with a constant message instead of surfacing a TypeError
40
+ // whose text can embed the raw key on every tool call.
41
+ return /^[\t\x20-\x7e\x80-\xff]*$/.test(value);
42
+ }
43
+ export function readN8nConnection(startup, env = process.env) {
44
+ const rawUrl = env.N8N_API_URL?.trim();
45
+ const apiKey = env.N8N_API_KEY?.trim();
46
+ if (!rawUrl) {
47
+ throw new ConfigurationError("api_url_missing", "N8N_API_URL", "N8N_API_URL is required for tools that connect to n8n.");
48
+ }
49
+ if (!apiKey) {
50
+ throw new ConfigurationError("api_key_missing", "N8N_API_KEY", "N8N_API_KEY is required for tools that connect to n8n.");
51
+ }
52
+ if (!isLegalHeaderValue(apiKey)) {
53
+ throw new ConfigurationError("api_key_invalid", "N8N_API_KEY", "N8N_API_KEY must be a valid HTTP header value.");
54
+ }
55
+ let apiUrl;
56
+ try {
57
+ apiUrl = new URL(rawUrl);
58
+ }
59
+ catch {
60
+ throw new ConfigurationError("api_url_invalid", "N8N_API_URL", "N8N_API_URL must be a valid absolute URL.");
61
+ }
62
+ if (apiUrl.protocol !== "http:" && apiUrl.protocol !== "https:") {
63
+ throw new ConfigurationError("api_url_scheme_unsupported", "N8N_API_URL", "N8N_API_URL must use HTTP or HTTPS.");
64
+ }
65
+ if (apiUrl.username || apiUrl.password) {
66
+ throw new ConfigurationError("api_url_embedded_credentials", "N8N_API_URL", "N8N_API_URL must not contain embedded credentials.");
67
+ }
68
+ if (apiUrl.search || apiUrl.hash) {
69
+ throw new ConfigurationError("api_url_query_or_fragment", "N8N_API_URL", "N8N_API_URL must not contain a query string or fragment.");
70
+ }
71
+ if (apiUrl.protocol === "http:" && !isLoopback(apiUrl.hostname) && !startup.allowInsecureHttp) {
72
+ throw new ConfigurationError("api_url_insecure_http", "N8N_API_URL", "Plaintext HTTP is allowed only for loopback URLs unless N8N_ALLOW_INSECURE_HTTP=1.");
73
+ }
74
+ apiUrl.pathname = apiUrl.pathname.replace(/\/+$/, "");
75
+ return { apiUrl, apiKey };
76
+ }
@@ -0,0 +1,47 @@
1
+ import { OFFICIAL_N8N_DOCUMENTATION_URLS } from "./official-urls.js";
2
+ export const NODE_DOCUMENTATION = Object.freeze({
3
+ webhook: Object.freeze({
4
+ type: "n8n-nodes-base.webhook",
5
+ title: "Webhook node",
6
+ summary: "Starts a workflow when its test or production webhook receives an HTTP request.",
7
+ guidance: Object.freeze([
8
+ "Use a unique path and the narrowest required HTTP method.",
9
+ "Configure authentication when the caller is not already protected by a trusted gateway.",
10
+ "Use Respond to Webhook when the response must depend on downstream workflow data.",
11
+ ]),
12
+ officialUrl: OFFICIAL_N8N_DOCUMENTATION_URLS.webhook,
13
+ }),
14
+ code: Object.freeze({
15
+ type: "n8n-nodes-base.code",
16
+ title: "Code node",
17
+ summary: "Runs bounded custom JavaScript or Python logic inside an n8n workflow.",
18
+ guidance: Object.freeze([
19
+ "Prefer standard nodes when they express the same operation clearly.",
20
+ "Treat input items as untrusted and return the item structure expected by downstream nodes.",
21
+ "Do not embed credentials or secrets in source code.",
22
+ ]),
23
+ officialUrl: OFFICIAL_N8N_DOCUMENTATION_URLS.code,
24
+ }),
25
+ "http-request": Object.freeze({
26
+ type: "n8n-nodes-base.httpRequest",
27
+ title: "HTTP Request node",
28
+ summary: "Calls an HTTP API from a workflow using configured credentials, headers, query parameters, and body data.",
29
+ guidance: Object.freeze([
30
+ "Use stored n8n credentials instead of literal authorization values.",
31
+ "Set explicit timeouts and retry only operations that are safe to repeat.",
32
+ "Validate and minimize response data before passing it downstream.",
33
+ ]),
34
+ officialUrl: OFFICIAL_N8N_DOCUMENTATION_URLS.httpRequest,
35
+ }),
36
+ if: Object.freeze({
37
+ type: "n8n-nodes-base.if",
38
+ title: "If node",
39
+ summary: "Routes each input item to a true or false output based on configured conditions.",
40
+ guidance: Object.freeze([
41
+ "Choose explicit type-aware operators and handle missing values intentionally.",
42
+ "Keep condition groups small enough to review and test.",
43
+ "Name the node after the business decision it represents.",
44
+ ]),
45
+ officialUrl: OFFICIAL_N8N_DOCUMENTATION_URLS.if,
46
+ }),
47
+ });
@@ -0,0 +1,11 @@
1
+ export const OFFICIAL_N8N_DOCUMENTATION_URLS = Object.freeze({
2
+ webhook: "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/",
3
+ code: "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.code/",
4
+ httpRequest: "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/",
5
+ if: "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.if/",
6
+ splitInBatches: "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.splitinbatches/",
7
+ });
8
+ const OFFICIAL_N8N_DOCUMENTATION_URL_SET = new Set(Object.values(OFFICIAL_N8N_DOCUMENTATION_URLS));
9
+ export function isOfficialN8nDocumentationUrl(value) {
10
+ return OFFICIAL_N8N_DOCUMENTATION_URL_SET.has(value);
11
+ }
package/dist/doctor.js ADDED
@@ -0,0 +1,88 @@
1
+ import { N8nApiError } from "./n8n/client.js";
2
+ export function buildDoctorReport(startup, connection, nodeVersion = process.versions.node) {
3
+ const parsedMajor = Number(nodeVersion.split(".")[0]);
4
+ const major = Number.isInteger(parsedMajor) ? parsedMajor : null;
5
+ const supported = major !== null && major >= 22;
6
+ return {
7
+ status: supported ? "pass" : "fail",
8
+ networkAccess: false,
9
+ node: { major, supported },
10
+ configuration: {
11
+ mode: startup.mode,
12
+ apiUrlConfigured: true,
13
+ apiKeyConfigured: connection.apiKey.length > 0,
14
+ transport: connection.apiUrl.protocol === "https:" ? "https" : "http",
15
+ insecureHttpExplicitlyAllowed: startup.allowInsecureHttp,
16
+ },
17
+ };
18
+ }
19
+ /** Documented support floor. Named in reports; never asserted as the remote version. */
20
+ export const DOCUMENTED_N8N_FLOOR = "n8n Community 2.30.5";
21
+ /**
22
+ * Bounded, value-free Public API reads whose joint availability distinguishes a
23
+ * supported-floor instance from a below-floor one. `GET /workflows` exists on every
24
+ * Public-API-enabled n8n release and acts as a reachability control; `GET /credentials`
25
+ * is available only from the documented 2.30.5 floor onward (docs/compatibility.md), so
26
+ * its absence on a reachable instance is a below-floor indicator. Each read fetches a
27
+ * single page of size 1 and inspects only availability — never any workflow, credential,
28
+ * or other value.
29
+ */
30
+ const FLOOR_PROBE_ENDPOINTS = [
31
+ { endpoint: "GET /workflows", path: "/workflows", role: "control" },
32
+ { endpoint: "GET /credentials", path: "/credentials", role: "floor_marker" },
33
+ ];
34
+ async function probeEndpointAvailability(client, path) {
35
+ try {
36
+ await client.request({ path, query: { limit: "1" } });
37
+ return "available";
38
+ }
39
+ catch (error) {
40
+ if (error instanceof N8nApiError && error.status === 404)
41
+ return "not_found";
42
+ return "error";
43
+ }
44
+ }
45
+ function deriveFloorDiagnosis(control, markerNotFound, markerError) {
46
+ // No usable reachability control means nothing can be concluded about the floor.
47
+ if (control !== "available")
48
+ return "inconclusive";
49
+ // A reachable instance missing a floor-marker namespace (404) is a below-floor signal.
50
+ if (markerNotFound)
51
+ return "below_floor_indicators";
52
+ // A marker that failed for a non-404 reason (403 key scope, 5xx) is not a version signal.
53
+ if (markerError)
54
+ return "inconclusive";
55
+ return "floor_compatible";
56
+ }
57
+ /**
58
+ * Bounded, read-only floor-compatibility probe against the operator-configured instance.
59
+ * Contacts a fixed set of cheap Public API reads (one page of size 1 each, existing client
60
+ * timeouts) and reports only per-endpoint availability plus an overall diagnosis. It never
61
+ * reads response data, never queries or claims a remote version, and never contacts anything
62
+ * other than the configured instance.
63
+ */
64
+ export async function probeFloorCompatibility(client) {
65
+ const endpoints = [];
66
+ let control = "error";
67
+ let markerNotFound = false;
68
+ let markerError = false;
69
+ for (const target of FLOOR_PROBE_ENDPOINTS) {
70
+ const availability = await probeEndpointAvailability(client, target.path);
71
+ endpoints.push({ endpoint: target.endpoint, availability });
72
+ if (target.role === "control") {
73
+ control = availability;
74
+ }
75
+ else if (availability === "not_found") {
76
+ markerNotFound = true;
77
+ }
78
+ else if (availability !== "available") {
79
+ markerError = true;
80
+ }
81
+ }
82
+ return {
83
+ diagnosis: deriveFloorDiagnosis(control, markerNotFound, markerError),
84
+ documentedFloor: DOCUMENTED_N8N_FLOOR,
85
+ remoteVersionDetected: false,
86
+ endpoints,
87
+ };
88
+ }
package/dist/index.js ADDED
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ import { writeSync } from "node:fs";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { ConfigurationError, readN8nConnection, readStartupConfig } from "./config.js";
5
+ import { createServer } from "./server.js";
6
+ import { buildDoctorReport, probeFloorCompatibility } from "./doctor.js";
7
+ import { N8nClient } from "./n8n/client.js";
8
+ import { PACKAGE_VERSION } from "./version.js";
9
+ const HELP = `n8n-mcp-community ${PACKAGE_VERSION}
10
+
11
+ Usage:
12
+ n8n-mcp-community Start the MCP stdio server
13
+ n8n-mcp-community doctor Validate configuration offline; set
14
+ N8N_MCP_DOCTOR_PROBE=1 to add a bounded
15
+ floor-compatibility probe
16
+ n8n-mcp-community --version Print the package version
17
+ n8n-mcp-community --help Show this help
18
+
19
+ The server defaults to N8N_MCP_MODE=read-only. Connected tools require
20
+ N8N_API_URL and N8N_API_KEY. Never pass secrets as command-line arguments.`;
21
+ async function runDoctor() {
22
+ const startup = readStartupConfig();
23
+ const connection = readN8nConnection(startup);
24
+ const report = buildDoctorReport(startup, connection);
25
+ if (process.env.N8N_MCP_DOCTOR_PROBE === "1") {
26
+ const compatibility = await probeFloorCompatibility(new N8nClient(connection));
27
+ console.log(JSON.stringify({ ...report, networkAccess: true, compatibility }, null, 2));
28
+ }
29
+ else {
30
+ console.log(JSON.stringify(report, null, 2));
31
+ }
32
+ if (report.status === "fail")
33
+ process.exitCode = 1;
34
+ }
35
+ async function main(args = process.argv.slice(2)) {
36
+ if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
37
+ console.log(HELP);
38
+ return;
39
+ }
40
+ if (args.length === 1 && (args[0] === "--version" || args[0] === "-v")) {
41
+ console.log(PACKAGE_VERSION);
42
+ return;
43
+ }
44
+ if (args.length === 1 && args[0] === "doctor") {
45
+ await runDoctor();
46
+ return;
47
+ }
48
+ if (args.length > 0) {
49
+ throw new Error("Unknown command or option. Use --help.");
50
+ }
51
+ const server = createServer();
52
+ await server.connect(new StdioServerTransport());
53
+ }
54
+ function isBrokenPipe(error) {
55
+ if (error === null || typeof error !== "object" || !("code" in error))
56
+ return false;
57
+ const code = error.code;
58
+ return code === "EPIPE" || code === "ERR_STREAM_DESTROYED";
59
+ }
60
+ let fatalHandled = false;
61
+ /**
62
+ * Emit exactly one constant, secret-free structured line on stderr and exit with a
63
+ * controlled code. stdout is the MCP protocol channel, so it is never written here.
64
+ * `writeSync` guarantees the line is flushed before `process.exit` tears the process
65
+ * down, and the guard makes repeated stream failures collapse to a single line.
66
+ */
67
+ function reportFatal(event, exitCode) {
68
+ if (fatalHandled)
69
+ return;
70
+ fatalHandled = true;
71
+ try {
72
+ writeSync(2, `${JSON.stringify({ event })}\n`);
73
+ }
74
+ catch {
75
+ // stderr itself is unavailable; there is nothing else we can safely do.
76
+ }
77
+ process.exit(exitCode);
78
+ }
79
+ /**
80
+ * Install process-level guards so a broken stdout pipe (the client's read end
81
+ * disappearing mid-session) or any other unhandled stream/async failure produces a
82
+ * controlled, single-line shutdown instead of a raw Node stack trace on stderr.
83
+ */
84
+ function installProcessGuards() {
85
+ // A broken stdout pipe means the MCP peer is gone; shut down cleanly (exit 0).
86
+ process.stdout.on("error", (error) => {
87
+ reportFatal("transport_closed", isBrokenPipe(error) ? 0 : 1);
88
+ });
89
+ process.on("uncaughtException", (error) => {
90
+ if (isBrokenPipe(error))
91
+ reportFatal("transport_closed", 0);
92
+ else
93
+ reportFatal("uncaught_exception", 1);
94
+ });
95
+ process.on("unhandledRejection", (reason) => {
96
+ if (isBrokenPipe(reason))
97
+ reportFatal("transport_closed", 0);
98
+ else
99
+ reportFatal("unhandled_rejection", 1);
100
+ });
101
+ }
102
+ installProcessGuards();
103
+ main().catch((error) => {
104
+ if (error instanceof ConfigurationError) {
105
+ console.error(JSON.stringify({
106
+ event: "startup_failed",
107
+ code: error.code,
108
+ reason: error.reason,
109
+ setting: error.setting,
110
+ }));
111
+ process.exitCode = 1;
112
+ return;
113
+ }
114
+ const code = error && typeof error === "object" && "code" in error && typeof error.code === "string"
115
+ ? error.code
116
+ : "startup_error";
117
+ console.error(JSON.stringify({ event: "startup_failed", code }));
118
+ process.exitCode = 1;
119
+ });