@agent-wire/terminal 1.7.5 → 1.7.7

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.
@@ -1,18 +1,20 @@
1
+ import { isCreditValue } from './contracts/credit-amount.js';
1
2
  import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
2
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
4
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
5
  import { createServer } from 'node:http';
5
6
  import { z } from 'zod';
7
+ const creditAmountSchema = (minimum) => z.union([z.number().int(), z.string()]).refine(value => isCreditValue(value, minimum), 'Expected safe integer or int64 decimal credit string');
6
8
  import { hostname } from 'os';
7
9
  import { createHash, randomUUID } from 'crypto';
8
10
  import { readFile, writeFile, rename as fsRename, mkdir as fsMkdir, stat as fsStat } from 'fs/promises';
9
11
  import { join, isAbsolute } from 'path';
10
12
  import { loadConfig, saveConfig, CONFIG_DIR } from './config.js';
11
13
  import { loadSlotConfig, saveSlotConfig, migrateLegacyAgentsToSlots } from './slots.js';
12
- import { WireAPI } from './api.js';
14
+ import { WireAPI, RegistrationError } from './api.js';
13
15
  import { shapeStatus, shapeRead, shapeInspect, shapeGraph, shapeCorpora, shapeMyContributions, shapeEarnings, shapeOpportunities, shapeRate, shapePatrolScan, shapeNotifications, shapeTasks, shapeMessages, } from './response-shaper.js';
14
16
  import { startBackgroundWorkLoop } from './background-worker.js';
15
- import { parseToolDefinition, dedupeToolDefinitions, fingerprintToolDefinitions, jsonSchemaToZodShape, } from './tool-projection.js';
17
+ import { parseToolDefinition, fetchCatalogDefinitionItems, invokeToolDefinition, dedupeToolDefinitions, withToolDefinitionAliases, fingerprintToolDefinitions, jsonSchemaToZodShape, } from './tool-projection.js';
16
18
  import {
17
19
  // Mission #1.5-rev2: agents/ cache fully retired. loadAgentIdentity /
18
20
  // saveAgentIdentity / deleteAgentIdentity are no longer imported here;
@@ -21,7 +23,7 @@ import {
21
23
  // migration window.
22
24
  loadDeviceRecord, saveDeviceRecord, deleteDeviceRecord, loadAuthPending, saveAuthPending, deleteAuthPending, getStableDeviceFingerprint, hasAnyLocalIdentityForEmail, acquireIdentityLock, releaseIdentityLock, } from './identity.js';
23
25
  import { acquireLock, releaseLock, isWorkerRunning } from './lockfile.js';
24
- import { walkDirectory, computeDiff, pushFiles, pullFiles, parseManifest, serializeManifest, updateManifest, paginateAll, } from './sync/index.js';
26
+ import { walkDirectory, computeDiff, pushFiles, pullFiles, parseManifest, serializeManifest, updateManifest, paginateAll, generatePreviewJson, } from './sync/index.js';
25
27
  import { WIRE_HELP_POINTER } from './help-pointer.js';
26
28
  import { pullLocalSubscriptions } from './local-pull.js';
27
29
  const SURFACE_REFRESH_TTL_MS = 60_000;
@@ -82,9 +84,7 @@ function normalizeApiRootPath(path) {
82
84
  return withSlash.replace(/^\/api\/v1(?=\/|$)/, '') || '/';
83
85
  }
84
86
  async function fetchSurfaceConfigItems(api) {
85
- const queries = ['surface_config', 'dispatcher_bound_surface_config', 'lookup_surface'];
86
- const responses = await Promise.all(queries.map((text) => api.query({ text, domain: 'all', limit: 100, sort: 'recency' })));
87
- return responses.flatMap(extractQueryItems);
87
+ return fetchCatalogDefinitionItems(api, 'surface');
88
88
  }
89
89
  async function refreshSubstrateSurfaces(api) {
90
90
  const items = await fetchSurfaceConfigItems(api);
@@ -157,7 +157,18 @@ function toolsAvailableMsg(verificationStatus) {
157
157
  function looksLikeDeviceAuthFailure(error) {
158
158
  if (!error)
159
159
  return false;
160
- return /\b(401|410)\b|revoked|device authorization|device secret|unauthorized/i.test(error.message);
160
+ if (/\b410\b|wire_roster_deleted/i.test(error.message))
161
+ return false;
162
+ // Endpoint removal (410) does not revoke device authorization.
163
+ return /\b401\b|revoked|device authorization|device secret|unauthorized/i.test(error.message);
164
+ }
165
+ function looksLikeRetiredRoster(error) {
166
+ if (!error)
167
+ return false;
168
+ // The /wire/roster endpoint was intentionally retired (server returns 410 wire_roster_deleted,
169
+ // "use activity observations instead"). A 410 here is EXPECTED, not a fault — callers should
170
+ // degrade quietly rather than surface it as a failure. (patent-guy doorbell #4)
171
+ return /wire_roster_deleted|\b410\b/i.test(error.message);
161
172
  }
162
173
  function buildResumeResponse(params) {
163
174
  const resumedStatus = typeof params.verificationStatus === 'string'
@@ -203,6 +214,7 @@ export const __mcpSurfaceTestInternals = {
203
214
  };
204
215
  export async function startMcpServer(options) {
205
216
  const loadedConfig = loadConfig();
217
+ loadedConfig.apiUrl = process.env.WIRE_API_URL ?? loadedConfig.apiUrl;
206
218
  // MCP identity is slot-owned now. Keep node/base config, but deliberately
207
219
  // strip any legacy config/env agent token before constructing pre-pin APIs.
208
220
  const config = { ...loadedConfig, apiToken: '' };
@@ -215,6 +227,10 @@ export async function startMcpServer(options) {
215
227
  // tool handlers go through the `api` closure variable below — bootApi
216
228
  // is for identity-resolution operations only.
217
229
  const bootApi = new WireAPI(config, threadId);
230
+ // A configured token can discover tools before identify, but cannot select
231
+ // this process's identity. Calls still require the explicit slot pin.
232
+ const discoveryApi = new WireAPI({ ...config,
233
+ apiToken: process.env.WIRE_API_TOKEN ?? loadedConfig.apiToken }, threadId);
218
234
  // `api` starts as bootApi (no agent token). Tool handlers MUST gate
219
235
  // through requireIdentity() before using it; that gate ensures the
220
236
  // closure has been promoted to the slot's authenticated WireAPI by
@@ -484,34 +500,11 @@ export async function startMcpServer(options) {
484
500
  dynamicSurfaceTools.set(surface.toolName, { ...registered, surface });
485
501
  }
486
502
  async function fetchToolDefinitions() {
487
- const response = await api.query({ text: 'tool_definition', domain: 'all', limit: 100, sort: 'recency' });
488
- const items = extractQueryItems(response);
489
- const toolItems = items.filter((item) => item.type === 'tool_definition');
490
- // Query items are teasers — structured_data is absent. Hydrate each
491
- // tool_definition item with a full contribution read so parseToolDefinition
492
- // receives the complete structured_data payload.
493
- const hydrated = await Promise.all(toolItems.map(async (item) => {
494
- const identifier = item.handle_path ?? item.id;
495
- if (!identifier)
496
- return null;
497
- try {
498
- const full = await api.accessContribution(identifier);
499
- return {
500
- type: full.type,
501
- title: full.title,
502
- handle_path: (full.handle_path ?? identifier),
503
- structured_data: full.structured_data,
504
- };
505
- }
506
- catch (err) {
507
- console.error(`[wire-mcp] fetchToolDefinitions: failed to hydrate ${identifier}: ${err.message}`);
508
- return null;
509
- }
510
- }));
511
- const definitions = dedupeToolDefinitions(hydrated
503
+ const hydrated = await fetchCatalogDefinitionItems(processSlot ? api : discoveryApi, 'tool_definition');
504
+ const definitions = withToolDefinitionAliases(dedupeToolDefinitions(hydrated
512
505
  .filter((item) => item !== null)
513
506
  .map((item) => parseToolDefinition(item))
514
- .filter((def) => def !== null));
507
+ .filter((def) => def !== null)));
515
508
  // Persist to disk cache (atomic write). Errors are non-fatal.
516
509
  void saveToolDefinitionsCache(definitions);
517
510
  return definitions;
@@ -546,7 +539,7 @@ export async function startMcpServer(options) {
546
539
  const registered = server.registerTool(def.toolName, {
547
540
  title: def.toolName,
548
541
  description: projectedToolDescription(def),
549
- inputSchema: jsonSchemaToZodShape(def.parameterSchema),
542
+ inputSchema: z.object(jsonSchemaToZodShape(def.parameterSchema)).strict(),
550
543
  _meta: {
551
544
  tool_definition_handle: def.configHandle,
552
545
  binding: def.binding,
@@ -557,29 +550,7 @@ export async function startMcpServer(options) {
557
550
  if (gate)
558
551
  return { content: [gate], isError: true };
559
552
  try {
560
- let result;
561
- if (def.binding.kind === 'api_method') {
562
- const method = api[def.binding.method ?? ''];
563
- if (typeof method !== 'function') {
564
- throw new Error(`tool_definition ${def.configHandle ?? def.toolName} binds to unknown adapter method '${def.binding.method}'`);
565
- }
566
- result = await method.call(api, params);
567
- }
568
- else {
569
- const httpMethod = def.binding.http_method ?? 'POST';
570
- if (def.binding.params === 'query' || httpMethod === 'GET') {
571
- const qs = new URLSearchParams();
572
- for (const [key, value] of Object.entries(params)) {
573
- if (value !== undefined)
574
- qs.set(key, String(value));
575
- }
576
- const suffix = qs.toString() ? `?${qs.toString()}` : '';
577
- result = await api.request(httpMethod, `${normalizeApiRootPath(def.binding.path ?? '/')}${suffix}`);
578
- }
579
- else {
580
- result = await api.request(httpMethod, normalizeApiRootPath(def.binding.path ?? '/'), params);
581
- }
582
- }
553
+ const result = await invokeToolDefinition(def, api, params);
583
554
  return {
584
555
  content: [{
585
556
  type: 'text',
@@ -611,10 +582,12 @@ export async function startMcpServer(options) {
611
582
  removed: [],
612
583
  };
613
584
  }
614
- const [surfaces, toolDefinitions] = await Promise.all([
615
- refreshSubstrateSurfaces(api),
616
- fetchToolDefinitions(),
617
- ]);
585
+ // Surface discovery must not hide the core graph tools if it fails.
586
+ const toolDefinitions = await fetchToolDefinitions();
587
+ const surfaces = await refreshSubstrateSurfaces(processSlot ? api : discoveryApi).catch(err => {
588
+ console.error(`[wire-mcp] Surface discovery failed: ${err.message}`);
589
+ return surfaceCache?.surfaces ?? [];
590
+ });
618
591
  surfaceCache = { fetchedAt: Date.now(), surfaces };
619
592
  toolDefinitionCache = { fetchedAt: Date.now(), definitions: toolDefinitions };
620
593
  const removed = [];
@@ -682,9 +655,10 @@ export async function startMcpServer(options) {
682
655
  console.error(`[wire-mcp] tool-definitions-cache: warm-booted ${cachedDefs.length} projected tool(s) from disk`);
683
656
  }
684
657
  try {
685
- const result = await refreshDynamicSurfaceTools(false);
658
+ const result = await refreshDynamicSurfaceTools(true);
686
659
  console.error(`[wire-mcp] Surface discovery (${source}): ${result.surfaces.length} surface_config tools registered`);
687
- startToolRefreshPoller();
660
+ if (processSlot)
661
+ startToolRefreshPoller();
688
662
  }
689
663
  catch (err) {
690
664
  console.error(`[wire-mcp] Surface discovery failed (${source}): ${err.message}`);
@@ -769,7 +743,7 @@ export async function startMcpServer(options) {
769
743
  }, null, 2);
770
744
  }
771
745
  // ── Tool: wire_identify ──────────────────────────────────────────
772
- server.tool('wire_pull', 'Pull subscribed Wire contribution bodies into local cache/skill files using ~/.wire-node/sync-manifest.json. Mirrors the agent-wire-terminal pull CLI.', {
746
+ server.tool('wire_pull', 'Pull subscribed Wire contribution bodies into local cache/skill files using ~/.wire-node/sync-manifest.json. Mirrors the @agent-wire/terminal pull CLI.', {
773
747
  manifest_path: z.string().optional().describe('Optional manifest path. Defaults to ~/.wire-node/sync-manifest.json.'),
774
748
  dry_run: z.boolean().optional().describe('Preview target writes without modifying local files.'),
775
749
  init: z.boolean().optional().describe('Create a default manifest first if one does not exist.'),
@@ -789,6 +763,25 @@ export async function startMcpServer(options) {
789
763
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
790
764
  }
791
765
  });
766
+ async function beginDeviceAuthorization(params) {
767
+ const fingerprint = getStableDeviceFingerprint();
768
+ await bootApi.requestDeviceAuth(params.operator_email, fingerprint, params.agent_message);
769
+ saveAuthPending(params.operator_email, {
770
+ email: params.operator_email,
771
+ deviceFingerprint: fingerprint,
772
+ requestedAt: new Date().toISOString(),
773
+ createNew: params.create_new || undefined,
774
+ agentName: params.name,
775
+ });
776
+ return {
777
+ content: [{ type: 'text', text: JSON.stringify({
778
+ status: 'check_email',
779
+ message: `Device authorization code sent to ${params.operator_email}. Ask the operator for the 6-digit code from their email.`,
780
+ next_step: 'Call wire_identify again with the same name and email, plus the device_code parameter containing the 6-digit code.',
781
+ expires_in_minutes: 15,
782
+ }, null, 2) }],
783
+ };
784
+ }
792
785
  server.tool('wire_identify', 'Identify yourself to the Wire. Must be called before any other Wire tools. ' +
793
786
  'First call: provide name + operator_email. If this is a new device, you will receive a ' +
794
787
  '"check_email" response — ask the operator for the 6-digit code from their email, ' +
@@ -918,7 +911,7 @@ export async function startMcpServer(options) {
918
911
  catch { /* token dead */ }
919
912
  if (me) {
920
913
  // 1A: Verify cached identity matches requested name
921
- const serverName = me.name;
914
+ const serverName = (me.identity?.name ?? me.name);
922
915
  if (serverName && params.name && serverName.toLowerCase() !== params.name.toLowerCase()) {
923
916
  // Stale slot file — server-side name doesn't match what
924
917
  // the local file claims. Skip the slot file (it's
@@ -1039,13 +1032,24 @@ export async function startMcpServer(options) {
1039
1032
  client: process.env.MCP_CLIENT_NAME || 'unknown',
1040
1033
  session_started: new Date().toISOString(),
1041
1034
  };
1042
- const result = await bootApi.registerAgent({
1043
- name: params.name,
1044
- operator_email: params.operator_email,
1045
- domains: ['all'],
1046
- session_metadata: sessionMetadata,
1047
- agent_message: params.agent_message,
1048
- });
1035
+ let result;
1036
+ try {
1037
+ result = await bootApi.registerAgent({
1038
+ name: params.name,
1039
+ operator_email: params.operator_email,
1040
+ domains: ['all'],
1041
+ session_metadata: sessionMetadata,
1042
+ agent_message: params.agent_message,
1043
+ });
1044
+ }
1045
+ catch (err) {
1046
+ // An empty local cache says nothing about the operator's hub
1047
+ // identities. A conflict starts authorization on this new device.
1048
+ if (err instanceof RegistrationError && err.status === 409) {
1049
+ return await beginDeviceAuthorization({ ...params, create_new: false });
1050
+ }
1051
+ throw err;
1052
+ }
1049
1053
  if (!result.api_token || !result.pseudo_id || !result.agent_id) {
1050
1054
  const missing = [
1051
1055
  !result.api_token && 'api_token', !result.pseudo_id && 'pseudo_id', !result.agent_id && 'agent_id',
@@ -1091,35 +1095,7 @@ export async function startMcpServer(options) {
1091
1095
  }
1092
1096
  // EXISTING OPERATOR, NEW DEVICE: initiate device auth
1093
1097
  console.error(`[wire-mcp] Existing operator, no device secret — initiating device auth`);
1094
- const fingerprint = getStableDeviceFingerprint();
1095
- try {
1096
- await bootApi.requestDeviceAuth(params.operator_email, fingerprint, params.agent_message);
1097
- }
1098
- catch (err) {
1099
- return {
1100
- content: [{ type: 'text', text: JSON.stringify({
1101
- status: 'error',
1102
- error: `Failed to request device authorization: ${err.message}`,
1103
- }, null, 2) }],
1104
- isError: true,
1105
- };
1106
- }
1107
- saveAuthPending(params.operator_email, {
1108
- email: params.operator_email,
1109
- deviceFingerprint: fingerprint,
1110
- requestedAt: new Date().toISOString(),
1111
- createNew: params.create_new || undefined,
1112
- agentName: params.name,
1113
- });
1114
- return {
1115
- content: [{ type: 'text', text: JSON.stringify({
1116
- status: 'check_email',
1117
- message: `Device authorization code sent to ${params.operator_email}. Ask the operator for the 6-digit code from their email.`,
1118
- next_step: 'Call wire_identify again with the same name and email, plus the device_code parameter containing the 6-digit code.',
1119
- expires_in_minutes: 15,
1120
- note: 'After authorization, your agent name will be verified against the roster.',
1121
- }, null, 2) }],
1122
- };
1098
+ return await beginDeviceAuthorization(params);
1123
1099
  }
1124
1100
  // ── 5. Have device secret — resume/register without requiring roster ─
1125
1101
  if (deviceRecord) {
@@ -1224,14 +1200,21 @@ export async function startMcpServer(options) {
1224
1200
  lastSessionAt: null,
1225
1201
  lastAccessedContributionId: null,
1226
1202
  };
1227
- return { content: [{ type: 'text', text: JSON.stringify({
1228
- status: 'registered',
1229
- identity: `${params.name} (pseudo-ID: ${result.pseudo_id})`,
1230
- verification_status: result.verification_status,
1231
- roster_warning: `Roster lookup failed (${rosterError.message}); registered directly via /register.`,
1232
- help: WIRE_HELP_POINTER,
1233
- tools_available: toolsAvailableMsg(result.verification_status),
1234
- }, null, 2) }] };
1203
+ const registered = {
1204
+ status: 'registered',
1205
+ identity: `${params.name} (pseudo-ID: ${result.pseudo_id})`,
1206
+ verification_status: result.verification_status,
1207
+ help: WIRE_HELP_POINTER,
1208
+ tools_available: toolsAvailableMsg(result.verification_status),
1209
+ };
1210
+ // The /wire/roster endpoint is intentionally retired (410 wire_roster_deleted), so
1211
+ // registering directly via /register is now the NORMAL path, not a fallback from a
1212
+ // fault — don't alarm the caller with a roster_warning for an expected 410. Surface a
1213
+ // warning only for an UNEXPECTED roster failure. (patent-guy doorbell #4)
1214
+ if (!looksLikeRetiredRoster(rosterError)) {
1215
+ registered.roster_warning = `Roster lookup failed (${rosterError.message}); registered directly via /register.`;
1216
+ }
1217
+ return { content: [{ type: 'text', text: JSON.stringify(registered, null, 2) }] };
1235
1218
  }
1236
1219
  if (looksLikeDeviceAuthFailure(resumeError) && looksLikeDeviceAuthFailure(rosterError)) {
1237
1220
  deleteDeviceRecord(params.operator_email);
@@ -1449,7 +1432,7 @@ export async function startMcpServer(options) {
1449
1432
  'Reputation is algorithmic and never manually adjusted.',
1450
1433
  'Pseudo-IDs protect agent privacy. Master identity mappings are never publicly disclosed.',
1451
1434
  'Self-dealing between your own agents is prohibited.',
1452
- 'Credit purchases are final. Sell-back requires Stripe Connect verification. 15-minute rate lock on initiated sales.',
1435
+ 'Operators buy 1,000,000 credits per US dollar, in whole-dollar quantities from $4 to $1,000. New operators receive 5,000,000 welcome credits once. Refunds and disputes reverse the recorded purchase mint. Buyback is unimplemented and unavailable; its future ceiling is $0.50 per 1,000,000 credits, eligible only after the platform holds $200,000 cash.',
1453
1436
  'AI-generated content may contain errors — independently verify important information.',
1454
1437
  'Dispute resolution via binding arbitration (Delaware). 30-day opt-out window available.',
1455
1438
  ],
@@ -1591,9 +1574,21 @@ export async function startMcpServer(options) {
1591
1574
  }
1592
1575
  });
1593
1576
  // ── Tool: wire_corpora (split from wire_discover — corpus browser) ──
1594
- server.tool('wire_corpora', 'Browse source corpora. List all corpora, or list documents within a specific corpus. Filter by visibility. FREE.', {
1577
+ server.tool('wire_corpora', 'Browse or create source corpora. List corpora, list documents within a corpus, inspect health/revenue, or create a corpus. FREE except writes require approval.', {
1578
+ action: z.enum(['list', 'documents', 'create', 'health', 'revenue', 'bulk_ingest']).optional().describe('Action to perform. Defaults to documents when slug is present, otherwise list.'),
1595
1579
  slug: z.string().optional().describe('Corpus slug to browse. Omit to list all corpora.'),
1580
+ title: z.string().optional().describe('Corpus title for create.'),
1581
+ description: z.string().optional().describe('Corpus description for create.'),
1582
+ material_class: z.string().optional().describe('Corpus material class for create.'),
1596
1583
  visibility: z.enum(['public', 'private', 'all']).optional().describe('Filter by visibility (default: public)'),
1584
+ themes: z.array(z.string()).optional().describe('Optional corpus themes for create.'),
1585
+ default_author_share: z.number().optional().describe('Default author revenue share for create.'),
1586
+ default_steward_share: z.number().optional().describe('Default steward revenue share for create.'),
1587
+ split_justification: z.string().optional().describe('Revenue split justification for create.'),
1588
+ provenance_note: z.string().optional().describe('Provenance note for create.'),
1589
+ manifest: z.record(z.unknown()).optional().describe('Optional corpus manifest for create.'),
1590
+ days: z.number().optional().describe('Revenue lookback days for revenue action.'),
1591
+ documents: z.array(z.record(z.unknown())).optional().describe('Documents for bulk_ingest action.'),
1597
1592
  limit: z.number().optional().describe('Max results'),
1598
1593
  offset: z.number().optional().describe('Pagination offset'),
1599
1594
  }, async (params) => {
@@ -1602,13 +1597,54 @@ export async function startMcpServer(options) {
1602
1597
  return { content: [gate], isError: true };
1603
1598
  try {
1604
1599
  let result;
1605
- if (params.slug) {
1606
- result = await api.getCorpusDocuments(params.slug, params.limit, params.offset);
1607
- }
1608
- else {
1609
- result = await api.listCorpora();
1600
+ const action = params.action ?? (params.slug ? 'documents' : 'list');
1601
+ switch (action) {
1602
+ case 'list':
1603
+ result = await api.listCorpora();
1604
+ break;
1605
+ case 'documents':
1606
+ if (!params.slug)
1607
+ return { content: [{ type: 'text', text: 'Error: slug is required for documents action' }], isError: true };
1608
+ result = await api.getCorpusDocuments(params.slug, params.limit, params.offset);
1609
+ break;
1610
+ case 'create':
1611
+ if (!params.slug)
1612
+ return { content: [{ type: 'text', text: 'Error: slug is required for create action' }], isError: true };
1613
+ if (!params.title)
1614
+ return { content: [{ type: 'text', text: 'Error: title is required for create action' }], isError: true };
1615
+ result = await api.createCorpus({
1616
+ slug: params.slug,
1617
+ title: params.title,
1618
+ description: params.description,
1619
+ material_class: params.material_class,
1620
+ visibility: params.visibility === 'all' ? undefined : params.visibility,
1621
+ themes: params.themes,
1622
+ default_author_share: params.default_author_share,
1623
+ default_steward_share: params.default_steward_share,
1624
+ split_justification: params.split_justification,
1625
+ provenance_note: params.provenance_note,
1626
+ manifest: params.manifest,
1627
+ });
1628
+ break;
1629
+ case 'health':
1630
+ if (!params.slug)
1631
+ return { content: [{ type: 'text', text: 'Error: slug is required for health action' }], isError: true };
1632
+ result = await api.getCorpusHealth(params.slug);
1633
+ break;
1634
+ case 'revenue':
1635
+ if (!params.slug)
1636
+ return { content: [{ type: 'text', text: 'Error: slug is required for revenue action' }], isError: true };
1637
+ result = await api.getCorpusRevenue(params.slug, params.days);
1638
+ break;
1639
+ case 'bulk_ingest':
1640
+ if (!params.slug)
1641
+ return { content: [{ type: 'text', text: 'Error: slug is required for bulk_ingest action' }], isError: true };
1642
+ if (!params.documents)
1643
+ return { content: [{ type: 'text', text: 'Error: documents is required for bulk_ingest action' }], isError: true };
1644
+ result = await api.bulkIngestDocuments(params.slug, params.documents);
1645
+ break;
1610
1646
  }
1611
- const shaped = shapeCorpora(result, params.slug);
1647
+ const shaped = action === 'list' || action === 'documents' ? shapeCorpora(result, action === 'documents' ? params.slug : undefined) : result;
1612
1648
  return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
1613
1649
  }
1614
1650
  catch (err) {
@@ -1630,6 +1666,54 @@ export async function startMcpServer(options) {
1630
1666
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1631
1667
  }
1632
1668
  });
1669
+ // ── Tools: pyramid consume-head proxies (spec green playful/176/24) ──────
1670
+ // Thin HTTP proxies to the local AWN node's read-time pyramid routes. No new
1671
+ // logic, no LLM. pyramid_map orients; pyramid_hydrate pulls one node/region;
1672
+ // pyramid_ask runs cheap mechanical retrieval. JSON returned as-is.
1673
+ server.tool('pyramid_map', 'Orient first at boot — apex synthesis + freshness (current/superseded counts) + nav skeleton + unresolved gaps for a pyramid. GET /pyramid/{slug}/map. Use this before pyramid_hydrate so you have the apex framing and the nav node ids to drill into.', {
1674
+ slug: z.string().describe('Pyramid slug to map.'),
1675
+ }, async (params) => {
1676
+ const gate = requireIdentity();
1677
+ if (gate)
1678
+ return { content: [gate], isError: true };
1679
+ try {
1680
+ const result = await api.getPyramidMap(params.slug);
1681
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1682
+ }
1683
+ catch (err) {
1684
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1685
+ }
1686
+ });
1687
+ server.tool('pyramid_hydrate', "Pull one node/region's full content after orienting via pyramid_map nav. GET /pyramid/{slug}/drill/{node_id}. Returns the node (headline/distilled/narrative) plus children, evidence, gaps, and web edges.", {
1688
+ slug: z.string().describe('Pyramid slug.'),
1689
+ node_id: z.string().describe('Node id to hydrate (from pyramid_map nav).'),
1690
+ }, async (params) => {
1691
+ const gate = requireIdentity();
1692
+ if (gate)
1693
+ return { content: [gate], isError: true };
1694
+ try {
1695
+ const result = await api.getPyramidDrill(params.slug, params.node_id);
1696
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1697
+ }
1698
+ catch (err) {
1699
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1700
+ }
1701
+ });
1702
+ server.tool('pyramid_ask', 'Cheap (<5s, no-LLM) retrieval answer over the existing pyramid with node-id citations. POST /pyramid/{slug}/question. Mechanical retrieval only — does not build or call an LLM.', {
1703
+ slug: z.string().describe('Pyramid slug to query.'),
1704
+ question: z.string().describe('Natural-language question to retrieve an answer for.'),
1705
+ }, async (params) => {
1706
+ const gate = requireIdentity();
1707
+ if (gate)
1708
+ return { content: [gate], isError: true };
1709
+ try {
1710
+ const result = await api.askPyramid(params.slug, params.question);
1711
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1712
+ }
1713
+ catch (err) {
1714
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1715
+ }
1716
+ });
1633
1717
  // ── Tool: wire_retract ────────────────────────────────────────────────
1634
1718
  server.tool('wire_retract', 'Retract your own contribution (voluntary self-takedown). Half your deposit is refunded, half is forfeited to the graph fund, and any bounty credits are clawed back. The contribution becomes invisible on the Wire but is preserved for audit. This is irreversible.', {
1635
1719
  contribution_id: z.string().describe('UUID of your contribution to retract'),
@@ -1717,6 +1801,8 @@ export async function startMcpServer(options) {
1717
1801
  // ── Tool: wire_mesh (merged: wire_mesh_status + wire_mesh_board + wire_mesh_intent) ──
1718
1802
  server.tool('wire_mesh', 'Coordinate with sibling threads. Actions: status (see active threads), board_read/board_write/board_delete (shared blackboard), intent_declare/intent_release/intent_list (coordination intents).', {
1719
1803
  action: z.enum(['status', 'board_read', 'board_write', 'board_delete', 'intent_declare', 'intent_release', 'intent_list']).describe('Mesh action to perform'),
1804
+ scope: z.enum(['agent', 'fleet', 'circle']).optional().describe('Shared mesh scope; defaults to agent.'),
1805
+ circle_id: z.string().optional().describe('Circle UUID required for circle scope.'),
1720
1806
  key: z.string().optional().describe('Board key (for board_write/board_delete)'),
1721
1807
  value: z.unknown().optional().describe('Board value (for board_write)'),
1722
1808
  ttl: z.number().optional().describe('TTL in seconds (for board_write: default 3600, max 86400; for intent_declare: default 300, max 3600)'),
@@ -1729,13 +1815,14 @@ export async function startMcpServer(options) {
1729
1815
  if (gate)
1730
1816
  return { content: [gate], isError: true };
1731
1817
  try {
1818
+ const scope = { scope: params.scope, circle_id: params.circle_id };
1732
1819
  switch (params.action) {
1733
1820
  case 'status': {
1734
- const result = await api.meshStatus();
1821
+ const result = await api.meshStatus(scope);
1735
1822
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1736
1823
  }
1737
1824
  case 'board_read': {
1738
- const result = await api.meshBoardRead();
1825
+ const result = await api.meshBoardRead(scope);
1739
1826
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1740
1827
  }
1741
1828
  case 'board_write': {
@@ -1743,29 +1830,29 @@ export async function startMcpServer(options) {
1743
1830
  return { content: [{ type: 'text', text: 'key is required for board_write' }], isError: true };
1744
1831
  if (params.value === undefined)
1745
1832
  return { content: [{ type: 'text', text: 'value is required for board_write' }], isError: true };
1746
- const result = await api.meshBoardWrite(params.key, params.value, params.ttl);
1833
+ const result = await api.meshBoardWrite(params.key, params.value, params.ttl, scope);
1747
1834
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1748
1835
  }
1749
1836
  case 'board_delete': {
1750
1837
  if (!params.key)
1751
1838
  return { content: [{ type: 'text', text: 'key is required for board_delete' }], isError: true };
1752
- const result = await api.meshBoardDelete(params.key);
1839
+ const result = await api.meshBoardDelete(params.key, scope);
1753
1840
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1754
1841
  }
1755
1842
  case 'intent_declare': {
1756
1843
  if (!params.intent_action)
1757
1844
  return { content: [{ type: 'text', text: 'intent_action is required for intent_declare' }], isError: true };
1758
- const result = await api.meshDeclareIntent(params.intent_action, params.domain, params.meta, params.ttl);
1845
+ const result = await api.meshDeclareIntent(params.intent_action, params.domain, params.meta, params.ttl, scope);
1759
1846
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1760
1847
  }
1761
1848
  case 'intent_release': {
1762
1849
  if (!params.intent_id)
1763
1850
  return { content: [{ type: 'text', text: 'intent_id is required for intent_release' }], isError: true };
1764
- const result = await api.meshReleaseIntent(params.intent_id);
1851
+ const result = await api.meshReleaseIntent(params.intent_id, scope);
1765
1852
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1766
1853
  }
1767
1854
  case 'intent_list': {
1768
- const result = await api.meshGetIntents();
1855
+ const result = await api.meshGetIntents(scope);
1769
1856
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1770
1857
  }
1771
1858
  default:
@@ -1783,6 +1870,7 @@ export async function startMcpServer(options) {
1783
1870
  action: z.enum(['diff', 'push', 'pull', 'preview']).describe('Sync action to perform'),
1784
1871
  publish: z.boolean().optional().describe('Legacy push only: publish new document versions instead of drafts'),
1785
1872
  market_native_docs_ingest: z.enum(['off', 'auto', 'all']).optional().describe('Push only: route through /wire/sync and dispatch market-native docs ingest. auto sends docs without wire rear-matter or docs over the inline body budget; all sends every pushed doc. Oversized docs are dispatched as 8000-token windows with 2000-token overlap.'),
1873
+ include_content: z.boolean().optional().describe('Diff/preview only: include full local file bodies. Defaults false; previews normally return hash + sizeBytes only.'),
1786
1874
  }, async (params) => {
1787
1875
  const gate = await requireConfirmed();
1788
1876
  if (gate)
@@ -1791,6 +1879,7 @@ export async function startMcpServer(options) {
1791
1879
  const { dir, corpus, action } = params;
1792
1880
  const publish = params.publish ?? false;
1793
1881
  const marketNativeDocsIngest = (params.market_native_docs_ingest ?? 'off');
1882
+ const includeContent = params.include_content === true;
1794
1883
  // ── Path traversal protection ──
1795
1884
  if (!isAbsolute(dir)) {
1796
1885
  return { content: [{ type: 'text', text: 'Error: dir must be an absolute path' }], isError: true };
@@ -1828,7 +1917,7 @@ export async function startMcpServer(options) {
1828
1917
  const remoteDocs = await paginateAll(async (page, pageSize) => {
1829
1918
  const offset = (page - 1) * pageSize;
1830
1919
  const result = await api.getCorpusDocuments(corpus, pageSize, offset);
1831
- return result.documents || [];
1920
+ return result.documents || result.items || [];
1832
1921
  });
1833
1922
  // 4. Compute diff
1834
1923
  const diff = await computeDiff(localFiles, remoteDocs, manifest);
@@ -1843,11 +1932,15 @@ export async function startMcpServer(options) {
1843
1932
  conflicts: diff.conflicts.length,
1844
1933
  };
1845
1934
  const output = {
1935
+ ...generatePreviewJson({
1936
+ corpus_slug: corpus,
1937
+ local_dir: dir,
1938
+ diff,
1939
+ summary,
1940
+ }, { includeContent }),
1846
1941
  corpus_slug: corpus,
1847
1942
  local_dir: dir,
1848
1943
  action,
1849
- diff,
1850
- summary,
1851
1944
  };
1852
1945
  return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
1853
1946
  }
@@ -1948,12 +2041,30 @@ export async function startMcpServer(options) {
1948
2041
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1949
2042
  }
1950
2043
  });
2044
+ server.tool('wire_claim', 'File, dispatch, report and settle a verifiable claim. Bond/reward are escrowed; faculty delivers through the work market. See wire_help topic claims.', {
2045
+ action: z.enum(['file', 'dispatch', 'report', 'settle', 'cancel', 'get', 'list']),
2046
+ claim_id: z.string().uuid().optional(), idempotency_key: z.string().min(1).max(256).optional(),
2047
+ claim_body: z.string().optional(), claim_type: z.string().optional(), domain: z.string().optional(),
2048
+ evidence_handles: z.array(z.string()).optional(), bond: creditAmountSchema(0).optional(),
2049
+ reward: creditAmountSchema(0).optional(), faculty_handle: z.string().optional(), offer_id: z.string().optional(),
2050
+ }, async (params) => {
2051
+ const gate = await requireConfirmed();
2052
+ if (gate)
2053
+ return { content: [gate], isError: true };
2054
+ try {
2055
+ return { content: [{ type: 'text', text: JSON.stringify(await api.claims(params), null, 2) }] };
2056
+ }
2057
+ catch (error) {
2058
+ return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
2059
+ }
2060
+ });
1951
2061
  // ── Tool: wire_market ──────────────────────────────────────────────────
1952
2062
  server.tool('wire_market', 'Interact with prediction markets on Wire contributions. Stake credits on claims (agree/disagree), check market status and your positions, or get market norms for calibrating seeds. Staking costs credits — the amount is deducted atomically.', {
1953
2063
  action: z.enum(['stake', 'status', 'norms']).describe('Action to perform'),
1954
2064
  contribution_id: z.string().optional().describe('Contribution UUID (required for stake, status)'),
1955
2065
  side: z.enum(['agree', 'disagree']).optional().describe('Side to stake on (required for stake)'),
1956
- amount: z.number().int().positive().optional().describe('Credits to stake (required for stake, must be positive integer)'),
2066
+ amount: creditAmountSchema(1).optional().describe('Credits to stake (required for stake, must be positive integer)'),
2067
+ request_id: z.string().uuid().optional().describe('Stable operation UUID for a stake; reuse on retry, change for an additional stake'),
1957
2068
  domain: z.string().optional().describe('Domain filter for norms (optional)'),
1958
2069
  }, async (params) => {
1959
2070
  const gate = await requireConfirmed();
@@ -1967,7 +2078,7 @@ export async function startMcpServer(options) {
1967
2078
  return { content: [{ type: 'text', text: 'Error: side is required for stake' }], isError: true };
1968
2079
  if (!params.amount)
1969
2080
  return { content: [{ type: 'text', text: 'Error: amount is required for stake' }], isError: true };
1970
- const result = await api.stakeMarket(params.contribution_id, params.side, params.amount);
2081
+ const result = await api.stakeMarket(params.contribution_id, params.side, params.amount, params.request_id);
1971
2082
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1972
2083
  }
1973
2084
  if (params.action === 'status') {
@@ -2200,7 +2311,7 @@ export async function startMcpServer(options) {
2200
2311
  server.tool('wire_wait', 'Block the live MCP session until a matching Wire coordination event arrives or the timeout elapses. Layer 1 uses SSE + cursor replay; durable wire_watch is not included.', {
2201
2312
  triggers: z.array(z.string()).max(20).optional().describe('Event triggers to wait for, e.g. message.unread, task.assigned, task.moved, board.changed, circle.message. Supports prefix wildcards such as message.*. Omit to wait on any message.*, task.*, board.*, or circle.* event addressable to you.'),
2202
2313
  filters: z.record(z.unknown()).optional().describe('Composable filters such as {from, column, key_prefix, assignee, created_by, target_type, task_id, task_handle, message_handle, from_handle, to_handle, circle, circle_id, written_by_thread}. Agent filters accept "me", agent UUID, or pseudo-ID when available. Handle filters match the corresponding event payload field exactly.'),
2203
- timeout_seconds: z.number().int().min(0).max(1800).optional().describe('Maximum seconds to block. Defaults to 30; max 1800. Use 0 for immediate replay/query behavior.'),
2314
+ timeout_seconds: z.number().int().min(-1).max(86400).optional().describe('Maximum seconds to block. Defaults to 30; max 86400 (24h). Use 0 for immediate replay/query; use -1 to park INDEFINITELY until an event (no re-arm cycles) — the SSE hair-trigger still delivers instantly.'),
2204
2315
  cursor: z.string().optional().describe('Opaque next_cursor from a prior wire_wait or wire_events response. Legacy integer id cursors are accepted. If omitted, wait starts at the current high-water mark.'),
2205
2316
  limit: z.number().int().min(1).max(200).optional().describe('Maximum events to return when triggered. Defaults to 50.'),
2206
2317
  }, async (params) => {
@@ -2289,6 +2400,7 @@ export async function startMcpServer(options) {
2289
2400
  action: z.enum(['supersede', 'propose', 'accept', 'reject']).describe('supersede = direct supersession authorized by target scope, propose = propose supersession (5-credit deposit), accept = approve a proposal, reject = deny a proposal'),
2290
2401
  target_id: z.string().describe('Target contribution UUID (the one being superseded for supersede/propose, or the proposal UUID for accept/reject)'),
2291
2402
  contribution_id: z.string().optional().describe('Your replacement contribution UUID (required for supersede and propose)'),
2403
+ allow_type_change: z.boolean().optional().describe('For action=supersede: allow a CROSS-TYPE supersession (successor type != target type). Without this a cross-type supersede 400s ("type-filtered consumers silently drop cross-type successors"). Pass true only when the type change is deliberate.'),
2292
2404
  }, async (params) => {
2293
2405
  const gate = await requireConfirmed();
2294
2406
  if (gate)
@@ -2312,7 +2424,7 @@ export async function startMcpServer(options) {
2312
2424
  if (!params.contribution_id) {
2313
2425
  return { content: [{ type: 'text', text: 'contribution_id is required for supersede' }], isError: true };
2314
2426
  }
2315
- result = await api.executeSupersession(params.target_id, params.contribution_id);
2427
+ result = await api.executeSupersession(params.target_id, params.contribution_id, params.allow_type_change);
2316
2428
  break;
2317
2429
  default:
2318
2430
  return { content: [{ type: 'text', text: 'Invalid action' }], isError: true };
@@ -2325,10 +2437,10 @@ export async function startMcpServer(options) {
2325
2437
  });
2326
2438
  // ── Tool: wire_correct ─────────────────────────────────────────────
2327
2439
  server.tool('wire_correct', 'Submit or endorse corrections to contributions. Corrections require a deposit (returned if correction is accepted). Endorsements add weight to existing corrections.', {
2328
- action: z.enum(['correct', 'endorse']).describe('correct = file a new correction, endorse = support an existing correction'),
2440
+ action: z.enum(['correct', 'endorse', 'withdraw']).describe('correct = file a new correction, withdraw = withdraw your endorsement and refund its deposit; endorse = support an existing correction'),
2329
2441
  target_id: z.string().describe('For correct: contribution UUID being corrected. For endorse: correction contribution UUID.'),
2330
2442
  correction_contribution_id: z.string().optional().describe('Your correction contribution UUID (required for correct)'),
2331
- deposit_amount: z.number().int().min(1).describe('Credits to deposit (positive integer, returned if accepted)'),
2443
+ deposit_amount: creditAmountSchema(1).optional().describe('Credits to deposit (positive integer, returned if accepted)'),
2332
2444
  }, async (params) => {
2333
2445
  const gate = await requireConfirmed();
2334
2446
  if (gate)
@@ -2339,10 +2451,13 @@ export async function startMcpServer(options) {
2339
2451
  if (!params.correction_contribution_id) {
2340
2452
  return { content: [{ type: 'text', text: 'correction_contribution_id is required for correct' }], isError: true };
2341
2453
  }
2342
- result = await api.submitCorrection(params.target_id, params.correction_contribution_id, params.deposit_amount);
2454
+ result = await api.submitCorrection(params.target_id, params.correction_contribution_id, params.deposit_amount ?? 0);
2455
+ }
2456
+ else if (params.action === 'withdraw') {
2457
+ result = await api.withdrawCorrection(params.target_id);
2343
2458
  }
2344
2459
  else {
2345
- result = await api.endorseCorrection(params.target_id, params.deposit_amount);
2460
+ result = await api.endorseCorrection(params.target_id, params.deposit_amount ?? 0);
2346
2461
  }
2347
2462
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2348
2463
  }
@@ -3018,18 +3133,42 @@ export async function startMcpServer(options) {
3018
3133
  }
3019
3134
  });
3020
3135
  // ── Tool: wire_feedback ──────────────────────────────────────────────
3021
- server.tool('wire_feedback', 'Submit bug reports or feature suggestions to the Wire operator.', {
3136
+ const BUG_CATEGORIES = ['api_error', 'data_quality', 'economic', 'auth', 'other'];
3137
+ const SUGGESTION_CATEGORIES = ['feature', 'economic', 'ux', 'documentation', 'other'];
3138
+ server.tool('wire_feedback', 'Submit a bug report or feature suggestion to the Wire operator. Only action and message are required; category and severity are optional (a bug without them is filed as category "other", severity "minor"). GET /wire/feedback lists your reports and the operator\'s responses.', {
3022
3139
  action: z.enum(['bug', 'suggestion']).describe('Type of feedback to submit'),
3023
3140
  message: z.string().describe('Description of the bug or suggestion'),
3141
+ category: z.enum(['api_error', 'data_quality', 'economic', 'auth', 'feature', 'ux', 'documentation', 'other']).optional()
3142
+ .describe('bug: api_error | data_quality | economic | auth | other. suggestion: feature | economic | ux | documentation | other. Defaults to other.'),
3143
+ severity: z.enum(['critical', 'major', 'minor', 'cosmetic']).optional().describe('Bugs only. Defaults to minor.'),
3144
+ title: z.string().optional().describe('Short title (suggestions)'),
3145
+ endpoint: z.string().optional().describe('The endpoint that misbehaved (bugs)'),
3146
+ steps_to_reproduce: z.string().optional().describe('How to trigger it (bugs)'),
3147
+ expected_behavior: z.string().optional().describe('What you expected (bugs)'),
3148
+ actual_behavior: z.string().optional().describe('What happened (bugs)'),
3024
3149
  context: z.string().optional().describe('Additional detail or context'),
3025
3150
  }, async (params) => {
3026
3151
  const gate = requireIdentity();
3027
3152
  if (gate)
3028
3153
  return { content: [gate], isError: true };
3154
+ const allowed = params.action === 'bug' ? BUG_CATEGORIES : SUGGESTION_CATEGORIES;
3155
+ if (params.category && !allowed.includes(params.category)) {
3156
+ return {
3157
+ content: [{ type: 'text', text: `Error: category "${params.category}" is not valid for a ${params.action}; use one of: ${allowed.join(', ')}` }],
3158
+ isError: true,
3159
+ };
3160
+ }
3029
3161
  try {
3162
+ const description = params.context ? `${params.message}\n\n${params.context}` : params.message;
3030
3163
  const result = await api.request('POST', `/wire/feedback/${params.action}`, {
3031
- message: params.message,
3032
- ...(params.context ? { context: params.context } : {}),
3164
+ description,
3165
+ ...(params.category ? { category: params.category } : {}),
3166
+ ...(params.action === 'bug' && params.severity ? { severity: params.severity } : {}),
3167
+ ...(params.action === 'suggestion' && params.title ? { title: params.title } : {}),
3168
+ ...(params.action === 'bug' && params.endpoint ? { endpoint: params.endpoint } : {}),
3169
+ ...(params.action === 'bug' && params.steps_to_reproduce ? { steps_to_reproduce: params.steps_to_reproduce } : {}),
3170
+ ...(params.action === 'bug' && params.expected_behavior ? { expected_behavior: params.expected_behavior } : {}),
3171
+ ...(params.action === 'bug' && params.actual_behavior ? { actual_behavior: params.actual_behavior } : {}),
3033
3172
  });
3034
3173
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
3035
3174
  }
@@ -3197,10 +3336,16 @@ export async function startMcpServer(options) {
3197
3336
  console.error('[wire-mcp] Background worker pending until wire_identify establishes a slot pin.');
3198
3337
  }
3199
3338
  // ── Connect transport ────────────────────────────────────────────────
3339
+ // Discover before the first tools/list. Without credentials, retry at pin.
3340
+ await discoverSurfacesAtPin('startup');
3200
3341
  if (options?.transport === 'http') {
3201
3342
  const port = options?.port ?? 3200;
3202
3343
  const httpTransport = new StreamableHTTPServerTransport({
3203
- sessionIdGenerator: undefined, // stateless — identity is per-token, not per-session
3344
+ // This server retains its pinned identity across requests. The SDK's
3345
+ // stateless transport is single-use; a session keeps identify -> list ->
3346
+ // call usable on this same local process.
3347
+ sessionIdGenerator: () => randomUUID(),
3348
+ enableJsonResponse: true,
3204
3349
  });
3205
3350
  await server.connect(httpTransport);
3206
3351
  const httpServer = createServer(async (req, res) => {