@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/server.js ADDED
@@ -0,0 +1,4420 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * NEUS MCP Server
4
+ *
5
+ * Model Context Protocol server for NEUS verification and proof workflows.
6
+ * Supports both stdio (local) and HTTP (production) transports.
7
+ * Implements MCP Elicitation for human-in-the-loop agent workflows.
8
+ *
9
+ * @license Apache-2.0
10
+ */
11
+
12
+ import { initTelemetry } from '../src/utils/telemetry.js';
13
+ initTelemetry();
14
+
15
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
16
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
17
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
18
+ import { createServer } from 'node:http';
19
+ import { createRequire } from 'node:module';
20
+ import { fileURLToPath } from 'node:url';
21
+ import { createCipheriv, createDecipheriv, randomBytes, randomUUID, scryptSync, timingSafeEqual } from 'node:crypto';
22
+ import { privateKeyToAccount } from 'viem/accounts';
23
+ import { z } from 'zod';
24
+ import vm from 'node:vm';
25
+ import { buildMcpConfig } from '../src/config/mcpConfig.js';
26
+
27
+ const mcp = buildMcpConfig(process.env);
28
+
29
+ const zeusMcpFileUrl = import.meta.url;
30
+ const requireFromMcp = createRequire(fileURLToPath(zeusMcpFileUrl));
31
+ const { buildZeusProofIndexTags, proofTagsMatchAllPrefixes } = requireFromMcp(
32
+ fileURLToPath(new URL('../src/utils/zeusProofIndexTags.cjs', import.meta.url)),
33
+ );
34
+ /** @type {Set<string>} */
35
+ const ZEUS_SANDBOX_REQUIRE_ALLOW = new Set(['nodemailer', 'https', 'http', 'url', 'querystring']);
36
+
37
+ /**
38
+ * @param {string} id
39
+ * @returns {unknown}
40
+ */
41
+ function requireInZeusSandbox(id) {
42
+ const mod = String(id || '').replace(/\\/g, '/');
43
+ if (!mod) {
44
+ throw new TypeError('require: module id is required');
45
+ }
46
+ if (mod.startsWith('node:') || (mod[0] !== '.' && mod[0] !== '/')) {
47
+ const root = mod.split('/')[0] || mod;
48
+ if (ZEUS_SANDBOX_REQUIRE_ALLOW.has(root) || mod === 'nodemailer' || mod.startsWith('nodemailer/')) {
49
+ return requireFromMcp(mod);
50
+ }
51
+ }
52
+ throw new Error(
53
+ `[Zeus sandbox] require('${id}') is not allowed. Allowed: ${[...ZEUS_SANDBOX_REQUIRE_ALLOW].join(', ')}.`
54
+ );
55
+ }
56
+
57
+ /**
58
+ * @returns {Record<string, string|undefined>}
59
+ */
60
+ function zeusSandboxGmailProcessEnv() {
61
+ return {
62
+ GMAIL_USER: process.env.GMAIL_USER,
63
+ GMAIL_APP_PASSWORD: process.env.GMAIL_APP_PASSWORD
64
+ };
65
+ }
66
+
67
+ /**
68
+ * McpServer validates tool arguments with Zod only; plain JSON Schema objects are not parsed correctly.
69
+ * TOOLS[].inputSchema stays the JSON Schema contract for /.well-known and docs.
70
+ * TOOL_ZOD_INPUT mirrors those shapes so tools/list exposes properties to MCP clients.
71
+ */
72
+
73
+ const NEUS_API_URL = mcp.neusApiUrl;
74
+ const NEUS_MCP_INFERENCE_BASE_URL = mcp.mcpInferenceBaseUrl;
75
+ const NEUS_MCP_INFERENCE_KEY = mcp.mcpInferenceKey;
76
+ const NEUS_MCP_INFERENCE_MODEL = mcp.mcpInferenceModel;
77
+ /** In-flight execute_agent_command jobs (per MCP process) for stop_agent_processing */
78
+ const agentInferenceJobs = new Map();
79
+ const MCP_APP_ID = mcp.mcpAppId;
80
+ // Process default for NEUS API calls (stdio / self-hosted). Not the MCP HTTP gateway secret.
81
+ const NEUS_AUTH_TOKEN = mcp.neusAuthToken;
82
+ const MCP_PUBLIC_URL = mcp.mcpPublicUrl;
83
+ const API_TIMEOUT_MS = 5 * 60 * 1000;
84
+ const API_MAX_RETRIES = mcp.apiMaxRetries;
85
+ const API_RETRY_BASE_MS = mcp.apiRetryBaseMs;
86
+ const SHUTDOWN_TIMEOUT_MS = mcp.shutdownTimeoutMs;
87
+
88
+ /** MCP initialize protocol version wire string (distinct from MCP installer manifest versioning). */
89
+ const MCP_PROTOCOL_VERSION = mcp.mcpProtocolVersion;
90
+
91
+ function log(level, event, details = {}) {
92
+ const payload = {
93
+ ts: new Date().toISOString(),
94
+ level,
95
+ event,
96
+ ...details
97
+ };
98
+ console.error(JSON.stringify(payload));
99
+ }
100
+
101
+ function sleep(ms) {
102
+ return new Promise((resolve) => setTimeout(resolve, ms));
103
+ }
104
+
105
+ const ENCRYPT_TAG_OPEN = '<encrypt>';
106
+ const ENCRYPT_TAG_CLOSE = '</encrypt>';
107
+ const ENCRYPT_REGEX = /<encrypt>([\s\S]*?)<\/encrypt>/g;
108
+ const SENSITIVE_KEY_RE = /(api[-_ ]?key|secret|password|passkey|private[-_ ]?key|token|authorization|client[-_ ]?secret|app[-_ ]?secret)/i;
109
+ const SENSITIVE_VALUE_RE = /(npk_[a-zA-Z0-9_-]{8,}|sk-[a-zA-Z0-9_-]{12,}|ghp_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9._-]{10,}\.[A-Za-z0-9._-]{10,})/;
110
+ let runtimeCryptoKey = null;
111
+ let loggedEphemeralCryptoWarning = false;
112
+
113
+ function getRuntimeCryptoKey() {
114
+ if (runtimeCryptoKey) return runtimeCryptoKey;
115
+ if (mcp.cryptoSecret) {
116
+ runtimeCryptoKey = scryptSync(mcp.cryptoSecret, 'neus-mcp-v1', 32);
117
+ return runtimeCryptoKey;
118
+ }
119
+ runtimeCryptoKey = randomBytes(32);
120
+ if (!loggedEphemeralCryptoWarning) {
121
+ loggedEphemeralCryptoWarning = true;
122
+ log('warn', 'crypto_ephemeral_key_in_use', {
123
+ message:
124
+ 'NEUS_MCP_CRYPTO_SECRET is not set. Generated an ephemeral encryption key for this process; encrypted text cannot be decrypted after restart.'
125
+ });
126
+ }
127
+ return runtimeCryptoKey;
128
+ }
129
+
130
+ function wrapEncryptedPayload(payload) {
131
+ return `${ENCRYPT_TAG_OPEN}${payload}${ENCRYPT_TAG_CLOSE}`;
132
+ }
133
+
134
+ function isWrappedEncrypted(value) {
135
+ if (typeof value !== 'string') return false;
136
+ const trimmed = value.trim();
137
+ return trimmed.startsWith(ENCRYPT_TAG_OPEN) && trimmed.endsWith(ENCRYPT_TAG_CLOSE);
138
+ }
139
+
140
+ function encryptPlaintext(plain) {
141
+ const text = String(plain ?? '');
142
+ const key = getRuntimeCryptoKey();
143
+ const iv = randomBytes(12);
144
+ const cipher = createCipheriv('aes-256-gcm', key, iv);
145
+ const ciphertext = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
146
+ const tag = cipher.getAuthTag();
147
+ return JSON.stringify({
148
+ v: 1,
149
+ alg: 'aes-256-gcm',
150
+ iv: iv.toString('base64'),
151
+ tag: tag.toString('base64'),
152
+ ct: ciphertext.toString('base64')
153
+ });
154
+ }
155
+
156
+ function decryptPayload(payload) {
157
+ const key = getRuntimeCryptoKey();
158
+ const parsed = typeof payload === 'string' ? JSON.parse(payload) : payload;
159
+ if (!parsed || Number(parsed.v) !== 1 || parsed.alg !== 'aes-256-gcm') {
160
+ throw new Error('Unsupported encrypted payload format');
161
+ }
162
+ const iv = Buffer.from(String(parsed.iv || ''), 'base64');
163
+ const tag = Buffer.from(String(parsed.tag || ''), 'base64');
164
+ const ct = Buffer.from(String(parsed.ct || ''), 'base64');
165
+ const decipher = createDecipheriv('aes-256-gcm', key, iv);
166
+ decipher.setAuthTag(tag);
167
+ const plain = Buffer.concat([decipher.update(ct), decipher.final()]);
168
+ return plain.toString('utf8');
169
+ }
170
+
171
+ function decryptEmbeddedTags(input) {
172
+ if (typeof input !== 'string') return input;
173
+ if (!input.includes(ENCRYPT_TAG_OPEN)) return input;
174
+ return input.replace(ENCRYPT_REGEX, (_m, payload) => {
175
+ try {
176
+ return decryptPayload(String(payload || '').trim());
177
+ } catch {
178
+ return _m;
179
+ }
180
+ });
181
+ }
182
+
183
+ function looksSensitiveValue(value) {
184
+ if (typeof value !== 'string') return false;
185
+ const text = value.trim();
186
+ if (!text) return false;
187
+ if (isWrappedEncrypted(text)) return false;
188
+ if (SENSITIVE_VALUE_RE.test(text)) return true;
189
+ if (text.length >= 24 && !/\s/.test(text) && /[A-Za-z]/.test(text) && /\d/.test(text)) return true;
190
+ return false;
191
+ }
192
+
193
+ function shouldEncryptKey(key) {
194
+ return typeof key === 'string' && SENSITIVE_KEY_RE.test(key);
195
+ }
196
+
197
+ function decryptArgsDeep(value) {
198
+ if (typeof value === 'string') return decryptEmbeddedTags(value);
199
+ if (Array.isArray(value)) return value.map((item) => decryptArgsDeep(item));
200
+ if (!value || typeof value !== 'object') return value;
201
+ const out = {};
202
+ for (const [k, v] of Object.entries(value)) out[k] = decryptArgsDeep(v);
203
+ return out;
204
+ }
205
+
206
+ /**
207
+ * neus_agent_create returns agent.generatedWallet with raw 0x address and private key. Both match
208
+ * looksSensitiveValue (long hex) or SENSITIVE_KEY_RE (privateKey) and would otherwise be wrapped
209
+ * in <encrypt>, breaking JSON consumers and local E2E.
210
+ */
211
+ function isExemptedAgentGeneratedWalletString(path, keyHint, value) {
212
+ if (typeof value !== 'string' || isWrappedEncrypted(value)) return false;
213
+ const p = String(path);
214
+ if (!p.includes('generatedWallet')) return false;
215
+ const t = value.trim();
216
+ if (p.endsWith('.address') && /^0x[0-9a-fA-F]{40}$/.test(t)) return true;
217
+ if ((keyHint === 'privateKey' || p.split('.').pop() === 'privateKey') && /^0x[0-9a-fA-F]{64}$/.test(t)) {
218
+ return true;
219
+ }
220
+ return false;
221
+ }
222
+
223
+ function isPublicHostedVerifyOrCheckoutUrlField(keyHint, value) {
224
+ if (keyHint !== 'hostedVerifyUrl' && keyHint !== 'hostedCheckoutUrl') return false;
225
+ if (typeof value !== 'string' || isWrappedEncrypted(value)) return false;
226
+ const t = value.trim();
227
+ return t.startsWith('https://') || t.startsWith('http://');
228
+ }
229
+
230
+ /** Public EVM addresses in tool JSON (not secrets) — would match looksSensitiveValue otherwise. */
231
+ function isExemptedPublicEvmAddressField(keyHint, value) {
232
+ if (typeof value !== 'string' || isWrappedEncrypted(value)) return false;
233
+ if (keyHint !== 'principal' && keyHint !== 'signer' && keyHint !== 'agentWallet' && keyHint !== 'controllerWallet' && keyHint !== 'walletAddress' && keyHint !== 'address') {
234
+ return false;
235
+ }
236
+ return /^0x[0-9a-fA-F]{40}$/.test(value.trim());
237
+ }
238
+
239
+ function encryptSensitiveDeep(value, path = '') {
240
+ if (typeof value === 'string') {
241
+ const keyHint = path.split('.').pop() || '';
242
+ if (isExemptedAgentGeneratedWalletString(path, keyHint, value)) {
243
+ return value;
244
+ }
245
+ if (isPublicHostedVerifyOrCheckoutUrlField(keyHint, value)) {
246
+ return value;
247
+ }
248
+ if (isExemptedPublicEvmAddressField(keyHint, value)) {
249
+ return value;
250
+ }
251
+ if ((shouldEncryptKey(keyHint) || looksSensitiveValue(value)) && !isWrappedEncrypted(value)) {
252
+ return wrapEncryptedPayload(encryptPlaintext(value));
253
+ }
254
+ return value;
255
+ }
256
+ if (Array.isArray(value)) return value.map((item) => encryptSensitiveDeep(item, path));
257
+ if (!value || typeof value !== 'object') return value;
258
+ const out = {};
259
+ for (const [k, v] of Object.entries(value)) {
260
+ const nextPath = path ? `${path}.${k}` : k;
261
+ if (typeof v === 'string' && shouldEncryptKey(k) && !isWrappedEncrypted(v)) {
262
+ if (isExemptedAgentGeneratedWalletString(nextPath, k, v)) {
263
+ out[k] = v;
264
+ continue;
265
+ }
266
+ out[k] = wrapEncryptedPayload(encryptPlaintext(v));
267
+ continue;
268
+ }
269
+ out[k] = encryptSensitiveDeep(v, nextPath);
270
+ }
271
+ return out;
272
+ }
273
+
274
+ function shouldRetryStatus(status) {
275
+ return Number.isFinite(Number(status)) && Number(status) >= 500;
276
+ }
277
+
278
+ function shouldRetryError(err) {
279
+ const code = String(err?.code || '').toUpperCase();
280
+ return code === 'ECONNRESET' || code === 'ETIMEDOUT' || code === 'ENOTFOUND' || code === 'EAI_AGAIN';
281
+ }
282
+
283
+ /**
284
+ * Normalize agent skill refs for agent-identity / agent-delegation payloads (aligns with verifiers).
285
+ * @param {unknown} skills
286
+ * @returns {object[]|null}
287
+ */
288
+ function normalizeMcpAgentSkills(skills) {
289
+ if (!Array.isArray(skills)) return null;
290
+ const cleaned = [];
291
+ const kinds = new Set(['native', 'integration', 'plugin', 'mcp', 'toolkit']);
292
+ for (const skill of skills.slice(0, 48)) {
293
+ if (!skill || typeof skill !== 'object') continue;
294
+ const id = typeof skill.id === 'string' ? skill.id.trim() : '';
295
+ if (!id || id.length > 64) continue;
296
+ cleaned.push({
297
+ id,
298
+ label: typeof skill.label === 'string' ? skill.label.trim().slice(0, 64) : id,
299
+ ...(typeof skill.version === 'string' ? { version: skill.version.trim().slice(0, 32) } : {}),
300
+ ...(typeof skill.provider === 'string' ? { provider: skill.provider.trim().slice(0, 64) } : {}),
301
+ ...(kinds.has(skill.kind) ? { kind: skill.kind } : {}),
302
+ ...(typeof skill.configId === 'string' ? { configId: skill.configId.trim().slice(0, 128) } : {}),
303
+ ...(typeof skill.enabled === 'boolean' ? { enabled: skill.enabled } : {}),
304
+ });
305
+ }
306
+ return cleaned.length > 0 ? cleaned : null;
307
+ }
308
+
309
+ /**
310
+ * @param {unknown} services
311
+ * @returns {object[]|null}
312
+ */
313
+ function normalizeMcpAgentServices(services) {
314
+ if (!Array.isArray(services)) return null;
315
+ const cleaned = [];
316
+ for (const s of services.slice(0, 16)) {
317
+ if (!s || typeof s !== 'object') continue;
318
+ const name = typeof s.name === 'string' ? s.name.trim().slice(0, 64) : '';
319
+ const endpoint = typeof s.endpoint === 'string' ? s.endpoint.trim().slice(0, 256) : '';
320
+ const version = typeof s.version === 'string' ? s.version.trim().slice(0, 32) : '';
321
+ if (!name || !endpoint) continue;
322
+ try {
323
+ const parsed = new URL(endpoint);
324
+ if (!parsed.protocol || !parsed.hostname) continue;
325
+ } catch {
326
+ continue;
327
+ }
328
+ cleaned.push({
329
+ name,
330
+ endpoint,
331
+ ...(version ? { version } : {}),
332
+ });
333
+ }
334
+ return cleaned.length > 0 ? cleaned : null;
335
+ }
336
+
337
+ // ============================================================================
338
+ // TOOL DEFINITIONS (single source of truth for MCP tool names and inputSchema)
339
+ // ============================================================================
340
+
341
+ const TOOLS = [
342
+ {
343
+ name: 'neus_context',
344
+ description:
345
+ 'NEUS MCP session home: product summary, setup, access-key mode, verifier summaries, reuse-first workflow, tools, and safety rules. Always call first.',
346
+ inputSchema: {
347
+ type: 'object',
348
+ properties: {},
349
+ additionalProperties: false
350
+ }
351
+ },
352
+ {
353
+ name: 'neus_verifiers_catalog',
354
+ description:
355
+ 'Full verifier directory with JSON schemas. Use after neus_context when payloads need exact fields beyond the summary.',
356
+ inputSchema: {
357
+ type: 'object',
358
+ properties: {},
359
+ additionalProperties: false
360
+ }
361
+ },
362
+ {
363
+ name: 'neus_proofs_check',
364
+ description: 'Checks whether existing proofs satisfy verifier IDs. Eligibility only; never creates proofs.',
365
+ inputSchema: {
366
+ type: 'object',
367
+ properties: {
368
+ wallet: { type: 'string', description: 'Identifier to check: wallet (0x.../base58) or DID (did:pkh:...)' },
369
+ verifiers: { type: 'array', items: { type: 'string' }, description: 'Verifier IDs to check (e.g., ["ownership-basic", "proof-of-human"]). Get IDs from neus_verifiers_catalog.' },
370
+ requireAll: { type: 'boolean', description: 'Require all verifiers (default: false = any)' },
371
+ minCount: { type: 'number', description: 'Minimum number of matching proofs (default: 1)' }
372
+ },
373
+ required: ['wallet', 'verifiers']
374
+ }
375
+ },
376
+ {
377
+ name: 'neus_verify',
378
+ description:
379
+ 'Creates or continues verification only when Profile access, wallet, and signing already satisfy NEUS for this verifier. Check existing receipts first.',
380
+ inputSchema: {
381
+ type: 'object',
382
+ properties: {
383
+ walletAddress: { type: 'string', description: 'Identifier to verify: wallet (EVM/Solana) or DID (did:pkh:...)' },
384
+ verifierIds: { type: 'array', items: { type: 'string' }, description: 'Verifier IDs to run (e.g., ["ownership-basic", "proof-of-human"])' },
385
+ data: {
386
+ type: 'object',
387
+ description:
388
+ 'Verifier payload from neus_verifiers_catalog. Omit or send {} when requiredFields are empty.'
389
+ },
390
+ chain: { type: 'string', description: 'Network identifier — omit when NEUS infers it from the address format.' },
391
+ signature: {
392
+ type: 'string',
393
+ description: 'Approver signature emitted by NEUS on the preparation response — omit on the preparation call.'
394
+ },
395
+ signedTimestamp: { type: 'number', description: 'Unix timestamp in ms (auto-generated if missing)' },
396
+ options: {
397
+ type: 'object',
398
+ description: 'Optional verification options',
399
+ properties: {
400
+ returnUrl: { type: 'string', description: 'Partner return URL for post-verification redirect (hosted flow)' }
401
+ }
402
+ }
403
+ },
404
+ required: ['walletAddress', 'verifierIds']
405
+ }
406
+ },
407
+ {
408
+ name: 'neus_verify_or_guide',
409
+ description:
410
+ 'Fallback orchestration after reuse checks: use only when proof, profile, payment, provider, wallet, or delegation setup is missing.',
411
+ inputSchema: {
412
+ type: 'object',
413
+ properties: {
414
+ walletAddress: { type: 'string', description: 'Identifier to verify: wallet (EVM/Solana) or DID (did:pkh:...)' },
415
+ verifierIds: { type: 'array', items: { type: 'string' }, description: 'Verifier IDs to check/create. Get IDs from neus_verifiers_catalog.' },
416
+ data: { type: 'object', description: 'Verifier-specific data payload (used only if verification needed)' },
417
+ requireAll: { type: 'boolean', description: 'Require all verifiers (default: false = any)' },
418
+ chain: { type: 'string', description: 'Optional network hint — omit when NEUS can infer it from the wallet format.' },
419
+ options: { type: 'object', description: 'Optional verification options' }
420
+ },
421
+ required: ['walletAddress', 'verifierIds']
422
+ }
423
+ },
424
+ {
425
+ name: 'neus_proofs_get',
426
+ description:
427
+ 'Reads proof records, tags, delegated agent reads, Profile summary fields, and live proof state. qHashes in the response are authoritative.',
428
+ inputSchema: {
429
+ type: 'object',
430
+ properties: {
431
+ identifier: { type: 'string', description: 'Wallet address (EVM 0x... or Solana base58) or DID (did:pkh:...)' },
432
+ limit: { type: 'number', description: 'Max proof records to return (default: 50, max: 100)' },
433
+ offset: { type: 'number', description: 'Pagination offset (default: 0)' },
434
+ tags: {
435
+ type: 'string',
436
+ description:
437
+ 'Optional comma-separated tag filter (use values you have seen on prior neus_proofs_get results).'
438
+ },
439
+ scope: {
440
+ type: 'string',
441
+ description:
442
+ 'Optional preset when tags omitted: memory | research | rules | instruction | tools | receipts.'
443
+ },
444
+ agentWallet: {
445
+ type: 'string',
446
+ description:
447
+ 'Optional agent wallet address for delegated proof reads — only when delegation is enabled for your account.'
448
+ },
449
+ q: { type: 'string', description: 'Full-text search (by-wallet ?q= when supported).' },
450
+ searchQuery: { type: 'string', description: 'Alias of q.' },
451
+ verifierId: {
452
+ type: 'string',
453
+ description:
454
+ 'Optional single verifier id filter (passed to GET /proofs/by-wallet ?verifierId=). Prefer this for targeted reads instead of fetching a large page and filtering client-side.'
455
+ },
456
+ tagPrefix: { type: 'string', description: 'After fetch, keep only proofs with a tag starting with this prefix.' },
457
+ tagContains: { type: 'string', description: 'After fetch, keep only proofs with any tag containing this substring.' },
458
+ tagPrefixesAll: {
459
+ type: 'string',
460
+ description:
461
+ 'After fetch, keep proofs where every comma-separated prefix matches the start of some tag (AND), e.g. memory:john_,topic:acme.'
462
+ }
463
+ },
464
+ required: ['identifier']
465
+ }
466
+ },
467
+ {
468
+ name: 'neus_me',
469
+ description:
470
+ 'Returns the signed-in NEUS Profile when a personal access key from Profile → Account is configured on this MCP session.',
471
+ inputSchema: {
472
+ type: 'object',
473
+ properties: {
474
+ identifier: {
475
+ type: 'string',
476
+ description: 'Optional wallet or DID. Omit to infer from the personal access key stored on this MCP session.'
477
+ }
478
+ },
479
+ additionalProperties: false
480
+ }
481
+ },
482
+ {
483
+ name: 'neus_agent_link',
484
+ description:
485
+ 'Readiness check for an agent wallet: returns linked when agent-identity and agent-delegation proofs exist; otherwise link_required with hostedVerifyUrl.',
486
+ inputSchema: {
487
+ type: 'object',
488
+ properties: {
489
+ agentWallet: { type: 'string', description: 'Agent wallet address (required)' },
490
+ principal: {
491
+ type: 'string',
492
+ description: 'Optional approving wallet/DID — omit once neus_me already clarified the approving account.'
493
+ }
494
+ },
495
+ required: ['agentWallet']
496
+ }
497
+ },
498
+ {
499
+ name: 'neus_agent_create',
500
+ description:
501
+ 'Prepares agent-identity and agent-delegation payloads (signatures and/or hostedVerifyUrl). Does not finalize receipts; confirm with neus_agent_link after signing. Skills/services fields are metadata only, not secrets.',
502
+ inputSchema: {
503
+ type: 'object',
504
+ properties: {
505
+ agentWallet: { type: 'string', description: 'Agent wallet, or the literal "generate" for an auto-generated key (integrator-only — store the private key securely).' },
506
+ controllerWallet: { type: 'string', description: 'Optional approving account wallet when you must supply it explicitly (must sign the access-approval step). Not the agent wallet.' },
507
+ agentId: { type: 'string', description: 'Human-readable agent identifier (e.g., "my-ai-assistant"). Max 128 chars.' },
508
+ agentLabel: { type: 'string', description: 'Display name for the agent (optional, defaults to agentId).' },
509
+ agentType: { type: 'string', description: 'Type of agent: "ai", "bot", "service", "automation", or "agent" (default: "ai").' },
510
+ description: { type: 'string', description: 'Agent description (optional, max 500 chars).' },
511
+ capabilities: { type: 'array', items: { type: 'string' }, description: 'Capability flags: "wallet", "signing", "spending", "publishing", "search", "browser", "mcp", "webhooks", "receipts", "proofs", "delegation".' },
512
+ permissions: { type: 'array', items: { type: 'string' }, description: 'Permissions to grant agent (e.g., ["read-proofs", "write-proofs", "invoke-services"]).' },
513
+ scope: { type: 'string', description: 'Delegation scope (default: "global"). Use "payments:x402" for payment agents.' },
514
+ expiresAt: { type: 'number', description: 'Unix timestamp in ms when delegation expires (optional, 0 = never).' },
515
+ maxSpend: {
516
+ type: 'string',
517
+ description:
518
+ 'Optional spend cap: whole-number string in token base units (1 — 78 digits, no decimal point). For USDC (typical x402 flows), use six decimal places — see SDK toAgentDelegationMaxSpend.',
519
+ },
520
+ chain: { type: 'string', description: 'CAIP-2 chain identifier. Auto-detected from wallet format if omitted.' },
521
+ preset: { type: 'string', description: 'Quick preset: "full-access" (all capabilities), "payments" (x402 scope), "readonly" (read-proofs only), "automation" (browser+mcp).' },
522
+ instructions: {
523
+ type: 'string',
524
+ description:
525
+ 'Optional operating instructions for agent-identity (max 16000 chars). Included in the signed identity payload: system or developer prompt, tool-use policy, and safety/PII rules — portable with the proof.',
526
+ },
527
+ skills: {
528
+ type: 'array',
529
+ description:
530
+ 'Optional AgentSkillRef[] for agent-identity (id, label, version, provider, kind, configId, enabled). configId is an optional opaque integration reference (not a secret, not an OAuth token, not credential material).',
531
+ items: { type: 'object', additionalProperties: true },
532
+ },
533
+ services: {
534
+ type: 'array',
535
+ description: 'Optional services for agent-identity: { name, endpoint (URL), version? }.',
536
+ items: { type: 'object', additionalProperties: true },
537
+ },
538
+ delegationInstructions: {
539
+ type: 'string',
540
+ description: 'Optional controller policy text for agent-delegation (max 16000 chars), signed by controllerWallet.',
541
+ },
542
+ delegationSkills: {
543
+ type: 'array',
544
+ description:
545
+ 'Optional AgentSkillRef[] for agent-delegation. configId is an optional opaque integration reference (not a secret, not an OAuth token, not credential material).',
546
+ items: { type: 'object', additionalProperties: true },
547
+ },
548
+ },
549
+ required: ['agentId']
550
+ }
551
+ },
552
+ {
553
+ name: 'zeus_url_fetch',
554
+ description:
555
+ 'Fetch public http(s) HTML/text quickly for Zeus. Blocks local/private/metadata hosts and truncates body by maxChars.',
556
+ inputSchema: {
557
+ type: 'object',
558
+ properties: {
559
+ url: { type: 'string', description: 'Public http(s) URL to fetch.' },
560
+ maxChars: { type: 'number', description: 'Max response chars (default 120000, max 400000).' }
561
+ },
562
+ required: ['url']
563
+ }
564
+ },
565
+ {
566
+ name: 'zeus_execute_javascript',
567
+ description:
568
+ 'Execute JavaScript in a Zeus sandbox (async/await). Use return or set `result`. `require` only allows `nodemailer` (Gmail: use `process.env.GMAIL_USER` / `process.env.GMAIL_APP_PASSWORD` on the MCP process — do not put secrets in proof text). For NEUS API calls from a hosted BFF, use the hub `zeus_execute_javascript` path which provides `zeus.get` / `zeus.post`. Response includes `logs` from `console` in the script.',
569
+ inputSchema: {
570
+ type: 'object',
571
+ properties: {
572
+ script: { type: 'string', description: 'JavaScript source. Set result = ... or return an expression.' },
573
+ input: { type: 'object', description: 'JSON input object available as input.' },
574
+ timeoutMs: { type: 'number', description: 'Timeout in ms (default 4000, max 15000).' }
575
+ },
576
+ required: ['script']
577
+ }
578
+ },
579
+ {
580
+ name: 'execute_javascript_code',
581
+ description:
582
+ 'Same behavior as zeus_execute_javascript (Zeus Node sandbox; response includes `logs` from the script — s console). Exposed for agents that need the dedicated name; profile Zeus chat also echoes `logs` to the user — s browser console when this tool is used from the BFF agent.',
583
+ inputSchema: {
584
+ type: 'object',
585
+ properties: {
586
+ script: { type: 'string', description: 'JavaScript source. Set result = ... or return an expression.' },
587
+ input: { type: 'object', description: 'JSON input object available as input.' },
588
+ timeoutMs: { type: 'number', description: 'Timeout in ms (default 4000, max 15000).' }
589
+ },
590
+ required: ['script']
591
+ }
592
+ },
593
+ {
594
+ name: 'zeus_skill_tools_list',
595
+ description:
596
+ 'Fast list of proof-backed Zeus tools/skills from wallet inventory (tags tool,skill,agent:tool,agent:skill).',
597
+ inputSchema: {
598
+ type: 'object',
599
+ properties: {
600
+ identifier: { type: 'string', description: 'Wallet/DID. Defaults to Bearer profile wallet when available.' },
601
+ limit: { type: 'number' },
602
+ offset: { type: 'number' },
603
+ agentWallet: { type: 'string' }
604
+ }
605
+ }
606
+ },
607
+ {
608
+ name: 'zeus_skill_tool_execute',
609
+ description:
610
+ 'Execute JavaScript stored in a tool/skill proof. Expected content: markdown summary, separator line -----, then JavaScript.',
611
+ inputSchema: {
612
+ type: 'object',
613
+ properties: {
614
+ qHash: { type: 'string' },
615
+ input: { type: 'object' },
616
+ timeoutMs: { type: 'number' }
617
+ },
618
+ required: ['qHash']
619
+ }
620
+ },
621
+ {
622
+ name: 'zeus_session_receipt_create',
623
+ description:
624
+ 'Create a private NEUS memory proof tagged receipt:chat (Zeus turn/session audit). Not shown on the trust graph; still readable by qHash. Requires Bearer profile wallet or explicit walletAddress.',
625
+ inputSchema: {
626
+ type: 'object',
627
+ properties: {
628
+ walletAddress: { type: 'string' },
629
+ title: { type: 'string' },
630
+ content: { type: 'string' },
631
+ tags: { type: 'array', items: { type: 'string' } },
632
+ asPrivate: { type: 'boolean', description: 'Ignored; receipts are always private.' }
633
+ },
634
+ required: ['content']
635
+ }
636
+ },
637
+ {
638
+ name: 'zeus_session_receipt_append',
639
+ description:
640
+ 'Append a response/update to an existing receipt by creating a superseding private proof tagged receipt:chat and supersedes:<qHash>.',
641
+ inputSchema: {
642
+ type: 'object',
643
+ properties: {
644
+ qHash: { type: 'string' },
645
+ walletAddress: { type: 'string' },
646
+ appendContent: { type: 'string' },
647
+ title: { type: 'string' },
648
+ asPrivate: { type: 'boolean', description: 'Ignored; receipts are always private.' }
649
+ },
650
+ required: ['qHash', 'appendContent']
651
+ }
652
+ },
653
+ {
654
+ name: 'list_tools',
655
+ description:
656
+ 'Dictionary (title -> qHash) and items list for proofs in the connected wallet tagged tool or skill (incl. agent:tool, agent:skill). Optional q / tagPrefix / tagContains filters (same as list_memories). Requires identifier or Bearer profile wallet.',
657
+ inputSchema: {
658
+ type: 'object',
659
+ properties: {
660
+ identifier: { type: 'string' },
661
+ limit: { type: 'number' },
662
+ offset: { type: 'number' },
663
+ agentWallet: { type: 'string' },
664
+ q: { type: 'string' },
665
+ searchQuery: { type: 'string' },
666
+ tagPrefix: { type: 'string' },
667
+ tagContains: { type: 'string' },
668
+ tagPrefixesAll: {
669
+ type: 'string',
670
+ description: 'AND filter: comma-separated tag prefixes; each must match start of some proof tag.'
671
+ }
672
+ }
673
+ }
674
+ },
675
+ {
676
+ name: 'list_all_available_tools',
677
+ description:
678
+ 'Return an array containing every callable MCP function name exposed by this protocol/mcp server.',
679
+ inputSchema: {
680
+ type: 'object',
681
+ properties: {},
682
+ additionalProperties: false
683
+ }
684
+ },
685
+ {
686
+ name: 'list_memories',
687
+ description:
688
+ 'Dictionary and items for proofs tagged memory (includes facet tags like memory:users_name, topic:*, domain:*, zeus:index:v3). Filter with tagPrefix / tagContains / tagPrefixesAll (AND on prefixes), or pass q/searchQuery for full-text when the API supports it. Requires identifier or Bearer profile wallet.',
689
+ inputSchema: {
690
+ type: 'object',
691
+ properties: {
692
+ identifier: { type: 'string' },
693
+ limit: { type: 'number' },
694
+ offset: { type: 'number' },
695
+ agentWallet: { type: 'string' },
696
+ q: { type: 'string', description: 'Full-text search forwarded to by-wallet ?q= when supported.' },
697
+ searchQuery: { type: 'string', description: 'Alias of q.' },
698
+ tagPrefix: { type: 'string', description: 'Only proofs with a tag starting with this (e.g. memory:john_)' },
699
+ tagContains: { type: 'string', description: 'Only proofs with any tag containing this substring.' },
700
+ tagPrefixesAll: {
701
+ type: 'string',
702
+ description: 'Comma-separated prefixes; every prefix must match the start of some tag on the proof.'
703
+ }
704
+ }
705
+ }
706
+ },
707
+ {
708
+ name: 'list_research',
709
+ description:
710
+ 'Dictionary and items for proofs tagged research. Same optional q / tagPrefix / tagContains / tagPrefixesAll filters as list_memories.',
711
+ inputSchema: {
712
+ type: 'object',
713
+ properties: {
714
+ identifier: { type: 'string' },
715
+ limit: { type: 'number' },
716
+ offset: { type: 'number' },
717
+ agentWallet: { type: 'string' },
718
+ q: { type: 'string' },
719
+ searchQuery: { type: 'string' },
720
+ tagPrefix: { type: 'string' },
721
+ tagContains: { type: 'string' },
722
+ tagPrefixesAll: { type: 'string' }
723
+ }
724
+ }
725
+ },
726
+ {
727
+ name: 'list_instructions',
728
+ description:
729
+ 'Dictionary and items for proofs tagged instruction or instructions. Optional q / tagPrefix / tagContains / tagPrefixesAll filters.',
730
+ inputSchema: {
731
+ type: 'object',
732
+ properties: {
733
+ identifier: { type: 'string' },
734
+ limit: { type: 'number' },
735
+ offset: { type: 'number' },
736
+ agentWallet: { type: 'string' },
737
+ q: { type: 'string' },
738
+ searchQuery: { type: 'string' },
739
+ tagPrefix: { type: 'string' },
740
+ tagContains: { type: 'string' },
741
+ tagPrefixesAll: { type: 'string' }
742
+ }
743
+ }
744
+ },
745
+ {
746
+ name: 'list_rules',
747
+ description:
748
+ 'Dictionary and items for proofs tagged rule or rules. Optional q / tagPrefix / tagContains / tagPrefixesAll filters.',
749
+ inputSchema: {
750
+ type: 'object',
751
+ properties: {
752
+ identifier: { type: 'string' },
753
+ limit: { type: 'number' },
754
+ offset: { type: 'number' },
755
+ agentWallet: { type: 'string' },
756
+ q: { type: 'string' },
757
+ searchQuery: { type: 'string' },
758
+ tagPrefix: { type: 'string' },
759
+ tagContains: { type: 'string' },
760
+ tagPrefixesAll: { type: 'string' }
761
+ }
762
+ }
763
+ },
764
+ {
765
+ name: 'list_mcps',
766
+ description:
767
+ 'Dictionary and items for proofs tagged mcp (MCP connection / integration records). Optional q / tagPrefix / tagContains / tagPrefixesAll filters.',
768
+ inputSchema: {
769
+ type: 'object',
770
+ properties: {
771
+ identifier: { type: 'string' },
772
+ limit: { type: 'number' },
773
+ offset: { type: 'number' },
774
+ agentWallet: { type: 'string' },
775
+ q: { type: 'string' },
776
+ searchQuery: { type: 'string' },
777
+ tagPrefix: { type: 'string' },
778
+ tagContains: { type: 'string' },
779
+ tagPrefixesAll: { type: 'string' }
780
+ }
781
+ }
782
+ },
783
+ {
784
+ name: 'get_tool',
785
+ description: 'Fetch a proof by qHash; must be tagged as a tool (or agent:tool).',
786
+ inputSchema: {
787
+ type: 'object',
788
+ properties: { qHash: { type: 'string' }, id: { type: 'string' } },
789
+ required: []
790
+ }
791
+ },
792
+ {
793
+ name: 'get_skill',
794
+ description: 'Fetch a proof by qHash; must be tagged as a skill (or agent:skill).',
795
+ inputSchema: {
796
+ type: 'object',
797
+ properties: { qHash: { type: 'string' }, id: { type: 'string' } },
798
+ required: []
799
+ }
800
+ },
801
+ {
802
+ name: 'get_memory',
803
+ description: 'Fetch a proof by qHash; must be tagged memory.',
804
+ inputSchema: {
805
+ type: 'object',
806
+ properties: { qHash: { type: 'string' }, id: { type: 'string' } },
807
+ required: []
808
+ }
809
+ },
810
+ {
811
+ name: 'get_instruction',
812
+ description: 'Fetch a proof by qHash; must be tagged instruction or instructions.',
813
+ inputSchema: {
814
+ type: 'object',
815
+ properties: { qHash: { type: 'string' }, id: { type: 'string' } },
816
+ required: []
817
+ }
818
+ },
819
+ {
820
+ name: 'get_rule',
821
+ description: 'Fetch a proof by qHash; must be tagged rule or rules.',
822
+ inputSchema: {
823
+ type: 'object',
824
+ properties: { qHash: { type: 'string' }, id: { type: 'string' } },
825
+ required: []
826
+ }
827
+ },
828
+ {
829
+ name: 'get_research',
830
+ description: 'Fetch a proof by qHash; must be tagged research.',
831
+ inputSchema: {
832
+ type: 'object',
833
+ properties: { qHash: { type: 'string' }, id: { type: 'string' } },
834
+ required: []
835
+ }
836
+ },
837
+ {
838
+ name: 'append_Content_to_memory',
839
+ description:
840
+ 'Append text to an existing memory-tagged proof by creating a superseding ownership-basic proof (supersedes:qHash). Regenerates dense Zeus index tags (memory:*, topic:*, domain:*, zeus:index:v3); optional indexFacetKeys adds explicit memory:slug facets.',
841
+ inputSchema: {
842
+ type: 'object',
843
+ properties: {
844
+ qHash: { type: 'string' },
845
+ appendContent: { type: 'string' },
846
+ content: { type: 'string' },
847
+ walletAddress: { type: 'string' },
848
+ asPrivate: { type: 'boolean' },
849
+ indexFacetKeys: {
850
+ type: 'array',
851
+ items: { type: 'string' },
852
+ description: 'Optional facet keys merged like zeus_memory_create (memory:slug or raw tag strings).'
853
+ }
854
+ },
855
+ required: ['qHash']
856
+ }
857
+ },
858
+ {
859
+ name: 'delete_revoke_memory',
860
+ description: 'Revoke a memory-tagged proof via POST /proofs/revoke-self (Bearer/session wallet).',
861
+ inputSchema: {
862
+ type: 'object',
863
+ properties: { qHash: { type: 'string' }, walletAddress: { type: 'string' } },
864
+ required: ['qHash']
865
+ }
866
+ },
867
+ {
868
+ name: 'list_agents',
869
+ description:
870
+ 'List agent-identity and agent-delegation derived entries for the wallet (config-style fields: capabilities, skills, permissions, scope, etc.).',
871
+ inputSchema: {
872
+ type: 'object',
873
+ properties: {
874
+ identifier: { type: 'string' },
875
+ limit: { type: 'number' },
876
+ agentWallet: { type: 'string' }
877
+ }
878
+ }
879
+ },
880
+ {
881
+ name: 'execute_agent_command',
882
+ description:
883
+ 'Run a one-shot non-streaming OpenAI-style chat for a delegate prompt. Requires NEUS_MCP_INFERENCE_API_KEY or OPENROUTER_API_KEY on the MCP host. Returns requestId for stop_agent_processing.',
884
+ inputSchema: {
885
+ type: 'object',
886
+ properties: {
887
+ prompt: { type: 'string' },
888
+ agentWallet: { type: 'string' },
889
+ agentId: { type: 'string' },
890
+ model: { type: 'string' },
891
+ systemPrompt: { type: 'string' },
892
+ temperature: { type: 'number' }
893
+ },
894
+ required: ['prompt']
895
+ }
896
+ },
897
+ {
898
+ name: 'stop_agent_processing',
899
+ description: 'Abort a running execute_agent_command by requestId.',
900
+ inputSchema: {
901
+ type: 'object',
902
+ properties: { requestId: { type: 'string' } },
903
+ required: ['requestId']
904
+ }
905
+ },
906
+ {
907
+ name: 'encrypt_text',
908
+ description:
909
+ 'Encrypt plaintext with server-side AES-256-GCM and return wrapped token as <encrypt>...</encrypt>. Use for API keys, app secrets, passwords, passkeys, and other sensitive credentials.',
910
+ inputSchema: {
911
+ type: 'object',
912
+ properties: {
913
+ text: { type: 'string', description: 'Plaintext to encrypt.' }
914
+ },
915
+ required: ['text']
916
+ }
917
+ },
918
+ {
919
+ name: 'decrypt_text',
920
+ description:
921
+ 'Decrypt an <encrypt>...</encrypt> value created by encrypt_text. Use only when plaintext is required for immediate tool execution.',
922
+ inputSchema: {
923
+ type: 'object',
924
+ properties: {
925
+ text: { type: 'string', description: 'Encrypted payload (wrapped or raw inner JSON payload).' }
926
+ },
927
+ required: ['text']
928
+ }
929
+ },
930
+ {
931
+ name: 'mcp_connection_create',
932
+ description:
933
+ 'Normalize a new Zeus-style MCP connection (id, type streamableHttp|stdio, url or command, args, env, headers). Returns connection JSON to merge in Zeus (zeus_mcp_upsert) - MCP server does not persist the browser local graph.',
934
+ inputSchema: {
935
+ type: 'object',
936
+ properties: {
937
+ id: { type: 'string' },
938
+ type: { type: 'string' },
939
+ url: { type: 'string' },
940
+ command: { type: 'string' },
941
+ args: { type: 'array', items: { type: 'string' } },
942
+ env: { type: 'object' },
943
+ headers: { type: 'object' },
944
+ description: { type: 'string' },
945
+ enabled: { type: 'boolean' }
946
+ },
947
+ required: []
948
+ }
949
+ },
950
+ {
951
+ name: 'mcp_connection_modify',
952
+ description:
953
+ 'Upsert/replace a connection in an optional mcpConnections array (pass full list as returned from Zeus zeus_mcp_list or your session); returns updated list.',
954
+ inputSchema: {
955
+ type: 'object',
956
+ properties: {
957
+ mcpConnections: { type: 'array', items: { type: 'object' } },
958
+ connection: { type: 'object' }
959
+ },
960
+ required: ['connection']
961
+ }
962
+ },
963
+ {
964
+ name: 'mcp_connection_delete',
965
+ description: 'Remove a connection by id (cannot delete neus-mcp). Optional mcpConnections list to filter.',
966
+ inputSchema: {
967
+ type: 'object',
968
+ properties: { id: { type: 'string' }, mcpConnections: { type: 'array', items: { type: 'object' } } },
969
+ required: ['id']
970
+ }
971
+ }
972
+ ];
973
+
974
+ /**
975
+ * Names advertised on the public hosted MCP and in unauthenticated discovery (`/.well-known/*`).
976
+ * Extended Zeus/admin-adjacent tools are omitted unless the deployment exposes them (see mcpConfig).
977
+ */
978
+ const PUBLIC_MCP_TOOL_NAMES_ORDERED = [
979
+ 'neus_context',
980
+ 'neus_verifiers_catalog',
981
+ 'neus_proofs_check',
982
+ 'neus_verify',
983
+ 'neus_verify_or_guide',
984
+ 'neus_proofs_get',
985
+ 'neus_me',
986
+ 'neus_agent_link',
987
+ 'neus_agent_create'
988
+ ];
989
+ const PUBLIC_MCP_TOOL_SET = new Set(PUBLIC_MCP_TOOL_NAMES_ORDERED);
990
+
991
+ function toolsForPublicDiscovery() {
992
+ return PUBLIC_MCP_TOOL_NAMES_ORDERED.map((name) => {
993
+ const tool = TOOLS.find((t) => t.name === name);
994
+ if (!tool) {
995
+ throw new Error(`PUBLIC_MCP_TOOL_NAMES_ORDERED references missing tool: ${name}`);
996
+ }
997
+ return { name: tool.name, description: tool.description, inputSchema: tool.inputSchema };
998
+ });
999
+ }
1000
+
1001
+ /**
1002
+ * MCP assistant instructions appended to MCP Server metadata.
1003
+ */
1004
+ function buildMcpAssistantInstructions(exposeExtendedTools) {
1005
+ const core = `NEUS MCP is how assistants work with NEUS proof receipts responsibly.
1006
+
1007
+ Always call neus_context before planning any proof work so you inherit the freshest guidance, URLs, and recommended order.
1008
+
1009
+ Products in one line: proofs are reusable trust receipts. Verification creates or refreshes those receipts across apps, agents, wallets, and organizations.
1010
+
1011
+ Connectivity:
1012
+ - Public MCP (no Profile key on this connection): follow neus_context, list verifiers through neus_verifiers_catalog when needed, run neus_proofs_check, read public proofs with neus_proofs_get, and use neus_verify_or_guide only when setup or verification is missing.
1013
+ - Profile-connected MCP (personal access key on this MCP session): confirm with neus_me, then reuse existing receipts with account-aware reads and checks before asking for browser or signing steps.
1014
+
1015
+ Suggested flow:
1016
+ 1) neus_context
1017
+ 2) neus_me when a Profile key is configured
1018
+ 3) neus_agent_link when an agent wallet is involved
1019
+ 4) neus_proofs_check for eligibility snapshots
1020
+ 5) neus_proofs_get when exact proof records or qHashes are needed
1021
+ 6) neus_verify when signing + profile prerequisites are already aligned
1022
+ 7) neus_verify_or_guide only when proof, profile, payment, provider, wallet, or delegation setup is missing
1023
+
1024
+ Guardrails:
1025
+ - Never invent qHashes, verifier IDs, statuses, eligibility, URLs, Profile fields, or tags.
1026
+ - Never ask people to paste secrets (keys, recovery phrases, OAuth tokens, passkeys).
1027
+ - Prefer silent receipt reuse. Share NEUS-hosted links only when a tool returns one because the runtime cannot complete the step.
1028
+
1029
+ Structured signing workflows: when neus_verify returns signing material once, finalize with follow-up submissions exactly as instructed; never repeat unfinished preparation steps blindly.`;
1030
+
1031
+ const toolsPublic =
1032
+ `Public MCP tools include: neus_context, neus_verifiers_catalog, neus_proofs_check, neus_verify, neus_verify_or_guide, neus_proofs_get, neus_me, neus_agent_create, neus_agent_link. Use only tools/list as the authoritative catalog.`;
1033
+
1034
+ const toolsExtended =
1035
+ `This deployment exposes additional MCP tools beyond the public nine names. Inspect tools/list and follow each description. Assume nothing about internal-only capability names unless the client publishes them openly.`;
1036
+
1037
+ const sensitivePublic = `Treat every token like production data: encourage hosted flows and discourage pasting authorization material into chats.`;
1038
+
1039
+ const sensitiveExtended = `When tooling requires encrypted envelopes, preserve the <encrypt>...</encrypt> pattern mandated by deployment policy; decrypt only immediately before executing the protected action.`;
1040
+
1041
+ if (exposeExtendedTools) {
1042
+ return `${core}
1043
+
1044
+ ${toolsExtended}
1045
+
1046
+ ${sensitiveExtended}`;
1047
+ }
1048
+ return `${core}
1049
+
1050
+ ${toolsPublic}
1051
+
1052
+ ${sensitivePublic}`;
1053
+ }
1054
+
1055
+ const optionalString = z.string().optional();
1056
+ const optionalNumber = z.number().optional();
1057
+ const optionalBoolean = z.boolean().optional();
1058
+
1059
+ const TOOL_ZOD_INPUT = {
1060
+ neus_context: z.object({}).strict(),
1061
+ neus_verifiers_catalog: z.object({}).strict(),
1062
+ neus_proofs_check: z
1063
+ .object({
1064
+ wallet: z.string(),
1065
+ verifiers: z.array(z.string()),
1066
+ requireAll: optionalBoolean,
1067
+ minCount: optionalNumber
1068
+ })
1069
+ .passthrough(),
1070
+ neus_verify: z
1071
+ .object({
1072
+ walletAddress: z.string(),
1073
+ verifierIds: z.array(z.string()),
1074
+ data: z.record(z.string(), z.unknown()).optional(),
1075
+ chain: optionalString,
1076
+ signature: optionalString,
1077
+ signedTimestamp: optionalNumber,
1078
+ options: z.object({ returnUrl: optionalString }).optional()
1079
+ })
1080
+ .passthrough(),
1081
+ neus_verify_or_guide: z
1082
+ .object({
1083
+ walletAddress: z.string(),
1084
+ verifierIds: z.array(z.string()),
1085
+ data: z.record(z.string(), z.unknown()).optional(),
1086
+ requireAll: optionalBoolean,
1087
+ chain: optionalString,
1088
+ options: z.record(z.string(), z.unknown()).optional()
1089
+ })
1090
+ .passthrough(),
1091
+ neus_proofs_get: z
1092
+ .object({
1093
+ identifier: z.string(),
1094
+ limit: optionalNumber,
1095
+ offset: optionalNumber,
1096
+ tags: optionalString,
1097
+ scope: optionalString,
1098
+ agentWallet: optionalString,
1099
+ q: optionalString,
1100
+ searchQuery: optionalString,
1101
+ tagPrefix: optionalString,
1102
+ tagContains: optionalString,
1103
+ tagPrefixesAll: optionalString,
1104
+ verifierId: optionalString
1105
+ })
1106
+ .passthrough(),
1107
+ neus_me: z
1108
+ .object({
1109
+ identifier: z.string().nullish()
1110
+ })
1111
+ .strict(),
1112
+ neus_agent_link: z
1113
+ .object({
1114
+ agentWallet: z.string(),
1115
+ principal: optionalString
1116
+ })
1117
+ .passthrough(),
1118
+ neus_agent_create: z
1119
+ .object({
1120
+ agentId: z.string(),
1121
+ agentWallet: optionalString,
1122
+ controllerWallet: optionalString,
1123
+ agentLabel: optionalString,
1124
+ agentType: optionalString,
1125
+ description: optionalString,
1126
+ capabilities: z.array(z.string()).optional(),
1127
+ permissions: z.array(z.string()).optional(),
1128
+ scope: optionalString,
1129
+ expiresAt: optionalNumber,
1130
+ maxSpend: optionalString,
1131
+ chain: optionalString,
1132
+ preset: optionalString,
1133
+ instructions: optionalString,
1134
+ skills: z.array(z.record(z.string(), z.unknown())).optional(),
1135
+ services: z.array(z.record(z.string(), z.unknown())).optional(),
1136
+ delegationInstructions: optionalString,
1137
+ delegationSkills: z.array(z.record(z.string(), z.unknown())).optional()
1138
+ })
1139
+ .passthrough(),
1140
+ zeus_url_fetch: z
1141
+ .object({
1142
+ url: z.string(),
1143
+ maxChars: optionalNumber
1144
+ })
1145
+ .passthrough(),
1146
+ zeus_execute_javascript: z
1147
+ .object({
1148
+ script: z.string(),
1149
+ input: z.record(z.string(), z.unknown()).optional(),
1150
+ timeoutMs: optionalNumber
1151
+ })
1152
+ .passthrough(),
1153
+ execute_javascript_code: z
1154
+ .object({
1155
+ script: z.string(),
1156
+ input: z.record(z.string(), z.unknown()).optional(),
1157
+ timeoutMs: optionalNumber
1158
+ })
1159
+ .passthrough(),
1160
+ zeus_skill_tools_list: z
1161
+ .object({
1162
+ identifier: optionalString,
1163
+ limit: optionalNumber,
1164
+ offset: optionalNumber,
1165
+ agentWallet: optionalString
1166
+ })
1167
+ .passthrough(),
1168
+ zeus_skill_tool_execute: z
1169
+ .object({
1170
+ qHash: z.string(),
1171
+ input: z.record(z.string(), z.unknown()).optional(),
1172
+ timeoutMs: optionalNumber
1173
+ })
1174
+ .passthrough(),
1175
+ zeus_session_receipt_create: z
1176
+ .object({
1177
+ walletAddress: optionalString,
1178
+ title: optionalString,
1179
+ content: z.string(),
1180
+ tags: z.array(z.string()).optional(),
1181
+ asPrivate: optionalBoolean
1182
+ })
1183
+ .passthrough(),
1184
+ zeus_session_receipt_append: z
1185
+ .object({
1186
+ qHash: z.string(),
1187
+ walletAddress: optionalString,
1188
+ appendContent: z.string(),
1189
+ title: optionalString,
1190
+ asPrivate: optionalBoolean
1191
+ })
1192
+ .passthrough(),
1193
+ list_tools: z
1194
+ .object({
1195
+ identifier: optionalString,
1196
+ limit: optionalNumber,
1197
+ offset: optionalNumber,
1198
+ agentWallet: optionalString,
1199
+ q: optionalString,
1200
+ searchQuery: optionalString,
1201
+ tagPrefix: optionalString,
1202
+ tagContains: optionalString,
1203
+ tagPrefixesAll: optionalString
1204
+ })
1205
+ .passthrough(),
1206
+ list_all_available_tools: z
1207
+ .object({})
1208
+ .passthrough(),
1209
+ list_memories: z
1210
+ .object({
1211
+ identifier: optionalString,
1212
+ limit: optionalNumber,
1213
+ offset: optionalNumber,
1214
+ agentWallet: optionalString,
1215
+ q: optionalString,
1216
+ searchQuery: optionalString,
1217
+ tagPrefix: optionalString,
1218
+ tagContains: optionalString,
1219
+ tagPrefixesAll: optionalString
1220
+ })
1221
+ .passthrough(),
1222
+ list_research: z
1223
+ .object({
1224
+ identifier: optionalString,
1225
+ limit: optionalNumber,
1226
+ offset: optionalNumber,
1227
+ agentWallet: optionalString,
1228
+ q: optionalString,
1229
+ searchQuery: optionalString,
1230
+ tagPrefix: optionalString,
1231
+ tagContains: optionalString,
1232
+ tagPrefixesAll: optionalString
1233
+ })
1234
+ .passthrough(),
1235
+ list_instructions: z
1236
+ .object({
1237
+ identifier: optionalString,
1238
+ limit: optionalNumber,
1239
+ offset: optionalNumber,
1240
+ agentWallet: optionalString,
1241
+ q: optionalString,
1242
+ searchQuery: optionalString,
1243
+ tagPrefix: optionalString,
1244
+ tagContains: optionalString,
1245
+ tagPrefixesAll: optionalString
1246
+ })
1247
+ .passthrough(),
1248
+ list_rules: z
1249
+ .object({
1250
+ identifier: optionalString,
1251
+ limit: optionalNumber,
1252
+ offset: optionalNumber,
1253
+ agentWallet: optionalString,
1254
+ q: optionalString,
1255
+ searchQuery: optionalString,
1256
+ tagPrefix: optionalString,
1257
+ tagContains: optionalString,
1258
+ tagPrefixesAll: optionalString
1259
+ })
1260
+ .passthrough(),
1261
+ list_mcps: z
1262
+ .object({
1263
+ identifier: optionalString,
1264
+ limit: optionalNumber,
1265
+ offset: optionalNumber,
1266
+ agentWallet: optionalString,
1267
+ q: optionalString,
1268
+ searchQuery: optionalString,
1269
+ tagPrefix: optionalString,
1270
+ tagContains: optionalString,
1271
+ tagPrefixesAll: optionalString
1272
+ })
1273
+ .passthrough(),
1274
+ get_tool: z
1275
+ .object({ qHash: optionalString, id: optionalString })
1276
+ .passthrough(),
1277
+ get_skill: z
1278
+ .object({ qHash: optionalString, id: optionalString })
1279
+ .passthrough(),
1280
+ get_memory: z
1281
+ .object({ qHash: optionalString, id: optionalString })
1282
+ .passthrough(),
1283
+ get_instruction: z
1284
+ .object({ qHash: optionalString, id: optionalString })
1285
+ .passthrough(),
1286
+ get_rule: z
1287
+ .object({ qHash: optionalString, id: optionalString })
1288
+ .passthrough(),
1289
+ get_research: z
1290
+ .object({ qHash: optionalString, id: optionalString })
1291
+ .passthrough(),
1292
+ append_Content_to_memory: z
1293
+ .object({
1294
+ qHash: z.string(),
1295
+ appendContent: optionalString,
1296
+ content: optionalString,
1297
+ walletAddress: optionalString,
1298
+ asPrivate: optionalBoolean,
1299
+ indexFacetKeys: z.array(z.string()).optional()
1300
+ })
1301
+ .passthrough(),
1302
+ delete_revoke_memory: z
1303
+ .object({ qHash: z.string(), walletAddress: optionalString })
1304
+ .passthrough(),
1305
+ list_agents: z
1306
+ .object({
1307
+ identifier: optionalString,
1308
+ limit: optionalNumber,
1309
+ agentWallet: optionalString
1310
+ })
1311
+ .passthrough(),
1312
+ execute_agent_command: z
1313
+ .object({
1314
+ prompt: z.string(),
1315
+ agentWallet: optionalString,
1316
+ agentId: optionalString,
1317
+ model: optionalString,
1318
+ systemPrompt: optionalString,
1319
+ temperature: optionalNumber
1320
+ })
1321
+ .passthrough(),
1322
+ stop_agent_processing: z
1323
+ .object({ requestId: z.string() })
1324
+ .passthrough(),
1325
+ encrypt_text: z
1326
+ .object({ text: z.string() })
1327
+ .passthrough(),
1328
+ decrypt_text: z
1329
+ .object({ text: z.string() })
1330
+ .passthrough(),
1331
+ mcp_connection_create: z
1332
+ .object({
1333
+ id: optionalString,
1334
+ type: optionalString,
1335
+ url: optionalString,
1336
+ command: optionalString,
1337
+ args: z.array(z.string()).optional(),
1338
+ env: z.record(z.string(), z.unknown()).optional(),
1339
+ headers: z.record(z.string(), z.unknown()).optional(),
1340
+ description: optionalString,
1341
+ enabled: optionalBoolean
1342
+ })
1343
+ .passthrough(),
1344
+ mcp_connection_modify: z
1345
+ .object({
1346
+ mcpConnections: z.array(z.record(z.string(), z.unknown())).optional(),
1347
+ connection: z.record(z.string(), z.unknown())
1348
+ })
1349
+ .passthrough(),
1350
+ mcp_connection_delete: z
1351
+ .object({ id: z.string(), mcpConnections: z.array(z.record(z.string(), z.unknown())).optional() })
1352
+ .passthrough()
1353
+ };
1354
+
1355
+ /**
1356
+ * Normalize verifier ID input without aliases.
1357
+ * Backend verifier registry is the single source of truth for accepted IDs.
1358
+ * @param {string} verifierId - Backend verifier ID
1359
+ * @returns {string} Trimmed verifier ID
1360
+ */
1361
+ function normalizeVerifierId(verifierId) {
1362
+ if (!verifierId || typeof verifierId !== 'string') return verifierId;
1363
+ return verifierId.trim();
1364
+ }
1365
+
1366
+ /**
1367
+ * Map GET /api/v1/verification/verifiers JSON body to MCP context rows.
1368
+ * API shape: { success, data: string[], metadata: { [id]: { ... } }, meta?: { ... } }
1369
+ */
1370
+ function mapVerifierCatalogToContextList(apiBody) {
1371
+ if (!apiBody || apiBody.error) return [];
1372
+ const raw = apiBody.data;
1373
+ const ids = Array.isArray(raw)
1374
+ ? raw
1375
+ : raw && typeof raw === 'object' && Array.isArray(raw.verifiers)
1376
+ ? raw.verifiers
1377
+ : [];
1378
+ const metadata =
1379
+ apiBody.metadata && typeof apiBody.metadata === 'object' && !Array.isArray(apiBody.metadata)
1380
+ ? apiBody.metadata
1381
+ : {};
1382
+ const out = [];
1383
+ for (const verifierId of ids) {
1384
+ const id = String(verifierId || '').trim();
1385
+ if (!id) continue;
1386
+ const meta = metadata[id] || {};
1387
+ const ds = meta.dataSchema && typeof meta.dataSchema === 'object' ? meta.dataSchema : {};
1388
+ const requiredFields = Array.isArray(ds.required) ? ds.required.filter((x) => typeof x === 'string') : [];
1389
+ out.push({
1390
+ id,
1391
+ category: meta.category,
1392
+ description: meta.description,
1393
+ flowType: meta.flowType,
1394
+ expiryType: meta.expiryType,
1395
+ requiredFields
1396
+ });
1397
+ }
1398
+ return out;
1399
+ }
1400
+
1401
+
1402
+ function sanitizePublicMcpCopy(value) {
1403
+ if (value == null) return undefined;
1404
+ const s = String(value).trim();
1405
+ if (!s) return undefined;
1406
+ if (/\b(qHash|zeus|hostinger|trust layer|verification mcp|runtime ledger|receipt chain|hub chain|\bhub\b)\b/i.test(s)) return undefined;
1407
+ return s;
1408
+ }
1409
+
1410
+ function buildVerifierSummaryForPublicContext(verifiersResult) {
1411
+ const rows = mapVerifierCatalogToContextList(verifiersResult);
1412
+ return rows.map((v) => {
1413
+ const row = { id: v.id };
1414
+ const desc = sanitizePublicMcpCopy(v.description);
1415
+ if (desc) row.description = desc.length > 480 ? `${desc.slice(0, 477)}...` : desc;
1416
+ const cat = sanitizePublicMcpCopy(v.category);
1417
+ if (cat) row.category = cat;
1418
+ const flow = sanitizePublicMcpCopy(v.flowType);
1419
+ if (flow) row.flowType = flow;
1420
+ if (Array.isArray(v.requiredFields) && v.requiredFields.length) row.requiredFields = v.requiredFields;
1421
+ return row;
1422
+ });
1423
+ }
1424
+
1425
+
1426
+ function buildPublicNeusContextPayload(ctx, _verifiersResult) {
1427
+ const hasBearer = !!ctx?.isAuthenticated;
1428
+ const verifierSummary = buildVerifierSummaryForPublicContext(_verifiersResult);
1429
+
1430
+ return {
1431
+ product: {
1432
+ name: 'NEUS',
1433
+ mcpName: 'NEUS MCP',
1434
+ headline:
1435
+ 'NEUS turns verification into reusable proof receipts for apps, agents, gates, policies, payments, and trusted actions.',
1436
+ summary:
1437
+ 'Create, check, read, and reuse proof receipts across apps, APIs, agents, wallets, people, organizations, and workflows.',
1438
+ receiptModel:
1439
+ 'A proof receipt is the durable record. Verification is how a receipt is created or refreshed.'
1440
+ },
1441
+
1442
+ setup: {
1443
+ mcpEndpoint: 'https://mcp.neus.network/mcp',
1444
+ profileUrl: 'https://neus.network/profile',
1445
+ accessKeysUrl: 'https://neus.network/profile?tab=account',
1446
+ hostedVerifyUrl: 'https://neus.network/verify',
1447
+ proofUrlPattern: 'https://neus.network/proof/[proof-id]',
1448
+ cli: {
1449
+ install: 'npx -y -p @neus/sdk neus init',
1450
+ auth: 'npx -y -p @neus/sdk neus auth --access-key <npk_...>',
1451
+ status: 'npx -y -p @neus/sdk neus status --json'
1452
+ }
1453
+ },
1454
+
1455
+ mode: {
1456
+ current: hasBearer ? 'profile-authenticated' : 'public',
1457
+ public:
1458
+ 'No Profile access key is on this MCP session. You still get verifier lists, eligibility checks, public-scope proof reads, and hosted setup links only when something must be completed outside MCP.',
1459
+ profileAuthenticated:
1460
+ 'A Profile access key is configured. Reuse account proof receipts, delegated reads, and matching-wallet verification paths before asking for browser or signing steps.'
1461
+ },
1462
+
1463
+ goldenPath: [
1464
+ 'Call neus_context first.',
1465
+ 'If a Profile access key is configured, call neus_me.',
1466
+ 'For agents, call neus_agent_link before assuming delegation is ready.',
1467
+ 'Call neus_proofs_check for required verifier IDs before issuing anything.',
1468
+ 'If requirements are satisfied, continue without hosted verify.',
1469
+ 'Call neus_proofs_get when exact proof records, tags, Profile summary, delegated context, or qHashes are needed.',
1470
+ 'Call neus_verify only when Profile, wallet, and signing prerequisites already match.',
1471
+ 'Call neus_verify_or_guide only when proof, profile, payment, provider, wallet, or delegation setup is missing.'
1472
+ ],
1473
+
1474
+ jobs: [
1475
+ {
1476
+ intent: 'Work with NEUS from chat',
1477
+ use: ['neus_context'],
1478
+ result:
1479
+ 'Load the current mode, verifier summary, and reuse-first order before choosing a tool.'
1480
+ },
1481
+ {
1482
+ intent: 'Connect a NEUS Profile',
1483
+ use: ['neus_me'],
1484
+ result:
1485
+ 'Run after adding a Profile access key. If it fails, create an access key from setup, run the CLI auth snippet, then retry neus_me.'
1486
+ },
1487
+ {
1488
+ intent: 'Check prerequisites',
1489
+ use: ['neus_proofs_check'],
1490
+ result: 'Eligibility snapshot only; it never creates proofs or opens hosted flows.'
1491
+ },
1492
+ {
1493
+ intent: 'Reuse a receipt',
1494
+ use: ['neus_proofs_check', 'neus_proofs_get'],
1495
+ result:
1496
+ 'If eligible, continue silently. Use qHashes returned by neus_proofs_get when you need receipt references.'
1497
+ },
1498
+ {
1499
+ intent: 'Finish missing setup',
1500
+ use: ['neus_verify_or_guide'],
1501
+ result:
1502
+ 'Use only after reuse checks show something is missing or requires browser-only consent, provider login, wallet signing, payment, or delegation setup.'
1503
+ },
1504
+ {
1505
+ intent: 'Understand existing proofs',
1506
+ use: ['neus_proofs_get'],
1507
+ result: 'Use returned proof records and qHashes; do not infer proof state from memory.'
1508
+ },
1509
+ {
1510
+ intent: 'Set up an agent',
1511
+ use: ['neus_agent_create', 'neus_agent_link'],
1512
+ result:
1513
+ 'Complete onboarding in order. Optional skills/services are descriptive metadata — they are never secret keys.'
1514
+ }
1515
+ ],
1516
+
1517
+ tools: {
1518
+ neus_context:
1519
+ 'Session home: setup, Profile access-key mode, verifier summaries, reuse-first workflow, tools, and safety rules. Always first.',
1520
+ neus_verifiers_catalog:
1521
+ 'Full verifier directory with schemas; after neus_context when you need payload fields beyond the summary.',
1522
+ neus_proofs_check: 'Checks existing proofs versus verifier IDs. Eligibility only; never mints proofs.',
1523
+ neus_verify:
1524
+ 'Creates or continues verification only when Profile, wallet, and signing already satisfy NEUS.',
1525
+ neus_verify_or_guide:
1526
+ 'Fallback when proof, profile, payment, provider, wallet, or delegation setup is missing; hosted URLs only when required.',
1527
+ neus_proofs_get:
1528
+ 'Proof records, tags, optional delegated reads, Profile fields, live state — IDs in the payload are authoritative.',
1529
+ neus_me:
1530
+ 'Signed-in Profile only when this MCP session has a personal access key from Profile — Account.',
1531
+ neus_agent_create:
1532
+ 'Prepares identity and delegation payloads; confirm with neus_agent_link after signing. Skills/services are metadata — not secrets.',
1533
+ neus_agent_link:
1534
+ 'Readiness for agent wallet: linked when identity and delegation proofs exist; otherwise next hosted step.'
1535
+ },
1536
+
1537
+ proofModel: {
1538
+ language: 'Prefer qHash, proof record, proof state, proof URL wording with people.',
1539
+ sourceOfTruth:
1540
+ 'Treat NEUS responses as authoritative. Never synthesize proofs, statuses, URLs, Profile fields, or tags.',
1541
+ hostedFallback:
1542
+ 'Use hosted links only when a NEUS tool returns one because the step cannot be completed inside MCP.'
1543
+ },
1544
+
1545
+ agentModel: {
1546
+ identity: 'Proof that pins the operational wallet plus how the agent introduces itself.',
1547
+ delegation: 'Proof showing which controller wallet trusts the agent wallet.',
1548
+ skills:
1549
+ 'Metadata rows that advertise capabilities — they never replace approvals, secrets, or real credentials.'
1550
+ },
1551
+
1552
+ safetyRules: [
1553
+ 'Re-query tools whenever facts might have changed — do not cache verifier outcomes, proofs, eligibility, URLs, Profile fields, or tags across long conversations.',
1554
+ 'Never ask anyone to paste private keys, NEUS personal access keys, wallet recovery phrases, API keys, OAuth tokens, passwords, passkeys, or session cookies.',
1555
+ 'When user action is required, share NEUS-hosted links returned by tools; do not manufacture URLs or prompt before reuse checks.',
1556
+ 'If richer access fails, remind people Profile access keys are created under Profile — Account and must be wired into their MCP client.',
1557
+ 'Prefer neus_verifiers_catalog for payload ambiguity and neus_proofs_get for live inventory — guesswork is forbidden.'
1558
+ ],
1559
+
1560
+ verifierSummary
1561
+ };
1562
+ }
1563
+
1564
+ // ============================================================================
1565
+ // ERROR REMEDIATION (actionable guidance for common errors)
1566
+ // ============================================================================
1567
+
1568
+ const ERROR_REMEDIATION = {
1569
+ 'VERIFIER_NOT_FOUND': {
1570
+ action: 'Re-open neus_verifiers_catalog and copy the verifier id verbatim.',
1571
+ hint: 'Identifiers are lowercase slugs supplied by NEUS — not guessed names.'
1572
+ },
1573
+ 'WALLET_NOT_FOUND': {
1574
+ action: 'Validate the wallet or DID formatting before retrying.',
1575
+ hint: 'EVM wallets start with 0x plus 40 hex characters; Solana addresses are base58; DIDs begin with did:pkh:'
1576
+ },
1577
+ 'CHAIN_CONTEXT_MISMATCH': {
1578
+ action: 'Make sure optional network hints match the wallet type you supplied.',
1579
+ hint: 'EVM tooling typically uses CAIP-style eip155 references; Solana uses solana:mainnet.'
1580
+ },
1581
+ 'SIGNATURE_REQUIRED': {
1582
+ action: 'The approver still needs to sign — use neus_verify_or_guide if they should complete steps in-browser.',
1583
+ hint: 'NEUS returns signer text on the preparation response; finalize with exactly one follow-up call.'
1584
+ },
1585
+ 'SESSION_EXPIRED': {
1586
+ action: 'Ask the participant to reopen https://neus.network/verify and finish sign-in.',
1587
+ hint: 'Short-lived auth codes expire quickly; fresh sessions pick up afterward.'
1588
+ },
1589
+ 'RATE_LIMITED': {
1590
+ action: 'Pause briefly before retrying, or elevate to a billing-backed path if quotas demand it.',
1591
+ hint: 'NEUS rate limits defend shared infrastructure.'
1592
+ },
1593
+ 'INSUFFICIENT_CREDITS': {
1594
+ action: 'Fund the workspace or retry after NEUS acknowledges payment.',
1595
+ hint: 'Credits cover verification throughput — see billing messaging from NEUS APIs.'
1596
+ },
1597
+ 'PAYMENT_REQUIRED': {
1598
+ action: 'Follow the billing handshake NEUS returns, then resubmit unchanged parameters.',
1599
+ hint: 'Use the hosted billing link if one is surfaced in the payload.'
1600
+ },
1601
+ 'VALIDATION_ERROR': {
1602
+ action: 'Align arguments with schemas from tools/list and neus_verifiers_catalog.',
1603
+ hint: 'Missing required fields appear on the verifier entry.'
1604
+ },
1605
+ 'AUTH_REQUIRED': {
1606
+ action: 'Add a NEUS personal access key to the MCP Authorization header or move the participant to guided verification.',
1607
+ hint: 'Create keys from Profile — Account and configure the client exactly as documented.'
1608
+ }
1609
+ };
1610
+
1611
+ // ============================================================================
1612
+ // CHAIN RESOLUTION — defaults from deployment configuration
1613
+ // ============================================================================
1614
+
1615
+ const HUB_CHAIN_ID = mcp.hubChainId;
1616
+ const HUB_CHAIN = `eip155:${HUB_CHAIN_ID}`;
1617
+ const SOLANA_MAINNET = 'solana:mainnet';
1618
+ /**
1619
+ * Detect wallet type from address format
1620
+ * @param {string} address - Wallet address
1621
+ * @returns {'evm'|'solana'|'unknown'}
1622
+ */
1623
+ function detectWalletType(address) {
1624
+ if (!address || typeof address !== 'string') return 'unknown';
1625
+ const trimmed = address.trim();
1626
+ // EVM: 0x followed by 40 hex chars
1627
+ if (/^0x[a-fA-F0-9]{40}$/i.test(trimmed)) return 'evm';
1628
+ // Solana: base58, typically 32-44 chars, no 0x prefix
1629
+ if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(trimmed)) return 'solana';
1630
+ return 'unknown';
1631
+ }
1632
+
1633
+ /**
1634
+ * Resolve identifier (wallet or DID) to DID and wallet address
1635
+ * @param {string} identifier - Wallet address or DID
1636
+ * @returns {{ did: string, walletAddress: string } | { error: string }}
1637
+ */
1638
+ function resolveIdentifierToDid(identifier) {
1639
+ if (!identifier || typeof identifier !== 'string') return { error: 'identifier is required' };
1640
+ const trimmed = identifier.trim();
1641
+ if (trimmed.startsWith('did:pkh:')) {
1642
+ const parts = trimmed.split(':');
1643
+ if (parts.length >= 5) {
1644
+ const namespace = parts[2];
1645
+ const walletAddress = namespace === 'eip155'
1646
+ ? parts.slice(4).join(':').toLowerCase()
1647
+ : parts.slice(4).join(':');
1648
+ return { did: trimmed, walletAddress };
1649
+ }
1650
+ }
1651
+ const walletType = detectWalletType(trimmed);
1652
+ if (walletType === 'evm') {
1653
+ const walletAddress = trimmed.toLowerCase();
1654
+ return { did: `did:pkh:eip155:${HUB_CHAIN_ID}:${walletAddress}`, walletAddress };
1655
+ }
1656
+ if (walletType === 'solana') {
1657
+ return { did: `did:pkh:solana:mainnet:${trimmed}`, walletAddress: trimmed };
1658
+ }
1659
+ return { error: 'Invalid identifier format. Use EVM address (0x...), Solana address, or DID (did:pkh:...)' };
1660
+ }
1661
+
1662
+ /**
1663
+ * Resolve CAIP-2 chain identifier from wallet address
1664
+ * - EVM addresses default to the configured protocol hub chain
1665
+ * - Solana addresses use solana:mainnet
1666
+ * - Explicit chain parameter takes precedence if valid
1667
+ * @param {string} walletAddress
1668
+ * @param {string} [explicitChain] - Optional explicit CAIP-2 chain
1669
+ * @returns {string} CAIP-2 chain identifier
1670
+ */
1671
+ function resolveChain(walletAddress, explicitChain) {
1672
+ // If explicit chain provided and valid, use it
1673
+ if (typeof explicitChain === 'string' && explicitChain.trim().length > 0) {
1674
+ return explicitChain.trim();
1675
+ }
1676
+ // Auto-detect from wallet address
1677
+ const walletType = detectWalletType(walletAddress);
1678
+ if (walletType === 'solana') return SOLANA_MAINNET;
1679
+ // Default to EVM hub chain
1680
+ return HUB_CHAIN;
1681
+ }
1682
+
1683
+ function normalizeWalletForMcp(address) {
1684
+ if (!address || typeof address !== 'string') return null;
1685
+ const trimmed = address.trim();
1686
+ const walletType = detectWalletType(trimmed);
1687
+ if (walletType === 'evm') return trimmed.toLowerCase();
1688
+ if (walletType === 'solana') return trimmed;
1689
+ return trimmed;
1690
+ }
1691
+
1692
+ function mcpAgentDelegationHeaders(agentWallet) {
1693
+ const aw = agentWallet ? normalizeWalletForMcp(String(agentWallet)) : null;
1694
+ return aw ? { 'x-agent-wallet': aw } : {};
1695
+ }
1696
+
1697
+ function normalizeStringArray(input) {
1698
+ if (!Array.isArray(input)) return [];
1699
+ return input.map((value) => (typeof value === 'string' ? value.trim() : '')).filter(Boolean);
1700
+ }
1701
+
1702
+ function scopeToProofTagsQuery(scopeRaw) {
1703
+ const s = String(scopeRaw || '').trim().toLowerCase();
1704
+ if (!s) return '';
1705
+ if (s === 'memory') return 'memory';
1706
+ if (s === 'research') return 'research';
1707
+ if (s === 'rules') return 'rule,rules';
1708
+ if (s === 'instruction') return 'instruction,instructions';
1709
+ if (s === 'tools') return 'tool,skill,agent:tool,agent:skill';
1710
+ if (s === 'receipts' || s === 'receipt') return 'receipt:chat,receipt:session,receipt:reference';
1711
+ return '';
1712
+ }
1713
+
1714
+ function proofOptionsForVisibility(visibility) {
1715
+ if (visibility === 'private') return { privacyLevel: 'private', publicDisplay: false };
1716
+ if (visibility === 'public') return { privacyLevel: 'public', publicDisplay: true };
1717
+ return { privacyLevel: 'public', publicDisplay: false };
1718
+ }
1719
+
1720
+ function isBlockedZeusFetchHost(hostname) {
1721
+ const h = String(hostname || '').toLowerCase().replace(/^\[|\]$/g, '');
1722
+ if (h === 'localhost' || h === '0.0.0.0' || h === '::1' || h === '::' || h === 'metadata.google.internal') return true;
1723
+ if (h.endsWith('.local') || h.endsWith('.lan') || h.endsWith('.internal')) return true;
1724
+ if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
1725
+ if (/^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
1726
+ if (/^192\.168\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
1727
+ if (/^172\.(1[6-9]|2[0-9]|3[0-1])\.\d{1,3}\.\d{1,3}$/.test(h)) return true;
1728
+ if (h.startsWith('169.254.') || h.startsWith('fe80:') || h.startsWith('fc00:') || h.startsWith('fd')) return true;
1729
+ return false;
1730
+ }
1731
+
1732
+ function getProofQHash(proof) {
1733
+ const raw = String(proof?.qHash || proof?.id || '').trim().toLowerCase();
1734
+ if (!raw) return '';
1735
+ const with0x = raw.startsWith('0x') ? raw : `0x${raw}`;
1736
+ return /^0x[a-f0-9]{64}$/.test(with0x) ? with0x : '';
1737
+ }
1738
+
1739
+ function getProofTitle(proof) {
1740
+ return String(proof?.title || proof?.meta?.title || proof?.reference?.title || proof?.verifierLabel || proof?.verifierId || 'Proof').trim();
1741
+ }
1742
+
1743
+ function getProofTags(proof) {
1744
+ const sources = [proof?.meta?.tags, proof?.tags, proof?.options?.tags];
1745
+ return Array.from(new Set(sources.flatMap((value) => (Array.isArray(value) ? value : typeof value === 'string' ? [value] : [])).map(String).map((x) => x.trim()).filter(Boolean)));
1746
+ }
1747
+
1748
+ function getProofContentText(rawProof) {
1749
+ const proof = rawProof?.data && typeof rawProof.data === 'object' ? rawProof.data : rawProof;
1750
+ const candidates = [
1751
+ proof?.content,
1752
+ proof?.data?.content,
1753
+ proof?.publicContent?.content,
1754
+ proof?.privateContent?.content,
1755
+ proof?.reference?.description,
1756
+ proof?.meta?.description
1757
+ ];
1758
+ for (const candidate of candidates) {
1759
+ if (typeof candidate === 'string' && candidate.trim()) return candidate;
1760
+ }
1761
+ return '';
1762
+ }
1763
+
1764
+ /**
1765
+ * Unwrap BFF-style proof record { data: { ... } } for tag/qHash reads.
1766
+ * @param {unknown} raw
1767
+ * @returns {object|null}
1768
+ */
1769
+ function unwrapApiProofRecord(raw) {
1770
+ if (!raw || typeof raw !== 'object') return null;
1771
+ if (raw.data && typeof raw.data === 'object' && !Array.isArray(raw.data)) {
1772
+ const d = /** @type {Record<string, unknown>} */ (raw.data);
1773
+ return { ...raw, ...d, qHash: d.qHash || raw.qHash, meta: d.meta || raw.meta, options: d.options || raw.options };
1774
+ }
1775
+ return /** @type {object} */ (raw);
1776
+ }
1777
+
1778
+ /**
1779
+ * @param {string} t
1780
+ * @param {'tool'|'skill'|'memory'|'research'|'instruction'|'rule'} category
1781
+ */
1782
+ function tagMatchesCategory(t, category) {
1783
+ const s = String(t || '')
1784
+ .trim()
1785
+ .toLowerCase();
1786
+ if (!s) return false;
1787
+ switch (category) {
1788
+ case 'tool':
1789
+ return s === 'tool' || s === 'agent:tool' || s.startsWith('tool:');
1790
+ case 'skill':
1791
+ return s === 'skill' || s === 'agent:skill' || s.startsWith('skill:');
1792
+ case 'memory':
1793
+ return s === 'memory' || s.startsWith('memory:');
1794
+ case 'research':
1795
+ return s === 'research' || s.startsWith('research:');
1796
+ case 'instruction':
1797
+ return s === 'instruction' || s === 'instructions' || s.startsWith('instruction:');
1798
+ case 'rule':
1799
+ return s === 'rule' || s === 'rules' || s.startsWith('rules:');
1800
+ default:
1801
+ return false;
1802
+ }
1803
+ }
1804
+
1805
+ /**
1806
+ * @param {unknown} rawProof
1807
+ * @param {'tool'|'skill'|'memory'|'research'|'instruction'|'rule'} category
1808
+ */
1809
+ function proofMatchesCategory(rawProof, category) {
1810
+ const p = unwrapApiProofRecord(rawProof) || (typeof rawProof === 'object' && rawProof ? rawProof : null);
1811
+ if (!p) return false;
1812
+ const tags = getProofTags(p);
1813
+ for (const t of tags) {
1814
+ if (tagMatchesCategory(t, category)) return true;
1815
+ }
1816
+ return false;
1817
+ }
1818
+
1819
+ /**
1820
+ * @param {Record<string, unknown>|undefined} rec
1821
+ * @returns {object|null}
1822
+ */
1823
+ function normalizeMcpConnectionEntry(rec) {
1824
+ if (!rec || typeof rec !== 'object') return null;
1825
+ const id = String(rec.id || '').trim();
1826
+ const command = String(rec.command || '').trim();
1827
+ const url = String(rec.url || '').trim();
1828
+ const inferred = command ? 'stdio' : url ? 'streamablehttp' : '';
1829
+ const typeRaw = String(rec.type || inferred || '')
1830
+ .trim()
1831
+ .toLowerCase();
1832
+ if (!id) return null;
1833
+ if (typeRaw !== 'streamablehttp' && typeRaw !== 'stdio') return null;
1834
+ const type = typeRaw === 'streamablehttp' ? 'streamableHttp' : 'stdio';
1835
+ const out = { id, type };
1836
+ if (url) out.url = url;
1837
+ if (command) out.command = command;
1838
+ const a = normalizeStringArray(rec.args);
1839
+ if (a.length) out.args = a;
1840
+ const env = rec.env;
1841
+ if (env && typeof env === 'object' && !Array.isArray(env)) {
1842
+ const e = /** @type {Record<string, string>} */ ({});
1843
+ for (const [k, v] of Object.entries(/** @type {Record<string, unknown>} */ (env))) {
1844
+ const key = String(k).trim();
1845
+ if (!key) continue;
1846
+ e[key] = v == null ? '' : String(v);
1847
+ }
1848
+ if (Object.keys(e).length) out.env = e;
1849
+ const headers = rec.headers;
1850
+ if (headers && typeof headers === 'object' && !Array.isArray(headers)) {
1851
+ const h = /** @type {Record<string, string>} */ ({});
1852
+ for (const [k, v] of Object.entries(/** @type {Record<string, unknown>} */ (headers))) {
1853
+ const key = String(k).trim();
1854
+ if (!key) continue;
1855
+ h[key] = v == null ? '' : String(v);
1856
+ }
1857
+ if (Object.keys(h).length) out.headers = h;
1858
+ }
1859
+ }
1860
+ const description = String(rec.description || '').trim();
1861
+ if (description) out.description = description;
1862
+ if (typeof rec.enabled === 'boolean') out.enabled = rec.enabled;
1863
+ return out;
1864
+ }
1865
+
1866
+ /**
1867
+ * @param {ReturnType<typeof createSessionCtx>} ctx
1868
+ * @param {object} args
1869
+ * @param {string} tagsComma
1870
+ */
1871
+ async function fetchProofTitleIdMap(ctx, args, tagsComma) {
1872
+ const identifier = String(args?.identifier || '').trim() || (await resolveAuthenticatedWallet(ctx));
1873
+ if (!identifier) {
1874
+ return { error: 'validation_error', message: 'identifier or Bearer profile wallet required' };
1875
+ }
1876
+ const limit = Math.min(Math.max(1, Number(args?.limit) || 200), 250);
1877
+ const offset = Math.max(0, Number(args?.offset) || 0);
1878
+ const query = new URLSearchParams({ limit: String(limit), offset: String(offset) });
1879
+ if (tagsComma) {
1880
+ const normalized = tagsComma
1881
+ .split(',')
1882
+ .map((t) => t.trim().toLowerCase())
1883
+ .filter(Boolean)
1884
+ .join(',');
1885
+ if (normalized) query.set('tags', normalized);
1886
+ }
1887
+ const qRaw = String(args?.q || args?.searchQuery || '').trim();
1888
+ if (qRaw) query.set('q', qRaw.slice(0, 280));
1889
+ if (!ctx.isAuthenticated) query.set('isPublicRead', '1');
1890
+ const result = await ctx.callApi(
1891
+ `/api/v1/proofs/by-wallet/${encodeURIComponent(identifier)}?${query.toString()}`,
1892
+ { headers: mcpAgentDelegationHeaders(args?.agentWallet) }
1893
+ );
1894
+ if (result?.error) return result;
1895
+ const proofs = Array.isArray(result?.data?.proofs)
1896
+ ? result.data.proofs
1897
+ : Array.isArray(result?.proofs)
1898
+ ? result.proofs
1899
+ : Array.isArray(result?.data)
1900
+ ? result.data
1901
+ : [];
1902
+ const tagPrefix = String(args?.tagPrefix || '')
1903
+ .trim()
1904
+ .toLowerCase();
1905
+ const tagContains = String(args?.tagContains || '')
1906
+ .trim()
1907
+ .toLowerCase();
1908
+ const tagPrefixesAll = String(args?.tagPrefixesAll || '')
1909
+ .trim()
1910
+ .toLowerCase();
1911
+ const dict = Object.create(null);
1912
+ const items = [];
1913
+ for (const pr of proofs) {
1914
+ const id = getProofQHash(pr);
1915
+ const title = getProofTitle(pr);
1916
+ if (!id) continue;
1917
+ const tags = getProofTags(pr).map((t) => String(t).toLowerCase());
1918
+ if (tagPrefix && !tags.some((t) => t.startsWith(tagPrefix))) continue;
1919
+ if (tagContains && !tags.some((t) => t.includes(tagContains))) continue;
1920
+ if (tagPrefixesAll && !proofTagsMatchAllPrefixes(tags, tagPrefixesAll)) continue;
1921
+ items.push({ id, title, tags: getProofTags(pr) });
1922
+ dict[title] = id;
1923
+ }
1924
+ return { success: true, items, dictionary: dict, count: items.length };
1925
+ }
1926
+
1927
+ /**
1928
+ * @param {unknown} raw
1929
+ * @returns {string} normalized 0x+64 or ''
1930
+ */
1931
+ function normalizeQHashInput(raw) {
1932
+ const a = String(raw || '').trim();
1933
+ if (!a) return '';
1934
+ const lower = a.toLowerCase();
1935
+ const h = lower.startsWith('0x') ? lower : `0x${lower.replace(/^0x/i, '')}`;
1936
+ return /^0x[a-f0-9]{64}$/.test(h) ? h : '';
1937
+ }
1938
+
1939
+ /**
1940
+ * @param {object} args
1941
+ * @param {ReturnType<typeof createSessionCtx>} ctx
1942
+ * @param {'tool'|'skill'|'memory'|'research'|'instruction'|'rule'} category
1943
+ */
1944
+ async function getCategorizedProofByHash(args, ctx, category) {
1945
+ const qHash = normalizeQHashInput(args?.qHash || args?.id);
1946
+ if (!qHash) return { error: 'validation_error', message: 'qHash or id (0x + 64 hex chars) is required' };
1947
+ const raw = await ctx.callApi(`/api/v1/proofs/${encodeURIComponent(qHash)}`);
1948
+ if (raw?.error) return raw;
1949
+ if (!proofMatchesCategory(raw, category)) {
1950
+ return { error: 'validation_error', message: `Proof is not in category "${category}" (check tags).` };
1951
+ }
1952
+ const un = unwrapApiProofRecord(raw) || raw;
1953
+ return {
1954
+ success: true,
1955
+ qHash: getProofQHash(un) || qHash,
1956
+ title: getProofTitle(un),
1957
+ tags: getProofTags(un),
1958
+ content: getProofContentText(un),
1959
+ record: un
1960
+ };
1961
+ }
1962
+
1963
+ function parseSkillToolProofSections(rawContent) {
1964
+ const content = String(rawContent || '');
1965
+ const match = /(?:^|\n)-{5,}\s*\n/.exec(content);
1966
+ if (!match) return { summaryMarkdown: content.trim(), script: '', separatorFound: false };
1967
+ const splitAt = match.index + (match[0].startsWith('\n') ? 1 : 0);
1968
+ const separatorLength = match[0].length - (match[0].startsWith('\n') ? 1 : 0);
1969
+ return {
1970
+ summaryMarkdown: content.slice(0, splitAt).trim(),
1971
+ script: content.slice(splitAt + separatorLength).trim(),
1972
+ separatorFound: true
1973
+ };
1974
+ }
1975
+
1976
+ /**
1977
+ * Run tool/proof JavaScript: async/await, optional require('nodemailer'), no full process.env (Gmail keys only).
1978
+ * @param {string} script
1979
+ * @param {unknown} input
1980
+ * @param {number|undefined} timeoutMs
1981
+ * @returns {Promise<{ value: unknown, logs: string[] }>}
1982
+ */
1983
+ async function runZeusSandbox(script, input, timeoutMs) {
1984
+ const safeTimeout = Math.max(250, Math.min(15000, Number(timeoutMs) || 4000));
1985
+ const logs = [];
1986
+ const sandbox = {
1987
+ input: input && typeof input === 'object' ? input : {},
1988
+ result: undefined,
1989
+ require: requireInZeusSandbox,
1990
+ process: { env: zeusSandboxGmailProcessEnv() },
1991
+ console: {
1992
+ log: (...a) => logs.push(a.map((x) => String(x)).join(" ")),
1993
+ warn: (...a) => logs.push(`[warn] ${a.map((x) => String(x)).join(" ")}`),
1994
+ error: (...a) => logs.push(`[error] ${a.map((x) => String(x)).join(" ")}`),
1995
+ },
1996
+ // Expose Node.js built-ins available from Node 18+ so scripts can call external APIs (Brave Search, etc.)
1997
+ // while still being restricted by requireInZeusSandbox on module imports.
1998
+ fetch: globalThis.fetch,
1999
+ URL: globalThis.URL,
2000
+ };
2001
+ const context = vm.createContext(sandbox, { name: 'zeus-mcp-tool' });
2002
+ const userSource = String(script || '');
2003
+ const wrapped = `(async () => {\n${userSource}\n})();`;
2004
+ const scriptObject = new vm.Script(wrapped, { filename: 'zeus-mcp-execute.mjs' });
2005
+ let runResult;
2006
+ try {
2007
+ runResult = scriptObject.runInContext(context, { timeout: safeTimeout });
2008
+ } catch (e) {
2009
+ const message = e instanceof Error ? e.message : String(e);
2010
+ throw new Error(`Script parse or sync error: ${message}`);
2011
+ }
2012
+ const output = await Promise.race([
2013
+ Promise.resolve(runResult),
2014
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Script timed out')), safeTimeout))
2015
+ ]);
2016
+ const finalOutput = typeof output === 'undefined' ? sandbox.result : output;
2017
+ return { value: finalOutput, logs };
2018
+ }
2019
+
2020
+ async function resolveReceiptWallet(args, ctx) {
2021
+ const explicit = normalizeWalletForMcp(String(args?.walletAddress || '').trim());
2022
+ if (explicit) return explicit;
2023
+ const authed = await resolveAuthenticatedWallet(ctx);
2024
+ return authed || null;
2025
+ }
2026
+
2027
+ /** Profile wallet from Bearer PAT or cookie — both hit /auth/me; same principal, not an extra session layer. */
2028
+ async function resolveAuthenticatedWallet(ctx) {
2029
+ if (!ctx?.isAuthenticated) return null;
2030
+ try {
2031
+ const meResult = await ctx.callApi('/api/v1/auth/me');
2032
+ if (typeof meResult?.data?.address === 'string' && meResult.data.address.trim()) {
2033
+ return normalizeWalletForMcp(meResult.data.address);
2034
+ }
2035
+ if (typeof meResult?.data?.primaryAccount === 'string' && meResult.data.primaryAccount.trim()) {
2036
+ return normalizeWalletForMcp(meResult.data.primaryAccount);
2037
+ }
2038
+ } catch {
2039
+ // Ignore lookup failures and fall back to explicit inputs.
2040
+ }
2041
+ return null;
2042
+ }
2043
+
2044
+ async function resolvePrincipalWallet(principal, ctx) {
2045
+ if (typeof principal === 'string' && principal.trim()) {
2046
+ const resolved = resolveIdentifierToDid(principal.trim());
2047
+ if (resolved?.error) {
2048
+ return { error: 'validation_error', message: resolved.error };
2049
+ }
2050
+ return { walletAddress: normalizeWalletForMcp(resolved.walletAddress) };
2051
+ }
2052
+
2053
+ return { walletAddress: await resolveAuthenticatedWallet(ctx) };
2054
+ }
2055
+
2056
+ function buildAgentHostedVerifyUrl(agentWallet, controllerWallet = null) {
2057
+ const params = new URLSearchParams({
2058
+ verifiers: 'agent-identity,agent-delegation',
2059
+ agentWallet: normalizeWalletForMcp(agentWallet) || String(agentWallet || '').trim()
2060
+ });
2061
+ if (controllerWallet) {
2062
+ params.set('controllerWallet', controllerWallet);
2063
+ }
2064
+ return `https://neus.network/verify?${params.toString()}`;
2065
+ }
2066
+
2067
+ // ============================================================================
2068
+ // API HELPERS
2069
+ // ============================================================================
2070
+
2071
+ /**
2072
+ * Call NEUS API with proper headers and timeout.
2073
+ * Auth resolution order: options.bearerToken > process-global NEUS_AUTH_TOKEN.
2074
+ */
2075
+ async function callNeusApi(endpoint, options = {}) {
2076
+ const url = `${NEUS_API_URL}${endpoint}`;
2077
+ const bearerToken = options.bearerToken;
2078
+ const headers = {
2079
+ 'Content-Type': 'application/json',
2080
+ 'X-Neus-App': MCP_APP_ID,
2081
+ 'X-Neus-Mcp': 'true',
2082
+ ...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {}),
2083
+ ...options.headers
2084
+ };
2085
+
2086
+ const maxRetries = Number.isFinite(Number(options.maxRetries))
2087
+ ? Math.max(0, Number(options.maxRetries))
2088
+ : API_MAX_RETRIES;
2089
+
2090
+ for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
2091
+ const controller = new AbortController();
2092
+ const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
2093
+ const method = options.method || 'GET';
2094
+ try {
2095
+ const response = await fetch(url, {
2096
+ method,
2097
+ headers,
2098
+ body: options.body ? JSON.stringify(options.body) : undefined,
2099
+ signal: controller.signal
2100
+ });
2101
+ clearTimeout(timeoutId);
2102
+
2103
+ // Handle 402 Payment Required (x402 standardized contract)
2104
+ if (response.status === 402) {
2105
+ const paymentRequiredRaw = response.headers.get('PAYMENT-REQUIRED');
2106
+ let paymentRequired = null;
2107
+ if (paymentRequiredRaw) {
2108
+ try {
2109
+ paymentRequired = JSON.parse(paymentRequiredRaw);
2110
+ } catch {
2111
+ paymentRequired = null;
2112
+ }
2113
+ }
2114
+ const data = await response.json().catch(() => ({}));
2115
+ if (!paymentRequired && data?.x402 && typeof data.x402 === 'object') {
2116
+ paymentRequired = data.x402;
2117
+ }
2118
+ const requiredCreditsRaw =
2119
+ paymentRequired?.requiredCredits ??
2120
+ paymentRequired?.creditsRequired ??
2121
+ data?.requiredCredits ??
2122
+ data?.error?.requiredCredits ??
2123
+ null;
2124
+ const requiredCredits = Number.isFinite(Number(requiredCreditsRaw))
2125
+ ? Number(requiredCreditsRaw)
2126
+ : null;
2127
+ const pricingUrl = typeof paymentRequired?.pay?.pricing === 'string'
2128
+ ? paymentRequired.pay.pricing
2129
+ : (typeof paymentRequired?.pricingUrl === 'string' ? paymentRequired.pricingUrl : null);
2130
+ const reason = typeof paymentRequired?.reason === 'string'
2131
+ ? paymentRequired.reason
2132
+ : (typeof data?.error?.code === 'string' ? data.error.code : 'payment_required');
2133
+ const message = data?.error?.message || data?.message || 'Payment required. Complete the NEUS billing step, then retry.';
2134
+ const receiptHeader = typeof paymentRequired?.receiptHeader === 'string'
2135
+ ? paymentRequired.receiptHeader
2136
+ : 'PAYMENT-SIGNATURE';
2137
+ const hostedCheckoutUrl = 'https://neus.network/verify';
2138
+ return {
2139
+ error: 'payment_required',
2140
+ reason,
2141
+ message,
2142
+ status: 402,
2143
+ requiredCredits,
2144
+ pricingUrl,
2145
+ hostedCheckoutUrl,
2146
+ receiptHeader,
2147
+ suggestedNextAction: `Fulfill billing through NEUS (header ${receiptHeader} when instructed), then repeat the MCP call unchanged. Hosted checkout stays available at ${hostedCheckoutUrl}.`
2148
+ };
2149
+ }
2150
+
2151
+ const data = await response.json().catch(() => ({}));
2152
+ if (!response.ok) {
2153
+ const errorCode = data.error?.code || 'api_error';
2154
+ const remediation = ERROR_REMEDIATION[errorCode?.toUpperCase()] || null;
2155
+ const mappedError = {
2156
+ error: errorCode,
2157
+ message: data.error?.message || data.message || `API error: ${response.status}`,
2158
+ status: response.status,
2159
+ statusCode: response.status,
2160
+ ...(remediation && { remediation }),
2161
+ ...(response.status === 409 && data?.error?.details?.nextStep
2162
+ ? { nextStep: data.error.details.nextStep }
2163
+ : {})
2164
+ };
2165
+ if (shouldRetryStatus(response.status) && attempt < maxRetries) {
2166
+ const delayMs = API_RETRY_BASE_MS * (2 ** attempt);
2167
+ log('warn', 'mcp_api_retry_status', { endpoint, method, attempt, status: response.status, delayMs });
2168
+ await sleep(delayMs);
2169
+ continue;
2170
+ }
2171
+ return mappedError;
2172
+ }
2173
+ return data;
2174
+ } catch (fetchError) {
2175
+ clearTimeout(timeoutId);
2176
+ const isAbort = fetchError?.name === 'AbortError';
2177
+ const canRetry = attempt < maxRetries && (isAbort || shouldRetryError(fetchError));
2178
+ if (canRetry) {
2179
+ const delayMs = API_RETRY_BASE_MS * (2 ** attempt);
2180
+ log('warn', 'mcp_api_retry_error', {
2181
+ endpoint,
2182
+ method,
2183
+ attempt,
2184
+ error: fetchError?.message || 'unknown_error',
2185
+ code: fetchError?.code || null,
2186
+ delayMs
2187
+ });
2188
+ await sleep(delayMs);
2189
+ continue;
2190
+ }
2191
+ if (isAbort) {
2192
+ return {
2193
+ error: 'timeout',
2194
+ message: 'Request timeout after 5 minutes',
2195
+ status: 504,
2196
+ statusCode: 504
2197
+ };
2198
+ }
2199
+ throw fetchError;
2200
+ }
2201
+ }
2202
+ }
2203
+
2204
+ // ============================================================================
2205
+ // SESSION CONTEXT
2206
+ // ============================================================================
2207
+
2208
+ /**
2209
+ * Create a request-scoped session context.
2210
+ * Auth resolution: per-session bearer > process-global NEUS_AUTH_TOKEN.
2211
+ * @param {string[]|null} exposedToolNames - Tool names registered for this session (tools/list + list_all_available_tools).
2212
+ */
2213
+ function createSessionCtx(sessionBearerToken, exposedToolNames = null) {
2214
+ const bearerToken = sessionBearerToken;
2215
+ return {
2216
+ bearerToken,
2217
+ isAuthenticated: !!bearerToken,
2218
+ exposedToolNames: exposedToolNames || [...PUBLIC_MCP_TOOL_NAMES_ORDERED],
2219
+ callApi(endpoint, options = {}) {
2220
+ return callNeusApi(endpoint, { ...options, bearerToken });
2221
+ }
2222
+ };
2223
+ }
2224
+
2225
+ // ============================================================================
2226
+ // TOOL HANDLERS
2227
+ // ============================================================================
2228
+
2229
+ const handlers = {
2230
+ async neus_verifiers_catalog(_args, ctx) {
2231
+ const result = await ctx.callApi('/api/v1/verification/verifiers');
2232
+ const data = result?.data;
2233
+ if (Array.isArray(data)) {
2234
+ return data;
2235
+ }
2236
+ // Defensive fallback for unexpected API shapes
2237
+ log('warn', 'mcp_verifiers_catalog_unexpected_shape', {
2238
+ type: typeof data,
2239
+ isArray: Array.isArray(data),
2240
+ keys: data && typeof data === 'object' ? Object.keys(data) : null
2241
+ });
2242
+ return [];
2243
+ },
2244
+
2245
+ // Gate Check Tools (v1-safe: eligibility-only)
2246
+ async neus_proofs_check(args, ctx) {
2247
+ const { wallet, verifiers, requireAll, minCount, handle, namespace } = args;
2248
+ const params = new URLSearchParams();
2249
+ const canonicalVerifiers = Array.isArray(verifiers)
2250
+ ? verifiers.map(v => normalizeVerifierId(v))
2251
+ : [normalizeVerifierId(String(verifiers || ''))];
2252
+ params.set('address', wallet);
2253
+ params.set('verifierIds', canonicalVerifiers.join(','));
2254
+ if (requireAll === true) params.set('requireAll', 'true');
2255
+ if (Number.isFinite(Number(minCount)) && Number(minCount) > 0) params.set('minCount', String(Number(minCount)));
2256
+ if (handle && typeof handle === 'string' && handle.trim()) params.set('handle', handle.trim());
2257
+ if (namespace && typeof namespace === 'string' && namespace.trim()) {
2258
+ params.set('namespace', namespace.trim().toLowerCase());
2259
+ }
2260
+
2261
+ const result = await ctx.callApi(`/api/v1/proofs/check?${params.toString()}`);
2262
+
2263
+ // SECURITY: Never return qHash lists or projections from gate checks via MCP.
2264
+ const eligible = Boolean(result?.data?.eligible);
2265
+ const matchedCount = Number.isFinite(Number(result?.data?.matchedCount)) ? Number(result.data.matchedCount) : 0;
2266
+ return {
2267
+ success: result?.success === true,
2268
+ eligible,
2269
+ matchedCount
2270
+ };
2271
+ },
2272
+
2273
+ async neus_verify(args, ctx) {
2274
+ const {
2275
+ walletAddress,
2276
+ verifierIds,
2277
+ data: dataArg,
2278
+ chain,
2279
+ signature,
2280
+ signedTimestamp,
2281
+ options = {}
2282
+ } = args || {};
2283
+ const data =
2284
+ dataArg && typeof dataArg === 'object' && !Array.isArray(dataArg) ? dataArg : {};
2285
+
2286
+ if (!walletAddress || typeof walletAddress !== 'string') {
2287
+ return { error: 'validation_error', message: 'walletAddress is required' };
2288
+ }
2289
+ if (!Array.isArray(verifierIds) || verifierIds.length === 0) {
2290
+ return { error: 'validation_error', message: 'verifierIds array with at least one ID is required' };
2291
+ }
2292
+
2293
+ const idResolve = resolveIdentifierToDid(String(walletAddress).trim());
2294
+ if (idResolve?.error) {
2295
+ return { error: 'validation_error', message: idResolve.error };
2296
+ }
2297
+ const subjectForUnsigned = idResolve.walletAddress;
2298
+
2299
+ const canonicalVerifierIds = verifierIds.map(v => normalizeVerifierId(v));
2300
+
2301
+ // Auto-detect chain from wallet address format
2302
+ const resolvedChain = resolveChain(subjectForUnsigned, chain);
2303
+
2304
+ const ts = Number.isFinite(Number(signedTimestamp)) && Number(signedTimestamp) > 0
2305
+ ? Number(signedTimestamp)
2306
+ : Date.now();
2307
+
2308
+ // Signature missing: authenticated principal (Bearer PAT or cookie via same /auth/me) matches wallet — API signatureless POST
2309
+ if (!signature || typeof signature !== 'string' || signature.trim().length === 0) {
2310
+ const claimedWallet = normalizeWalletForMcp(subjectForUnsigned);
2311
+ if (ctx.isAuthenticated && claimedWallet) {
2312
+ const principalWallet = await resolveAuthenticatedWallet(ctx);
2313
+ if (principalWallet && principalWallet === claimedWallet) {
2314
+ const verifyBodySession = {
2315
+ walletAddress: subjectForUnsigned,
2316
+ verifierIds: canonicalVerifierIds,
2317
+ data,
2318
+ chain: resolvedChain,
2319
+ options
2320
+ };
2321
+ const verifyResult = await ctx.callApi('/api/v1/verification', {
2322
+ method: 'POST',
2323
+ body: verifyBodySession
2324
+ });
2325
+
2326
+ if (verifyResult?.status === 409 && verifyResult?.nextStep) {
2327
+ return {
2328
+ error: verifyResult.error,
2329
+ message: verifyResult.message,
2330
+ status: 409,
2331
+ nextStep: verifyResult.nextStep,
2332
+ path: 'principal_direct'
2333
+ };
2334
+ }
2335
+
2336
+ if (verifyResult?.error) {
2337
+ return {
2338
+ error: verifyResult.error,
2339
+ message: verifyResult.message || 'Verification failed',
2340
+ path: 'principal_direct',
2341
+ hint:
2342
+ 'If this verifier requires browser-only consent, provider login, payment, or wallet setup, call neus_verify_or_guide and use the returned hostedVerifyUrl.'
2343
+ };
2344
+ }
2345
+
2346
+ const qHashS = verifyResult?.data?.qHash;
2347
+ if (qHashS) log('info', 'mcp_qhash_produced', { tool: 'neus_verify', path: 'principal_direct' });
2348
+
2349
+ return {
2350
+ success: verifyResult?.success === true,
2351
+ status: verifyResult?.data?.status,
2352
+ qHash: qHashS,
2353
+ path: 'principal_direct',
2354
+ message: 'Proof request submitted using authenticated API principal (Bearer or cookie; no separate wallet signature).'
2355
+ };
2356
+ }
2357
+
2358
+ if (principalWallet && principalWallet !== claimedWallet) {
2359
+ return {
2360
+ error: 'wallet_session_mismatch',
2361
+ message:
2362
+ 'Bearer is for a different NEUS profile wallet than walletAddress. Use an access key for the matching profile, switch walletAddress to the profile wallet, or use the hosted URL returned by neus_verify_or_guide.',
2363
+ next_action: 'hosted_login_or_browser_or_sign',
2364
+ hostedLoginUrl: 'https://neus.network/verify?intent=login'
2365
+ };
2366
+ }
2367
+ }
2368
+
2369
+ const standardizeBody = {
2370
+ walletAddress: subjectForUnsigned,
2371
+ verifierIds: canonicalVerifierIds,
2372
+ data,
2373
+ signedTimestamp: ts,
2374
+ chain: resolvedChain
2375
+ };
2376
+
2377
+ const standardizeResult = await ctx.callApi('/api/v1/verification/standardize', {
2378
+ method: 'POST',
2379
+ body: standardizeBody
2380
+ });
2381
+
2382
+ if (standardizeResult?.error) return { error: standardizeResult.error, message: standardizeResult.message || 'Signing string generation failed' };
2383
+
2384
+ const signerString = standardizeResult?.data?.signerString;
2385
+ const standardizedChain = standardizeResult?.data?.bodyPreview?.chain || resolvedChain;
2386
+ if (signerString) log('info', 'mcp_elicitation_generated', { tool: 'neus_verify' });
2387
+
2388
+ const base = 'https://neus.network/verify';
2389
+ const params = new URLSearchParams({ verifiers: canonicalVerifierIds.join(',') });
2390
+ const returnUrl = typeof options?.returnUrl === 'string' ? options.returnUrl.trim() : null;
2391
+ if (returnUrl) params.set('returnUrl', returnUrl);
2392
+ const hostedVerifyUrl = `${base}?${params.toString()}`;
2393
+
2394
+ const sigVersion = standardizeResult?.data?.sigVersion ?? null;
2395
+ const bodyPreview = standardizeResult?.data?.bodyPreview ?? null;
2396
+
2397
+ return {
2398
+ status: 'verification_required',
2399
+ next_action: 'verify',
2400
+ path: 'standardize_or_browser',
2401
+ standardizeAlreadyApplied: true,
2402
+ elicitation: {
2403
+ required: true,
2404
+ mode: 'url_or_sign',
2405
+ message:
2406
+ 'Prefer an access key whose profile wallet matches walletAddress. If browser-only consent, provider login, payment, or wallet setup is required, open hostedVerifyUrl. If you already have the signer available, sign signerString and call neus_verify again with the same walletAddress, verifierIds, data, signedTimestamp, chain, plus signature.',
2407
+ signerString,
2408
+ signedTimestamp: ts,
2409
+ chain: standardizedChain,
2410
+ hostedVerifyUrl,
2411
+ sigVersion,
2412
+ bodyPreview
2413
+ }
2414
+ };
2415
+ }
2416
+
2417
+ // Signature provided: call verification endpoint
2418
+ const verifyBody = {
2419
+ walletAddress,
2420
+ verifierIds: canonicalVerifierIds,
2421
+ data,
2422
+ signature,
2423
+ signedTimestamp: ts,
2424
+ chain: resolvedChain,
2425
+ options
2426
+ };
2427
+
2428
+ const verifyResult = await ctx.callApi('/api/v1/verification', {
2429
+ method: 'POST',
2430
+ body: verifyBody
2431
+ });
2432
+
2433
+ if (verifyResult?.status === 409 && verifyResult?.nextStep) {
2434
+ return {
2435
+ error: verifyResult.error,
2436
+ message: verifyResult.message,
2437
+ status: 409,
2438
+ nextStep: verifyResult.nextStep
2439
+ };
2440
+ }
2441
+
2442
+ if (verifyResult?.error) return { error: verifyResult.error, message: verifyResult.message || 'Verification failed' };
2443
+
2444
+ const qHash = verifyResult?.data?.qHash;
2445
+ if (qHash) log('info', 'mcp_qhash_produced', { tool: 'neus_verify' });
2446
+
2447
+ return {
2448
+ success: verifyResult?.success === true,
2449
+ status: verifyResult?.data?.status,
2450
+ qHash
2451
+ };
2452
+ },
2453
+
2454
+ async neus_verify_or_guide(args, ctx) {
2455
+ const {
2456
+ walletAddress,
2457
+ verifierIds,
2458
+ requireAll,
2459
+ options = {}
2460
+ } = args || {};
2461
+
2462
+ if (!walletAddress || typeof walletAddress !== 'string') {
2463
+ return { error: 'validation_error', message: 'walletAddress is required' };
2464
+ }
2465
+ if (!Array.isArray(verifierIds) || verifierIds.length === 0) {
2466
+ return { error: 'validation_error', message: 'verifierIds array with at least one ID is required' };
2467
+ }
2468
+
2469
+ const idRes = resolveIdentifierToDid(String(walletAddress).trim());
2470
+ if (idRes?.error) {
2471
+ return { error: 'validation_error', message: idRes.error };
2472
+ }
2473
+ const subjectWallet = idRes.walletAddress;
2474
+
2475
+ const canonicalVerifierIds = verifierIds.map(v => normalizeVerifierId(v));
2476
+ const requireAllBool = requireAll === true;
2477
+ const threshold = requireAllBool ? canonicalVerifierIds.length : 1;
2478
+
2479
+ // Targeted reads: one small by-verifier page per required id (Cosmos filters at query time).
2480
+ let proofs = [];
2481
+ try {
2482
+ const pages = await Promise.all(
2483
+ canonicalVerifierIds.map((vid) =>
2484
+ handlers.neus_proofs_get(
2485
+ { identifier: subjectWallet, limit: 30, verifierId: normalizeVerifierId(vid) },
2486
+ ctx
2487
+ )
2488
+ )
2489
+ );
2490
+ const byQ = new Map();
2491
+ for (const page of pages) {
2492
+ const list = page?.data?.proofs || [];
2493
+ for (const p of list) {
2494
+ const qh = p && typeof p.qHash === 'string' ? p.qHash : '';
2495
+ if (qh && !byQ.has(qh)) byQ.set(qh, p);
2496
+ }
2497
+ }
2498
+ proofs = [...byQ.values()];
2499
+ } catch {}
2500
+
2501
+ const matchingProofs = proofs.filter(p => {
2502
+ if (!p || !Array.isArray(p.verifierIds)) return false;
2503
+ if (requireAllBool) {
2504
+ return canonicalVerifierIds.every(id => p.verifierIds.includes(id));
2505
+ }
2506
+ return canonicalVerifierIds.some(id => p.verifierIds.includes(id));
2507
+ });
2508
+
2509
+ const matchedCount = matchingProofs.length;
2510
+ if (matchedCount >= threshold) {
2511
+ return {
2512
+ success: true,
2513
+ action: 'already_verified',
2514
+ eligible: true,
2515
+ matchedCount,
2516
+ proofs: matchingProofs.slice(0, 20),
2517
+ message: 'Proof requirements already satisfied'
2518
+ };
2519
+ }
2520
+
2521
+ const base = 'https://neus.network/verify';
2522
+ const params = new URLSearchParams({ verifiers: canonicalVerifierIds.join(',') });
2523
+ const returnUrl = typeof options?.returnUrl === 'string' ? options.returnUrl.trim() : null;
2524
+ if (returnUrl) params.set('returnUrl', returnUrl);
2525
+ const hostedVerifyUrl = `${base}?${params.toString()}`;
2526
+
2527
+ let bearerHint = null;
2528
+ if (ctx.isAuthenticated) {
2529
+ const sw = await resolveAuthenticatedWallet(ctx);
2530
+ const cw = normalizeWalletForMcp(subjectWallet);
2531
+ if (sw && cw && sw === cw) {
2532
+ bearerHint =
2533
+ 'Bearer principal wallet matches. Try neus_verify with the same walletAddress and verifierIds first for instant verifiers. If NEUS returns a browser, provider, payment, or wallet setup requirement, open hostedVerifyUrl.';
2534
+ } else if (sw && cw && sw !== cw) {
2535
+ bearerHint =
2536
+ 'Bearer principal differs from walletAddress. Use an access key for the matching profile or open hostedVerifyUrl.';
2537
+ } else {
2538
+ bearerHint =
2539
+ 'Bearer is set but the profile wallet could not be resolved. Open hostedVerifyUrl or rotate the personal access key in Profile.';
2540
+ }
2541
+ } else {
2542
+ bearerHint =
2543
+ 'No Bearer on MCP. Public checks still work. For account-aware reuse, add a personal access key. Otherwise open hostedVerifyUrl when setup is missing.';
2544
+ }
2545
+
2546
+ return {
2547
+ success: false,
2548
+ action: 'verification_required',
2549
+ eligible: false,
2550
+ matchedCount,
2551
+ hostedVerifyUrl,
2552
+ requiredVerifiers: canonicalVerifierIds,
2553
+ bearerHint,
2554
+ sessionHint: bearerHint,
2555
+ next_action: 'url_handoff_or_authenticated_verify',
2556
+ message:
2557
+ 'Proof requirements are not satisfied. Use neus_verify only when Bearer profile wallet matches walletAddress and the verifier can complete inside MCP. Otherwise open hostedVerifyUrl for the missing setup step: ' +
2558
+ hostedVerifyUrl
2559
+ };
2560
+ },
2561
+
2562
+ /**
2563
+ * Identity context: principal, agents, auth status.
2564
+ * Omit identifier to resolve from the Bearer-authenticated profile (personal access key).
2565
+ */
2566
+ async neus_me(args, ctx) {
2567
+ const { identifier } = args || {};
2568
+ const uiUrl = 'https://neus.network/verify?intent=login';
2569
+
2570
+ if (!identifier && ctx.isAuthenticated) {
2571
+ const meResult = await ctx.callApi('/api/v1/auth/me');
2572
+ if (meResult?.error || meResult?.status === 401) {
2573
+ return {
2574
+ status: 'auth_required',
2575
+ principal: null,
2576
+ agents: [],
2577
+ next_action: 'auth_required',
2578
+ ui_url: uiUrl,
2579
+ message: 'Bearer rejected or expired. Mint or rotate your personal access key (profile — Access keys) and update MCP Authorization: Bearer.'
2580
+ };
2581
+ }
2582
+ const data = meResult?.data;
2583
+ if (!data?.authenticated || !data?.did) {
2584
+ return {
2585
+ status: 'auth_required',
2586
+ principal: null,
2587
+ agents: [],
2588
+ next_action: 'auth_required',
2589
+ ui_url: uiUrl,
2590
+ message: 'Bearer rejected or profile not loaded. Use a valid personal access key as Bearer, or open profile to sign in and mint a key.'
2591
+ };
2592
+ }
2593
+ const did = data.did;
2594
+ const profileResult = await ctx.callApi(`/api/v1/profile/${encodeURIComponent(did)}?includeProofsSummary=true`);
2595
+ const associations = profileResult?.data?.associations;
2596
+ const agents = Array.isArray(associations?.agents) ? associations.agents : [];
2597
+ return {
2598
+ status: 'ok',
2599
+ principal: {
2600
+ did: data.did,
2601
+ handle: data.handle || data.profileSummary?.displayName || null,
2602
+ primaryAccount: data.primaryAccount || data.address || null
2603
+ },
2604
+ agents,
2605
+ next_action: null,
2606
+ ui_url: null
2607
+ };
2608
+ }
2609
+
2610
+ if (!identifier || typeof identifier !== 'string') {
2611
+ return {
2612
+ status: 'auth_required',
2613
+ principal: null,
2614
+ agents: [],
2615
+ next_action: ctx.isAuthenticated ? null : 'auth_required',
2616
+ ui_url: ctx.isAuthenticated ? null : uiUrl,
2617
+ message: ctx.isAuthenticated ? null : 'Add Authorization: Bearer with your personal access key (profile — Access keys), or pass identifier (wallet/DID) for public principal reads.',
2618
+ ...(ctx.isAuthenticated ? {} : {
2619
+ authGuidance: {
2620
+ loginUrl: 'https://neus.network/verify?intent=login',
2621
+ docs: 'https://docs.neus.network/mcp/auth'
2622
+ }
2623
+ })
2624
+ };
2625
+ }
2626
+ const resolved = resolveIdentifierToDid(identifier.trim());
2627
+ if (resolved.error) {
2628
+ return { error: 'validation_error', message: resolved.error };
2629
+ }
2630
+ const profileResult = await ctx.callApi(
2631
+ `/api/v1/profile/${encodeURIComponent(resolved.did)}?includeProofsSummary=true${ctx.isAuthenticated ? '' : '&isPublicRead=true'}`
2632
+ );
2633
+ if (profileResult?.error && profileResult?.status === 404) {
2634
+ return {
2635
+ status: 'ok',
2636
+ principal: { did: resolved.did, handle: null, primaryAccount: resolved.walletAddress },
2637
+ agents: [],
2638
+ next_action: null,
2639
+ ui_url: null
2640
+ };
2641
+ }
2642
+ const data = profileResult?.data;
2643
+ const associations = data?.associations;
2644
+ const agents = Array.isArray(associations?.agents) ? associations.agents : [];
2645
+ const profile = data?.profile || data;
2646
+ const handle = profile?.metadata?.pseudonymId || profile?.displayName || null;
2647
+ return {
2648
+ status: 'ok',
2649
+ principal: {
2650
+ did: resolved.did,
2651
+ handle: handle || null,
2652
+ primaryAccount: resolved.walletAddress
2653
+ },
2654
+ agents,
2655
+ next_action: null,
2656
+ ui_url: null
2657
+ };
2658
+ },
2659
+
2660
+ /**
2661
+ * Check agent readiness: agent-identity + agent-delegation.
2662
+ * Returns linked (compat) + guided next steps with hostedVerifyUrl.
2663
+ */
2664
+ async neus_agent_link(args, ctx) {
2665
+ const { agentWallet, principal } = args || {};
2666
+ if (!agentWallet || typeof agentWallet !== 'string') {
2667
+ return { error: 'validation_error', message: 'agentWallet is required' };
2668
+ }
2669
+ const trimmedAgent = normalizeWalletForMcp(agentWallet);
2670
+
2671
+ const resolvedPrincipal = await resolvePrincipalWallet(principal, ctx);
2672
+ if (resolvedPrincipal?.error) return resolvedPrincipal;
2673
+ const resolvedControllerWallet = normalizeWalletForMcp(resolvedPrincipal?.walletAddress);
2674
+
2675
+ // Direct lookup: use neus_proofs_get instead of gate checks.
2676
+ // Gate checks (proofs/check) are for allowlists/campaigns; identity/delegation
2677
+ // are single-record lookups — get the actual record and return it.
2678
+ let identityProof = null;
2679
+ let delegationProof = null;
2680
+
2681
+ try {
2682
+ const [idPage, delPage] = await Promise.all([
2683
+ handlers.neus_proofs_get({ identifier: trimmedAgent, limit: 25, verifierId: 'agent-identity' }, ctx),
2684
+ handlers.neus_proofs_get({ identifier: trimmedAgent, limit: 25, verifierId: 'agent-delegation' }, ctx)
2685
+ ]);
2686
+ const agentIdProofs = idPage?.data?.proofs || [];
2687
+ const agentDelProofs = delPage?.data?.proofs || [];
2688
+ identityProof =
2689
+ agentIdProofs.find(p => Array.isArray(p?.verifierIds) && p.verifierIds.includes('agent-identity')) || null;
2690
+ delegationProof =
2691
+ agentDelProofs.find(p => Array.isArray(p?.verifierIds) && p.verifierIds.includes('agent-delegation')) || null;
2692
+ } catch {}
2693
+
2694
+ // Also check controller wallet for delegation proof
2695
+ if (!delegationProof && resolvedControllerWallet) {
2696
+ try {
2697
+ const controllerRecords = await handlers.neus_proofs_get(
2698
+ { identifier: resolvedControllerWallet, limit: 25, verifierId: 'agent-delegation' },
2699
+ ctx
2700
+ );
2701
+ const proofs = controllerRecords?.data?.proofs || [];
2702
+ delegationProof =
2703
+ proofs.find(p => Array.isArray(p?.verifierIds) && p.verifierIds.includes('agent-delegation')) || null;
2704
+ } catch {}
2705
+ }
2706
+
2707
+ if (identityProof && delegationProof) {
2708
+ return {
2709
+ status: 'ok',
2710
+ linked: true,
2711
+ message: 'This agent wallet has both identity and approved access on file.',
2712
+ principal: resolvedControllerWallet || null,
2713
+ proofs: { identity: identityProof, delegation: delegationProof }
2714
+ };
2715
+ }
2716
+
2717
+ const hostedVerifyUrl = buildAgentHostedVerifyUrl(trimmedAgent, resolvedControllerWallet);
2718
+ const missingVerifiers = [];
2719
+ if (!identityProof) missingVerifiers.push('agent-identity');
2720
+ if (!delegationProof) missingVerifiers.push('agent-delegation');
2721
+
2722
+ return {
2723
+ status: 'link_required',
2724
+ linked: false,
2725
+ next_action: 'finish_hosted_setup',
2726
+ nextSteps: {
2727
+ action: 'open_hosted_verify',
2728
+ message: resolvedControllerWallet
2729
+ ? 'Open hostedVerifyUrl once to connect the agent wallet and approve access from your account.'
2730
+ : 'Open hostedVerifyUrl once. Sign in to NEUS or pass the approving account so the approval step can be checked.',
2731
+ hostedVerifyUrl,
2732
+ requiredVerifiers: missingVerifiers.length > 0 ? missingVerifiers : ['agent-identity', 'agent-delegation'],
2733
+ recommendedTool: 'neus_agent_create'
2734
+ },
2735
+ hostedVerifyUrl,
2736
+ ...(resolvedControllerWallet ? { principal: resolvedControllerWallet } : {})
2737
+ };
2738
+ },
2739
+
2740
+ /**
2741
+ * CREATE agent identity and delegation proofs.
2742
+ * NO GATE CHECK - this is for creation, not verification.
2743
+ * Returns two separate signing requests:
2744
+ * 1. agent-identity: must be signed by agentWallet (self-attestation)
2745
+ * 2. agent-delegation: must be signed by controllerWallet
2746
+ *
2747
+ * Quick Win Features:
2748
+ * - preset: "full-access", "payments", "readonly", "automation"
2749
+ * - agentWallet="generate": auto-generate a random EVM address
2750
+ * - controllerWallet omitted: resolved from the active session
2751
+ */
2752
+ async neus_agent_create(args, ctx) {
2753
+ const {
2754
+ agentWallet,
2755
+ controllerWallet,
2756
+ agentId,
2757
+ agentLabel,
2758
+ agentType,
2759
+ description,
2760
+ capabilities,
2761
+ permissions,
2762
+ scope,
2763
+ expiresAt,
2764
+ maxSpend,
2765
+ chain,
2766
+ preset,
2767
+ instructions,
2768
+ skills,
2769
+ services,
2770
+ delegationInstructions,
2771
+ delegationSkills,
2772
+ } = args || {};
2773
+
2774
+ // Validate required fields
2775
+ if (!agentId || typeof agentId !== 'string' || agentId.trim().length === 0) {
2776
+ return { error: 'validation_error', message: 'agentId is required (1-128 chars)' };
2777
+ }
2778
+
2779
+ const trimmedAgentId = agentId.trim();
2780
+ if (trimmedAgentId.length > 128) {
2781
+ return { error: 'validation_error', message: 'agentId must be 1-128 characters' };
2782
+ }
2783
+
2784
+ // Apply preset defaults
2785
+ const PRESETS = {
2786
+ 'full-access': {
2787
+ capabilities: ['wallet', 'signing', 'spending', 'publishing', 'search', 'browser', 'mcp', 'webhooks', 'receipts', 'proofs', 'delegation'],
2788
+ permissions: ['read-proofs', 'write-proofs', 'invoke-services', 'manage-delegations'],
2789
+ scope: 'global'
2790
+ },
2791
+ 'payments': {
2792
+ capabilities: ['wallet', 'signing', 'spending'],
2793
+ permissions: ['execute-payments', 'read-proofs'],
2794
+ scope: 'payments:x402'
2795
+ },
2796
+ 'readonly': {
2797
+ capabilities: ['proofs', 'search'],
2798
+ permissions: ['read-proofs'],
2799
+ scope: 'global'
2800
+ },
2801
+ 'automation': {
2802
+ capabilities: ['browser', 'mcp', 'webhooks', 'proofs'],
2803
+ permissions: ['read-proofs', 'write-proofs', 'invoke-services'],
2804
+ scope: 'global'
2805
+ }
2806
+ };
2807
+
2808
+ const activePreset = preset && typeof preset === 'string' && PRESETS[preset.trim().toLowerCase()]
2809
+ ? PRESETS[preset.trim().toLowerCase()]
2810
+ : null;
2811
+
2812
+ // Resolve capabilities (preset or explicit)
2813
+ const resolvedCapabilities = activePreset?.capabilities || capabilities;
2814
+ const resolvedPermissions = activePreset?.permissions || permissions;
2815
+ const resolvedScope = activePreset?.scope || scope;
2816
+
2817
+ // Resolve controller wallet
2818
+ let resolvedControllerWallet = controllerWallet;
2819
+ if (!resolvedControllerWallet || typeof resolvedControllerWallet !== 'string' || resolvedControllerWallet.trim() === '') {
2820
+ if (ctx.isAuthenticated) {
2821
+ try {
2822
+ const meResult = await ctx.callApi('/api/v1/auth/me');
2823
+ if (meResult?.data?.address) {
2824
+ resolvedControllerWallet = meResult.data.address;
2825
+ } else if (meResult?.data?.primaryAccount) {
2826
+ resolvedControllerWallet = meResult.data.primaryAccount;
2827
+ }
2828
+ } catch {
2829
+ // Session not available, will fail validation below
2830
+ }
2831
+ }
2832
+ }
2833
+
2834
+ // Generate agent wallet if requested
2835
+ let resolvedAgentWallet = agentWallet;
2836
+ let generatedWallet = null;
2837
+ if (!resolvedAgentWallet || resolvedAgentWallet.trim().toLowerCase() === 'generate') {
2838
+ const { randomBytes } = await import('crypto');
2839
+ const privateKeyHex = `0x${randomBytes(32).toString('hex')}`;
2840
+ const generatedAccount = privateKeyToAccount(privateKeyHex);
2841
+ generatedWallet = { privateKey: privateKeyHex };
2842
+ resolvedAgentWallet = generatedAccount.address;
2843
+ }
2844
+
2845
+ if (!resolvedAgentWallet || typeof resolvedAgentWallet !== 'string') {
2846
+ return { error: 'validation_error', message: 'agentWallet is required (or use "generate" to auto-create)' };
2847
+ }
2848
+ if (!resolvedControllerWallet || typeof resolvedControllerWallet !== 'string') {
2849
+ return { error: 'validation_error', message: 'Approving account wallet is required — sign in to NEUS or pass the wallet that will approve access (controllerWallet).' };
2850
+ }
2851
+
2852
+ const trimmedAgentWallet = resolvedAgentWallet.trim();
2853
+ const trimmedControllerWallet = resolvedControllerWallet.trim();
2854
+
2855
+ // Validate wallet formats
2856
+ const agentWalletType = detectWalletType(trimmedAgentWallet);
2857
+ const controllerWalletType = detectWalletType(trimmedControllerWallet);
2858
+
2859
+ if (agentWalletType === 'unknown') {
2860
+ return { error: 'validation_error', message: 'Invalid agentWallet format. Use EVM (0x...) or Solana (base58) address.' };
2861
+ }
2862
+ if (controllerWalletType === 'unknown') {
2863
+ return { error: 'validation_error', message: 'Invalid wallet address for approving account (controllerWallet). Use EVM (0x...) or Solana (base58).' };
2864
+ }
2865
+
2866
+ // Resolve chain
2867
+ const resolvedChain = resolveChain(trimmedAgentWallet, chain);
2868
+
2869
+ // Validate agentType
2870
+ const allowedAgentTypes = ['ai', 'bot', 'service', 'automation', 'agent'];
2871
+ const normalizedAgentType = agentType && allowedAgentTypes.includes(agentType.trim().toLowerCase())
2872
+ ? agentType.trim().toLowerCase()
2873
+ : 'ai';
2874
+
2875
+ // Normalize capabilities (structured object format)
2876
+ const allowedCapabilities = new Set(['wallet', 'signing', 'spending', 'publishing', 'search', 'browser', 'mcp', 'webhooks', 'receipts', 'proofs', 'delegation']);
2877
+ let normalizedCapabilities = null;
2878
+ if (Array.isArray(resolvedCapabilities)) {
2879
+ const caps = {};
2880
+ for (const cap of resolvedCapabilities) {
2881
+ if (typeof cap === 'string' && allowedCapabilities.has(cap.trim().toLowerCase())) {
2882
+ caps[cap.trim().toLowerCase()] = true;
2883
+ }
2884
+ }
2885
+ if (Object.keys(caps).length > 0) normalizedCapabilities = caps;
2886
+ }
2887
+
2888
+ // Build agent-identity data (signed by AGENT wallet - self-attestation)
2889
+ const normalizedDescription = description && typeof description === 'string'
2890
+ ? description.trim().slice(0, 500)
2891
+ : '';
2892
+ const normalizedPermissions = Array.isArray(resolvedPermissions)
2893
+ ? resolvedPermissions.filter(p => typeof p === 'string').slice(0, 32)
2894
+ : [];
2895
+ const normalizedExpiresAt = typeof expiresAt === 'number' && expiresAt > Date.now()
2896
+ ? expiresAt
2897
+ : null;
2898
+
2899
+ const trimmedMaxSpend =
2900
+ maxSpend !== undefined && maxSpend !== null && String(maxSpend).trim() !== ''
2901
+ ? String(maxSpend).trim()
2902
+ : '';
2903
+ if (trimmedMaxSpend && !/^[0-9]{1,78}$/.test(trimmedMaxSpend)) {
2904
+ return {
2905
+ error: 'validation_error',
2906
+ message:
2907
+ 'maxSpend must be a whole-number string in token base units (1 — 78 digits, no decimal point). Convert a display amount with SDK toAgentDelegationMaxSpend(amount, decimalPlaces).',
2908
+ };
2909
+ }
2910
+
2911
+ let normalizedIdentityInstructions = null;
2912
+ if (instructions !== undefined && instructions !== null && String(instructions).trim() !== '') {
2913
+ if (typeof instructions !== 'string') {
2914
+ return { error: 'validation_error', message: 'instructions must be a string when provided' };
2915
+ }
2916
+ normalizedIdentityInstructions = instructions.trim().slice(0, 16000);
2917
+ }
2918
+
2919
+ const normalizedIdentitySkills = normalizeMcpAgentSkills(skills);
2920
+ const normalizedIdentityServices = normalizeMcpAgentServices(services);
2921
+
2922
+ let normalizedDelegationInstructions = null;
2923
+ if (delegationInstructions !== undefined && delegationInstructions !== null && String(delegationInstructions).trim() !== '') {
2924
+ if (typeof delegationInstructions !== 'string') {
2925
+ return { error: 'validation_error', message: 'delegationInstructions must be a string when provided' };
2926
+ }
2927
+ normalizedDelegationInstructions = delegationInstructions.trim().slice(0, 16000);
2928
+ }
2929
+
2930
+ const normalizedDelegationSkills = normalizeMcpAgentSkills(delegationSkills);
2931
+
2932
+ const agentIdentityData = {
2933
+ agentId: trimmedAgentId,
2934
+ agentWallet: trimmedAgentWallet,
2935
+ agentLabel: agentLabel && typeof agentLabel === 'string' ? agentLabel.trim().slice(0, 128) : trimmedAgentId,
2936
+ agentType: normalizedAgentType,
2937
+ ...(normalizedDescription ? { description: normalizedDescription } : {}),
2938
+ ...(normalizedCapabilities ? { capabilities: normalizedCapabilities } : {}),
2939
+ ...(normalizedIdentityInstructions ? { instructions: normalizedIdentityInstructions } : {}),
2940
+ ...(normalizedIdentitySkills ? { skills: normalizedIdentitySkills } : {}),
2941
+ ...(normalizedIdentityServices ? { services: normalizedIdentityServices } : {}),
2942
+ };
2943
+
2944
+ // Build agent-delegation data (signed by CONTROLLER wallet)
2945
+ const delegationData = {
2946
+ controllerWallet: trimmedControllerWallet,
2947
+ agentWallet: trimmedAgentWallet,
2948
+ agentId: trimmedAgentId,
2949
+ scope: resolvedScope && typeof resolvedScope === 'string' ? resolvedScope.trim().slice(0, 128) : 'global',
2950
+ ...(normalizedPermissions.length > 0 ? { permissions: normalizedPermissions } : {}),
2951
+ ...(normalizedExpiresAt ? { expiresAt: normalizedExpiresAt } : {}),
2952
+ ...(trimmedMaxSpend ? { maxSpend: trimmedMaxSpend } : {}),
2953
+ ...(normalizedDelegationInstructions ? { instructions: normalizedDelegationInstructions } : {}),
2954
+ ...(normalizedDelegationSkills ? { skills: normalizedDelegationSkills } : {}),
2955
+ };
2956
+
2957
+ // Get signing strings for both proofs
2958
+ const ts = Date.now();
2959
+
2960
+ // Step 1: Get agent-identity signing string (agent signs for itself)
2961
+ const identityStandardizeResult = await ctx.callApi('/api/v1/verification/standardize', {
2962
+ method: 'POST',
2963
+ body: {
2964
+ walletAddress: trimmedAgentWallet,
2965
+ verifierIds: ['agent-identity'],
2966
+ data: agentIdentityData,
2967
+ signedTimestamp: ts,
2968
+ chain: resolvedChain
2969
+ }
2970
+ });
2971
+
2972
+ if (identityStandardizeResult?.error) {
2973
+ return { error: 'standardize_error', message: identityStandardizeResult.message || 'Failed to prepare agent-identity proof' };
2974
+ }
2975
+
2976
+ // Step 2: Get agent-delegation signing string (controller signs)
2977
+ const delegationStandardizeResult = await ctx.callApi('/api/v1/verification/standardize', {
2978
+ method: 'POST',
2979
+ body: {
2980
+ walletAddress: trimmedControllerWallet,
2981
+ verifierIds: ['agent-delegation'],
2982
+ data: delegationData,
2983
+ signedTimestamp: ts,
2984
+ chain: resolvedChain
2985
+ }
2986
+ });
2987
+
2988
+ if (delegationStandardizeResult?.error) {
2989
+ return { error: 'standardize_error', message: delegationStandardizeResult.message || 'Failed to prepare agent-delegation proof' };
2990
+ }
2991
+
2992
+ const identitySignerString = identityStandardizeResult?.data?.signerString;
2993
+ const delegationSignerString = delegationStandardizeResult?.data?.signerString;
2994
+
2995
+ // Build hosted URL for the full flow
2996
+ const base = 'https://neus.network/verify';
2997
+ const hostedParams = new URLSearchParams({
2998
+ verifiers: 'agent-identity,agent-delegation',
2999
+ agentWallet: trimmedAgentWallet,
3000
+ controllerWallet: trimmedControllerWallet
3001
+ });
3002
+ const hostedVerifyUrl = `${base}?${hostedParams.toString()}`;
3003
+
3004
+ log('info', 'mcp_agent_create_prepared', { agentId: trimmedAgentId, agentWallet: trimmedAgentWallet, controllerWallet: trimmedControllerWallet });
3005
+
3006
+ const response = {
3007
+ status: 'signatures_required',
3008
+ action: 'create_agent',
3009
+ message: `Agent "${trimmedAgentId}" setup is prepared. Open hostedVerifyUrl if browser confirmation is needed, then call neus_agent_link to confirm readiness.`,
3010
+ agent: {
3011
+ agentId: trimmedAgentId,
3012
+ agentWallet: trimmedAgentWallet,
3013
+ controllerWallet: trimmedControllerWallet,
3014
+ chain: resolvedChain,
3015
+ preset: preset || null,
3016
+ generatedWallet: generatedWallet ? { address: trimmedAgentWallet, privateKey: generatedWallet.privateKey, warning: 'SECURELY STORE THIS PRIVATE KEY - IT CANNOT BE RECOVERED' } : null
3017
+ },
3018
+ signatures: {
3019
+ step1_agent_identity: {
3020
+ description: 'Step 1: confirm the agent with the agent wallet.',
3021
+ signer: trimmedAgentWallet,
3022
+ signerString: identitySignerString,
3023
+ signedTimestamp: ts,
3024
+ verifierId: 'agent-identity',
3025
+ data: agentIdentityData,
3026
+ nextCall: {
3027
+ tool: 'neus_verify',
3028
+ params: {
3029
+ walletAddress: trimmedAgentWallet,
3030
+ verifierIds: ['agent-identity'],
3031
+ data: agentIdentityData,
3032
+ chain: resolvedChain,
3033
+ signedTimestamp: ts,
3034
+ signature: '<AGENT_SIGNATURE_HERE>'
3035
+ }
3036
+ }
3037
+ },
3038
+ step2_agent_delegation: {
3039
+ description: 'Step 2: approve access from the account wallet.',
3040
+ signer: trimmedControllerWallet,
3041
+ signerString: delegationSignerString,
3042
+ signedTimestamp: ts,
3043
+ verifierId: 'agent-delegation',
3044
+ data: delegationData,
3045
+ nextCall: {
3046
+ tool: 'neus_verify',
3047
+ params: {
3048
+ walletAddress: trimmedControllerWallet,
3049
+ verifierIds: ['agent-delegation'],
3050
+ data: delegationData,
3051
+ chain: resolvedChain,
3052
+ signedTimestamp: ts,
3053
+ signature: '<CONTROLLER_SIGNATURE_HERE>'
3054
+ }
3055
+ }
3056
+ }
3057
+ },
3058
+ executionOrder: [
3059
+ '1. Connect the agent wallet and confirm agent details',
3060
+ '2. From your account wallet, approve what the agent can do',
3061
+ '3. Finish both steps (hosted flow or signatures) before treating the agent as fully set up'
3062
+ ],
3063
+ hostedVerifyUrl
3064
+ };
3065
+
3066
+ if (generatedWallet) {
3067
+ response.generatedWalletNotice = 'Advanced use only: this response includes the generated private key. Your runtime must store it securely and use it to sign as the agent.';
3068
+ }
3069
+
3070
+ return response;
3071
+ },
3072
+
3073
+ /**
3074
+ * Get portable proof context: proof records + profile summary
3075
+ * Composes GET /proofs/byWallet and GET /profile/:did with includeProofsSummary
3076
+ */
3077
+ async neus_proofs_get(args, ctx) {
3078
+ const {
3079
+ identifier,
3080
+ limit: inputLimit,
3081
+ offset: inputOffset,
3082
+ tags: tagsArg,
3083
+ scope,
3084
+ agentWallet,
3085
+ q: qArg,
3086
+ searchQuery,
3087
+ tagPrefix,
3088
+ tagContains,
3089
+ tagPrefixesAll,
3090
+ verifierId: verifierIdArg
3091
+ } = args || {};
3092
+
3093
+ if (!identifier || typeof identifier !== 'string') {
3094
+ return { error: 'validation_error', message: 'identifier is required (wallet address or DID)' };
3095
+ }
3096
+
3097
+ const trimmedId = identifier.trim();
3098
+
3099
+ // Resolve identifier to wallet, DID, and chain
3100
+ let walletAddress = null;
3101
+ let did = null;
3102
+ let idType = 'unknown';
3103
+ let resolvedChain = HUB_CHAIN;
3104
+
3105
+ if (trimmedId.startsWith('did:pkh:')) {
3106
+ // DID format: did:pkh:namespace:reference:address (e.g., did:pkh:eip155:<hubChainId>:0x...)
3107
+ did = trimmedId;
3108
+ idType = 'did';
3109
+ const parts = trimmedId.split(':');
3110
+ if (parts.length >= 5) {
3111
+ const namespace = parts[2];
3112
+ if (namespace === 'eip155') {
3113
+ walletAddress = parts.slice(4).join(':').toLowerCase();
3114
+ resolvedChain = `eip155:${parts[3]}`;
3115
+ } else if (namespace === 'solana') {
3116
+ walletAddress = parts.slice(4).join(':');
3117
+ resolvedChain = `solana:${parts[3]}`;
3118
+ }
3119
+ }
3120
+ } else {
3121
+ // Wallet address - auto-detect type
3122
+ const walletType = detectWalletType(trimmedId);
3123
+ if (walletType === 'evm') {
3124
+ walletAddress = trimmedId.toLowerCase();
3125
+ idType = 'wallet';
3126
+ resolvedChain = HUB_CHAIN;
3127
+ did = `did:pkh:eip155:${HUB_CHAIN_ID}:${walletAddress}`;
3128
+ } else if (walletType === 'solana') {
3129
+ walletAddress = trimmedId;
3130
+ idType = 'wallet';
3131
+ resolvedChain = SOLANA_MAINNET;
3132
+ did = `did:pkh:solana:mainnet:${walletAddress}`;
3133
+ } else {
3134
+ return { error: 'validation_error', message: 'Invalid identifier format. Use EVM address (0x...), Solana address, or DID (did:pkh:...)' };
3135
+ }
3136
+ }
3137
+
3138
+ // Pagination params
3139
+ const limit = Math.min(Math.max(1, Number(inputLimit) || 50), 100);
3140
+ const offset = Math.max(0, Number(inputOffset) || 0);
3141
+
3142
+ const proofsParams = new URLSearchParams();
3143
+ proofsParams.set('limit', String(limit));
3144
+ proofsParams.set('offset', String(offset));
3145
+ const tagsFromArgs = typeof tagsArg === 'string' && tagsArg.trim() ? tagsArg.trim() : '';
3146
+ const scopeCsv = scopeToProofTagsQuery(scope);
3147
+ const tagsMerged = tagsFromArgs || scopeCsv;
3148
+ if (tagsMerged) {
3149
+ const normalized = tagsMerged
3150
+ .split(',')
3151
+ .map((t) => t.trim().toLowerCase())
3152
+ .filter(Boolean)
3153
+ .join(',');
3154
+ if (normalized) proofsParams.set('tags', normalized);
3155
+ }
3156
+ const qText = String(searchQuery || qArg || '')
3157
+ .trim()
3158
+ .slice(0, 280);
3159
+ if (qText) proofsParams.set('q', qText);
3160
+ const verifierIdForApi = typeof verifierIdArg === 'string' ? verifierIdArg.trim().toLowerCase() : '';
3161
+ if (verifierIdForApi) proofsParams.set('verifierId', verifierIdForApi);
3162
+ if (!ctx.isAuthenticated) {
3163
+ proofsParams.set('isPublicRead', '1');
3164
+ }
3165
+
3166
+ const profileQs = ctx.isAuthenticated ? 'includeProofsSummary=true' : 'includeProofsSummary=true&isPublicRead=true';
3167
+ const delegationHeaders = mcpAgentDelegationHeaders(agentWallet);
3168
+ const [proofsResult, profileResult] = await Promise.all([
3169
+ ctx.callApi(`/api/v1/proofs/by-wallet/${encodeURIComponent(trimmedId)}?${proofsParams.toString()}`, {
3170
+ headers: delegationHeaders
3171
+ }),
3172
+ ctx.callApi(`/api/v1/profile/${encodeURIComponent(did)}?${profileQs}`)
3173
+ ]);
3174
+
3175
+ // Build response
3176
+ const response = {
3177
+ success: true,
3178
+ data: {
3179
+ proofs: [],
3180
+ profileSummary: null,
3181
+ resolved: {
3182
+ identifier: trimmedId,
3183
+ walletAddress,
3184
+ did,
3185
+ idType,
3186
+ chain: resolvedChain
3187
+ },
3188
+ pagination: {
3189
+ limit,
3190
+ offset,
3191
+ totalCount: null,
3192
+ hasMore: false,
3193
+ nextOffset: null
3194
+ }
3195
+ }
3196
+ };
3197
+
3198
+ // Process proofs result
3199
+ if (proofsResult?.error) {
3200
+ log('warn', 'mcp_proofs_error', { error: proofsResult.message || proofsResult.error });
3201
+ } else if (proofsResult?.data) {
3202
+ let proofList = Array.isArray(proofsResult.data.proofs) ? proofsResult.data.proofs : [];
3203
+ const pfx = String(tagPrefix || '')
3204
+ .trim()
3205
+ .toLowerCase();
3206
+ const cont = String(tagContains || '')
3207
+ .trim()
3208
+ .toLowerCase();
3209
+ const allPfx = String(tagPrefixesAll || '')
3210
+ .trim()
3211
+ .toLowerCase();
3212
+ if (pfx || cont || allPfx) {
3213
+ proofList = proofList.filter((pr) => {
3214
+ const p = unwrapApiProofRecord(pr) || pr;
3215
+ const tags = getProofTags(p).map((t) => String(t).toLowerCase());
3216
+ if (pfx && !tags.some((t) => t.startsWith(pfx))) return false;
3217
+ if (cont && !tags.some((t) => t.includes(cont))) return false;
3218
+ if (allPfx && !proofTagsMatchAllPrefixes(tags, allPfx)) return false;
3219
+ return true;
3220
+ });
3221
+ }
3222
+ response.data.proofs = proofList;
3223
+ response.data.pagination.totalCount =
3224
+ (typeof proofsResult.data.totalCount === 'number' && Number.isFinite(proofsResult.data.totalCount))
3225
+ ? proofsResult.data.totalCount
3226
+ : null;
3227
+ response.data.pagination.hasMore = Boolean(proofsResult.data.hasMore);
3228
+ response.data.pagination.nextOffset = proofsResult.data.nextOffset ?? null;
3229
+ // Use resolved data from API if available
3230
+ if (proofsResult.data.resolved) {
3231
+ response.data.resolved.walletAddress = proofsResult.data.resolved.walletAddress || walletAddress;
3232
+ response.data.resolved.idType = proofsResult.data.resolved.idType || idType;
3233
+ }
3234
+ }
3235
+
3236
+ // Process profile result
3237
+ if (profileResult?.error) {
3238
+ // Profile not found is common for new wallets; not a failure
3239
+ if (profileResult.status !== 404) {
3240
+ log('warn', 'mcp_profile_error', { error: profileResult.message || profileResult.error });
3241
+ }
3242
+ } else if (profileResult?.data) {
3243
+ response.data.profileSummary = {
3244
+ profile: profileResult.data.profile || null,
3245
+ proofsSummary: profileResult.data.proofs || null,
3246
+ linkedAccounts: profileResult.data.linkedAccounts || {}
3247
+ };
3248
+ }
3249
+
3250
+ // ========================================================================
3251
+ // AGENT CONTEXT ENHANCEMENT (Admin AI Framework Integration)
3252
+ // Extract agent identities and delegations from proofs
3253
+ // ========================================================================
3254
+ const proofs = response.data.proofs || [];
3255
+
3256
+ // Extract agent identities
3257
+ const agentIdentities = [];
3258
+ const agentDelegations = [];
3259
+ const agentServices = [];
3260
+
3261
+ for (const proof of proofs) {
3262
+ const verifiedVerifiers = Array.isArray(proof.verifiedVerifiers) ? proof.verifiedVerifiers : [];
3263
+
3264
+ for (const vv of verifiedVerifiers) {
3265
+ const verifierId = String(vv.verifierId || '').trim();
3266
+ const vvData = vv.data || {};
3267
+
3268
+ // Agent Identity extraction
3269
+ if (verifierId === 'agent-identity') {
3270
+ // Capabilities: stored as object {wallet: true, signing: false, ...}
3271
+ // Convert to array of enabled capability keys for MCP response
3272
+ let capabilitiesArray = [];
3273
+ if (vvData.capabilities && typeof vvData.capabilities === 'object' && !Array.isArray(vvData.capabilities)) {
3274
+ capabilitiesArray = Object.entries(vvData.capabilities)
3275
+ .filter(([_, enabled]) => enabled === true)
3276
+ .map(([key]) => key);
3277
+ } else if (Array.isArray(vvData.capabilities)) {
3278
+ capabilitiesArray = vvData.capabilities;
3279
+ }
3280
+
3281
+ agentIdentities.push({
3282
+ agentId: vvData.agentId || null,
3283
+ agentWallet: vvData.agentWallet || null,
3284
+ agentLabel: vvData.agentLabel || vvData.agentId || 'Unnamed Agent',
3285
+ agentType: vvData.agentType || 'agent',
3286
+ agentAccountId: vvData.agentAccountId || null,
3287
+ agentChainRef: vvData.agentChainRef || null,
3288
+ description: vvData.description || null,
3289
+ capabilities: capabilitiesArray,
3290
+ skills: Array.isArray(vvData.skills) ? vvData.skills : [],
3291
+ instructions: vvData.instructions || null,
3292
+ services: Array.isArray(vvData.services) ? vvData.services : []
3293
+ });
3294
+
3295
+ // Collect services
3296
+ if (Array.isArray(vvData.services)) {
3297
+ for (const svc of vvData.services) {
3298
+ if (svc && typeof svc === 'object') {
3299
+ agentServices.push({
3300
+ name: svc.name || 'Unknown Service',
3301
+ endpoint: svc.endpoint || null,
3302
+ version: svc.version || null
3303
+ });
3304
+ }
3305
+ }
3306
+ }
3307
+ }
3308
+
3309
+ // Agent Delegation extraction
3310
+ if (verifierId === 'agent-delegation') {
3311
+ const expiresAt = vvData.expiresAt || null;
3312
+ const isExpired = expiresAt && Number.isFinite(Number(expiresAt)) && Number(expiresAt) <= Date.now();
3313
+
3314
+ agentDelegations.push({
3315
+ controllerWallet: vvData.controllerWallet || null,
3316
+ controllerAccountId: vvData.controllerAccountId || null,
3317
+ controllerChainRef: vvData.controllerChainRef || null,
3318
+ agentWallet: vvData.agentWallet || null,
3319
+ agentAccountId: vvData.agentAccountId || null,
3320
+ agentChainRef: vvData.agentChainRef || null,
3321
+ agentId: vvData.agentId || null,
3322
+ scope: vvData.scope || 'global',
3323
+ permissions: Array.isArray(vvData.permissions) ? vvData.permissions : [],
3324
+ maxSpend: vvData.maxSpend || null,
3325
+ expiresAt: expiresAt || null,
3326
+ isExpired: Boolean(isExpired),
3327
+ allowedPaymentTypes: Array.isArray(vvData.allowedPaymentTypes) ? vvData.allowedPaymentTypes : [],
3328
+ receiptDisclosure: vvData.receiptDisclosure || null,
3329
+ instructions: vvData.instructions || null,
3330
+ skills: Array.isArray(vvData.skills) ? vvData.skills : []
3331
+ });
3332
+ }
3333
+ }
3334
+ }
3335
+
3336
+ // Calculate context pack summary
3337
+ const activeDelegations = agentDelegations.filter(d => !d.isExpired).length;
3338
+ const allCapabilities = [];
3339
+ const allSkills = [];
3340
+ const seenCapabilityKeys = new Set();
3341
+ const seenSkillIds = new Set();
3342
+
3343
+ for (const identity of agentIdentities) {
3344
+ // Capabilities are already arrays of strings
3345
+ if (Array.isArray(identity.capabilities)) {
3346
+ for (const cap of identity.capabilities) {
3347
+ const key = String(cap).toLowerCase();
3348
+ if (!seenCapabilityKeys.has(key)) {
3349
+ seenCapabilityKeys.add(key);
3350
+ allCapabilities.push(cap);
3351
+ }
3352
+ }
3353
+ }
3354
+ // Skills are objects {id, label, version, ...} - dedupe by id
3355
+ if (Array.isArray(identity.skills)) {
3356
+ for (const skill of identity.skills) {
3357
+ const skillId = (skill && typeof skill === 'object' && skill.id) ? String(skill.id) : null;
3358
+ if (skillId && !seenSkillIds.has(skillId)) {
3359
+ seenSkillIds.add(skillId);
3360
+ allSkills.push(skill);
3361
+ }
3362
+ }
3363
+ }
3364
+ }
3365
+
3366
+ // Add agent context to response
3367
+ response.data.agentContext = {
3368
+ identities: agentIdentities,
3369
+ delegations: agentDelegations,
3370
+ services: agentServices
3371
+ };
3372
+
3373
+ response.data.contextPack = {
3374
+ identityCount: agentIdentities.length,
3375
+ delegationCount: agentDelegations.length,
3376
+ activeDelegations: activeDelegations,
3377
+ capabilitiesSummary: allCapabilities.slice(0, 20), // Top 20 unique capabilities
3378
+ skillsSummary: allSkills.slice(0, 30) // Top 30 unique skills
3379
+ };
3380
+
3381
+ return response;
3382
+ },
3383
+
3384
+ async zeus_url_fetch(args) {
3385
+ const raw = String(args?.url || '').trim();
3386
+ if (!raw) return { error: 'validation_error', message: 'url is required' };
3387
+ let url;
3388
+ try {
3389
+ url = new URL(raw);
3390
+ } catch {
3391
+ return { error: 'validation_error', message: 'Invalid URL' };
3392
+ }
3393
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
3394
+ return { error: 'validation_error', message: 'Only http and https URLs are allowed' };
3395
+ }
3396
+ if (isBlockedZeusFetchHost(url.hostname)) {
3397
+ return { error: 'blocked_host', message: 'This host is blocked (local/private/metadata addresses).' };
3398
+ }
3399
+ const maxChars = Math.min(400000, Math.max(5000, Number(args?.maxChars) || 120000));
3400
+ const controller = new AbortController();
3401
+ const timeout = setTimeout(() => controller.abort(), 22000);
3402
+ try {
3403
+ const response = await fetch(url.toString(), {
3404
+ method: 'GET',
3405
+ redirect: 'follow',
3406
+ signal: controller.signal,
3407
+ headers: {
3408
+ accept: 'text/html, text/plain, application/json;q=0.8, */*;q=0.2',
3409
+ 'user-agent': 'NEUS-Zeus-MCP/1.0'
3410
+ }
3411
+ });
3412
+ const contentType = response.headers.get('content-type') || '';
3413
+ const body = await response.text();
3414
+ return {
3415
+ success: response.ok,
3416
+ status: response.status,
3417
+ url: response.url,
3418
+ contentType: contentType.split(';')[0] || 'unknown',
3419
+ truncated: body.length > maxChars,
3420
+ body: body.slice(0, maxChars)
3421
+ };
3422
+ } finally {
3423
+ clearTimeout(timeout);
3424
+ }
3425
+ },
3426
+
3427
+ async zeus_execute_javascript(args) {
3428
+ try {
3429
+ const { value, logs } = await runZeusSandbox(String(args?.script || ''), args?.input, args?.timeoutMs);
3430
+ return { success: true, output: value, logs };
3431
+ } catch (error) {
3432
+ return { error: 'javascript_execution_error', message: String(error?.message || error || 'Execution failed') };
3433
+ }
3434
+ },
3435
+
3436
+ async execute_javascript_code(args) {
3437
+ return handlers.zeus_execute_javascript(args);
3438
+ },
3439
+
3440
+ async zeus_skill_tools_list(args, ctx) {
3441
+ const identifier = String(args?.identifier || '').trim() || (await resolveAuthenticatedWallet(ctx));
3442
+ if (!identifier) return { error: 'validation_error', message: 'identifier or Bearer profile wallet required' };
3443
+ const limit = Math.min(Math.max(1, Number(args?.limit) || 100), 250);
3444
+ const offset = Math.max(0, Number(args?.offset) || 0);
3445
+ const query = new URLSearchParams({
3446
+ limit: String(limit),
3447
+ offset: String(offset),
3448
+ tags: 'tool,skill,agent:tool,agent:skill'
3449
+ });
3450
+ if (!ctx.isAuthenticated) query.set('isPublicRead', '1');
3451
+ const result = await ctx.callApi(`/api/v1/proofs/by-wallet/${encodeURIComponent(identifier)}?${query.toString()}`, {
3452
+ headers: mcpAgentDelegationHeaders(args?.agentWallet)
3453
+ });
3454
+ const proofs = Array.isArray(result?.data?.proofs)
3455
+ ? result.data.proofs
3456
+ : Array.isArray(result?.proofs)
3457
+ ? result.proofs
3458
+ : Array.isArray(result?.data)
3459
+ ? result.data
3460
+ : [];
3461
+ return {
3462
+ success: result?.error ? false : true,
3463
+ items: proofs.map((proof) => ({
3464
+ qHash: getProofQHash(proof),
3465
+ title: getProofTitle(proof),
3466
+ tags: getProofTags(proof)
3467
+ })).filter((item) => item.qHash),
3468
+ rawCount: proofs.length
3469
+ };
3470
+ },
3471
+
3472
+ async zeus_skill_tool_execute(args, ctx) {
3473
+ const qHash = String(args?.qHash || '').trim();
3474
+ if (!/^0x[a-fA-F0-9]{64}$/.test(qHash)) return { error: 'validation_error', message: 'valid qHash required' };
3475
+ const proof = await ctx.callApi(`/api/v1/proofs/${encodeURIComponent(qHash)}`);
3476
+ if (proof?.error) return proof;
3477
+ const content = getProofContentText(proof);
3478
+ const parsed = parseSkillToolProofSections(content);
3479
+ if (!parsed.separatorFound || !parsed.script) {
3480
+ return {
3481
+ error: 'invalid_tool_proof',
3482
+ message: 'Tool proof must contain markdown summary, separator line -----, then JavaScript.'
3483
+ };
3484
+ }
3485
+ try {
3486
+ const { value, logs } = await runZeusSandbox(parsed.script, args?.input, args?.timeoutMs);
3487
+ return { success: true, qHash, summaryMarkdown: parsed.summaryMarkdown, output: value, logs };
3488
+ } catch (error) {
3489
+ return { error: 'javascript_execution_error', message: String(error?.message || error || 'Execution failed') };
3490
+ }
3491
+ },
3492
+
3493
+ async zeus_session_receipt_create(args, ctx) {
3494
+ // Skip receipt creation in local dev to avoid persistent storage clutter / stuck execution.
3495
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') {
3496
+ return { success: true, skipped: true, note: 'Skipped in dev ? receipts not created on localhost.' };
3497
+ }
3498
+ const walletAddress = await resolveReceiptWallet(args, ctx);
3499
+ const content = String(args?.content || '').trim();
3500
+ if (!walletAddress || !content) {
3501
+ return { error: 'validation_error', message: 'walletAddress/Bearer profile wallet and content required' };
3502
+ }
3503
+ const visibility = 'private';
3504
+ const extraTags = normalizeStringArray(args?.tags).filter((t) => !/^receipt:/i.test(t));
3505
+ const tags = Array.from(new Set([...extraTags, 'receipt:chat']));
3506
+ const title = String(args?.title || '').trim() || `Zeus session receipt — ${new Date().toISOString().slice(0, 19).replace('T', ' ')} UTC`;
3507
+ const body = {
3508
+ walletAddress,
3509
+ verifierIds: ['ownership-basic'],
3510
+ data: {
3511
+ owner: walletAddress,
3512
+ content: content.slice(0, 48000),
3513
+ contentType: 'text/plain'
3514
+ },
3515
+ options: {
3516
+ ...proofOptionsForVisibility(visibility),
3517
+ storeOriginalContent: true,
3518
+ meta: { title, visibility, tags }
3519
+ },
3520
+ meta: { title, visibility, tags }
3521
+ };
3522
+ const result = await ctx.callApi('/api/v1/verification', { method: 'POST', body });
3523
+ return { success: !result?.error, receipt: result, qHash: result?.data?.qHash || result?.qHash || null };
3524
+ },
3525
+
3526
+ async zeus_session_receipt_append(args, ctx) {
3527
+ const qHash = String(args?.qHash || '').trim();
3528
+ const appendContent = String(args?.appendContent || '').trim();
3529
+ if (!/^0x[a-fA-F0-9]{64}$/.test(qHash) || !appendContent) {
3530
+ return { error: 'validation_error', message: 'qHash and appendContent required' };
3531
+ }
3532
+ const existing = await ctx.callApi(`/api/v1/proofs/${encodeURIComponent(qHash)}`);
3533
+ if (existing?.error) return existing;
3534
+ const previous = getProofContentText(existing);
3535
+ const title = String(args?.title || existing?.data?.meta?.title || existing?.meta?.title || 'Zeus session receipt').trim();
3536
+ const content = [previous, `\n\n---\n\n## Appended ${new Date().toISOString()}\n\n${appendContent}`].join('').trim();
3537
+ const created = await handlers.zeus_session_receipt_create({
3538
+ ...args,
3539
+ title,
3540
+ content,
3541
+ tags: [`supersedes:${qHash.toLowerCase()}`],
3542
+ asPrivate: args?.asPrivate
3543
+ }, ctx);
3544
+ return { ...created, previousQHash: qHash, action: 'append_created_superseding_receipt' };
3545
+ },
3546
+
3547
+ async list_tools(args, ctx) {
3548
+ return fetchProofTitleIdMap(ctx, args, 'tool,skill,agent:tool,agent:skill');
3549
+ },
3550
+
3551
+ async list_all_available_tools(_args, ctx) {
3552
+ return ctx.exposedToolNames || [...PUBLIC_MCP_TOOL_NAMES_ORDERED];
3553
+ },
3554
+
3555
+ async list_memories(args, ctx) {
3556
+ return fetchProofTitleIdMap(ctx, args, 'memory');
3557
+ },
3558
+
3559
+ async list_research(args, ctx) {
3560
+ return fetchProofTitleIdMap(ctx, args, 'research');
3561
+ },
3562
+
3563
+ async list_instructions(args, ctx) {
3564
+ return fetchProofTitleIdMap(ctx, args, 'instruction,instructions');
3565
+ },
3566
+
3567
+ async list_rules(args, ctx) {
3568
+ return fetchProofTitleIdMap(ctx, args, 'rule,rules');
3569
+ },
3570
+
3571
+ async list_mcps(args, ctx) {
3572
+ return fetchProofTitleIdMap(ctx, args, 'mcp');
3573
+ },
3574
+
3575
+ async get_tool(args, ctx) {
3576
+ return getCategorizedProofByHash(args, ctx, 'tool');
3577
+ },
3578
+
3579
+ async get_skill(args, ctx) {
3580
+ return getCategorizedProofByHash(args, ctx, 'skill');
3581
+ },
3582
+
3583
+ async get_memory(args, ctx) {
3584
+ return getCategorizedProofByHash(args, ctx, 'memory');
3585
+ },
3586
+
3587
+ async get_instruction(args, ctx) {
3588
+ return getCategorizedProofByHash(args, ctx, 'instruction');
3589
+ },
3590
+
3591
+ async get_rule(args, ctx) {
3592
+ return getCategorizedProofByHash(args, ctx, 'rule');
3593
+ },
3594
+
3595
+ async get_research(args, ctx) {
3596
+ return getCategorizedProofByHash(args, ctx, 'research');
3597
+ },
3598
+
3599
+ async append_Content_to_memory(args, ctx) {
3600
+ const qHash = normalizeQHashInput(args?.qHash);
3601
+ const append = String(args?.appendContent || args?.content || '').trim();
3602
+ if (!qHash) return { error: 'validation_error', message: 'qHash is required' };
3603
+ if (!append) return { error: 'validation_error', message: 'appendContent or content is required' };
3604
+ const raw = await ctx.callApi(`/api/v1/proofs/${encodeURIComponent(qHash)}`);
3605
+ if (raw?.error) return raw;
3606
+ const un = unwrapApiProofRecord(raw) || raw;
3607
+ if (!proofMatchesCategory(un, 'memory') && !proofMatchesCategory(raw, 'memory')) {
3608
+ return { error: 'validation_error', message: 'Target proof must be tagged as memory' };
3609
+ }
3610
+ const previous = getProofContentText(un);
3611
+ const wallet = await resolveReceiptWallet(args, ctx);
3612
+ if (!wallet) return { error: 'validation_error', message: 'walletAddress or authenticated profile wallet is required' };
3613
+ const newContent = [previous, append].filter(Boolean).join('\n\n').trim();
3614
+ const baseTitle = getProofTitle(un);
3615
+ const title = `Memory (append)${baseTitle && baseTitle !== 'Proof' ? ` — ${baseTitle}` : ''}`.trim();
3616
+ const supTag = `supersedes:${qHash.toLowerCase()}`;
3617
+ const dense = buildZeusProofIndexTags({
3618
+ category: 'memory',
3619
+ title,
3620
+ content: newContent.slice(0, 28000),
3621
+ facetKeys: normalizeStringArray(args?.indexFacetKeys)
3622
+ });
3623
+ const tags = Array.from(new Set(['memory', supTag, 'zappend', ...dense]));
3624
+ const body = {
3625
+ walletAddress: wallet,
3626
+ verifierIds: ['ownership-basic'],
3627
+ data: {
3628
+ owner: wallet,
3629
+ content: newContent.slice(0, 48000),
3630
+ contentType: 'text/plain'
3631
+ },
3632
+ options: {
3633
+ ...proofOptionsForVisibility(args?.asPrivate === true ? 'private' : 'unlisted'),
3634
+ storeOriginalContent: true,
3635
+ meta: { title, visibility: args?.asPrivate === true ? 'private' : 'unlisted', tags }
3636
+ },
3637
+ meta: { title, visibility: args?.asPrivate === true ? 'private' : 'unlisted', tags }
3638
+ };
3639
+ const result = await ctx.callApi('/api/v1/verification', { method: 'POST', body });
3640
+ return { success: !result?.error, previousQHash: qHash, newProof: result, qHash: result?.data?.qHash || result?.qHash || null };
3641
+ },
3642
+
3643
+ async delete_revoke_memory(args, ctx) {
3644
+ const qHash = normalizeQHashInput(args?.qHash);
3645
+ if (!qHash) return { error: 'validation_error', message: 'qHash is required' };
3646
+ const raw = await ctx.callApi(`/api/v1/proofs/${encodeURIComponent(qHash)}`);
3647
+ if (raw?.error) return raw;
3648
+ const un = unwrapApiProofRecord(raw) || raw;
3649
+ if (!proofMatchesCategory(un, 'memory') && !proofMatchesCategory(raw, 'memory')) {
3650
+ return { error: 'validation_error', message: 'Target proof must be tagged as memory' };
3651
+ }
3652
+ const wallet = await resolvePrincipalWallet(
3653
+ args?.walletAddress && typeof args.walletAddress === 'string' ? String(args.walletAddress) : null,
3654
+ ctx
3655
+ );
3656
+ if (wallet?.error) return wallet;
3657
+ if (!wallet?.walletAddress) {
3658
+ return { error: 'validation_error', message: 'wallet or Bearer session required to revoke' };
3659
+ }
3660
+ return ctx.callApi(`/api/v1/proofs/revoke-self/${encodeURIComponent(qHash)}`, {
3661
+ method: 'POST',
3662
+ body: { walletAddress: wallet.walletAddress }
3663
+ });
3664
+ },
3665
+
3666
+ async list_agents(args, ctx) {
3667
+ const ident = String(args?.identifier || '').trim() || (await resolveAuthenticatedWallet(ctx));
3668
+ if (!ident) return { error: 'validation_error', message: 'identifier or Bearer profile wallet is required' };
3669
+ return handlers.neus_proofs_get(
3670
+ {
3671
+ identifier: ident,
3672
+ limit: Math.min(Math.max(1, Number(args?.limit) || 120), 200),
3673
+ offset: 0,
3674
+ agentWallet: args?.agentWallet
3675
+ },
3676
+ ctx
3677
+ ).then((r) => {
3678
+ if (r?.error) return r;
3679
+ const ag = r?.data?.agentContext;
3680
+ if (!ag) return { success: true, identities: [], delegations: [], services: [] };
3681
+ return {
3682
+ success: true,
3683
+ identities: ag.identities || [],
3684
+ delegations: ag.delegations || [],
3685
+ services: ag.services || [],
3686
+ contextPack: r.data?.contextPack || null
3687
+ };
3688
+ });
3689
+ },
3690
+
3691
+ async execute_agent_command(args, ctx) {
3692
+ if (!NEUS_MCP_INFERENCE_KEY) {
3693
+ return {
3694
+ error: 'configuration_error',
3695
+ message:
3696
+ 'Set NEUS_MCP_INFERENCE_API_KEY or OPENROUTER_API_KEY in the MCP server environment, or use Zeus in the web app for full agent chat (session cookie).'
3697
+ };
3698
+ }
3699
+ const prompt = String(args?.prompt || '').trim();
3700
+ if (!prompt) return { error: 'validation_error', message: 'prompt is required' };
3701
+ const requestId = randomUUID();
3702
+ const ac = new AbortController();
3703
+ agentInferenceJobs.set(requestId, { controller: ac, startedAt: Date.now() });
3704
+ const model = String(args?.model || NEUS_MCP_INFERENCE_MODEL).trim();
3705
+ const temperature = Math.min(2, Math.max(0, Number.isFinite(Number(args?.temperature)) ? Number(args.temperature) : 0.3));
3706
+ const agentW = String(args?.agentWallet || '').trim();
3707
+ const agentId = String(args?.agentId || '').trim();
3708
+ const baseSystem =
3709
+ String(args?.systemPrompt || '').trim() ||
3710
+ `You are a delegated NEUS agent task runner. Be concise. User wallet context may be available via the caller.\n${
3711
+ agentW ? `Agent wallet: ${agentW}.\n` : ''
3712
+ }${agentId ? `Agent id: ${agentId}.\n` : ''}`;
3713
+ const body = {
3714
+ model,
3715
+ temperature,
3716
+ messages: [
3717
+ { role: 'system', content: baseSystem },
3718
+ { role: 'user', content: prompt }
3719
+ ]
3720
+ };
3721
+ const url = `${NEUS_MCP_INFERENCE_BASE_URL}/chat/completions`;
3722
+ const t = setTimeout(() => ac.abort(), 120000);
3723
+ try {
3724
+ const res = await fetch(url, {
3725
+ method: 'POST',
3726
+ signal: ac.signal,
3727
+ headers: {
3728
+ 'Content-Type': 'application/json',
3729
+ Authorization: `Bearer ${NEUS_MCP_INFERENCE_KEY}`,
3730
+ Referer: MCP_PUBLIC_URL,
3731
+ 'X-Title': 'NEUS-MCP-execute_agent_command'
3732
+ },
3733
+ body: JSON.stringify(body)
3734
+ });
3735
+ const data = await res.json().catch(() => ({}));
3736
+ if (!res.ok) {
3737
+ return {
3738
+ error: 'inference_error',
3739
+ status: res.status,
3740
+ message: data?.error?.message || data?.message || 'Inference request failed',
3741
+ requestId
3742
+ };
3743
+ }
3744
+ const text =
3745
+ (Array.isArray(data?.choices) && data.choices[0] && (data.choices[0].message?.content || data.choices[0].text)) ||
3746
+ (typeof data?.output_text === 'string' ? data.output_text : null) ||
3747
+ '';
3748
+ return {
3749
+ success: true,
3750
+ requestId,
3751
+ model,
3752
+ response: typeof text === 'string' ? text : String(text),
3753
+ note: 'OpenAI-compatible /chat/completions. To cancel mid-flight, call stop_agent_processing with requestId (same MCP process).'
3754
+ };
3755
+ } catch (e) {
3756
+ if (e?.name === 'AbortError') {
3757
+ return { error: 'aborted', requestId, message: 'Request aborted' };
3758
+ }
3759
+ return { error: 'inference_error', message: String(e?.message || e), requestId };
3760
+ } finally {
3761
+ clearTimeout(t);
3762
+ agentInferenceJobs.delete(requestId);
3763
+ }
3764
+ },
3765
+
3766
+ async stop_agent_processing(args) {
3767
+ const id = String(args?.requestId || '').trim();
3768
+ if (!id) return { error: 'validation_error', message: 'requestId is required' };
3769
+ const job = agentInferenceJobs.get(id);
3770
+ if (!job) return { success: false, message: 'No active job with that requestId' };
3771
+ try {
3772
+ job.controller.abort();
3773
+ } catch { /* */ }
3774
+ agentInferenceJobs.delete(id);
3775
+ return { success: true, stopped: true, requestId: id };
3776
+ },
3777
+
3778
+ async encrypt_text(args) {
3779
+ const text = String(args?.text || '');
3780
+ if (!text.trim()) return { error: 'validation_error', message: 'text is required' };
3781
+ return {
3782
+ success: true,
3783
+ encrypted: wrapEncryptedPayload(encryptPlaintext(text)),
3784
+ note: 'Store and transmit this value as-is. Decrypt only when plaintext is required for immediate execution.'
3785
+ };
3786
+ },
3787
+
3788
+ async decrypt_text(args) {
3789
+ const raw = String(args?.text || '').trim();
3790
+ if (!raw) return { error: 'validation_error', message: 'text is required' };
3791
+ const payload =
3792
+ raw.startsWith(ENCRYPT_TAG_OPEN) && raw.endsWith(ENCRYPT_TAG_CLOSE)
3793
+ ? raw.slice(ENCRYPT_TAG_OPEN.length, raw.length - ENCRYPT_TAG_CLOSE.length).trim()
3794
+ : raw;
3795
+ try {
3796
+ return {
3797
+ success: true,
3798
+ plaintext: decryptPayload(payload),
3799
+ note: 'Plaintext was decrypted for immediate-use execution context.'
3800
+ };
3801
+ } catch (error) {
3802
+ return {
3803
+ error: 'validation_error',
3804
+ message: `Failed to decrypt payload: ${String(error?.message || error)}`
3805
+ };
3806
+ }
3807
+ },
3808
+
3809
+ async mcp_connection_create(args) {
3810
+ const n = normalizeMcpConnectionEntry(args);
3811
+ if (!n) return { error: 'validation_error', message: 'Valid id, type (streamableHttp|stdio), and url or command are required' };
3812
+ return { success: true, connection: n, hint: 'Hosted MCP returns normalized config — merge according to your client — s documented MCP wiring.' };
3813
+ },
3814
+
3815
+ async mcp_connection_modify(args) {
3816
+ const conn = normalizeMcpConnectionEntry(args?.connection);
3817
+ if (!conn) return { error: 'validation_error', message: 'connection object with id, type, and url|command is required' };
3818
+ const list = Array.isArray(args?.mcpConnections) ? args.mcpConnections : [];
3819
+ const norm = list.map((x) => normalizeMcpConnectionEntry(x)).filter((c) => c != null);
3820
+ const next = norm.some((c) => c.id === conn.id) ? norm.map((c) => (c.id === conn.id ? conn : c)) : [...norm, conn];
3821
+ return { success: true, mcpConnections: next, action: 'upsert' };
3822
+ },
3823
+
3824
+ async mcp_connection_delete(args) {
3825
+ const id = String(args?.id || '').trim();
3826
+ if (!id) return { error: 'validation_error', message: 'id is required' };
3827
+ if (id.toLowerCase() === 'neus-mcp' || id.toLowerCase() === 'neus') {
3828
+ return { error: 'validation_error', message: 'The default NEUS MCP connection id cannot be deleted via this tool.' };
3829
+ }
3830
+ const list = Array.isArray(args?.mcpConnections) ? args.mcpConnections : null;
3831
+ if (!list) {
3832
+ return { success: true, mcpConnections: [], deleted: id, note: 'No mcpConnections list passed — pass your current MCP connection list when your client expects it.' };
3833
+ }
3834
+ const next = list
3835
+ .map((x) => normalizeMcpConnectionEntry(x))
3836
+ .filter((c) => c != null && c.id !== id);
3837
+ return { success: true, mcpConnections: next, deleted: id };
3838
+ },
3839
+
3840
+ // ============================================================================
3841
+ // NEUS CONTEXT — Single entry point for all NEUS context
3842
+ // ============================================================================
3843
+
3844
+ async neus_context(_args, ctx) {
3845
+ const verifiersResult = await ctx.callApi('/api/v1/verification/verifiers');
3846
+ return buildPublicNeusContextPayload(ctx, verifiersResult);
3847
+ }
3848
+ };
3849
+
3850
+ /**
3851
+ * Extended/internal tools follow MCP_EXTENDED_TOOLS_POLICY.
3852
+ * Defaults to off everywhere. Only `admin_wallet` can expose them, and only for a Bearer
3853
+ * profile wallet listed in MCP_ADMIN_ADDRESSES / ADMIN_ADDRESSES.
3854
+ */
3855
+ async function resolveMcpExtendedToolsExposure(sessionBearerToken) {
3856
+ const policy = mcp.mcpExtendedToolsPolicy;
3857
+ if (policy === 'off') return false;
3858
+ const ctx = createSessionCtx(sessionBearerToken, [...PUBLIC_MCP_TOOL_NAMES_ORDERED]);
3859
+ const wallet = await resolveAuthenticatedWallet(ctx);
3860
+ if (!wallet) return false;
3861
+ return mcp.mcpAdminWalletSet.has(wallet.toLowerCase());
3862
+ }
3863
+
3864
+ // ============================================================================
3865
+ // MCP SERVER SETUP
3866
+ // ============================================================================
3867
+
3868
+ /**
3869
+ * @param {string} name
3870
+ * @param {Record<string, unknown>} args
3871
+ * @param {ReturnType<typeof createSessionCtx>} sessionCtx
3872
+ */
3873
+ async function runNeusToolCall(name, args, sessionCtx) {
3874
+ const handler = handlers[name];
3875
+ if (!handler) {
3876
+ return {
3877
+ content: [{ type: 'text', text: JSON.stringify({ error: 'unknown_tool', message: `Unknown tool: ${name}` }) }],
3878
+ isError: true
3879
+ };
3880
+ }
3881
+
3882
+ try {
3883
+ const decryptedArgs = decryptArgsDeep(args || {});
3884
+ const result = await handler(decryptedArgs, sessionCtx);
3885
+ const protectedResult = encryptSensitiveDeep(result);
3886
+ const isError = result?.error !== undefined;
3887
+
3888
+ let statusCode = null;
3889
+ if (result?.status) {
3890
+ statusCode = Number(result.status);
3891
+ } else if (result?.error && typeof result.error === 'object' && result.error.statusCode) {
3892
+ statusCode = Number(result.error.statusCode);
3893
+ }
3894
+
3895
+ if (protectedResult?.elicitation?.required) {
3896
+ return {
3897
+ content: [{
3898
+ type: 'text',
3899
+ text: JSON.stringify(protectedResult, null, 2)
3900
+ }],
3901
+ isError: false,
3902
+ meta: { elicitationRequired: true }
3903
+ };
3904
+ }
3905
+
3906
+ if (statusCode && statusCode >= 400) {
3907
+ return {
3908
+ content: [{
3909
+ type: 'text',
3910
+ text: JSON.stringify({
3911
+ error: protectedResult?.error || 'api_error',
3912
+ message: protectedResult?.message || `Request failed (${statusCode})`,
3913
+ statusCode,
3914
+ ...(protectedResult?.nextStep ? { nextStep: protectedResult.nextStep } : {}),
3915
+ ...(protectedResult?.remediation ? { remediation: protectedResult.remediation } : {}),
3916
+ ...(protectedResult?.hostedCheckoutUrl ? { hostedCheckoutUrl: protectedResult.hostedCheckoutUrl } : {})
3917
+ }, null, 2)
3918
+ }],
3919
+ isError: true
3920
+ };
3921
+ }
3922
+
3923
+ return {
3924
+ content: [{ type: 'text', text: JSON.stringify(protectedResult, null, 2) }],
3925
+ isError
3926
+ };
3927
+ } catch (err) {
3928
+ const statusCode = err?.statusCode || (err?.status >= 400 && err?.status < 600 ? err.status : 500);
3929
+ const is4xx = statusCode >= 400 && statusCode < 500;
3930
+
3931
+ return {
3932
+ content: [{
3933
+ type: 'text',
3934
+ text: JSON.stringify({
3935
+ error: 'handler_error',
3936
+ message: err.message,
3937
+ errorType: is4xx ? 'client_error' : 'server_error',
3938
+ statusCode
3939
+ })
3940
+ }],
3941
+ isError: true
3942
+ };
3943
+ }
3944
+ }
3945
+
3946
+ /**
3947
+ * Factory to create a new MCP Server instance with request-scoped auth.
3948
+ * Each client session gets its own McpServer instance and session context.
3949
+ * @param {string|null} sessionBearerToken - Per-session bearer token (from client HTTP header)
3950
+ * @param {{ exposeExtendedTools?: boolean }} [options]
3951
+ * @returns {McpServer}
3952
+ */
3953
+ function createNeusServer(sessionBearerToken = null, options = {}) {
3954
+ const exposeExtendedTools = options.exposeExtendedTools === true;
3955
+ const exposedToolNames = exposeExtendedTools
3956
+ ? TOOLS.map((t) => t.name)
3957
+ : [...PUBLIC_MCP_TOOL_NAMES_ORDERED];
3958
+ const sessionCtx = createSessionCtx(sessionBearerToken, exposedToolNames);
3959
+
3960
+ const srv = new McpServer(
3961
+ { name: 'neus-mcp', version: '1.0.0' },
3962
+ {
3963
+ capabilities: {
3964
+ tools: {},
3965
+ elicitation: {}
3966
+ },
3967
+ instructions: `${buildMcpAssistantInstructions(exposeExtendedTools)}
3968
+
3969
+ Identifier formats:
3970
+ - EVM wallet: 0x... (40 hex chars)
3971
+ - Solana wallet: base58 (32-44 chars)
3972
+ - DID: did:pkh:eip155:<hubChainId>:0x...`
3973
+ }
3974
+ );
3975
+
3976
+ const toolsToRegister = toolDefinitionsForExposure(exposeExtendedTools);
3977
+
3978
+ for (const tool of toolsToRegister) {
3979
+ const zodInput = TOOL_ZOD_INPUT[tool.name];
3980
+ if (!zodInput) {
3981
+ throw new Error(`Missing TOOL_ZOD_INPUT for ${tool.name}`);
3982
+ }
3983
+ srv.registerTool(
3984
+ tool.name,
3985
+ {
3986
+ description: tool.description,
3987
+ inputSchema: zodInput
3988
+ },
3989
+ async (args) => runNeusToolCall(tool.name, args, sessionCtx)
3990
+ );
3991
+ }
3992
+
3993
+ return srv;
3994
+ }
3995
+
3996
+ function toolDefinitionsForExposure(exposeExtendedTools = false) {
3997
+ return exposeExtendedTools ? TOOLS : TOOLS.filter((t) => PUBLIC_MCP_TOOL_SET.has(t.name));
3998
+ }
3999
+
4000
+ function toolNamesForExposure(exposeExtendedTools = false) {
4001
+ return toolDefinitionsForExposure(exposeExtendedTools).map((t) => t.name);
4002
+ }
4003
+
4004
+ // ============================================================================
4005
+ // SERVER STARTUP
4006
+ // ============================================================================
4007
+
4008
+ async function main() {
4009
+ // Default to HTTP (open hosted mode). Set MCP_TRANSPORT=stdio for local/CLI use.
4010
+ const transportMode = mcp.mcpTransport;
4011
+
4012
+ if (transportMode === 'http') {
4013
+ // MCP_AUTH_TOKEN: optional shared secret for HTTP MCP when MCP_REQUIRE_AUTH=true (self-hosted). Never forwarded to the NEUS API.
4014
+ const authToken = mcp.mcpAuthToken;
4015
+ const requireAuthEnabled = mcp.mcpRequireAuth;
4016
+ // Hosted default: OPEN (no MCP session secret). API enforces signer + payment + rate limits.
4017
+ // Private mode: set MCP_REQUIRE_AUTH=true and MCP_AUTH_TOKEN=<token>.
4018
+
4019
+ const allowedOrigins = mcp.mcpAllowedOrigins;
4020
+ const allowedHosts = mcp.mcpAllowedHosts;
4021
+ const enableDnsRebindingProtection = allowedOrigins.length > 0 || allowedHosts.length > 0;
4022
+
4023
+ const extractNeusSessionToken = (req) => {
4024
+ const neusSession = String(req?.headers?.['x-neus-session'] || '').trim();
4025
+ if (neusSession) return neusSession;
4026
+ if (!requireAuthEnabled) {
4027
+ const authHeader = String(req?.headers?.authorization || '').trim();
4028
+ if (authHeader.startsWith('Bearer ')) return authHeader.slice('Bearer '.length).trim();
4029
+ }
4030
+ return null;
4031
+ };
4032
+
4033
+ const createStatelessTransport = async (req) => {
4034
+ const transport = new StreamableHTTPServerTransport({
4035
+ sessionIdGenerator: undefined,
4036
+ allowedOrigins,
4037
+ allowedHosts,
4038
+ enableDnsRebindingProtection
4039
+ });
4040
+ const neusToken = extractNeusSessionToken(req);
4041
+ const exposeExtended = await resolveMcpExtendedToolsExposure(neusToken);
4042
+ const srv = createNeusServer(neusToken, { exposeExtendedTools: exposeExtended });
4043
+ await srv.connect(transport);
4044
+ log('info', 'mcp_stateless_transport_created', {
4045
+ authenticated: !!neusToken,
4046
+ exposeExtendedTools: exposeExtended,
4047
+ extendedToolsPolicy: mcp.mcpExtendedToolsPolicy
4048
+ });
4049
+ return { server: srv, transport };
4050
+ };
4051
+
4052
+ const port = mcp.port;
4053
+ const host = mcp.host;
4054
+ const mcpPath = '/mcp';
4055
+ const isMcpPath = (pathname) => pathname === mcpPath || pathname === '/';
4056
+
4057
+ const safeEqual = (a, b) => {
4058
+ try {
4059
+ const aa = Buffer.from(String(a), 'utf8');
4060
+ const bb = Buffer.from(String(b), 'utf8');
4061
+ if (aa.length !== bb.length) return false;
4062
+ return timingSafeEqual(aa, bb);
4063
+ } catch {
4064
+ return false;
4065
+ }
4066
+ };
4067
+ const setErrorCorsHeaders = (req, res) => {
4068
+ const reqOrigin = String(req?.headers?.origin || '').trim();
4069
+ const origin = allowedOrigins.length === 0
4070
+ ? '*'
4071
+ : (reqOrigin && allowedOrigins.includes(reqOrigin) ? reqOrigin : allowedOrigins[0]);
4072
+ res.setHeader('Access-Control-Allow-Origin', origin);
4073
+ res.setHeader('Access-Control-Allow-Methods', 'GET,POST,DELETE,OPTIONS');
4074
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-Neus-Session,mcp-protocol-version,Mcp-Session-Id');
4075
+ res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
4076
+ if (origin !== '*') {
4077
+ const existingVary = String(res.getHeader('Vary') || '').trim();
4078
+ res.setHeader('Vary', existingVary ? `${existingVary}, Origin` : 'Origin');
4079
+ }
4080
+ };
4081
+ const writeErrorJson = (req, res, statusCode, payload) => {
4082
+ setErrorCorsHeaders(req, res);
4083
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
4084
+ res.end(JSON.stringify(payload));
4085
+ };
4086
+
4087
+ const requireAuth = (req, res) => {
4088
+ if (!requireAuthEnabled) return true; // Open mode: no auth required
4089
+ if (!authToken) {
4090
+ writeErrorJson(req, res, 500, { error: 'misconfigured_auth', message: 'MCP_REQUIRE_AUTH=true but MCP_AUTH_TOKEN is empty' });
4091
+ return false;
4092
+ }
4093
+ const header = String(req.headers.authorization || '').trim();
4094
+ const token = header.startsWith('Bearer ') ? header.slice('Bearer '.length).trim() : '';
4095
+ if (!token || !safeEqual(token, authToken)) {
4096
+ writeErrorJson(req, res, 401, { error: 'unauthorized' });
4097
+ return false;
4098
+ }
4099
+ return true;
4100
+ };
4101
+
4102
+ const requireProtocolHeader = (req, res) => {
4103
+ // DELETE terminate requests from some clients omit protocol header.
4104
+ if (String(req?.method || '').toUpperCase() === 'DELETE') return true;
4105
+ let v = req.headers['mcp-protocol-version'] || req.headers['MCP-Protocol-Version'];
4106
+ if (!v) {
4107
+ v = MCP_PROTOCOL_VERSION;
4108
+ req.headers['mcp-protocol-version'] = v;
4109
+ }
4110
+ return true;
4111
+ };
4112
+
4113
+ const mcpWellKnownPath = '/.well-known/mcp.json';
4114
+ const serverCardPath = '/.well-known/mcp/server-card.json';
4115
+ const getCorsOrigin = (req) => {
4116
+ const reqOrigin = String(req?.headers?.origin || '').trim();
4117
+ if (allowedOrigins.length === 0) return '*';
4118
+ if (reqOrigin && allowedOrigins.includes(reqOrigin)) return reqOrigin;
4119
+ return allowedOrigins[0];
4120
+ };
4121
+ const setCorsHeaders = (req, res) => {
4122
+ const origin = getCorsOrigin(req);
4123
+ res.setHeader('Access-Control-Allow-Origin', origin);
4124
+ res.setHeader('Access-Control-Allow-Methods', 'GET,POST,DELETE,OPTIONS');
4125
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-Neus-Session,mcp-protocol-version,Mcp-Session-Id');
4126
+ res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
4127
+ res.setHeader('Access-Control-Max-Age', '86400');
4128
+ if (origin !== '*') {
4129
+ const existingVary = String(res.getHeader('Vary') || '').trim();
4130
+ res.setHeader('Vary', existingVary ? `${existingVary}, Origin` : 'Origin');
4131
+ }
4132
+ };
4133
+ const writeJson = (req, res, statusCode, payload) => {
4134
+ setCorsHeaders(req, res);
4135
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
4136
+ res.end(JSON.stringify(payload));
4137
+ };
4138
+
4139
+ const httpServer = createServer(async (req, res) => {
4140
+ // Generate request ID for logging
4141
+ const requestId = randomUUID();
4142
+ req.requestId = requestId;
4143
+
4144
+ log('info', 'mcp_request_start', {
4145
+ requestId,
4146
+ method: req?.method,
4147
+ url: req?.url
4148
+ });
4149
+
4150
+ const mcpRequestStart = Date.now();
4151
+ res.on('finish', () => {
4152
+ const durationMs = Date.now() - mcpRequestStart;
4153
+ log('info', 'neus.mcp.request.completed', {
4154
+ requestId,
4155
+ method: req.method,
4156
+ path: req._mcpPathname || null,
4157
+ statusCode: res.statusCode,
4158
+ durationMs,
4159
+ toolName: req._mcpParsedBody?.params?.name ?? null,
4160
+ agentId: req.agentId ?? null,
4161
+ errorCode: req.errorCode ?? null,
4162
+ service: 'protocol-mcp',
4163
+ environment: process.env.NODE_ENV || 'production'
4164
+ });
4165
+ });
4166
+
4167
+ try {
4168
+ if (!req || !res) return;
4169
+ if (!req.url) {
4170
+ return writeJson(req, res, 400, { error: 'missing_url', requestId });
4171
+ }
4172
+
4173
+ const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
4174
+ req._mcpPathname = url.pathname;
4175
+ if (req.method === 'OPTIONS' && (isMcpPath(url.pathname) || url.pathname === '/health' || url.pathname === '/health/ready' || url.pathname === mcpWellKnownPath || url.pathname === serverCardPath)) {
4176
+ setCorsHeaders(req, res);
4177
+ res.writeHead(204);
4178
+ return res.end();
4179
+ }
4180
+
4181
+ // Health check endpoint (no auth required)
4182
+ if (url.pathname === '/health') {
4183
+ return writeJson(req, res, 200, {
4184
+ status: 'ok',
4185
+ timestamp: Date.now(),
4186
+ requestId
4187
+ });
4188
+ }
4189
+
4190
+ // Readiness endpoint (dependency-aware)
4191
+ if (url.pathname === '/health/ready') {
4192
+ const readyResult = await callNeusApi('/api/v1/health', { maxRetries: 0 });
4193
+ if (readyResult?.error) {
4194
+ return writeJson(req, res, 503, {
4195
+ status: 'degraded',
4196
+ ready: false,
4197
+ dependency: 'api.neus.network',
4198
+ reason: readyResult.message || readyResult.error,
4199
+ requestId
4200
+ });
4201
+ }
4202
+ return writeJson(req, res, 200, {
4203
+ status: 'ok',
4204
+ ready: true,
4205
+ dependency: 'api.neus.network',
4206
+ requestId
4207
+ });
4208
+ }
4209
+
4210
+ if (url.pathname === mcpWellKnownPath) {
4211
+ return writeJson(req, res, 200, {
4212
+ version: '1.0',
4213
+ protocolVersion: MCP_PROTOCOL_VERSION,
4214
+ serverInfo: { name: 'neus-mcp', title: 'NEUS MCP', version: '1.0.0' },
4215
+ description:
4216
+ 'NEUS hosted MCP: verifiers, proofs, verify flows, agents. Always call neus_context first.',
4217
+ transport: 'streamable-http',
4218
+ url: `${MCP_PUBLIC_URL}${mcpPath}`,
4219
+ capabilities: { tools: true, elicitation: true },
4220
+ tools: toolsForPublicDiscovery(),
4221
+ documentationUrl: 'https://docs.neus.network/mcp/overview',
4222
+ iconUrl: 'https://neus.network/images/neus-brand-pack/neus-mark-128.png'
4223
+ });
4224
+ }
4225
+
4226
+ if (url.pathname === serverCardPath) {
4227
+ return writeJson(req, res, 200, {
4228
+ name: 'neus-mcp',
4229
+ title: 'NEUS MCP',
4230
+ version: '1.0.0',
4231
+ description:
4232
+ 'NEUS hosted MCP: verifiers, proofs, verify flows, agents. Always call neus_context first.',
4233
+ transport: 'streamable-http',
4234
+ url: `${MCP_PUBLIC_URL}${mcpPath}`,
4235
+ documentationUrl: 'https://docs.neus.network/mcp/overview',
4236
+ iconUrl: 'https://neus.network/images/neus-brand-pack/neus-mark-128.png',
4237
+ capabilities: { tools: true, elicitation: true },
4238
+ tools: toolsForPublicDiscovery()
4239
+ });
4240
+ }
4241
+
4242
+ if (!isMcpPath(url.pathname)) {
4243
+ return writeJson(req, res, 404, { error: 'not_found', requestId });
4244
+ }
4245
+
4246
+ // Ensure transport responses expose session headers for browser/Electron clients.
4247
+ setCorsHeaders(req, res);
4248
+
4249
+ if (!requireAuth(req, res)) return;
4250
+ if (!requireProtocolHeader(req, res)) return;
4251
+
4252
+
4253
+ if (req.method === 'POST') {
4254
+ let raw = '';
4255
+ req.setEncoding('utf8');
4256
+ req.on('data', (chunk) => { raw += chunk; });
4257
+ req.on('end', async () => {
4258
+ let parsedBody;
4259
+ try {
4260
+ parsedBody = raw ? JSON.parse(raw) : undefined;
4261
+ req._mcpParsedBody = parsedBody;
4262
+ } catch {
4263
+ return writeJson(req, res, 400, { error: 'invalid_json', requestId });
4264
+ }
4265
+
4266
+ // Add timeout protection (5 minutes)
4267
+ const timeoutMs = 5 * 60 * 1000;
4268
+ const timeoutId = setTimeout(() => {
4269
+ log('error', 'mcp_request_timeout', { requestId, timeoutMs, method: req.method, path: url.pathname });
4270
+ if (!res.headersSent) {
4271
+ writeJson(req, res, 504, {
4272
+ error: 'timeout',
4273
+ message: 'Request timeout after 5 minutes',
4274
+ requestId
4275
+ });
4276
+ }
4277
+ }, timeoutMs);
4278
+
4279
+ try {
4280
+ const { server: requestServer, transport: httpTransport } = await createStatelessTransport(req);
4281
+ res.once('close', () => {
4282
+ try {
4283
+ httpTransport.close();
4284
+ } catch (e) {
4285
+ log('warn', 'mcp_transport_close_error', { requestId, message: e?.message });
4286
+ }
4287
+ try {
4288
+ if (typeof requestServer?.close === 'function') requestServer.close();
4289
+ } catch (e) {
4290
+ log('warn', 'mcp_server_close_error', { requestId, message: e?.message });
4291
+ }
4292
+ });
4293
+ await httpTransport.handleRequest(req, res, parsedBody);
4294
+ clearTimeout(timeoutId);
4295
+ } catch (transportError) {
4296
+ clearTimeout(timeoutId);
4297
+ log('error', 'mcp_transport_error', { requestId, message: transportError?.message || 'unknown_error' });
4298
+ if (!res.headersSent) {
4299
+ writeJson(req, res, 500, {
4300
+ error: 'transport_error',
4301
+ message: transportError.message,
4302
+ requestId
4303
+ });
4304
+ }
4305
+ }
4306
+ });
4307
+ return;
4308
+ }
4309
+
4310
+ if (req.method === 'GET' || req.method === 'DELETE') {
4311
+ return writeJson(req, res, 405, {
4312
+ jsonrpc: '2.0',
4313
+ error: {
4314
+ code: -32000,
4315
+ message: 'Method not allowed.'
4316
+ },
4317
+ id: null,
4318
+ requestId
4319
+ });
4320
+ }
4321
+
4322
+ return writeJson(req, res, 405, { error: 'method_not_allowed', requestId });
4323
+ } catch (err) {
4324
+ // Map error types to HTTP status codes
4325
+ const statusCode = err?.statusCode || (err?.status >= 400 && err?.status < 600 ? err.status : 500);
4326
+ const is4xx = statusCode >= 400 && statusCode < 500;
4327
+ const is5xx = statusCode >= 500;
4328
+
4329
+ log('error', 'mcp_request_error', {
4330
+ requestId,
4331
+ statusCode,
4332
+ error: err?.message || 'unknown',
4333
+ is4xx,
4334
+ is5xx
4335
+ });
4336
+
4337
+ writeJson(req, res, statusCode, {
4338
+ error: is4xx ? 'client_error' : 'server_error',
4339
+ message: err?.message || 'unknown',
4340
+ requestId,
4341
+ statusCode
4342
+ });
4343
+ }
4344
+ });
4345
+
4346
+ httpServer.listen(port, host, () => {
4347
+ log('info', 'mcp_http_started', {
4348
+ publicUrl: `${MCP_PUBLIC_URL}${mcpPath}`,
4349
+ bind: `http://${host}:${port}${mcpPath}`,
4350
+ api: NEUS_API_URL,
4351
+ appId: MCP_APP_ID,
4352
+ toolsTotal: TOOLS.length,
4353
+ toolsPublicDiscovery: PUBLIC_MCP_TOOL_NAMES_ORDERED.length,
4354
+ extendedToolsPolicy: mcp.mcpExtendedToolsPolicy
4355
+ });
4356
+ if (!enableDnsRebindingProtection) {
4357
+ log('warn', 'mcp_dns_rebinding_protection_disabled', {
4358
+ message: 'Set MCP_ALLOWED_ORIGINS and/or MCP_ALLOWED_HOSTS in production.'
4359
+ });
4360
+ }
4361
+ });
4362
+
4363
+ let shuttingDown = false;
4364
+ const shutdown = (signal) => {
4365
+ if (shuttingDown) return;
4366
+ shuttingDown = true;
4367
+ log('info', 'mcp_shutdown_start', { signal });
4368
+ const forceExitTimer = setTimeout(() => {
4369
+ log('error', 'mcp_shutdown_forced', { signal, timeoutMs: SHUTDOWN_TIMEOUT_MS });
4370
+ process.exit(1);
4371
+ }, SHUTDOWN_TIMEOUT_MS);
4372
+ forceExitTimer.unref?.();
4373
+ httpServer.close((err) => {
4374
+ if (err) {
4375
+ log('error', 'mcp_shutdown_error', { signal, message: err.message });
4376
+ process.exit(1);
4377
+ }
4378
+ clearTimeout(forceExitTimer);
4379
+ log('info', 'mcp_shutdown_complete', { signal });
4380
+ process.exit(0);
4381
+ });
4382
+ };
4383
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
4384
+ process.on('SIGINT', () => shutdown('SIGINT'));
4385
+
4386
+ return;
4387
+ }
4388
+
4389
+ // MCP_TRANSPORT=stdio uses the same public allowlist by default as hosted MCP.
4390
+ const stdioExposeExtended = await resolveMcpExtendedToolsExposure(null);
4391
+ const stdioServer = createNeusServer(null, { exposeExtendedTools: stdioExposeExtended });
4392
+ const stdioTransport = new StdioServerTransport();
4393
+ await stdioServer.connect(stdioTransport);
4394
+ log('info', 'mcp_stdio_started', {
4395
+ api: NEUS_API_URL,
4396
+ appId: MCP_APP_ID,
4397
+ toolsRegistered: stdioExposeExtended ? TOOLS.length : PUBLIC_MCP_TOOL_NAMES_ORDERED.length,
4398
+ exposeExtendedTools: stdioExposeExtended,
4399
+ extendedToolsPolicy: mcp.mcpExtendedToolsPolicy
4400
+ });
4401
+ }
4402
+
4403
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
4404
+ main().catch((err) => {
4405
+ log('error', 'mcp_fatal', {
4406
+ message: err?.message || 'unknown_error',
4407
+ stack: err?.stack || null
4408
+ });
4409
+ process.exit(1);
4410
+ });
4411
+ }
4412
+
4413
+ export {
4414
+ buildMcpAssistantInstructions,
4415
+ createNeusServer,
4416
+ resolveMcpExtendedToolsExposure,
4417
+ toolNamesForExposure,
4418
+ toolsForPublicDiscovery,
4419
+ PUBLIC_MCP_TOOL_NAMES_ORDERED
4420
+ };