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.
- package/CHANGELOG.md +116 -0
- package/CODE_OF_CONDUCT.md +51 -0
- package/CONTRIBUTING.md +69 -0
- package/LICENSE +21 -0
- package/README.md +271 -0
- package/ROADMAP.md +41 -0
- package/SECURITY.md +53 -0
- package/SUPPORT.md +19 -0
- package/THIRD_PARTY_NOTICES.md +48 -0
- package/dist/config.js +76 -0
- package/dist/content/node-docs.js +47 -0
- package/dist/content/official-urls.js +11 -0
- package/dist/doctor.js +88 -0
- package/dist/index.js +119 -0
- package/dist/introspect/collector.js +642 -0
- package/dist/introspect/contracts.js +309 -0
- package/dist/introspect/engine.js +184 -0
- package/dist/introspect/http-client.js +142 -0
- package/dist/introspect/index.js +5 -0
- package/dist/introspect/order.js +3 -0
- package/dist/introspect/render.js +25 -0
- package/dist/introspect/rule-ids.js +25 -0
- package/dist/introspect/rules.js +905 -0
- package/dist/introspect/sanitize.js +66 -0
- package/dist/n8n/client.js +170 -0
- package/dist/prompts.js +69 -0
- package/dist/resources.js +42 -0
- package/dist/security/operation-policy.js +20 -0
- package/dist/security/redaction.js +253 -0
- package/dist/server.js +16 -0
- package/dist/tools/common.js +54 -0
- package/dist/tools/credentials.js +259 -0
- package/dist/tools/definition.js +169 -0
- package/dist/tools/endpoint-contracts.js +53 -0
- package/dist/tools/executions.js +180 -0
- package/dist/tools/registry.js +24 -0
- package/dist/tools/schemas.js +203 -0
- package/dist/tools/tags.js +73 -0
- package/dist/tools/users.js +130 -0
- package/dist/tools/utilities.js +347 -0
- package/dist/tools/workflows.js +771 -0
- package/dist/version.js +1 -0
- package/docs/README.md +21 -0
- package/docs/architecture.md +122 -0
- package/docs/assets/demo.gif +0 -0
- package/docs/clients.md +95 -0
- package/docs/compatibility.md +112 -0
- package/docs/configuration.md +87 -0
- package/docs/demo-transcript.md +31 -0
- package/docs/examples.md +86 -0
- package/docs/faq.md +90 -0
- package/docs/getting-started.md +93 -0
- package/docs/installation.md +120 -0
- package/docs/provenance.md +152 -0
- package/docs/security-model.md +170 -0
- package/docs/tools.md +937 -0
- package/docs/troubleshooting.md +185 -0
- package/package.json +126 -0
- 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
|
+
});
|