@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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@neus/mcp-server` are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
|
+
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
- **SSOT:** MCP process env is built in `../src/config/mcpConfig.js` (`buildMcpConfig`); Zeus index tags live in `../src/utils/zeusProofIndexTags.cjs`. The container image is built with `docker build -f mcp/Dockerfile` from the protocol repo root.
|
|
8
|
+
- This package is **not yet published** to the public npm registry. A manual publish workflow exists; when a release is published, it will be listed below with a version and date.
|
|
9
|
+
|
|
10
|
+
### Current tree (1.0.0)
|
|
11
|
+
|
|
12
|
+
- **Transport:** Streamable HTTP (default); `MCP_TRANSPORT=stdio` for local CLI. Hosted URL: `https://mcp.neus.network/mcp` (see [MCP docs](https://docs.neus.network/mcp/overview)).
|
|
13
|
+
- **Registry:** `server.json` follows the MCP server registry schema. Its **`version`** field is **manifest / implementation semver** for registry consumers — **not** MCP wire **`protocolVersion`** (see **Runtime** below). **`$schema`** date is the registry JSON schema revision, not the wire revision.
|
|
14
|
+
- **Tools:** `neus_context`, `neus_verifiers_catalog`, `neus_proofs_check`, `neus_verify`, `neus_verify_or_guide`, `neus_proofs_get`, `neus_me`, `neus_agent_link`, `neus_agent_create`.
|
|
15
|
+
- **Proof reads:** `neus_proofs_get` supports optional `tags` and `agentWallet` aligned with the HTTP proof-by-wallet contract; consolidated read path (no separate index/content tools).
|
|
16
|
+
- **Context / copy:** Server `instructions` match public doc session order (context → optional Bearer + `neus_me` → work → `neus_proofs_get`); Bearer framed as optional. Tool descriptions verb-first; `neus_context` includes structured `proofCreation` where applicable.
|
|
17
|
+
- **neus_verify:** `data` optional and defaults to `{}` when verifiers have no `requiredFields` (Zod + API aligned).
|
|
18
|
+
- **Runtime:** Node `>=22.13` (see `package.json` `engines`). MCP wire `protocolVersion` / `mcp-protocol-version`: **`2025-11-25`**. Issues: [github.com/neus/network/issues](https://github.com/neus/network/issues). Implementation is maintained in the NEUS API repository; sources are not vendored into the public `neus/network` tree.
|
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# NEUS MCP (server package)
|
|
2
|
+
|
|
3
|
+
**Integrators:** use the **[hosted MCP endpoint](https://docs.neus.network/mcp/setup)**. Connect once, add an access key when account-aware reuse is needed, and let tools check existing receipts before any browser handoff.
|
|
4
|
+
|
|
5
|
+
**Production URL:** `https://mcp.neus.network/mcp`
|
|
6
|
+
|
|
7
|
+
This directory is the **`@neus/mcp-server`** package (see `package.json`). It requires **Node >= 22.13** (see `package.json`). Most projects connect to the hosted URL above and do not need a local install.
|
|
8
|
+
|
|
9
|
+
**npm:** `@neus/mcp-server` may not yet be published on the public npm registry. Run from this repo (`npm ci` in `mcp/`, then `node server.js` or `npm start`), or use the hosted endpoint.
|
|
10
|
+
|
|
11
|
+
Public guides: **[docs.neus.network/mcp](https://docs.neus.network/mcp/overview)**.
|
|
12
|
+
|
|
13
|
+
**Distribution:** Apache-2.0. The **`server.json`** file beside this README is MCP **installer / discovery manifest** metadata (`version` follows this package; MCP wire revision is negotiated at session initialize). **Documentation** stays on [docs.neus.network/mcp](https://docs.neus.network/mcp/overview). **Issues** use the **[neus/network](https://github.com/neus/network/issues)** tracker. Implementation is maintained **in the NEUS API repository** (not mirrored as a second `server.js` under the public repo tree).
|
|
14
|
+
|
|
15
|
+
## Public Tools And Golden Path
|
|
16
|
+
|
|
17
|
+
By default, **`server.js`** registers **nine** hosted tools. Models should **call neus_context first** each session. The **`server.json`** top-level `description` matches the MCP Registry length limit (≤100 chars) and stays aligned with this README.
|
|
18
|
+
|
|
19
|
+
1. Call **`neus_context`** first every session.
|
|
20
|
+
2. If a Profile access key is configured, call **`neus_me`**.
|
|
21
|
+
3. For agents, call **`neus_agent_link`** before assuming identity or delegation is ready.
|
|
22
|
+
4. Use **`neus_proofs_check`** to check access without creating anything.
|
|
23
|
+
5. If requirements are satisfied, continue without hosted verify.
|
|
24
|
+
6. Use **`neus_proofs_get`** when you need authoritative proof records, tags, delegated reads, or qHashes.
|
|
25
|
+
7. Use **`neus_verify`** only when your Profile personal access key, signing, and wallet identifiers already match NEUS guidance for this verifier path.
|
|
26
|
+
8. Use **`neus_verify_or_guide`** only when proof, profile, payment, provider, wallet, or delegation setup is missing.
|
|
27
|
+
9. Use **`neus_agent_create`** only when agent setup is missing, then confirm with **`neus_agent_link`**.
|
|
28
|
+
|
|
29
|
+
Also use **`neus_verifiers_catalog`** when **`neus_context`** is not enough for JSON schemas. After **`neus_context`**, when a Profile access key is configured on this MCP session, call **`neus_me`** and expect **`status: ok`** before relying on profile-scoped behavior.
|
|
30
|
+
|
|
31
|
+
Inputs are validated with **Zod** at runtime. When your MCP client attaches **`Authorization: Bearer`** plus the **personal access key** minted under Profile → Account, NEUS resolves the signed-in Profile ([auth docs](https://docs.neus.network/mcp/setup)).
|
|
32
|
+
|
|
33
|
+
### Tool list
|
|
34
|
+
|
|
35
|
+
| Tool | Role |
|
|
36
|
+
| --- | --- |
|
|
37
|
+
| `neus_context` | Session home — URLs, CLI, auth mode, `goldenPath`, `jobs`, `verifierSummary`, reminders |
|
|
38
|
+
| `neus_verifiers_catalog` | Full verifier metadata and schemas |
|
|
39
|
+
| `neus_proofs_check` | Eligibility only (no new proofs) |
|
|
40
|
+
| `neus_verify_or_guide` | Fallback orchestration when setup or browser-only consent is missing |
|
|
41
|
+
| `neus_verify` | Create or continue verification when the Profile + signing path is correct |
|
|
42
|
+
| `neus_proofs_get` | Proof records, inventory, tags, delegated reads |
|
|
43
|
+
| `neus_me` | Signed-in Profile (when MCP carries your personal access key) |
|
|
44
|
+
| `neus_agent_create` | Start missing agent identity + delegation setup |
|
|
45
|
+
| `neus_agent_link` | Confirm agent readiness |
|
|
46
|
+
|
|
47
|
+
Deploy-only helpers may appear in extended developer builds; hosted production stays on the nine-tool surface above.
|
|
48
|
+
|
|
49
|
+
## Checks
|
|
50
|
+
|
|
51
|
+
Validate the public packaging contract (manifest ↔ `server.js` framing): **`npm run test:public-contract`**
|
|
52
|
+
|
|
53
|
+
Local MCP smoke (**context + discovery**): **`npm run test:e2e:local`**
|
|
54
|
+
|
|
55
|
+
Hosted MCP smoke (**same against production URL**): **`npm run test:e2e:live`**
|
|
56
|
+
|
|
57
|
+
Agent create/link flow (**local server**): **`npm run test:e2e:agent`**
|
|
58
|
+
|
|
59
|
+
**`neus_context`** regression (URLs from env — see **`test-agent-context.js`**): **`npm run test:agent-context`**
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
Apache-2.0
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
5
|
+
|
|
6
|
+
const PORT = 3105;
|
|
7
|
+
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
|
8
|
+
const CONTROLLER_WALLET = '0x0000000000000000000000000000000000000001';
|
|
9
|
+
|
|
10
|
+
function wait(ms) {
|
|
11
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function waitForHealth(url, timeoutMs = 15000) {
|
|
15
|
+
const startedAt = Date.now();
|
|
16
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
17
|
+
try {
|
|
18
|
+
const res = await fetch(`${url}/health`);
|
|
19
|
+
if (res.ok) return true;
|
|
20
|
+
} catch {
|
|
21
|
+
// Ignore until ready.
|
|
22
|
+
}
|
|
23
|
+
await wait(250);
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseSseJson(raw) {
|
|
29
|
+
const trimmed = String(raw || '').trim();
|
|
30
|
+
if (trimmed.startsWith('{')) {
|
|
31
|
+
return JSON.parse(trimmed);
|
|
32
|
+
}
|
|
33
|
+
const dataLine = String(raw || '')
|
|
34
|
+
.split(/\r?\n/)
|
|
35
|
+
.find((line) => line.startsWith('data: '));
|
|
36
|
+
if (!dataLine) {
|
|
37
|
+
throw new Error(`Unexpected MCP response: ${raw}`);
|
|
38
|
+
}
|
|
39
|
+
return JSON.parse(dataLine.slice('data: '.length));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function initMcpSession(baseUrl) {
|
|
43
|
+
const headers = {
|
|
44
|
+
'content-type': 'application/json',
|
|
45
|
+
'mcp-protocol-version': '2025-11-25',
|
|
46
|
+
accept: 'application/json, text/event-stream',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers,
|
|
52
|
+
body: JSON.stringify({
|
|
53
|
+
jsonrpc: '2.0',
|
|
54
|
+
id: 1,
|
|
55
|
+
method: 'initialize',
|
|
56
|
+
params: {
|
|
57
|
+
protocolVersion: '2025-11-25',
|
|
58
|
+
capabilities: {},
|
|
59
|
+
clientInfo: { name: 'agent-smoke', version: '1.0.0' },
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const raw = await response.text();
|
|
65
|
+
const sessionId = response.headers.get('mcp-session-id');
|
|
66
|
+
if (sessionId) {
|
|
67
|
+
let detail = raw.slice(0, 2000);
|
|
68
|
+
try {
|
|
69
|
+
const j = parseSseJson(raw);
|
|
70
|
+
if (j?.error) detail = JSON.stringify(j.error);
|
|
71
|
+
} catch {
|
|
72
|
+
// keep truncated raw
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`MCP initialize should be stateless and must not return mcp-session-id ${sessionId}: ${detail}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
parseSseJson(raw);
|
|
78
|
+
return { headers };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function listTools(baseUrl, headers, id) {
|
|
82
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers,
|
|
85
|
+
body: JSON.stringify({
|
|
86
|
+
jsonrpc: '2.0',
|
|
87
|
+
id,
|
|
88
|
+
method: 'tools/list',
|
|
89
|
+
params: {},
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const raw = await response.text();
|
|
94
|
+
const payload = parseSseJson(raw);
|
|
95
|
+
return payload?.result?.tools || [];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function callTool(baseUrl, headers, id, name, args) {
|
|
99
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers,
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
jsonrpc: '2.0',
|
|
104
|
+
id,
|
|
105
|
+
method: 'tools/call',
|
|
106
|
+
params: {
|
|
107
|
+
name,
|
|
108
|
+
arguments: args,
|
|
109
|
+
},
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const raw = await response.text();
|
|
114
|
+
const payload = parseSseJson(raw);
|
|
115
|
+
const text = payload?.result?.content?.[0]?.text;
|
|
116
|
+
if (!text) {
|
|
117
|
+
throw new Error(`Missing tool payload for ${name}: ${raw}`);
|
|
118
|
+
}
|
|
119
|
+
return JSON.parse(text);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function main() {
|
|
123
|
+
console.log('\n=== NEUS MCP Agent Local E2E ===');
|
|
124
|
+
console.log(`Spawning MCP on ${BASE_URL}`);
|
|
125
|
+
|
|
126
|
+
const child = spawn(process.execPath, ['server.js'], {
|
|
127
|
+
cwd: new URL('.', import.meta.url),
|
|
128
|
+
env: {
|
|
129
|
+
...process.env,
|
|
130
|
+
MCP_TRANSPORT: 'http',
|
|
131
|
+
HOST: '127.0.0.1',
|
|
132
|
+
PORT: String(PORT),
|
|
133
|
+
},
|
|
134
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
child.stdout.on('data', () => {});
|
|
138
|
+
child.stderr.on('data', () => {});
|
|
139
|
+
|
|
140
|
+
let headers = null;
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const ready = await waitForHealth(BASE_URL);
|
|
144
|
+
if (!ready) {
|
|
145
|
+
throw new Error('MCP server did not become healthy in time');
|
|
146
|
+
}
|
|
147
|
+
console.log(' ✓ Health endpoint');
|
|
148
|
+
|
|
149
|
+
({ headers } = await initMcpSession(BASE_URL));
|
|
150
|
+
console.log(' ✓ MCP stateless initialize');
|
|
151
|
+
|
|
152
|
+
const tools = await listTools(BASE_URL, headers, 99);
|
|
153
|
+
const verifyTool = tools.find((t) => t.name === 'neus_verify');
|
|
154
|
+
const props = Object.keys(verifyTool?.inputSchema?.properties || {});
|
|
155
|
+
if (!props.includes('walletAddress') || !props.includes('verifierIds') || !props.includes('data')) {
|
|
156
|
+
throw new Error(`tools/list must expose neus_verify args; got properties: ${props.join(',')}`);
|
|
157
|
+
}
|
|
158
|
+
console.log(' ✓ tools/list exposes neus_verify parameter schema');
|
|
159
|
+
|
|
160
|
+
const proofsCheckTool = tools.find((t) => t.name === 'neus_proofs_check');
|
|
161
|
+
const proofsCheckProps = Object.keys(proofsCheckTool?.inputSchema?.properties || {});
|
|
162
|
+
if (proofsCheckProps.includes('sponsorGrant')) {
|
|
163
|
+
throw new Error(`tools/list must not expose legacy sponsorGrant; got properties: ${proofsCheckProps.join(',')}`);
|
|
164
|
+
}
|
|
165
|
+
console.log(' legacy sponsorGrant input hidden from tools/list');
|
|
166
|
+
|
|
167
|
+
const createResult = await callTool(BASE_URL, headers, 2, 'neus_agent_create', {
|
|
168
|
+
agentId: 'smoke-agent',
|
|
169
|
+
agentWallet: 'generate',
|
|
170
|
+
controllerWallet: CONTROLLER_WALLET,
|
|
171
|
+
});
|
|
172
|
+
if (createResult?.status !== 'signatures_required') {
|
|
173
|
+
throw new Error(`Unexpected create status: ${JSON.stringify(createResult)}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const generatedWallet = createResult?.agent?.generatedWallet;
|
|
177
|
+
if (!generatedWallet?.privateKey || !generatedWallet?.address) {
|
|
178
|
+
throw new Error(`Missing generated wallet payload: ${JSON.stringify(createResult)}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const derivedAddress = privateKeyToAccount(generatedWallet.privateKey).address.toLowerCase();
|
|
182
|
+
if (derivedAddress !== String(generatedWallet.address).toLowerCase()) {
|
|
183
|
+
throw new Error(`Generated wallet mismatch: ${generatedWallet.address} !== ${derivedAddress}`);
|
|
184
|
+
}
|
|
185
|
+
console.log(' ✓ Generated wallet key/address pair matches');
|
|
186
|
+
|
|
187
|
+
if (!String(createResult.hostedVerifyUrl || '').includes('agentWallet=')) {
|
|
188
|
+
throw new Error(`Missing agentWallet in hostedVerifyUrl: ${createResult.hostedVerifyUrl}`);
|
|
189
|
+
}
|
|
190
|
+
if (!String(createResult.hostedVerifyUrl || '').includes('controllerWallet=')) {
|
|
191
|
+
throw new Error(`Missing controllerWallet in hostedVerifyUrl: ${createResult.hostedVerifyUrl}`);
|
|
192
|
+
}
|
|
193
|
+
console.log(' ✓ Agent create hostedVerifyUrl includes agent + controller context');
|
|
194
|
+
|
|
195
|
+
const linkResult = await callTool(BASE_URL, headers, 3, 'neus_agent_link', {
|
|
196
|
+
agentWallet: createResult.agent.agentWallet,
|
|
197
|
+
principal: CONTROLLER_WALLET,
|
|
198
|
+
});
|
|
199
|
+
if (linkResult?.status !== 'link_required') {
|
|
200
|
+
throw new Error(`Unexpected link status: ${JSON.stringify(linkResult)}`);
|
|
201
|
+
}
|
|
202
|
+
if (linkResult?.nextSteps?.action !== 'open_hosted_verify') {
|
|
203
|
+
throw new Error(`Unexpected next step action: ${JSON.stringify(linkResult)}`);
|
|
204
|
+
}
|
|
205
|
+
if (linkResult?.principal?.toLowerCase() !== CONTROLLER_WALLET.toLowerCase()) {
|
|
206
|
+
throw new Error(`Principal was not preserved: ${JSON.stringify(linkResult)}`);
|
|
207
|
+
}
|
|
208
|
+
console.log(' ✓ Agent link returns browser-first guidance with principal context');
|
|
209
|
+
|
|
210
|
+
console.log('\n--- Summary ---');
|
|
211
|
+
console.log('Passed: 5 Failed: 0');
|
|
212
|
+
} finally {
|
|
213
|
+
child.kill('SIGTERM');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
main().catch((error) => {
|
|
218
|
+
console.error(error);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hosted NEUS MCP smoke (production by default). Set MCP_E2E_BASE_URL to override.
|
|
5
|
+
*
|
|
6
|
+
* Hosted MCP may be stateful or stateless during rollout. If initialize returns
|
|
7
|
+
* mcp-session-id, reuse it and send notifications/initialized before tools/list.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const BASE_URL = String(process.env.MCP_E2E_BASE_URL || 'https://mcp.neus.network').replace(/\/$/, '');
|
|
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 unwrapDataArray(value, label) {
|
|
38
|
+
if (Array.isArray(value)) return value;
|
|
39
|
+
if (value?.success === true && Array.isArray(value.data)) return value.data;
|
|
40
|
+
throw new Error(`${label} must return an array or { success: true, data: array }, got: ${JSON.stringify(value).slice(0, 400)}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function parseSseJson(raw) {
|
|
44
|
+
const trimmed = String(raw || '').trim();
|
|
45
|
+
if (!trimmed) return null;
|
|
46
|
+
if (trimmed.startsWith('{')) {
|
|
47
|
+
return JSON.parse(trimmed);
|
|
48
|
+
}
|
|
49
|
+
const dataLine = String(raw || '')
|
|
50
|
+
.split(/\r?\n/)
|
|
51
|
+
.find((line) => line.startsWith('data: '));
|
|
52
|
+
if (!dataLine) {
|
|
53
|
+
throw new Error(`Unexpected MCP response: ${raw.slice(0, 500)}`);
|
|
54
|
+
}
|
|
55
|
+
return JSON.parse(dataLine.slice('data: '.length));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const COMMON_HEADERS = {
|
|
59
|
+
'content-type': 'application/json',
|
|
60
|
+
'mcp-protocol-version': '2025-11-25',
|
|
61
|
+
accept: 'application/json, text/event-stream'
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
async function post(baseUrl, body, extraHeaders = {}) {
|
|
65
|
+
const response = await fetch(`${baseUrl}/mcp`, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { ...COMMON_HEADERS, ...extraHeaders },
|
|
68
|
+
body: JSON.stringify(body)
|
|
69
|
+
});
|
|
70
|
+
const raw = await response.text();
|
|
71
|
+
return {
|
|
72
|
+
status: response.status,
|
|
73
|
+
payload: parseSseJson(raw),
|
|
74
|
+
sessionId: response.headers.get('mcp-session-id')
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function callTool(baseUrl, headers, id, name, args) {
|
|
79
|
+
const { payload } = await post(baseUrl, {
|
|
80
|
+
jsonrpc: '2.0',
|
|
81
|
+
id,
|
|
82
|
+
method: 'tools/call',
|
|
83
|
+
params: { name, arguments: args }
|
|
84
|
+
}, headers);
|
|
85
|
+
const text = payload?.result?.content?.[0]?.text;
|
|
86
|
+
if (!text) {
|
|
87
|
+
throw new Error(`Missing tool payload for ${name}: ${JSON.stringify(payload).slice(0, 400)}`);
|
|
88
|
+
}
|
|
89
|
+
return JSON.parse(text);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function main() {
|
|
93
|
+
console.log('\n=== NEUS MCP hosted E2E ===');
|
|
94
|
+
console.log(`BASE_URL=${BASE_URL}`);
|
|
95
|
+
|
|
96
|
+
// initialize
|
|
97
|
+
const { status: initStatus, payload: initPayload, sessionId } = await post(BASE_URL, {
|
|
98
|
+
jsonrpc: '2.0',
|
|
99
|
+
id: 1,
|
|
100
|
+
method: 'initialize',
|
|
101
|
+
params: {
|
|
102
|
+
protocolVersion: '2025-11-25',
|
|
103
|
+
capabilities: {},
|
|
104
|
+
clientInfo: { name: 'mcp-live-e2e', version: '1.0.0' }
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
if (initStatus !== 200) {
|
|
108
|
+
throw new Error(`Initialize HTTP ${initStatus}: ${JSON.stringify(initPayload).slice(0, 400)}`);
|
|
109
|
+
}
|
|
110
|
+
if (!initPayload?.result?.protocolVersion) {
|
|
111
|
+
throw new Error(`Initialize missing protocolVersion: ${JSON.stringify(initPayload).slice(0, 400)}`);
|
|
112
|
+
}
|
|
113
|
+
const sessionHeaders = sessionId ? { 'mcp-session-id': sessionId } : {};
|
|
114
|
+
if (sessionId) {
|
|
115
|
+
const { status: initializedStatus } = await post(BASE_URL, {
|
|
116
|
+
jsonrpc: '2.0',
|
|
117
|
+
method: 'notifications/initialized',
|
|
118
|
+
params: {}
|
|
119
|
+
}, sessionHeaders);
|
|
120
|
+
if (initializedStatus !== 202 && initializedStatus !== 200) {
|
|
121
|
+
throw new Error(`notifications/initialized HTTP ${initializedStatus}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
console.log(sessionId ? ' ✓ initialize (stateful streamable HTTP)' : ' ✓ initialize (stateless HTTP)');
|
|
125
|
+
|
|
126
|
+
// tools/list
|
|
127
|
+
const { payload: listPayload } = await post(BASE_URL, {
|
|
128
|
+
jsonrpc: '2.0',
|
|
129
|
+
id: 2,
|
|
130
|
+
method: 'tools/list',
|
|
131
|
+
params: {}
|
|
132
|
+
}, sessionHeaders);
|
|
133
|
+
if (listPayload?.error) {
|
|
134
|
+
throw new Error(`tools/list failed: ${JSON.stringify(listPayload.error).slice(0, 400)}`);
|
|
135
|
+
}
|
|
136
|
+
const tools = listPayload?.result?.tools || [];
|
|
137
|
+
const names = tools.map((t) => t.name);
|
|
138
|
+
if (names.join(',') !== EXPECTED_TOOLS.join(',')) {
|
|
139
|
+
throw new Error(`Unexpected hosted tool list.\nExpected: ${EXPECTED_TOOLS.join(', ')}\nGot: ${names.join(', ')}`);
|
|
140
|
+
}
|
|
141
|
+
console.log(' ✓ tools/list');
|
|
142
|
+
|
|
143
|
+
// neus_context
|
|
144
|
+
const ctx = await callTool(BASE_URL, sessionHeaders, 3, 'neus_context', {});
|
|
145
|
+
const keys = Object.keys(ctx).sort();
|
|
146
|
+
const expectedKeys = [...NEUS_CONTEXT_KEYS].sort();
|
|
147
|
+
if (keys.join(',') !== expectedKeys.join(',')) {
|
|
148
|
+
throw new Error(`Unexpected neus_context shape.\nExpected keys: ${expectedKeys.join(', ')}\nGot: ${keys.join(', ')}`);
|
|
149
|
+
}
|
|
150
|
+
if (!Array.isArray(ctx.goldenPath) || ctx.goldenPath.length === 0) {
|
|
151
|
+
throw new Error('neus_context.goldenPath must be a non-empty array');
|
|
152
|
+
}
|
|
153
|
+
if (!Array.isArray(ctx.verifierSummary)) {
|
|
154
|
+
throw new Error('neus_context.verifierSummary must be an array');
|
|
155
|
+
}
|
|
156
|
+
console.log(' ✓ neus_context contract (ten keys)');
|
|
157
|
+
|
|
158
|
+
// neus_verifiers_catalog
|
|
159
|
+
const cat = unwrapDataArray(await callTool(BASE_URL, sessionHeaders, 4, 'neus_verifiers_catalog', {}), 'neus_verifiers_catalog');
|
|
160
|
+
console.log(` ✓ neus_verifiers_catalog (array, length=${cat.length})`);
|
|
161
|
+
|
|
162
|
+
// neus_proofs_check
|
|
163
|
+
const chk = await callTool(BASE_URL, sessionHeaders, 5, 'neus_proofs_check', {
|
|
164
|
+
wallet: '0x0000000000000000000000000000000000000001',
|
|
165
|
+
verifiers: ['ownership-basic']
|
|
166
|
+
});
|
|
167
|
+
if (typeof chk?.eligible !== 'boolean') {
|
|
168
|
+
throw new Error(`neus_proofs_check missing eligible boolean: ${JSON.stringify(chk).slice(0, 400)}`);
|
|
169
|
+
}
|
|
170
|
+
console.log(' ✓ neus_proofs_check');
|
|
171
|
+
|
|
172
|
+
// neus_verify_or_guide
|
|
173
|
+
const vog = await callTool(BASE_URL, sessionHeaders, 6, 'neus_verify_or_guide', {
|
|
174
|
+
walletAddress: '0x0000000000000000000000000000000000000001',
|
|
175
|
+
verifierIds: ['ownership-basic']
|
|
176
|
+
});
|
|
177
|
+
if (typeof vog?.eligible !== 'boolean') {
|
|
178
|
+
throw new Error(`neus_verify_or_guide missing eligible boolean: ${JSON.stringify(vog).slice(0, 400)}`);
|
|
179
|
+
}
|
|
180
|
+
console.log(' ✓ neus_verify_or_guide');
|
|
181
|
+
|
|
182
|
+
// neus_proofs_get
|
|
183
|
+
const pg = await callTool(BASE_URL, sessionHeaders, 7, 'neus_proofs_get', {
|
|
184
|
+
identifier: '0x0000000000000000000000000000000000000001',
|
|
185
|
+
limit: 5
|
|
186
|
+
});
|
|
187
|
+
if (!pg || typeof pg !== 'object' || !Array.isArray(pg.data?.proofs)) {
|
|
188
|
+
throw new Error(`neus_proofs_get missing data.proofs array: ${JSON.stringify(pg).slice(0, 400)}`);
|
|
189
|
+
}
|
|
190
|
+
console.log(' ✓ neus_proofs_get');
|
|
191
|
+
|
|
192
|
+
// neus_me
|
|
193
|
+
const me = await callTool(BASE_URL, sessionHeaders, 8, 'neus_me', {});
|
|
194
|
+
if (typeof me?.status !== 'string') {
|
|
195
|
+
throw new Error(`neus_me missing status string: ${JSON.stringify(me).slice(0, 400)}`);
|
|
196
|
+
}
|
|
197
|
+
console.log(' ✓ neus_me');
|
|
198
|
+
|
|
199
|
+
// neus_agent_link
|
|
200
|
+
const al = await callTool(BASE_URL, sessionHeaders, 9, 'neus_agent_link', {
|
|
201
|
+
agentWallet: '0x0000000000000000000000000000000000000001'
|
|
202
|
+
});
|
|
203
|
+
if (typeof al?.status !== 'string') {
|
|
204
|
+
throw new Error(`neus_agent_link missing status string: ${JSON.stringify(al).slice(0, 400)}`);
|
|
205
|
+
}
|
|
206
|
+
console.log(' ✓ neus_agent_link');
|
|
207
|
+
|
|
208
|
+
// neus_agent_create
|
|
209
|
+
const ac = await callTool(BASE_URL, sessionHeaders, 10, 'neus_agent_create', {
|
|
210
|
+
agentId: 'e2e-test-agent',
|
|
211
|
+
agentWallet: 'generate'
|
|
212
|
+
});
|
|
213
|
+
if (typeof ac?.status !== 'string' && typeof ac?.error !== 'string') {
|
|
214
|
+
throw new Error(`neus_agent_create missing status or error: ${JSON.stringify(ac).slice(0, 400)}`);
|
|
215
|
+
}
|
|
216
|
+
console.log(' ✓ neus_agent_create');
|
|
217
|
+
|
|
218
|
+
// neus_verify (preparation step)
|
|
219
|
+
const v = await callTool(BASE_URL, sessionHeaders, 11, 'neus_verify', {
|
|
220
|
+
walletAddress: '0x0000000000000000000000000000000000000001',
|
|
221
|
+
verifierIds: ['ownership-basic']
|
|
222
|
+
});
|
|
223
|
+
if (typeof v?.status !== 'string' && typeof v?.error !== 'string') {
|
|
224
|
+
throw new Error(`neus_verify missing status or error: ${JSON.stringify(v).slice(0, 400)}`);
|
|
225
|
+
}
|
|
226
|
+
console.log(' ✓ neus_verify');
|
|
227
|
+
|
|
228
|
+
console.log('\n--- Summary ---');
|
|
229
|
+
console.log('Passed');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
main().catch((err) => {
|
|
233
|
+
console.error(err);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
});
|