@projectsolo/solo-mission-mcp 0.23.2 → 0.23.3
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/.github/workflows/lint.yml +42 -0
- package/.github/workflows/release.yml +20 -8
- package/.github/workflows/test.yml +37 -0
- package/DEVELOPER_README.md +3 -1
- package/README.md +3 -1
- package/dist/index.js +26 -1397
- package/dist/server-U6ZDX3NA.js +1403 -0
- package/package.json +2 -2
- package/src/index.ts +14 -86
- package/src/nodeVersion.test.ts +67 -0
- package/src/nodeVersion.ts +49 -0
- package/src/server.ts +92 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@projectsolo/solo-mission-mcp",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.3",
|
|
4
4
|
"description": "MCP server for SoloMission Platform — lets AI agents create missions, browse humans, and chat.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -30,6 +30,6 @@
|
|
|
30
30
|
"vitest": "^4.1.11"
|
|
31
31
|
},
|
|
32
32
|
"engines": {
|
|
33
|
-
"node": ">=
|
|
33
|
+
"node": "^22.23.4 || ^24.22.1 || >=26.12.1"
|
|
34
34
|
}
|
|
35
35
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,92 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* Entry point. Refuses to run on a Node.js that still trusts the eMudhra CAs compromised in
|
|
4
|
+
* October 2026 (see nodeVersion.ts), and only then loads the server. There is deliberately no
|
|
5
|
+
* override.
|
|
6
|
+
*
|
|
7
|
+
* The server is imported dynamically so that nothing else is evaluated before the check. A static
|
|
8
|
+
* import would be hoisted above it, and the bundler moves shared modules such as config.ts (which
|
|
9
|
+
* reads SOLO_AGENT_KEY as it loads) into chunks that run before this file's own code. Keep
|
|
10
|
+
* nodeVersion.js the only static import here.
|
|
5
11
|
*/
|
|
12
|
+
import { isPatchedNode, unpatchedNodeMessage } from './nodeVersion.js';
|
|
6
13
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
ListToolsRequestSchema,
|
|
12
|
-
} from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
-
|
|
14
|
-
// Tool definitions
|
|
15
|
-
import { missionTools, handleMissionTool } from './tools/missions.js';
|
|
16
|
-
import { humanTools, handleHumanTool } from './tools/humans.js';
|
|
17
|
-
import { conversationTools, handleConversationTool } from './tools/conversations.js';
|
|
18
|
-
import { realtimeTools, handleRealtimeTool } from './tools/realtime.js';
|
|
19
|
-
import { agentTools, handleAgentTool } from './tools/agent.js';
|
|
20
|
-
import { trackTools, handleTrackTool } from './tools/tracks.js';
|
|
21
|
-
import { solanaTools, SOLANA_TOOL_NAMES, handleSolanaTool } from './tools/solana.js';
|
|
22
|
-
|
|
23
|
-
const ALL_TOOLS = [...agentTools, ...missionTools, ...humanTools, ...conversationTools, ...realtimeTools, ...trackTools, ...solanaTools];
|
|
24
|
-
|
|
25
|
-
const AGENT_TOOL_NAMES = new Set(agentTools.map((t) => t.name));
|
|
26
|
-
const MISSION_TOOL_NAMES = new Set(missionTools.map((t) => t.name));
|
|
27
|
-
const HUMAN_TOOL_NAMES = new Set(humanTools.map((t) => t.name));
|
|
28
|
-
const CONVERSATION_TOOL_NAMES = new Set(conversationTools.map((t) => t.name));
|
|
29
|
-
const REALTIME_TOOL_NAMES = new Set(realtimeTools.map((t) => t.name));
|
|
30
|
-
const TRACK_TOOL_NAMES = new Set(trackTools.map((t) => t.name));
|
|
31
|
-
|
|
32
|
-
const server = new Server(
|
|
33
|
-
{ name: 'solo-mission-mcp', version: '0.23.1' },
|
|
34
|
-
{ capabilities: { tools: {} } }
|
|
35
|
-
);
|
|
36
|
-
|
|
37
|
-
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
38
|
-
tools: ALL_TOOLS,
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
42
|
-
const { name, arguments: args = {} } = request.params;
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
let result: unknown;
|
|
46
|
-
|
|
47
|
-
if (AGENT_TOOL_NAMES.has(name)) {
|
|
48
|
-
result = await handleAgentTool(name, args as Record<string, any>);
|
|
49
|
-
} else if (MISSION_TOOL_NAMES.has(name)) {
|
|
50
|
-
result = await handleMissionTool(name, args as Record<string, any>);
|
|
51
|
-
} else if (HUMAN_TOOL_NAMES.has(name)) {
|
|
52
|
-
result = await handleHumanTool(name, args as Record<string, any>);
|
|
53
|
-
} else if (CONVERSATION_TOOL_NAMES.has(name)) {
|
|
54
|
-
result = await handleConversationTool(name, args as Record<string, any>);
|
|
55
|
-
} else if (REALTIME_TOOL_NAMES.has(name)) {
|
|
56
|
-
result = await handleRealtimeTool(name, args as Record<string, any>);
|
|
57
|
-
} else if (TRACK_TOOL_NAMES.has(name)) {
|
|
58
|
-
result = await handleTrackTool(name, args as Record<string, any>);
|
|
59
|
-
} else if (SOLANA_TOOL_NAMES.has(name)) {
|
|
60
|
-
result = await handleSolanaTool(name, args as Record<string, any>);
|
|
61
|
-
} else {
|
|
62
|
-
return {
|
|
63
|
-
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
64
|
-
isError: true,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
return {
|
|
69
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
70
|
-
};
|
|
71
|
-
} catch (error: any) {
|
|
72
|
-
const message = error?.response?.data
|
|
73
|
-
? JSON.stringify(error.response.data)
|
|
74
|
-
: (error?.message ?? String(error));
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
78
|
-
isError: true,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
async function main() {
|
|
84
|
-
const transport = new StdioServerTransport();
|
|
85
|
-
await server.connect(transport);
|
|
86
|
-
// Server runs until process exits
|
|
14
|
+
if (!isPatchedNode(process.versions.node)) {
|
|
15
|
+
// stderr only: stdout is the MCP protocol channel.
|
|
16
|
+
process.stderr.write(`${unpatchedNodeMessage(process.versions.node)}\n`);
|
|
17
|
+
process.exit(1);
|
|
87
18
|
}
|
|
88
19
|
|
|
89
|
-
|
|
90
|
-
console.error('Failed to start solo-mission-mcp:', err);
|
|
91
|
-
process.exit(1);
|
|
92
|
-
});
|
|
20
|
+
await import('./server.js');
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The server must refuse every Node.js that still trusts the eMudhra CAs compromised in October 2026.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
5
|
+
import { describe, it, expect } from 'vitest'
|
|
6
|
+
import { isPatchedNode, unpatchedNodeMessage } from './nodeVersion'
|
|
7
|
+
|
|
8
|
+
describe('isPatchedNode', () => {
|
|
9
|
+
it.each([
|
|
10
|
+
['20.19.0', false],
|
|
11
|
+
['22.23.3', false],
|
|
12
|
+
['22.23.4', true],
|
|
13
|
+
['23.11.1', false],
|
|
14
|
+
['24.22.0', false],
|
|
15
|
+
['24.22.1', true],
|
|
16
|
+
['25.9.0', false],
|
|
17
|
+
['26.12.0', false],
|
|
18
|
+
['26.12.1', true],
|
|
19
|
+
['26.13.0', true],
|
|
20
|
+
['27.0.0', true],
|
|
21
|
+
])('%s -> %s', (version, patched) => {
|
|
22
|
+
expect(isPatchedNode(version)).toBe(patched)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('compares the minor before the patch', () => {
|
|
26
|
+
expect(isPatchedNode('22.22.9')).toBe(false)
|
|
27
|
+
expect(isPatchedNode('22.24.0')).toBe(true)
|
|
28
|
+
expect(isPatchedNode('24.21.9')).toBe(false)
|
|
29
|
+
expect(isPatchedNode('24.23.0')).toBe(true)
|
|
30
|
+
expect(isPatchedNode('26.11.9')).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('refuses old and end-of-life majors at any version', () => {
|
|
34
|
+
for (const v of ['18.20.8', '21.7.3', '23.99.99', '25.99.99']) expect(isPatchedNode(v)).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('treats a prerelease of a fixed version as unpatched', () => {
|
|
38
|
+
expect(isPatchedNode('22.23.4-pre')).toBe(false)
|
|
39
|
+
expect(isPatchedNode('22.23.5-pre')).toBe(true)
|
|
40
|
+
expect(isPatchedNode('27.0.0-alpha.0.0.0')).toBe(true)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('fails closed on anything it cannot parse', () => {
|
|
44
|
+
for (const v of ['', '22', '22.23', '22.23.4.1', 'abc', ' 22.23.4']) expect(isPatchedNode(v)).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('unpatchedNodeMessage', () => {
|
|
49
|
+
it('names the running version and the fixed releases', () => {
|
|
50
|
+
expect(unpatchedNodeMessage('20.19.0')).toBe(
|
|
51
|
+
'solo-mission-mcp: Node.js v20.19.0 trusts certificate authorities compromised in October 2026 (eMudhra), ' +
|
|
52
|
+
'so a network attacker could read or change your SOLO traffic, including your agent key. ' +
|
|
53
|
+
'Upgrade to Node.js 22.23.4+, 24.22.1+ or 26.12.1+ and restart.',
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('index.ts', () => {
|
|
59
|
+
// A static import is evaluated before the check runs, so the entry may statically import only
|
|
60
|
+
// the side-effect-free check and must load the server after it.
|
|
61
|
+
it('statically imports nothing but the version check', () => {
|
|
62
|
+
const src = readFileSync(new URL('./index.ts', import.meta.url), 'utf8')
|
|
63
|
+
const staticImports = [...src.matchAll(/^import\s[^'"]*['"]([^'"]+)['"]/gm)].map((m) => m[1])
|
|
64
|
+
expect(staticImports).toEqual(['./nodeVersion.js'])
|
|
65
|
+
expect(src.indexOf("await import('./server.js')")).toBeGreaterThan(src.indexOf('process.exit(1)'))
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which Node.js releases this server will run on.
|
|
3
|
+
*
|
|
4
|
+
* In October 2026 the private keys of eight eMudhra intermediate CAs turned out to be recoverable
|
|
5
|
+
* from their public certificates, so anyone can forge a certificate for any domain, the mission
|
|
6
|
+
* API's included. Node.js checks TLS against its own bundled root list and does no revocation
|
|
7
|
+
* checking, so a Node that still bundles those roots accepts the forgeries. The security releases
|
|
8
|
+
* of 2026-10-20/21 removed them: 22.23.4, 24.22.1 and 26.12.1. Node 20, 21, 23 and 25 are
|
|
9
|
+
* end-of-life and will never get the fix.
|
|
10
|
+
*
|
|
11
|
+
* `index.ts` applies this at startup. This module has no side effects so it can be tested.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The first fixed [minor, patch] for each supported major. */
|
|
15
|
+
const FIXED_IN: Readonly<Record<number, readonly [number, number]>> = {
|
|
16
|
+
22: [23, 4],
|
|
17
|
+
24: [22, 1],
|
|
18
|
+
26: [12, 1],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** Every major from this one on was cut after the fix. */
|
|
22
|
+
const FIRST_FIXED_MAJOR = 27;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether `version` (in `process.versions.node` form, e.g. "22.23.4") is a patched release.
|
|
26
|
+
* Anything it can't parse counts as unpatched, and so does a prerelease of a fixed version
|
|
27
|
+
* ("22.23.4-pre" predates 22.23.4).
|
|
28
|
+
*/
|
|
29
|
+
export function isPatchedNode(version: string): boolean {
|
|
30
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(-.+)?$/.exec(version);
|
|
31
|
+
if (!m) return false;
|
|
32
|
+
const major = Number(m[1]);
|
|
33
|
+
const minor = Number(m[2]);
|
|
34
|
+
const patch = Number(m[3]);
|
|
35
|
+
if (major >= FIRST_FIXED_MAJOR) return true;
|
|
36
|
+
const fixed = FIXED_IN[major];
|
|
37
|
+
if (!fixed) return false;
|
|
38
|
+
if (minor !== fixed[0]) return minor > fixed[0];
|
|
39
|
+
if (patch !== fixed[1]) return patch > fixed[1];
|
|
40
|
+
return m[4] === undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function unpatchedNodeMessage(version: string): string {
|
|
44
|
+
return (
|
|
45
|
+
`solo-mission-mcp: Node.js v${version} trusts certificate authorities compromised in October 2026 (eMudhra), ` +
|
|
46
|
+
'so a network attacker could read or change your SOLO traffic, including your agent key. ' +
|
|
47
|
+
'Upgrade to Node.js 22.23.4+, 24.22.1+ or 26.12.1+ and restart.'
|
|
48
|
+
);
|
|
49
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Solo Mission MCP Server
|
|
3
|
+
* Exposes MCP tools for AI agents to interact with the Solo Mission Platform.
|
|
4
|
+
* Loaded by index.ts only after the Node.js version check passes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
8
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
9
|
+
import {
|
|
10
|
+
CallToolRequestSchema,
|
|
11
|
+
ListToolsRequestSchema,
|
|
12
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
+
|
|
14
|
+
// Tool definitions
|
|
15
|
+
import { missionTools, handleMissionTool } from './tools/missions.js';
|
|
16
|
+
import { humanTools, handleHumanTool } from './tools/humans.js';
|
|
17
|
+
import { conversationTools, handleConversationTool } from './tools/conversations.js';
|
|
18
|
+
import { realtimeTools, handleRealtimeTool } from './tools/realtime.js';
|
|
19
|
+
import { agentTools, handleAgentTool } from './tools/agent.js';
|
|
20
|
+
import { trackTools, handleTrackTool } from './tools/tracks.js';
|
|
21
|
+
import { solanaTools, SOLANA_TOOL_NAMES, handleSolanaTool } from './tools/solana.js';
|
|
22
|
+
|
|
23
|
+
const ALL_TOOLS = [...agentTools, ...missionTools, ...humanTools, ...conversationTools, ...realtimeTools, ...trackTools, ...solanaTools];
|
|
24
|
+
|
|
25
|
+
const AGENT_TOOL_NAMES = new Set(agentTools.map((t) => t.name));
|
|
26
|
+
const MISSION_TOOL_NAMES = new Set(missionTools.map((t) => t.name));
|
|
27
|
+
const HUMAN_TOOL_NAMES = new Set(humanTools.map((t) => t.name));
|
|
28
|
+
const CONVERSATION_TOOL_NAMES = new Set(conversationTools.map((t) => t.name));
|
|
29
|
+
const REALTIME_TOOL_NAMES = new Set(realtimeTools.map((t) => t.name));
|
|
30
|
+
const TRACK_TOOL_NAMES = new Set(trackTools.map((t) => t.name));
|
|
31
|
+
|
|
32
|
+
const server = new Server(
|
|
33
|
+
{ name: 'solo-mission-mcp', version: '0.23.3' },
|
|
34
|
+
{ capabilities: { tools: {} } }
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
38
|
+
tools: ALL_TOOLS,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
42
|
+
const { name, arguments: args = {} } = request.params;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
let result: unknown;
|
|
46
|
+
|
|
47
|
+
if (AGENT_TOOL_NAMES.has(name)) {
|
|
48
|
+
result = await handleAgentTool(name, args as Record<string, any>);
|
|
49
|
+
} else if (MISSION_TOOL_NAMES.has(name)) {
|
|
50
|
+
result = await handleMissionTool(name, args as Record<string, any>);
|
|
51
|
+
} else if (HUMAN_TOOL_NAMES.has(name)) {
|
|
52
|
+
result = await handleHumanTool(name, args as Record<string, any>);
|
|
53
|
+
} else if (CONVERSATION_TOOL_NAMES.has(name)) {
|
|
54
|
+
result = await handleConversationTool(name, args as Record<string, any>);
|
|
55
|
+
} else if (REALTIME_TOOL_NAMES.has(name)) {
|
|
56
|
+
result = await handleRealtimeTool(name, args as Record<string, any>);
|
|
57
|
+
} else if (TRACK_TOOL_NAMES.has(name)) {
|
|
58
|
+
result = await handleTrackTool(name, args as Record<string, any>);
|
|
59
|
+
} else if (SOLANA_TOOL_NAMES.has(name)) {
|
|
60
|
+
result = await handleSolanaTool(name, args as Record<string, any>);
|
|
61
|
+
} else {
|
|
62
|
+
return {
|
|
63
|
+
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
|
|
64
|
+
isError: true,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
70
|
+
};
|
|
71
|
+
} catch (error: any) {
|
|
72
|
+
const message = error?.response?.data
|
|
73
|
+
? JSON.stringify(error.response.data)
|
|
74
|
+
: (error?.message ?? String(error));
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: 'text', text: `Error: ${message}` }],
|
|
78
|
+
isError: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
async function main() {
|
|
84
|
+
const transport = new StdioServerTransport();
|
|
85
|
+
await server.connect(transport);
|
|
86
|
+
// Server runs until process exits
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
main().catch((err) => {
|
|
90
|
+
console.error('Failed to start solo-mission-mcp:', err);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
});
|