@neus/mcp-server 1.0.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 +18 -0
- package/README.md +63 -0
- package/e2e-mcp-agent-local.test.js +220 -0
- package/e2e-mcp-live.test.js +235 -0
- package/e2e-mcp-local.test.js +263 -0
- package/package.json +67 -0
- package/server.js +4420 -0
- package/server.json +134 -0
- package/test-agent-context.js +103 -0
- package/test-public-mcp-contract.js +195 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Local NEUS MCP smoke: health, discovery, ten-key neus_context contract.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
const PORT = 3110;
|
|
10
|
+
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
11
|
+
|
|
12
|
+
const EXPECTED_TOOLS = [
|
|
13
|
+
'neus_context',
|
|
14
|
+
'neus_verifiers_catalog',
|
|
15
|
+
'neus_proofs_check',
|
|
16
|
+
'neus_verify',
|
|
17
|
+
'neus_verify_or_guide',
|
|
18
|
+
'neus_proofs_get',
|
|
19
|
+
'neus_me',
|
|
20
|
+
'neus_agent_link',
|
|
21
|
+
'neus_agent_create'
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const NEUS_CONTEXT_KEYS = [
|
|
25
|
+
'product',
|
|
26
|
+
'setup',
|
|
27
|
+
'mode',
|
|
28
|
+
'goldenPath',
|
|
29
|
+
'jobs',
|
|
30
|
+
'tools',
|
|
31
|
+
'proofModel',
|
|
32
|
+
'agentModel',
|
|
33
|
+
'safetyRules',
|
|
34
|
+
'verifierSummary'
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function wait(ms) {
|
|
38
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function waitForHealth(url, timeoutMs = 20000) {
|
|
42
|
+
const startedAt = Date.now();
|
|
43
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
44
|
+
try {
|
|
45
|
+
const res = await fetch(`${url}/health`);
|
|
46
|
+
if (res.ok) return true;
|
|
47
|
+
} catch {
|
|
48
|
+
// ignore
|
|
49
|
+
}
|
|
50
|
+
await wait(250);
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseSseJson(raw) {
|
|
56
|
+
const trimmed = String(raw || '').trim();
|
|
57
|
+
if (trimmed.startsWith('{')) {
|
|
58
|
+
return JSON.parse(trimmed);
|
|
59
|
+
}
|
|
60
|
+
const dataLine = String(raw || '')
|
|
61
|
+
.split(/\r?\n/)
|
|
62
|
+
.find((line) => line.startsWith('data: '));
|
|
63
|
+
if (!dataLine) {
|
|
64
|
+
throw new Error(`Unexpected MCP response: ${raw}`);
|
|
65
|
+
}
|
|
66
|
+
return JSON.parse(dataLine.slice('data: '.length));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function initMcpSession(baseUrl) {
|
|
70
|
+
const headers = {
|
|
71
|
+
'content-type': 'application/json',
|
|
72
|
+
'mcp-protocol-version': '2025-11-25',
|
|
73
|
+
accept: 'application/json, text/event-stream'
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers,
|
|
79
|
+
body: JSON.stringify({
|
|
80
|
+
jsonrpc: '2.0',
|
|
81
|
+
id: 1,
|
|
82
|
+
method: 'initialize',
|
|
83
|
+
params: {
|
|
84
|
+
protocolVersion: '2025-11-25',
|
|
85
|
+
capabilities: {},
|
|
86
|
+
clientInfo: { name: 'mcp-local-e2e', version: '1.0.0' }
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const raw = await response.text();
|
|
92
|
+
const sessionId = response.headers.get('mcp-session-id');
|
|
93
|
+
if (sessionId) {
|
|
94
|
+
throw new Error(`MCP initialize should be stateless and must not return mcp-session-id: ${sessionId}`);
|
|
95
|
+
}
|
|
96
|
+
parseSseJson(raw);
|
|
97
|
+
return { headers };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function listTools(baseUrl, headers, id) {
|
|
101
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers,
|
|
104
|
+
body: JSON.stringify({
|
|
105
|
+
jsonrpc: '2.0',
|
|
106
|
+
id,
|
|
107
|
+
method: 'tools/list',
|
|
108
|
+
params: {}
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const raw = await response.text();
|
|
113
|
+
const payload = parseSseJson(raw);
|
|
114
|
+
return payload?.result?.tools || [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function callTool(baseUrl, headers, id, name, args) {
|
|
118
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
119
|
+
method: 'POST',
|
|
120
|
+
headers,
|
|
121
|
+
body: JSON.stringify({
|
|
122
|
+
jsonrpc: '2.0',
|
|
123
|
+
id,
|
|
124
|
+
method: 'tools/call',
|
|
125
|
+
params: {
|
|
126
|
+
name,
|
|
127
|
+
arguments: args
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const raw = await response.text();
|
|
133
|
+
const payload = parseSseJson(raw);
|
|
134
|
+
const text = payload?.result?.content?.[0]?.text;
|
|
135
|
+
if (!text) {
|
|
136
|
+
throw new Error(`Missing tool payload for ${name}: ${raw}`);
|
|
137
|
+
}
|
|
138
|
+
return JSON.parse(text);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function main() {
|
|
142
|
+
console.log('\n=== NEUS MCP local E2E ===');
|
|
143
|
+
console.log(`Spawning MCP on ${BASE_URL}`);
|
|
144
|
+
|
|
145
|
+
const child = spawn(process.execPath, ['server.js'], {
|
|
146
|
+
cwd: new URL('.', import.meta.url),
|
|
147
|
+
env: {
|
|
148
|
+
...process.env,
|
|
149
|
+
MCP_TRANSPORT: 'http',
|
|
150
|
+
HOST: '127.0.0.1',
|
|
151
|
+
PORT: String(PORT)
|
|
152
|
+
},
|
|
153
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
child.stdout.on('data', () => {});
|
|
157
|
+
child.stderr.on('data', () => {});
|
|
158
|
+
|
|
159
|
+
let headers = null;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const ready = await waitForHealth(BASE_URL);
|
|
163
|
+
if (!ready) throw new Error('MCP server did not become healthy in time');
|
|
164
|
+
console.log(' ✓ Health endpoint');
|
|
165
|
+
|
|
166
|
+
({ headers } = await initMcpSession(BASE_URL));
|
|
167
|
+
console.log(' ✓ MCP stateless initialize');
|
|
168
|
+
|
|
169
|
+
const tools = await listTools(BASE_URL, headers, 10);
|
|
170
|
+
const names = tools.map((t) => t.name);
|
|
171
|
+
if (names.join(',') !== EXPECTED_TOOLS.join(',')) {
|
|
172
|
+
throw new Error(`Unexpected public tool list.\nExpected: ${EXPECTED_TOOLS.join(', ')}\nGot: ${names.join(', ')}`);
|
|
173
|
+
}
|
|
174
|
+
console.log(' ✓ tools/list matches nine-tool public surface');
|
|
175
|
+
|
|
176
|
+
const ctx = await callTool(BASE_URL, headers, 11, 'neus_context', {});
|
|
177
|
+
const keys = Object.keys(ctx).sort();
|
|
178
|
+
const expectedKeys = [...NEUS_CONTEXT_KEYS].sort();
|
|
179
|
+
if (keys.join(',') !== expectedKeys.join(',')) {
|
|
180
|
+
throw new Error(`Unexpected neus_context shape.\nExpected keys: ${expectedKeys.join(', ')}\nGot: ${keys.join(', ')}`);
|
|
181
|
+
}
|
|
182
|
+
if (!Array.isArray(ctx.goldenPath) || ctx.goldenPath.length === 0) {
|
|
183
|
+
throw new Error('neus_context.goldenPath must be a non-empty array');
|
|
184
|
+
}
|
|
185
|
+
if (!Array.isArray(ctx.verifierSummary)) {
|
|
186
|
+
throw new Error('neus_context.verifierSummary must be an array');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const cat = await callTool(BASE_URL, headers, 11, 'neus_verifiers_catalog', {});
|
|
190
|
+
if (!Array.isArray(cat)) {
|
|
191
|
+
throw new Error(`neus_verifiers_catalog must return an array, got: ${JSON.stringify(cat).slice(0, 400)}`);
|
|
192
|
+
}
|
|
193
|
+
console.log(` ✓ neus_verifiers_catalog (array, length=${cat.length})`);
|
|
194
|
+
|
|
195
|
+
const chk = await callTool(BASE_URL, headers, 12, 'neus_proofs_check', {
|
|
196
|
+
wallet: '0x0000000000000000000000000000000000000001',
|
|
197
|
+
verifiers: ['ownership-basic']
|
|
198
|
+
});
|
|
199
|
+
if (typeof chk?.eligible !== 'boolean') {
|
|
200
|
+
throw new Error(`neus_proofs_check missing eligible boolean: ${JSON.stringify(chk).slice(0, 400)}`);
|
|
201
|
+
}
|
|
202
|
+
console.log(' ✓ neus_proofs_check');
|
|
203
|
+
|
|
204
|
+
const vog = await callTool(BASE_URL, headers, 13, 'neus_verify_or_guide', {
|
|
205
|
+
walletAddress: '0x0000000000000000000000000000000000000001',
|
|
206
|
+
verifierIds: ['ownership-basic']
|
|
207
|
+
});
|
|
208
|
+
if (typeof vog?.eligible !== 'boolean') {
|
|
209
|
+
throw new Error(`neus_verify_or_guide missing eligible boolean: ${JSON.stringify(vog).slice(0, 400)}`);
|
|
210
|
+
}
|
|
211
|
+
console.log(' ✓ neus_verify_or_guide');
|
|
212
|
+
|
|
213
|
+
const pg = await callTool(BASE_URL, headers, 14, 'neus_proofs_get', {
|
|
214
|
+
identifier: '0x0000000000000000000000000000000000000001',
|
|
215
|
+
limit: 5
|
|
216
|
+
});
|
|
217
|
+
if (!pg || typeof pg !== 'object' || !Array.isArray(pg.data?.proofs)) {
|
|
218
|
+
throw new Error(`neus_proofs_get missing data.proofs array: ${JSON.stringify(pg).slice(0, 400)}`);
|
|
219
|
+
}
|
|
220
|
+
console.log(' ✓ neus_proofs_get');
|
|
221
|
+
|
|
222
|
+
const me = await callTool(BASE_URL, headers, 15, 'neus_me', {});
|
|
223
|
+
if (typeof me?.status !== 'string') {
|
|
224
|
+
throw new Error(`neus_me missing status string: ${JSON.stringify(me).slice(0, 400)}`);
|
|
225
|
+
}
|
|
226
|
+
console.log(' ✓ neus_me');
|
|
227
|
+
|
|
228
|
+
const al = await callTool(BASE_URL, headers, 16, 'neus_agent_link', {
|
|
229
|
+
agentWallet: '0x0000000000000000000000000000000000000001'
|
|
230
|
+
});
|
|
231
|
+
if (typeof al?.status !== 'string') {
|
|
232
|
+
throw new Error(`neus_agent_link missing status string: ${JSON.stringify(al).slice(0, 400)}`);
|
|
233
|
+
}
|
|
234
|
+
console.log(' ✓ neus_agent_link');
|
|
235
|
+
|
|
236
|
+
const ac = await callTool(BASE_URL, headers, 17, 'neus_agent_create', {
|
|
237
|
+
agentId: 'e2e-test-agent',
|
|
238
|
+
agentWallet: 'generate'
|
|
239
|
+
});
|
|
240
|
+
if (typeof ac?.status !== 'string' && typeof ac?.error !== 'string') {
|
|
241
|
+
throw new Error(`neus_agent_create missing status or error: ${JSON.stringify(ac).slice(0, 400)}`);
|
|
242
|
+
}
|
|
243
|
+
console.log(' ✓ neus_agent_create');
|
|
244
|
+
|
|
245
|
+
const v = await callTool(BASE_URL, headers, 18, 'neus_verify', {
|
|
246
|
+
walletAddress: '0x0000000000000000000000000000000000000001',
|
|
247
|
+
verifierIds: ['ownership-basic']
|
|
248
|
+
});
|
|
249
|
+
if (typeof v?.status !== 'string' && typeof v?.error !== 'string') {
|
|
250
|
+
throw new Error(`neus_verify missing status or error: ${JSON.stringify(v).slice(0, 400)}`);
|
|
251
|
+
}
|
|
252
|
+
console.log(' ✓ neus_verify');
|
|
253
|
+
console.log('\n--- Summary ---');
|
|
254
|
+
console.log('Passed');
|
|
255
|
+
} finally {
|
|
256
|
+
child.kill('SIGTERM');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
main().catch((error) => {
|
|
261
|
+
console.error(error);
|
|
262
|
+
process.exit(1);
|
|
263
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@neus/mcp-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"mcpName": "io.github.neus/neus-mcp",
|
|
5
|
+
"description": "NEUS MCP over HTTP: verifiers, proof checks, guided verify, reads, agent identity/delegation.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "server.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"neus-mcp": "server.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "node server.js",
|
|
13
|
+
"dev": "node --watch server.js",
|
|
14
|
+
"test:e2e:local": "node e2e-mcp-local.test.js",
|
|
15
|
+
"test:e2e:live": "node e2e-mcp-live.test.js",
|
|
16
|
+
"test:e2e:agent": "node e2e-mcp-agent-local.test.js",
|
|
17
|
+
"test:agent-context": "node test-agent-context.js",
|
|
18
|
+
"test:public-contract": "node test-public-mcp-contract.js"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=22.13.0"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@azure/monitor-opentelemetry": "^1.11.0",
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
26
|
+
"@opentelemetry/api": "^1.9.0",
|
|
27
|
+
"@opentelemetry/resources": "^2.7.1",
|
|
28
|
+
"viem": "^2.47.6",
|
|
29
|
+
"zod": "^4.3.6"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"neus",
|
|
33
|
+
"mcp",
|
|
34
|
+
"model-context-protocol",
|
|
35
|
+
"verification",
|
|
36
|
+
"proofs",
|
|
37
|
+
"agents",
|
|
38
|
+
"gate-check",
|
|
39
|
+
"ai-agents",
|
|
40
|
+
"identity",
|
|
41
|
+
"web3",
|
|
42
|
+
"wallet-verification",
|
|
43
|
+
"sybil-resistance"
|
|
44
|
+
],
|
|
45
|
+
"license": "Apache-2.0",
|
|
46
|
+
"author": "NEUS Network <info@neus.network>",
|
|
47
|
+
"homepage": "https://neus.network",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/neus/network/issues"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/neus/protocol.git",
|
|
54
|
+
"directory": "mcp"
|
|
55
|
+
},
|
|
56
|
+
"files": [
|
|
57
|
+
"server.js",
|
|
58
|
+
"README.md",
|
|
59
|
+
"server.json",
|
|
60
|
+
"CHANGELOG.md",
|
|
61
|
+
"e2e-mcp-local.test.js",
|
|
62
|
+
"e2e-mcp-live.test.js",
|
|
63
|
+
"e2e-mcp-agent-local.test.js",
|
|
64
|
+
"test-agent-context.js",
|
|
65
|
+
"test-public-mcp-contract.js"
|
|
66
|
+
]
|
|
67
|
+
}
|