@gmag11/nodered-mcp-server 1.0.1

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 (89) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +162 -0
  3. package/index.js +133 -0
  4. package/package.json +58 -0
  5. package/resources/skills/nodered-flow-builder/SKILL.md +659 -0
  6. package/resources/skills/nodered-flow-layout/SKILL.md +395 -0
  7. package/resources/skills/nodered-flowfuse-dashboard/SKILL.md +941 -0
  8. package/resources/skills/nodered-fundamentals/SKILL.md +323 -0
  9. package/resources/skills/nodered-jsonata/SKILL.md +1039 -0
  10. package/resources/skills/nodered-mustache/SKILL.md +588 -0
  11. package/resources/skills/nodered-node-reference/SKILL.md +1020 -0
  12. package/resources/skills/nodered-node-reference/examples/common.json +113 -0
  13. package/resources/skills/nodered-node-reference/examples/network.json +107 -0
  14. package/resources/skills/nodered-node-reference/examples/parser.json +147 -0
  15. package/resources/skills/nodered-node-reference/examples/sequence.json +141 -0
  16. package/resources/skills/nodered-node-reference/examples/storage.json +104 -0
  17. package/resources/skills/nodered-patterns/SKILL.md +414 -0
  18. package/resources/skills/nodered-patterns/examples/error-handler.json +72 -0
  19. package/resources/skills/nodered-patterns/examples/http-endpoint.json +42 -0
  20. package/resources/skills/nodered-patterns/examples/mqtt-subscriber.json +47 -0
  21. package/resources/skills/nodered-patterns/examples/timer-flow.json +50 -0
  22. package/resources/skills/nodered-subflows/SKILL.md +261 -0
  23. package/resources/skills/nodered-uibuilder/SKILL.md +500 -0
  24. package/src/auth/api-key-verifier.js +36 -0
  25. package/src/auth/composite-verifier.js +59 -0
  26. package/src/auth/config.js +106 -0
  27. package/src/auth/oauth-clients-store.js +107 -0
  28. package/src/auth/oauth-provider.js +149 -0
  29. package/src/auth/oauth-token-store.js +312 -0
  30. package/src/nodered/auth.js +158 -0
  31. package/src/nodered/client.js +199 -0
  32. package/src/nodered/comms-client.js +500 -0
  33. package/src/renderer/colors.js +161 -0
  34. package/src/renderer/geometry.js +115 -0
  35. package/src/renderer/html-builder.js +571 -0
  36. package/src/renderer/index.js +51 -0
  37. package/src/renderer/ir-builder.js +161 -0
  38. package/src/renderer/layout.js +126 -0
  39. package/src/renderer/mermaid-builder.js +109 -0
  40. package/src/renderer/svg-builder.js +228 -0
  41. package/src/schemas/responses.js +283 -0
  42. package/src/server.js +844 -0
  43. package/src/skills/loader.js +84 -0
  44. package/src/staging-store.js +258 -0
  45. package/src/tools/add-nodes-to-group.js +216 -0
  46. package/src/tools/connect-nodes.js +115 -0
  47. package/src/tools/constants.js +45 -0
  48. package/src/tools/create-flow.js +87 -0
  49. package/src/tools/create-node.js +126 -0
  50. package/src/tools/create-subflow-instance.js +123 -0
  51. package/src/tools/create-subflow.js +101 -0
  52. package/src/tools/delete-context.js +60 -0
  53. package/src/tools/delete-flow.js +81 -0
  54. package/src/tools/delete-group.js +116 -0
  55. package/src/tools/delete-node.js +73 -0
  56. package/src/tools/delete-subflow.js +103 -0
  57. package/src/tools/deploy.js +94 -0
  58. package/src/tools/disconnect-nodes.js +158 -0
  59. package/src/tools/export-flow.js +161 -0
  60. package/src/tools/export-subflow.js +78 -0
  61. package/src/tools/flow-utils.js +376 -0
  62. package/src/tools/get-config-nodes.js +86 -0
  63. package/src/tools/get-context.js +76 -0
  64. package/src/tools/get-flow-diagram.js +99 -0
  65. package/src/tools/get-flow-nodes.js +116 -0
  66. package/src/tools/get-flows.js +74 -0
  67. package/src/tools/get-node-detail.js +77 -0
  68. package/src/tools/get-node-type-detail.js +92 -0
  69. package/src/tools/get-palette-nodes.js +63 -0
  70. package/src/tools/get-staging-status.js +34 -0
  71. package/src/tools/get-subflow-detail.js +110 -0
  72. package/src/tools/get-subflows.js +105 -0
  73. package/src/tools/import-flow.js +310 -0
  74. package/src/tools/inject-message.js +117 -0
  75. package/src/tools/install-node.js +31 -0
  76. package/src/tools/read-debug-messages.js +155 -0
  77. package/src/tools/refresh-staging.js +62 -0
  78. package/src/tools/remove-nodes-from-group.js +162 -0
  79. package/src/tools/render-staging.js +69 -0
  80. package/src/tools/response-utils.js +42 -0
  81. package/src/tools/search-nodes.js +134 -0
  82. package/src/tools/uninstall-node.js +31 -0
  83. package/src/tools/update-flow.js +95 -0
  84. package/src/tools/update-group.js +77 -0
  85. package/src/tools/update-node.js +132 -0
  86. package/src/tools/update-subflow.js +84 -0
  87. package/src/transport/http.js +252 -0
  88. package/src/transport/stdio.js +16 -0
  89. package/src/transport/ws-server.js +223 -0
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Node-RED authentication module.
3
+ *
4
+ * Supports three modes:
5
+ * - none: no authentication required
6
+ * - credentials: username/password → Bearer token
7
+ * - apikey: static API key used directly as Bearer token
8
+ */
9
+
10
+ /**
11
+ * Detect the authentication mode of a Node-RED instance.
12
+ *
13
+ * If NODERED_API_KEY is set, returns 'apikey' immediately without probing.
14
+ * Otherwise calls GET /auth/login to determine the mode.
15
+ *
16
+ * @param {string} baseUrl - Node-RED instance URL
17
+ * @param {string} [apiKey] - Static API key (skips detection if provided)
18
+ * @returns {Promise<'none'|'credentials'|'apikey'>}
19
+ */
20
+ export async function detectAuthMode(baseUrl, apiKey) {
21
+ if (apiKey) {
22
+ return 'apikey';
23
+ }
24
+
25
+ const res = await fetch(`${baseUrl}/auth/login`);
26
+ if (!res.ok) {
27
+ throw new Error(`Failed to detect auth mode: GET /auth/login returned ${res.status}. Check that the Node-RED instance is running and accessible at the configured baseUrl.`);
28
+ }
29
+
30
+ const body = await res.json();
31
+
32
+ // Empty object means no authentication configured
33
+ if (!body.type) {
34
+ return 'none';
35
+ }
36
+
37
+ if (body.type === 'credentials') {
38
+ return 'credentials';
39
+ }
40
+
41
+ throw new Error(
42
+ `Unsupported Node-RED auth type: "${body.type}". ` +
43
+ `Set NODERED_API_KEY in your environment to authenticate with a static token, or configure credentials via NODERED_USERNAME and NODERED_PASSWORD.`
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Obtain an access token from Node-RED using the credentials grant.
49
+ *
50
+ * @param {string} baseUrl - Node-RED instance URL
51
+ * @param {string} username
52
+ * @param {string} password
53
+ * @returns {Promise<string>} The access_token
54
+ */
55
+ export async function getToken(baseUrl, username, password) {
56
+ const res = await fetch(`${baseUrl}/auth/token`, {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
59
+ body: new URLSearchParams({
60
+ client_id: 'node-red-admin',
61
+ grant_type: 'password',
62
+ scope: '*',
63
+ username,
64
+ password,
65
+ }),
66
+ });
67
+
68
+ if (!res.ok) {
69
+ throw new Error(`Failed to obtain Node-RED token: POST /auth/token returned ${res.status}. Check that NODERED_USERNAME and NODERED_PASSWORD are correct and that the credentials auth is enabled in Node-RED.`);
70
+ }
71
+
72
+ const body = await res.json();
73
+ return body.access_token;
74
+ }
75
+
76
+ /**
77
+ * Manages authentication state for a single Node-RED instance.
78
+ */
79
+ export class AuthManager {
80
+ #baseUrl;
81
+ #username;
82
+ #password;
83
+ #apiKey;
84
+ #token = null;
85
+ #mode = null;
86
+
87
+ /**
88
+ * @param {object} config
89
+ * @param {string} config.baseUrl - Node-RED instance URL
90
+ * @param {string} [config.username]
91
+ * @param {string} [config.password]
92
+ * @param {string} [config.apiKey]
93
+ */
94
+ constructor({ baseUrl, username, password, apiKey }) {
95
+ this.#baseUrl = baseUrl;
96
+ this.#username = username;
97
+ this.#password = password;
98
+ this.#apiKey = apiKey;
99
+ }
100
+
101
+ /**
102
+ * Initialize the auth manager: detect mode and acquire token if needed.
103
+ * Must be called once before using getAuthHeader().
104
+ */
105
+ async init() {
106
+ this.#mode = await detectAuthMode(this.#baseUrl, this.#apiKey);
107
+
108
+ if (this.#mode === 'credentials') {
109
+ if (!this.#username || !this.#password) {
110
+ throw new Error(
111
+ 'Node-RED requires credentials authentication but NODERED_USERNAME and/or NODERED_PASSWORD are not set. Set these environment variables to authenticate, or use NODERED_API_KEY for static token auth.'
112
+ );
113
+ }
114
+ this.#token = await getToken(this.#baseUrl, this.#username, this.#password);
115
+ } else if (this.#mode === 'apikey') {
116
+ this.#token = this.#apiKey;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Returns the current auth mode.
122
+ * @returns {'none'|'credentials'|'apikey'|null}
123
+ */
124
+ get mode() {
125
+ return this.#mode;
126
+ }
127
+
128
+ /**
129
+ * Returns the Authorization header value, or null if no auth is needed.
130
+ * @returns {string|null}
131
+ */
132
+ getAuthHeader() {
133
+ if (this.#mode === 'none' || !this.#token) {
134
+ return null;
135
+ }
136
+ return `Bearer ${this.#token}`;
137
+ }
138
+
139
+ /**
140
+ * Invalidate the current token and re-authenticate.
141
+ * Only applicable for credentials mode.
142
+ * @returns {Promise<void>}
143
+ */
144
+ async reauthenticate() {
145
+ if (this.#mode !== 'credentials') {
146
+ return;
147
+ }
148
+ this.#token = null;
149
+ this.#token = await getToken(this.#baseUrl, this.#username, this.#password);
150
+ }
151
+
152
+ /**
153
+ * Invalidate the current token (e.g., after a 401).
154
+ */
155
+ invalidateToken() {
156
+ this.#token = null;
157
+ }
158
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Generic HTTP client for the Node-RED Admin API.
3
+ *
4
+ * Wraps native fetch with:
5
+ * - Automatic Node-RED-API-Version: v2 header
6
+ * - Authorization header via AuthManager
7
+ * - 401 retry (re-authenticate once then retry)
8
+ * - Descriptive errors for non-2xx responses
9
+ */
10
+
11
+ /**
12
+ * Create a Node-RED API client bound to an AuthManager.
13
+ *
14
+ * @param {string} baseUrl - Node-RED instance URL
15
+ * @param {import('./auth.js').AuthManager} authManager
16
+ * @returns {{ request: (method: string, path: string, body?: any, extraHeaders?: object) => Promise<any>, requestText: (method: string, path: string) => Promise<string>, putFlows: (flowsPayload: any, deployType?: string) => Promise<any>, post: (path: string, body?: any) => Promise<any> }}
17
+ */
18
+ export function createNodeRedClient(baseUrl, authManager) {
19
+ /**
20
+ * Make a request to the Node-RED Admin API.
21
+ *
22
+ * @param {string} method - HTTP method (GET, POST, PUT, DELETE)
23
+ * @param {string} path - API path (e.g., '/flows')
24
+ * @param {any} [body] - Request body (will be JSON-serialized)
25
+ * @returns {Promise<any>} Parsed JSON response, or raw text if the response is not valid JSON
26
+ */
27
+ async function request(method, path, body, extraHeaders) {
28
+ const url = `${baseUrl}${path}`;
29
+
30
+ const res = await doFetch(method, url, body, extraHeaders);
31
+
32
+ // Handle 401: invalidate token, re-authenticate, retry once
33
+ if (res.status === 401) {
34
+ await authManager.reauthenticate();
35
+ const retryRes = await doFetch(method, url, body, extraHeaders);
36
+
37
+ if (!retryRes.ok) {
38
+ const retryBody = await safeReadBody(retryRes);
39
+ throw new Error(
40
+ `Node-RED API error after re-auth: ${method} ${path} returned ${retryRes.status}` +
41
+ (retryBody ? ` — ${retryBody}` : '') +
42
+ '. Check your Node-RED credentials, instance URL, and that the Node-RED server is running.'
43
+ );
44
+ }
45
+
46
+ return retryRes.status === 204 ? null : parseResponseBody(retryRes);
47
+ }
48
+
49
+ if (!res.ok) {
50
+ const errorBody = await safeReadBody(res);
51
+ throw new Error(
52
+ `Node-RED API error: ${method} ${path} returned ${res.status}` +
53
+ (errorBody ? ` — ${errorBody}` : '') +
54
+ '. Check your Node-RED connection and credentials. If the error persists, try refresh-staging to ensure you are working with the latest server state.'
55
+ );
56
+ }
57
+
58
+ return res.status === 204 ? null : parseResponseBody(res);
59
+ }
60
+
61
+ /**
62
+ * Read the response body and parse as JSON. If JSON parsing fails,
63
+ * return the raw text (some Node-RED endpoints return plain text).
64
+ *
65
+ * @param {Response} res - Fetch Response object
66
+ * @returns {Promise<any>} Parsed JSON or raw text
67
+ */
68
+ async function parseResponseBody(res) {
69
+ const text = await res.text();
70
+ try {
71
+ return JSON.parse(text);
72
+ } catch {
73
+ return text;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Internal fetch helper that builds the correct headers.
79
+ */
80
+ async function doFetch(method, url, body, extraHeaders) {
81
+ console.error(`[NodeRed-MCP] → ${method} ${url}`);
82
+
83
+ const headers = {
84
+ 'Node-RED-API-Version': 'v2',
85
+ 'Accept': 'application/json',
86
+ };
87
+
88
+ const authHeader = authManager.getAuthHeader();
89
+ if (authHeader) {
90
+ headers['Authorization'] = authHeader;
91
+ }
92
+
93
+ if (body !== undefined) {
94
+ headers['Content-Type'] = 'application/json';
95
+ }
96
+
97
+ if (extraHeaders) {
98
+ Object.assign(headers, extraHeaders);
99
+ }
100
+
101
+ return fetch(url, {
102
+ method,
103
+ headers,
104
+ body: body !== undefined ? JSON.stringify(body) : undefined,
105
+ }).then(res => {
106
+ console.error(`[NodeRed-MCP] ← ${res.status} ${method} ${url}`);
107
+ return res;
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Deploy the full flows payload to Node-RED using PUT /flows.
113
+ *
114
+ * @param {object} flowsPayload - The flows payload (must include `rev` from GET /flows)
115
+ * @param {string} [deployType='flows'] - Value for the `Node-RED-Deployment-Type` header
116
+ * @returns {Promise<any>} Parsed JSON response
117
+ */
118
+ async function putFlows(flowsPayload, deployType = 'flows') {
119
+ return request('POST', '/flows', flowsPayload, {
120
+ 'Node-RED-Deployment-Type': deployType,
121
+ });
122
+ }
123
+
124
+ /**
125
+ * Make a request to the Node-RED Admin API and return the raw response body as text.
126
+ * Useful for endpoints that return HTML (e.g. GET /nodes with Accept: text/html).
127
+ *
128
+ * @param {string} method - HTTP method (GET, POST, PUT, DELETE)
129
+ * @param {string} path - API path (e.g., '/nodes')
130
+ * @returns {Promise<string>} Raw response body as a string
131
+ */
132
+ async function requestText(method, path) {
133
+ const url = `${baseUrl}${path}`;
134
+ const headers = {
135
+ 'Node-RED-API-Version': 'v2',
136
+ };
137
+
138
+ const authHeader = authManager.getAuthHeader();
139
+ if (authHeader) {
140
+ headers['Authorization'] = authHeader;
141
+ }
142
+
143
+ let res = await fetch(url, { method, headers });
144
+
145
+ if (res.status === 401) {
146
+ await authManager.reauthenticate();
147
+ const authHeader2 = authManager.getAuthHeader();
148
+ if (authHeader2) {
149
+ headers['Authorization'] = authHeader2;
150
+ }
151
+ res = await fetch(url, { method, headers });
152
+
153
+ if (!res.ok) {
154
+ const retryBody = await safeReadBody(res);
155
+ throw new Error(
156
+ `Node-RED API error after re-auth: ${method} ${path} returned ${res.status}` +
157
+ (retryBody ? ` — ${retryBody}` : '') +
158
+ '. Check your Node-RED credentials, instance URL, and that the Node-RED server is running.'
159
+ );
160
+ }
161
+ } else if (!res.ok) {
162
+ const errorBody = await safeReadBody(res);
163
+ throw new Error(
164
+ `Node-RED API error: ${method} ${path} returned ${res.status}` +
165
+ (errorBody ? ` — ${errorBody}` : '') +
166
+ '. Check your Node-RED connection and credentials. If the error persists, try refresh-staging to ensure you are working with the latest server state.'
167
+ );
168
+ }
169
+
170
+ return res.text();
171
+ }
172
+
173
+ /**
174
+ * Make a POST request to the Node-RED Admin API.
175
+ * Convenience wrapper around request('POST', ...).
176
+ *
177
+ * @param {string} path - API path (e.g., '/inject/:nodeId')
178
+ * @param {any} [body] - Optional request body
179
+ * @returns {Promise<any>} Parsed JSON response
180
+ */
181
+ async function post(path, body) {
182
+ return request('POST', path, body);
183
+ }
184
+
185
+ return { request, requestText, putFlows, post };
186
+ }
187
+
188
+ /**
189
+ * Safely read the response body as text for error messages.
190
+ * Returns null if the body cannot be read.
191
+ */
192
+ async function safeReadBody(res) {
193
+ try {
194
+ const text = await res.text();
195
+ return text.length > 200 ? text.slice(0, 200) + '…' : text;
196
+ } catch {
197
+ return null;
198
+ }
199
+ }