@withone/mcp 2.3.1 → 2.3.2
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/README.md +2 -0
- package/build/index.js +13 -0
- package/build/node-check.js +83 -0
- package/build/node-check.test.js +50 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -166,6 +166,8 @@ If you used `one init`, the configuration below is already done for you. These e
|
|
|
166
166
|
npx @withone/mcp
|
|
167
167
|
```
|
|
168
168
|
|
|
169
|
+
Requires Node.js 22.23.4+, 24.22.1+ or 26.12.1+. Older Node trusts the compromised eMudhra CA certificates (October 2026), so a network attacker could intercept `ONE_SECRET`. On an older Node the server still starts, but every tool returns an error with the command that updates Node for how it was installed. If you can't update yet, `ONE_ALLOW_UNPATCHED_NODE=1` lets it call One anyway; you remain exposed while it is set.
|
|
170
|
+
|
|
169
171
|
### Claude Desktop / Cursor
|
|
170
172
|
|
|
171
173
|
Add the following to your MCP config:
|
package/build/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { OneClient } from './client.js';
|
|
|
14
14
|
import { isMethodAllowed, isActionAllowed, buildIntegrationsResponse, } from './helpers.js';
|
|
15
15
|
import { findOneActions } from './find.js';
|
|
16
16
|
import { ToolError, toolErrorResult } from './tool-error.js';
|
|
17
|
+
import { unpatchedNodeRefusal } from './node-check.js';
|
|
17
18
|
import { listOneIntegrationsToolConfig, findOneActionsToolConfig, executeOneActionToolConfig } from './schemas.js';
|
|
18
19
|
import { createRequire } from 'node:module';
|
|
19
20
|
const SERVER_NAME = "one-mcp-server";
|
|
@@ -38,6 +39,12 @@ const ONE_ACTION_IDS = process.env.ONE_ACTION_IDS
|
|
|
38
39
|
? process.env.ONE_ACTION_IDS.split(",").map(s => s.trim()).filter(Boolean)
|
|
39
40
|
: ["*"];
|
|
40
41
|
const ONE_KNOWLEDGE_AGENT = process.env.ONE_KNOWLEDGE_AGENT === "true";
|
|
42
|
+
// eMudhra CA compromise: on a Node without the fix every tool answers this
|
|
43
|
+
// instead of calling One (see node-check.ts).
|
|
44
|
+
const NODE_REFUSAL = unpatchedNodeRefusal();
|
|
45
|
+
if (NODE_REFUSAL) {
|
|
46
|
+
console.error(NODE_REFUSAL);
|
|
47
|
+
}
|
|
41
48
|
const oneClient = new OneClient({
|
|
42
49
|
secret: ONE_SECRET,
|
|
43
50
|
baseUrl: ONE_BASE_URL,
|
|
@@ -74,15 +81,21 @@ const server = new McpServer({
|
|
|
74
81
|
version: SERVER_VERSION,
|
|
75
82
|
});
|
|
76
83
|
server.registerTool("list_one_integrations", listOneIntegrationsToolConfig, async (args) => {
|
|
84
|
+
if (NODE_REFUSAL)
|
|
85
|
+
return toolErrorResult(new ToolError(NODE_REFUSAL));
|
|
77
86
|
await initializeOne();
|
|
78
87
|
return await handleGetIntegrations(args);
|
|
79
88
|
});
|
|
80
89
|
server.registerTool("find_one_actions", findOneActionsToolConfig, async (args) => {
|
|
90
|
+
if (NODE_REFUSAL)
|
|
91
|
+
return toolErrorResult(new ToolError(NODE_REFUSAL));
|
|
81
92
|
await initializeOne();
|
|
82
93
|
return await handleFindOneActions(args);
|
|
83
94
|
});
|
|
84
95
|
if (!ONE_KNOWLEDGE_AGENT) {
|
|
85
96
|
server.registerTool("execute_one_action", executeOneActionToolConfig, async (args) => {
|
|
97
|
+
if (NODE_REFUSAL)
|
|
98
|
+
return toolErrorResult(new ToolError(NODE_REFUSAL));
|
|
86
99
|
await initializeOne();
|
|
87
100
|
return await handleExecuteOneAction(args);
|
|
88
101
|
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node.js TLS guard
|
|
3
|
+
*
|
|
4
|
+
* Eight eMudhra/emSign intermediate CAs were created with 8-bit private keys
|
|
5
|
+
* (October 2026, RUSTSEC-2026-0381), so anyone can compute them and sign a
|
|
6
|
+
* certificate for any domain, including api.withone.ai. Node.js blocks them
|
|
7
|
+
* inside the TLS handshake from 22.23.4, 24.22.1 and 26.12.1
|
|
8
|
+
* (nodejs/node#66871). On any older Node, a network attacker can intercept
|
|
9
|
+
* this server's traffic and the ONE_SECRET it sends.
|
|
10
|
+
*
|
|
11
|
+
* An MCP server that exits at startup only shows up as "failed to start" in
|
|
12
|
+
* the client, so on an unpatched Node the server still starts but every tool
|
|
13
|
+
* returns this refusal, before any request, for the agent to pass on.
|
|
14
|
+
*
|
|
15
|
+
* @fileoverview Refuses to call One from a Node without the eMudhra fix
|
|
16
|
+
* @author One
|
|
17
|
+
*/
|
|
18
|
+
/** The first patch of each Node line that ships the blocklist. Odd lines (23, 25) are end-of-life and never got it. */
|
|
19
|
+
const FIXED_IN = {
|
|
20
|
+
22: [23, 4],
|
|
21
|
+
24: [22, 1],
|
|
22
|
+
26: [12, 1],
|
|
23
|
+
};
|
|
24
|
+
/** The first Node line released after the fix, so it has it from .0. */
|
|
25
|
+
const FIRST_LINE_WITH_FIX = 27;
|
|
26
|
+
/** The line to move to from a Node line that never got the fix. */
|
|
27
|
+
const RECOMMENDED_LINE = 24;
|
|
28
|
+
export const REQUIRED_NODE = '22.23.4+, 24.22.1+ or 26.12.1+';
|
|
29
|
+
/** Set to 1 to call One from an unpatched Node anyway. */
|
|
30
|
+
export const ALLOW_UNPATCHED_ENV = 'ONE_ALLOW_UNPATCHED_NODE';
|
|
31
|
+
function parseVersion(version) {
|
|
32
|
+
const [major, minor, patch] = version.split('.').map((part) => Number.parseInt(part, 10));
|
|
33
|
+
return [major, minor, patch ?? 0];
|
|
34
|
+
}
|
|
35
|
+
/** True when this Node version rejects chains through the compromised eMudhra intermediates. */
|
|
36
|
+
export function nodeHasTlsFix(version = process.versions.node) {
|
|
37
|
+
const [major, minor, patch] = parseVersion(version);
|
|
38
|
+
if (!Number.isFinite(major))
|
|
39
|
+
return false;
|
|
40
|
+
if (major >= FIRST_LINE_WITH_FIX)
|
|
41
|
+
return true;
|
|
42
|
+
const fixed = FIXED_IN[major];
|
|
43
|
+
if (!fixed)
|
|
44
|
+
return false;
|
|
45
|
+
const [fixedMinor, fixedPatch] = fixed;
|
|
46
|
+
return minor > fixedMinor || (minor === fixedMinor && patch >= fixedPatch);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The command that updates Node for however it was installed, judged from the
|
|
50
|
+
* path of the running binary. A line that got the fix stays on its line; any
|
|
51
|
+
* other moves to the recommended one.
|
|
52
|
+
*/
|
|
53
|
+
export function upgradeCommand(version = process.versions.node, execPath = process.execPath) {
|
|
54
|
+
const [major] = parseVersion(version);
|
|
55
|
+
const line = FIXED_IN[major] ? major : RECOMMENDED_LINE;
|
|
56
|
+
const path = execPath.replace(/\\/g, '/');
|
|
57
|
+
if (/\/\.nvm\/versions\/node\//.test(path))
|
|
58
|
+
return `nvm install ${line} && nvm alias default ${line}`;
|
|
59
|
+
if (/\/AppData\/(Roaming|Local)\/nvm\//i.test(path))
|
|
60
|
+
return `nvm install ${line} && nvm use ${line}`;
|
|
61
|
+
if (/\/\.?volta\//i.test(path))
|
|
62
|
+
return `volta install node@${line}`;
|
|
63
|
+
if (/fnm/i.test(path))
|
|
64
|
+
return `fnm install ${line} && fnm default ${line}`;
|
|
65
|
+
const brew = path.match(/\/Cellar\/(node(?:@\d+)?)\//);
|
|
66
|
+
if (brew) {
|
|
67
|
+
const formula = brew[1] === 'node' || FIXED_IN[major] ? brew[1] : `node@${line}`;
|
|
68
|
+
return formula === brew[1] ? `brew upgrade ${formula}` : `brew install ${formula}`;
|
|
69
|
+
}
|
|
70
|
+
return `download Node.js ${line} from https://nodejs.org`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* What every tool answers on an unpatched Node, or null when it may call One:
|
|
74
|
+
* the Node has the fix, or ONE_ALLOW_UNPATCHED_NODE=1.
|
|
75
|
+
*/
|
|
76
|
+
export function unpatchedNodeRefusal(version = process.versions.node, execPath = process.execPath, env = process.env) {
|
|
77
|
+
if (nodeHasTlsFix(version) || env[ALLOW_UNPATCHED_ENV] === '1')
|
|
78
|
+
return null;
|
|
79
|
+
return (`The One MCP server is running on Node.js v${version}, which trusts the compromised eMudhra ` +
|
|
80
|
+
`certificates, so a network attacker could intercept its traffic and the user's One API key. ` +
|
|
81
|
+
`It will not call One until Node.js is updated to ${REQUIRED_NODE} (Node 18, 20, 23 and 25 never got the fix). ` +
|
|
82
|
+
`Don't retry: ask the user to run \`${upgradeCommand(version, execPath)}\`, then restart their MCP client.`);
|
|
83
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { nodeHasTlsFix, unpatchedNodeRefusal, upgradeCommand } from './node-check.js';
|
|
4
|
+
describe('nodeHasTlsFix', () => {
|
|
5
|
+
it('knows the first fixed patch of each supported line', () => {
|
|
6
|
+
assert.equal(nodeHasTlsFix('22.23.4'), true);
|
|
7
|
+
assert.equal(nodeHasTlsFix('22.23.3'), false);
|
|
8
|
+
assert.equal(nodeHasTlsFix('22.24.0'), true);
|
|
9
|
+
assert.equal(nodeHasTlsFix('24.22.1'), true);
|
|
10
|
+
assert.equal(nodeHasTlsFix('24.22.0'), false);
|
|
11
|
+
assert.equal(nodeHasTlsFix('26.12.1'), true);
|
|
12
|
+
assert.equal(nodeHasTlsFix('26.12.0'), false);
|
|
13
|
+
});
|
|
14
|
+
it('treats end-of-life lines as unfixed and later lines as fixed', () => {
|
|
15
|
+
for (const v of ['18.20.8', '20.19.5', '23.11.1', '25.9.0']) {
|
|
16
|
+
assert.equal(nodeHasTlsFix(v), false, v);
|
|
17
|
+
}
|
|
18
|
+
assert.equal(nodeHasTlsFix('27.0.0'), true);
|
|
19
|
+
assert.equal(nodeHasTlsFix('garbage'), false);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe('upgradeCommand', () => {
|
|
23
|
+
it('keeps a fixed line and moves an end-of-life one to 24', () => {
|
|
24
|
+
assert.equal(upgradeCommand('22.10.0', '/Users/me/.nvm/versions/node/v22.10.0/bin/node'), 'nvm install 22 && nvm alias default 22');
|
|
25
|
+
assert.equal(upgradeCommand('20.19.5', '/Users/me/.nvm/versions/node/v20.19.5/bin/node'), 'nvm install 24 && nvm alias default 24');
|
|
26
|
+
});
|
|
27
|
+
it('matches each version manager by the binary path', () => {
|
|
28
|
+
assert.equal(upgradeCommand('20.19.5', 'C:\\Users\\me\\AppData\\Roaming\\nvm\\v20.19.5\\node.exe'), 'nvm install 24 && nvm use 24');
|
|
29
|
+
assert.equal(upgradeCommand('20.19.5', '/Users/me/.volta/tools/image/node/20.19.5/bin/node'), 'volta install node@24');
|
|
30
|
+
assert.equal(upgradeCommand('20.19.5', '/Users/me/.local/share/fnm/node-versions/v20.19.5/installation/bin/node'), 'fnm install 24 && fnm default 24');
|
|
31
|
+
assert.equal(upgradeCommand('26.1.0', '/opt/homebrew/Cellar/node/26.1.0/bin/node'), 'brew upgrade node');
|
|
32
|
+
assert.equal(upgradeCommand('20.19.5', '/opt/homebrew/Cellar/node@20/20.19.5/bin/node'), 'brew install node@24');
|
|
33
|
+
assert.equal(upgradeCommand('20.19.5', '/usr/local/bin/node'), 'download Node.js 24 from https://nodejs.org');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('unpatchedNodeRefusal', () => {
|
|
37
|
+
it('is null on a patched Node', () => {
|
|
38
|
+
assert.equal(unpatchedNodeRefusal('24.22.1', '/usr/local/bin/node', {}), null);
|
|
39
|
+
});
|
|
40
|
+
it('tells the agent the version, the fix and the command to give the user', () => {
|
|
41
|
+
const refusal = unpatchedNodeRefusal('20.19.5', '/Users/me/.nvm/versions/node/v20.19.5/bin/node', {}) ?? '';
|
|
42
|
+
assert.match(refusal, /Node\.js v20\.19\.5/);
|
|
43
|
+
assert.match(refusal, /22\.23\.4\+, 24\.22\.1\+ or 26\.12\.1\+/);
|
|
44
|
+
assert.match(refusal, /`nvm install 24 && nvm alias default 24`/);
|
|
45
|
+
assert.match(refusal, /Don't retry/);
|
|
46
|
+
});
|
|
47
|
+
it('steps aside when ONE_ALLOW_UNPATCHED_NODE=1', () => {
|
|
48
|
+
assert.equal(unpatchedNodeRefusal('20.19.5', '/usr/local/bin/node', { ONE_ALLOW_UNPATCHED_NODE: '1' }), null);
|
|
49
|
+
});
|
|
50
|
+
});
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@withone/mcp",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "A Model Context Protocol Server for One",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"mcp": "
|
|
7
|
+
"mcp": "build/index.js"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"build"
|
|
@@ -35,7 +35,10 @@
|
|
|
35
35
|
"license": "MIT",
|
|
36
36
|
"repository": {
|
|
37
37
|
"type": "git",
|
|
38
|
-
"url": "https://github.com/withoneai/mcp"
|
|
38
|
+
"url": "git+https://github.com/withoneai/mcp.git"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": "^22.23.4 || ^24.22.1 || >=26.12.1"
|
|
39
42
|
},
|
|
40
43
|
"mcpName": "ai.withone/mcp"
|
|
41
44
|
}
|