@agent-wire/terminal 1.7.9 → 1.7.10

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,3 +1,5 @@
1
+ import { b107Copy } from './b107-copy.js';
2
+ import { readIdentifierShape, normalizeReadIdentifier } from './read-tool-input.js';
1
3
  import { isCreditValue } from './contracts/credit-amount.js';
2
4
  import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
3
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -11,12 +13,13 @@ import { readFile, writeFile, rename as fsRename, mkdir as fsMkdir, stat as fsSt
11
13
  import { join, isAbsolute } from 'path';
12
14
  import { loadConfig, saveConfig, CONFIG_DIR } from './config.js';
13
15
  import { resolveApiToken } from './api-token.js';
14
- import { loadSlotConfig, saveSlotConfig, migrateLegacyAgentsToSlots } from './slots.js';
16
+ import { CatalogIdentityMismatch, startCatalogFollow, waitForCatalogReady } from './catalog-follow.js';
17
+ import { loadSlotConfig, saveSlotConfig, slotPath, migrateLegacyAgentsToSlots } from './slots.js';
15
18
  import { WireAPI, WireApiRefusal, RegistrationError } from './api.js';
16
19
  import { resumeToken } from './resume.js';
17
20
  import { shapeStatus, shapeRead, shapeInspect, shapeGraph, shapeCorpora, shapeMyContributions, shapeEarnings, shapeOpportunities, shapeRate, shapePatrolScan, shapeNotifications, shapeTasks, shapeMessages, } from './response-shaper.js';
18
21
  import { startBackgroundWorkLoop } from './background-worker.js';
19
- import { parseToolDefinition, fetchCatalogDefinitionItems, invokeToolDefinition, dedupeToolDefinitions, withToolDefinitionAliases, fingerprintToolDefinitions, jsonSchemaToZodShape, } from './tool-projection.js';
22
+ import { parseToolDefinition, fetchCatalogDefinitionItems, invokeToolDefinition, dedupeToolDefinitions, withToolDefinitionAliases, fingerprintToolDefinitions, toolDefinitionInputShape, } from './tool-projection.js';
20
23
  import {
21
24
  // Mission #1.5-rev2: agents/ cache fully retired. loadAgentIdentity /
22
25
  // saveAgentIdentity / deleteAgentIdentity are no longer imported here;
@@ -29,7 +32,6 @@ import { walkDirectory, computeDiff, pushFiles, pullFiles, parseManifest, serial
29
32
  import { WIRE_HELP_POINTER } from './help-pointer.js';
30
33
  import { pullLocalSubscriptions } from './local-pull.js';
31
34
  const SURFACE_REFRESH_TTL_MS = 60_000;
32
- const DEFAULT_TOOL_REFRESH_POLL_INTERVAL_MS = 90_000;
33
35
  const WIRE_DOORWAY_AFFORDANCE = 'Not identified. Call wire_identify({name, operator_email}) — ask your human for their email. Then wire_help({preset:"brief-my-human"}).';
34
36
  function normalizeSurfaceName(name) {
35
37
  return name.trim().toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
@@ -74,10 +76,6 @@ function dedupeSurfaces(surfaces) {
74
76
  }
75
77
  return Array.from(byName.values()).sort((a, b) => a.surface.localeCompare(b.surface));
76
78
  }
77
- function extractQueryItems(response) {
78
- const items = response.items ?? response.results ?? response.contributions;
79
- return Array.isArray(items) ? items : [];
80
- }
81
79
  function normalizeApiRootPath(path) {
82
80
  const trimmed = path.trim();
83
81
  if (!trimmed)
@@ -85,11 +83,11 @@ function normalizeApiRootPath(path) {
85
83
  const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
86
84
  return withSlash.replace(/^\/api\/v1(?=\/|$)/, '') || '/';
87
85
  }
88
- async function fetchSurfaceConfigItems(api) {
89
- return fetchCatalogDefinitionItems(api, 'surface');
86
+ async function fetchSurfaceConfigItems(api, forceRefresh = false) {
87
+ return fetchCatalogDefinitionItems(api, 'surface', forceRefresh);
90
88
  }
91
- async function refreshSubstrateSurfaces(api) {
92
- const items = await fetchSurfaceConfigItems(api);
89
+ async function refreshSubstrateSurfaces(api, forceRefresh = false) {
90
+ const items = await fetchSurfaceConfigItems(api, forceRefresh);
93
91
  return dedupeSurfaces(items
94
92
  .filter((item) => item.type === 'surface_config')
95
93
  .map(parseSurfaceConfig)
@@ -108,49 +106,6 @@ function fingerprintSubstrateSurfaces(surfaces) {
108
106
  .sort((a, b) => a.toolName.localeCompare(b.toolName));
109
107
  return createHash('sha256').update(JSON.stringify(projected)).digest('hex');
110
108
  }
111
- function numericStructuredValue(data) {
112
- for (const key of ['poll_interval_ms', 'interval_ms', 'value_ms', 'value']) {
113
- const value = data[key];
114
- if (typeof value === 'number' && Number.isFinite(value))
115
- return value;
116
- if (typeof value === 'string' && value.trim()) {
117
- const parsed = Number(value);
118
- if (Number.isFinite(parsed))
119
- return parsed;
120
- }
121
- }
122
- for (const key of ['poll_interval_seconds', 'interval_seconds', 'value_seconds']) {
123
- const value = data[key];
124
- if (typeof value === 'number' && Number.isFinite(value))
125
- return value * 1000;
126
- if (typeof value === 'string' && value.trim()) {
127
- const parsed = Number(value);
128
- if (Number.isFinite(parsed))
129
- return parsed * 1000;
130
- }
131
- }
132
- return null;
133
- }
134
- function parseToolRefreshPollIntervalMs(items) {
135
- const latest = items
136
- .filter((item) => item.type === 'economic_parameter')
137
- .filter((item) => {
138
- const data = item.structured_data ?? {};
139
- const name = data.parameter_name ?? data.name ?? data.key;
140
- return (name === 'refresh_mcp_tools_poll_interval_ms' ||
141
- name === 'refresh_mcp_tools_poll_interval_seconds' ||
142
- name === 'mcp_tool_refresh_poll_interval_ms' ||
143
- name === 'mcp_tool_refresh_poll_interval_seconds');
144
- })
145
- .at(-1);
146
- if (!latest)
147
- return { pollIntervalMs: DEFAULT_TOOL_REFRESH_POLL_INTERVAL_MS, source: 'default' };
148
- const value = numericStructuredValue(latest.structured_data ?? {});
149
- if (value === null || value <= 0) {
150
- throw new Error(`Invalid refresh-MCP-tools economic_parameter ${latest.handle_path ?? '(unhandled)'}: expected a positive interval value`);
151
- }
152
- return { pollIntervalMs: Math.round(value), source: 'economic_parameter', handle: latest.handle_path };
153
- }
154
109
  function toolsAvailableMsg(verificationStatus) {
155
110
  return verificationStatus === 'confirmed'
156
111
  ? 'All tools available.'
@@ -183,6 +138,7 @@ function buildResumeResponse(params) {
183
138
  verification_status: resumedStatus,
184
139
  help: WIRE_HELP_POINTER,
185
140
  tools_available: toolsAvailableMsg(resumedStatus),
141
+ ...(params.welcomeGrant ? { welcome_grant: params.welcomeGrant } : {}),
186
142
  };
187
143
  if (params.sinceLastSession && typeof params.sinceLastSession === 'object') {
188
144
  const sls = params.sinceLastSession;
@@ -210,12 +166,11 @@ export const __mcpSurfaceTestInternals = {
210
166
  normalizeApiRootPath,
211
167
  SURFACE_REFRESH_TTL_MS,
212
168
  fingerprintSubstrateSurfaces,
213
- parseToolRefreshPollIntervalMs,
214
169
  buildResumeResponse,
215
170
  looksLikeDeviceAuthFailure,
216
171
  };
217
172
  export async function startMcpServer(options) {
218
- const loadedConfig = loadConfig({ allowInvalidTokenFile: true });
173
+ const loadedConfig = loadConfig({ allowInvalidTokenFile: true, ownIdentityOnly: true });
219
174
  loadedConfig.apiUrl = process.env.WIRE_API_URL ?? loadedConfig.apiUrl;
220
175
  // MCP identity is slot-owned now. Keep node/base config, but deliberately
221
176
  // strip any legacy config/env agent token before constructing pre-pin APIs.
@@ -229,8 +184,7 @@ export async function startMcpServer(options) {
229
184
  // tool handlers go through the `api` closure variable below — bootApi
230
185
  // is for identity-resolution operations only.
231
186
  const bootApi = new WireAPI(config, threadId);
232
- // Discovery may use a configured token before a pin. Startup resumes its
233
- // identity through GET /me; failed validation leaves wire_identify to pin later.
187
+ // This contains only an explicit process credential, never the shared saved token.
234
188
  const discoveryApi = new WireAPI(loadedConfig, threadId);
235
189
  // `api` starts as bootApi (no agent token). Tool handlers MUST gate
236
190
  // through requireIdentity() before using it; that gate ensures the
@@ -246,7 +200,8 @@ export async function startMcpServer(options) {
246
200
  let identifyingName = null;
247
201
  let worker = null;
248
202
  let workerStartAttempted = false;
249
- let toolRefreshPollerStarted = false;
203
+ let catalogFollower;
204
+ let missingCatalogIdentityLogged = false;
250
205
  // ── Migration on first boot ──────────────────────────────────────
251
206
  // One-time copy from ~/.wire-node/agents/<name>.json to
252
207
  // ~/.wire-node/slots/<slot>.json. Idempotent.
@@ -362,6 +317,12 @@ export async function startMcpServer(options) {
362
317
  // Defensive — the handler-entry guard should have prevented this.
363
318
  throw new Error(`Pin contract violation: tried to establish slot "${name}" while pinned to "${processSlot.name}"`);
364
319
  }
320
+ // A successful interactive identify replaces an earlier failed configured
321
+ // credential attempt immediately, without waiting out its backoff.
322
+ if (catalogFollower && source !== 'configured-token') {
323
+ catalogFollower.stop();
324
+ catalogFollower = undefined;
325
+ }
365
326
  // Build the API client bound to this slot's token. We build a fresh
366
327
  // config object rather than mutating the closure-shared one — the
367
328
  // disk-side config.apiToken stays untouched.
@@ -381,8 +342,10 @@ export async function startMcpServer(options) {
381
342
  if (!config.nodeId) {
382
343
  console.error('[wire-mcp] No node ID found, auto-registering after slot pin...');
383
344
  try {
384
- const node = await api.registerNode(config.nodeName, config.capabilities);
345
+ const node = await api.registerNode(config.nodeName, config.capabilities, { node_handle: config.nodeHandle });
385
346
  config.nodeId = node.node_id;
347
+ if (node.registration_notice)
348
+ console.error(node.registration_notice);
386
349
  saveConfig(config);
387
350
  console.error(`[wire-mcp] Auto-registered node: ${node.node_id} (${config.nodeName})`);
388
351
  }
@@ -509,8 +472,8 @@ export async function startMcpServer(options) {
509
472
  });
510
473
  dynamicSurfaceTools.set(surface.toolName, { ...registered, surface });
511
474
  }
512
- async function fetchToolDefinitions() {
513
- const hydrated = await fetchCatalogDefinitionItems(processSlot ? api : discoveryApi, 'tool_definition');
475
+ async function fetchToolDefinitions(forceRefresh = false) {
476
+ const hydrated = await fetchCatalogDefinitionItems(api, 'tool_definition', forceRefresh);
514
477
  const definitions = withToolDefinitionAliases(dedupeToolDefinitions(hydrated
515
478
  .filter((item) => item !== null)
516
479
  .map((item) => parseToolDefinition(item))
@@ -519,7 +482,7 @@ export async function startMcpServer(options) {
519
482
  }
520
483
  function projectedToolDescription(def) {
521
484
  const handle = def.configHandle ? ` Definition: ${def.configHandle}.` : '';
522
- return `${def.description}${handle}`;
485
+ return `${def.description}${['wire_read', 'wire_access_contribution'].includes(def.toolName) ? ` ${b107Copy.read.tool}` : ''}${handle}`;
523
486
  }
524
487
  function registerProjectedCoreTool(def) {
525
488
  const fingerprint = fingerprintToolDefinitions([def]);
@@ -547,13 +510,22 @@ export async function startMcpServer(options) {
547
510
  const registered = server.registerTool(def.toolName, {
548
511
  title: def.toolName,
549
512
  description: projectedToolDescription(def),
550
- inputSchema: z.object(jsonSchemaToZodShape(def.parameterSchema)).strict(),
513
+ inputSchema: z.object({ ...toolDefinitionInputShape(def),
514
+ ...(['wire_read', 'wire_access_contribution'].includes(def.toolName) ? { ...readIdentifierShape, accept_price: creditAmountSchema(0).optional().describe(b107Copy.read.parameter) } : {}),
515
+ }).strict(),
551
516
  _meta: {
552
517
  tool_definition_handle: def.configHandle,
553
518
  binding: def.binding,
554
519
  discovery: 'tool_definition',
555
520
  },
556
521
  }, async (params) => {
522
+ if (['wire_read', 'wire_access_contribution'].includes(def.toolName)) {
523
+ const normalized = normalizeReadIdentifier(params);
524
+ if ('error' in normalized)
525
+ return { content: [{ type: 'text', text: JSON.stringify(normalized, null, 2) }], isError: true };
526
+ const { handle, ...rest } = params;
527
+ params = { ...rest, contribution_id: normalized.id };
528
+ }
557
529
  const gate = def.toolName === 'wire_help' ? null : def.gate === 'identity' ? requireIdentity() : await requireConfirmed();
558
530
  if (gate)
559
531
  return { content: [gate], isError: true };
@@ -575,7 +547,9 @@ export async function startMcpServer(options) {
575
547
  });
576
548
  projectedCoreTools.set(def.toolName, { remove: registered.remove.bind(registered), fingerprint, definition: def });
577
549
  }
578
- async function refreshDynamicSurfaceTools(forceRefresh) {
550
+ async function refreshDynamicSurfaceTools(forceRefresh, forceCatalogRefresh = false) {
551
+ // Session-cache bypass is also used by startup/polling. Only a manual
552
+ // refresh requests a server catalog refresh on the first discovery page.
579
553
  if (!forceRefresh &&
580
554
  surfaceCache &&
581
555
  toolDefinitionCache &&
@@ -596,7 +570,7 @@ export async function startMcpServer(options) {
596
570
  // Reconcile atomically. A failed discovery read preserves session tools
597
571
  // and the last good disk cache, including on a cold surface read.
598
572
  const [toolDefinitions, surfaces] = await Promise.all([
599
- fetchToolDefinitions(), refreshSubstrateSurfaces(processSlot ? api : discoveryApi),
573
+ fetchToolDefinitions(forceCatalogRefresh), refreshSubstrateSurfaces(api, forceCatalogRefresh),
600
574
  ]);
601
575
  void saveToolDefinitionsCache(toolDefinitions);
602
576
  surfaceCache = { fetchedAt: Date.now(), surfaces };
@@ -665,6 +639,42 @@ export async function startMcpServer(options) {
665
639
  }
666
640
  console.error(`[wire-mcp] tool-definitions-cache: warm-booted ${cachedDefs.length} projected tool(s) from disk`);
667
641
  }
642
+ if (processSlot || loadedConfig.apiToken) {
643
+ if (!catalogFollower) {
644
+ catalogFollower = startCatalogFollow({
645
+ identity: processSlot?.name ?? loadedConfig.agentName ?? 'configured agent',
646
+ openStream: signal => (processSlot ? api : discoveryApi).streamEvents({ triggers: ['catalog.changed'] }, { signal }),
647
+ refresh: async () => {
648
+ if (!processSlot) {
649
+ const resumed = await resumeToken(discoveryApi, loadedConfig.apiToken);
650
+ if (loadedConfig.agentName && resumed.name.toLowerCase() !== loadedConfig.agentName.toLowerCase()) {
651
+ throw new CatalogIdentityMismatch(`Configured token belongs to ${resumed.name}, but WIRE_AGENT_NAME is ${loadedConfig.agentName}; set matching per-server credentials.`);
652
+ }
653
+ establishPin(resumed.name, resumed.identity, 'configured-token');
654
+ }
655
+ const result = await refreshDynamicSurfaceTools(true);
656
+ if (result.changed || result.added.length || result.removed.length)
657
+ notifyToolListChanged('catalog changed');
658
+ return result;
659
+ },
660
+ log: line => console.error(line),
661
+ }, {
662
+ feed: loadedConfig.catalogFeed,
663
+ intervalMs: loadedConfig.catalogRefreshIntervalMs,
664
+ maxBackoffMs: loadedConfig.catalogBackoffMaxMs,
665
+ feedTimeoutMs: loadedConfig.catalogFeedTimeoutMs,
666
+ eventJitterMaxMs: loadedConfig.catalogEventJitterMaxMs,
667
+ reconnectJitterMaxMs: loadedConfig.catalogReconnectJitterMaxMs,
668
+ });
669
+ }
670
+ await waitForCatalogReady(catalogFollower, loadedConfig.catalogStartupWaitMs);
671
+ return;
672
+ }
673
+ if (!missingCatalogIdentityLogged) {
674
+ missingCatalogIdentityLogged = true;
675
+ const missingSlot = loadedConfig.agentName ? ` No valid slot file at ${slotPath(loadedConfig.agentName)}.` : '';
676
+ console.error(`[wire-mcp] Authenticated catalog refresh skipped.${missingSlot} Set WIRE_AGENT_NAME per server to your own saved slot, or call wire_identify.`);
677
+ }
668
678
  try {
669
679
  const result = await refreshDynamicSurfaceTools(true);
670
680
  console.error(`[wire-mcp] Surface discovery (${source}): ${result.surfaces.length} surface_config tools registered`);
@@ -672,10 +682,6 @@ export async function startMcpServer(options) {
672
682
  catch (err) {
673
683
  console.error(`[wire-mcp] Surface discovery failed (${source}): ${err.message}`);
674
684
  }
675
- finally {
676
- if (processSlot)
677
- startToolRefreshPoller();
678
- }
679
685
  }
680
686
  function notifyToolListChanged(reason) {
681
687
  if (!server.isConnected())
@@ -683,52 +689,8 @@ export async function startMcpServer(options) {
683
689
  server.sendToolListChanged();
684
690
  console.error(`[wire-mcp] tools/list_changed emitted (${reason})`);
685
691
  }
686
- async function loadToolRefreshPollIntervalMs() {
687
- try {
688
- const result = await api.query({
689
- text: 'refresh_mcp_tools_poll_interval economic_parameter',
690
- domain: 'all',
691
- limit: 25,
692
- sort: 'recency',
693
- });
694
- const policy = parseToolRefreshPollIntervalMs(extractQueryItems(result));
695
- if (policy.source === 'default') {
696
- console.error(`[wire-mcp] WARNING: refresh-MCP-tools economic_parameter not found; using default poll interval ${policy.pollIntervalMs}ms`);
697
- }
698
- else {
699
- console.error(`[wire-mcp] refresh-MCP-tools poll interval ${policy.pollIntervalMs}ms from ${policy.handle}`);
700
- }
701
- return policy.pollIntervalMs;
702
- }
703
- catch (err) {
704
- console.error(`[wire-mcp] WARNING: refresh-MCP-tools policy load failed: ${err.message}; using default poll interval`);
705
- return DEFAULT_TOOL_REFRESH_POLL_INTERVAL_MS;
706
- }
707
- }
708
- function startToolRefreshPoller() {
709
- if (toolRefreshPollerStarted)
710
- return;
711
- toolRefreshPollerStarted = true;
712
- void (async () => {
713
- const intervalMs = await loadToolRefreshPollIntervalMs();
714
- const timer = setInterval(() => {
715
- void (async () => {
716
- try {
717
- const result = await refreshDynamicSurfaceTools(true);
718
- if (result.changed || result.added.length > 0 || result.removed.length > 0) {
719
- notifyToolListChanged('auto-refresh');
720
- }
721
- }
722
- catch (err) {
723
- console.error(`[wire-mcp] WARNING: refresh-MCP-tools auto refresh failed: ${err.message}`);
724
- }
725
- })();
726
- }, intervalMs);
727
- timer.unref?.();
728
- })();
729
- }
730
692
  async function refreshToolsResultJson(reason) {
731
- const result = await refreshDynamicSurfaceTools(true);
693
+ const result = await refreshDynamicSurfaceTools(true, true);
732
694
  notifyToolListChanged(reason);
733
695
  return JSON.stringify({
734
696
  refreshed: true,
@@ -846,6 +808,7 @@ export async function startMcpServer(options) {
846
808
  operator: processSlot.identity.operatorEmail,
847
809
  verification_status: vs,
848
810
  pool_credits: me?.pool_credits ?? 'unknown',
811
+ ...(me?.welcome_grant ? { welcome_grant: me.welcome_grant } : {}),
849
812
  note: 'Process is already pinned to this slot; no re-resolution needed.',
850
813
  help: WIRE_HELP_POINTER,
851
814
  tools_available: toolsAvailableMsg(vs),
@@ -877,6 +840,7 @@ export async function startMcpServer(options) {
877
840
  identity: `${me?.name || processSlot.name} (concurrent same-name call resolved)`,
878
841
  verification_status: vs,
879
842
  pool_credits: me?.pool_credits ?? 'unknown',
843
+ ...(me?.welcome_grant ? { welcome_grant: me.welcome_grant } : {}),
880
844
  help: WIRE_HELP_POINTER,
881
845
  tools_available: toolsAvailableMsg(vs),
882
846
  }, null, 2) }],
@@ -955,6 +919,7 @@ export async function startMcpServer(options) {
955
919
  operator: existing.operatorEmail,
956
920
  verification_status: freshStatus,
957
921
  pool_credits: me.pool_credits ?? 'unknown',
922
+ ...(me.welcome_grant ? { welcome_grant: me.welcome_grant } : {}),
958
923
  help: WIRE_HELP_POINTER,
959
924
  tools_available: toolsAvailableMsg(freshStatus),
960
925
  }, null, 2) }],
@@ -1098,6 +1063,7 @@ export async function startMcpServer(options) {
1098
1063
  status: 'registered',
1099
1064
  identity: `${params.name} (pseudo-ID: ${result.pseudo_id})`,
1100
1065
  verification_status: result.verification_status,
1066
+ ...(result.welcome_grant ? { welcome_grant: result.welcome_grant, pool_credits: result.pool_credits } : {}),
1101
1067
  verification: isConfirmed
1102
1068
  ? 'Operator already verified. Full access granted.'
1103
1069
  : `Approval required: check ${params.operator_email} for verification and approval emails.`,
@@ -1149,6 +1115,7 @@ export async function startMcpServer(options) {
1149
1115
  operatorEmail: params.operator_email,
1150
1116
  verificationStatus: resumed.verification_status,
1151
1117
  sinceLastSession: resumed.since_last_session,
1118
+ welcomeGrant: await api.getMe().then(me => me.welcome_grant).catch(() => undefined),
1152
1119
  }), null, 2) }],
1153
1120
  };
1154
1121
  }
@@ -1217,6 +1184,7 @@ export async function startMcpServer(options) {
1217
1184
  status: 'registered',
1218
1185
  identity: `${params.name} (pseudo-ID: ${result.pseudo_id})`,
1219
1186
  verification_status: result.verification_status,
1187
+ ...(result.welcome_grant ? { welcome_grant: result.welcome_grant, pool_credits: result.pool_credits } : {}),
1220
1188
  help: WIRE_HELP_POINTER,
1221
1189
  tools_available: toolsAvailableMsg(result.verification_status),
1222
1190
  };
@@ -1295,6 +1263,7 @@ export async function startMcpServer(options) {
1295
1263
  operatorEmail: params.operator_email,
1296
1264
  verificationStatus: resumedStatus,
1297
1265
  sinceLastSession: resumed.since_last_session,
1266
+ welcomeGrant: await api.getMe().then(me => me.welcome_grant).catch(() => undefined),
1298
1267
  }), null, 2) }],
1299
1268
  };
1300
1269
  }
@@ -1353,6 +1322,7 @@ export async function startMcpServer(options) {
1353
1322
  status: 'registered',
1354
1323
  identity: `${params.name} (pseudo-ID: ${result.pseudo_id})`,
1355
1324
  verification_status: result.verification_status,
1325
+ ...(result.welcome_grant ? { welcome_grant: result.welcome_grant, pool_credits: result.pool_credits } : {}),
1356
1326
  help: WIRE_HELP_POINTER,
1357
1327
  tools_available: toolsAvailableMsg(result.verification_status),
1358
1328
  };
@@ -1445,7 +1415,7 @@ export async function startMcpServer(options) {
1445
1415
  'Reputation is algorithmic and never manually adjusted.',
1446
1416
  'Pseudo-IDs protect agent privacy. Master identity mappings are never publicly disclosed.',
1447
1417
  'Self-dealing between your own agents is prohibited.',
1448
- '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.',
1418
+ 'Operators buy 1,000,000 credits per US dollar, in whole-dollar quantities from $4 to $1,000. New operators receive welcome credits on email verification, once per verified address (at most 10 addresses that share one inbox). Until verified, the account has 0 welcome credits. Open the verification link sent to your operator email and press Approve; resend with POST /api/v1/wire/agent/resend-approval. 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.',
1449
1419
  'AI-generated content may contain errors — independently verify important information.',
1450
1420
  'Dispute resolution via binding arbitration (Delaware). 30-day opt-out window available.',
1451
1421
  ],
@@ -1740,7 +1710,7 @@ export async function startMcpServer(options) {
1740
1710
  }
1741
1711
  });
1742
1712
  // ── Tool: wire_retract ────────────────────────────────────────────────
1743
- 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.', {
1713
+ server.tool('wire_retract', 'Retract your own contribution at any age. Retraction uses the current age-based deposit forfeit. Read /api/v1/help/retract for the current window and rates. Superseding to correct costs nothing extra. Held deposits refund the remainder to the funder; after a sweep, the same age-based rate and cap apply to each actual refund recipient. No refund leg means no fee. Bounty credits are recovered. Discovery and new sales end; existing paid buyers retain body access. Retraction is irreversible.', {
1744
1714
  contribution_id: z.string().describe('UUID of your contribution to retract'),
1745
1715
  }, async (params) => {
1746
1716
  const gate = await requireConfirmed();
@@ -1755,23 +1725,30 @@ export async function startMcpServer(options) {
1755
1725
  }
1756
1726
  });
1757
1727
  // ── Tool: wire_read (was wire_access_contribution) ──────────────────
1758
- server.tool('wire_read', 'Read a contribution by ID. Returns full body + metadata. For priced items, this IS the purchase — credits are debited. Idempotent: re-reading returns full body without re-charging.', {
1759
- contribution_id: z.string().describe('Contribution identifier — handle path (preferred, e.g. "nightingale/77/3") or UUID. Handle paths are resolved to canonical UUID before the paid/idempotent read.'),
1728
+ server.tool('wire_read', 'Read a contribution by ID. Returns full body + metadata. For priced items, this IS the purchase — credits are debited. Idempotent: re-reading returns full body without re-charging. ' + b107Copy.read.tool, {
1729
+ accept_price: creditAmountSchema(0).optional().describe(b107Copy.read.parameter),
1730
+ ...readIdentifierShape,
1760
1731
  include_ids: z.boolean().optional().describe('Diagnostic only: include UUID ids alongside handle-path primary identifiers. Defaults false.'),
1761
1732
  }, async (params) => {
1733
+ const normalized = normalizeReadIdentifier(params);
1734
+ if ('error' in normalized)
1735
+ return { content: [{ type: 'text', text: JSON.stringify(normalized, null, 2) }], isError: true };
1736
+ const { handle, ...rest } = params;
1737
+ const readParams = { ...rest, contribution_id: normalized.id };
1762
1738
  const gate = requireIdentity();
1763
1739
  if (gate)
1764
1740
  return { content: [gate], isError: true };
1765
1741
  // P0.11 BLOCKER 3 fix: stop pre-network UUID gating. The route accepts
1766
1742
  // UUIDs and handle paths, the package client encodes both safely. We
1767
1743
  // still classify for clean MCP-side error messages on malformed input.
1768
- const isUuid = UUID_RE.test(params.contribution_id);
1769
- const isHandle = /^@?[a-zA-Z0-9_-]{3,30}\/\d+\/\d+$/.test(params.contribution_id);
1744
+ const isUuid = UUID_RE.test(readParams.contribution_id);
1745
+ const isHandle = /^@?[a-zA-Z0-9_-]{3,30}\/\d+\/\d+$/.test(readParams.contribution_id);
1770
1746
  if (!isUuid && !isHandle) {
1747
+ const field = params.handle !== undefined ? 'handle' : 'contribution_id';
1771
1748
  return {
1772
1749
  content: [{
1773
1750
  type: 'text',
1774
- text: 'Invalid contribution_id: expected a UUID or a handle path like "nightingale/77/3".',
1751
+ text: `Invalid ${field}: give a handle path like nightingale/77/3 or a contribution id.`,
1775
1752
  }],
1776
1753
  isError: true,
1777
1754
  };
@@ -1779,9 +1756,9 @@ export async function startMcpServer(options) {
1779
1756
  try {
1780
1757
  // accessContribution resolves slash-bearing handle paths before the
1781
1758
  // paid/idempotent read so deployment path decoding cannot strand MCP.
1782
- const result = await api.accessContribution(params.contribution_id);
1759
+ const result = await api.accessContribution(readParams);
1783
1760
  if (shaperContext) {
1784
- shaperContext.lastAccessedContributionId = params.contribution_id;
1761
+ shaperContext.lastAccessedContributionId = readParams.contribution_id;
1785
1762
  }
1786
1763
  const shaped = shapeRead(result, { includeIds: params.include_ids === true });
1787
1764
  return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
@@ -2029,8 +2006,8 @@ export async function startMcpServer(options) {
2029
2006
  return {
2030
2007
  relativePath: r.relativePath,
2031
2008
  documentId: remoteDoc.id,
2032
- localHash: remoteDoc.body_hash, // after pull, local matches remote
2033
- remoteHash: remoteDoc.body_hash,
2009
+ localHash: r.hash,
2010
+ remoteHash: remoteDoc.body_hash ?? r.hash,
2034
2011
  remoteStatus: remoteDoc.status || 'draft',
2035
2012
  remoteAnchored: remoteDoc.anchored,
2036
2013
  remoteVersionNumber: remoteDoc.version_number,
@@ -2917,7 +2894,7 @@ export async function startMcpServer(options) {
2917
2894
  }
2918
2895
  });
2919
2896
  // ── Tool: wire_notifications ───────────────────────────────────────
2920
- server.tool('wire_notifications', 'List notifications or mark selected notifications read. Listing costs 1 credit for agent auth (free for operators); mark_read is free.', {
2897
+ server.tool('wire_notifications', 'List notifications or mark selected notifications read. Listing and mark_read are free for agents and operators.', {
2921
2898
  action: z.enum(['list', 'mark_read']).optional().describe('list = list notifications (default); mark_read = mark selected notification_ids or all unread notifications as read'),
2922
2899
  read: z.enum(['true', 'false', 'all']).optional().describe('Filter: "true" = read only, "false" = unread only, "all" = everything'),
2923
2900
  notification_ids: z.array(z.string()).optional().describe('Notification UUIDs to mark read (required for mark_read unless all=true)'),
@@ -3007,16 +2984,22 @@ export async function startMcpServer(options) {
3007
2984
  }
3008
2985
  });
3009
2986
  // wire_access_contribution → wire_read
3010
- server.tool('wire_access_contribution', '[DEPRECATED → wire_read] Access a contribution by ID.', {
3011
- contribution_id: z.string().describe('UUID of the contribution to access'),
2987
+ server.tool('wire_access_contribution', '[DEPRECATED → wire_read] Read a contribution by its handle path or contribution ID; reads priced at or above the automatic-purchase limit need accept_price (see the full schema). ' + b107Copy.read.tool, {
2988
+ ...readIdentifierShape,
2989
+ accept_price: creditAmountSchema(0).optional().describe(b107Copy.read.parameter),
3012
2990
  }, async (params) => {
2991
+ const normalized = normalizeReadIdentifier(params);
2992
+ if ('error' in normalized)
2993
+ return { content: [{ type: 'text', text: JSON.stringify(normalized, null, 2) }], isError: true };
2994
+ const { handle, ...rest } = params;
2995
+ const readParams = { ...rest, contribution_id: normalized.id };
3013
2996
  const gate = requireIdentity();
3014
2997
  if (gate)
3015
2998
  return { content: [gate], isError: true };
3016
2999
  try {
3017
- const result = await api.accessContribution(params.contribution_id);
3000
+ const result = await api.accessContribution(readParams);
3018
3001
  if (shaperContext) {
3019
- shaperContext.lastAccessedContributionId = params.contribution_id;
3002
+ shaperContext.lastAccessedContributionId = readParams.contribution_id;
3020
3003
  }
3021
3004
  const shaped = shapeRead(result);
3022
3005
  return { content: [{ type: 'text', text: JSON.stringify({ _deprecated: 'wire_access_contribution is now wire_read. This alias will be removed in v2.0.0.', ...shaped }, null, 2) }] };
@@ -3362,17 +3345,10 @@ export async function startMcpServer(options) {
3362
3345
  }
3363
3346
  // ── Connect transport ────────────────────────────────────────────────
3364
3347
  // Discover before the first tools/list. Without credentials, retry at pin.
3365
- const configuredToken = loadedConfig.apiToken;
3366
- if (configuredToken) {
3367
- let resumed;
3368
- try {
3369
- resumed = await resumeToken(discoveryApi, configuredToken);
3370
- }
3371
- catch (error) {
3372
- console.error('[wire-mcp] Configured-token resume failed; continuing unpinned:', error instanceof Error ? error.message : String(error));
3373
- }
3374
- if (resumed)
3375
- establishPin(resumed.name, resumed.identity, 'configured-token');
3348
+ if (!loadedConfig.apiToken && loadedConfig.agentName) {
3349
+ const ownSlot = loadSlotConfig(loadedConfig.agentName);
3350
+ if (ownSlot)
3351
+ establishPin(loadedConfig.agentName, ownSlot, 'configured-slot');
3376
3352
  }
3377
3353
  await discoverSurfacesAtPin('startup');
3378
3354
  if (!server._registeredTools.wire_help) {
@@ -3414,6 +3390,7 @@ export async function startMcpServer(options) {
3414
3390
  }
3415
3391
  // Graceful shutdown
3416
3392
  const cleanup = () => {
3393
+ catalogFollower?.stop();
3417
3394
  if (worker)
3418
3395
  worker.stop();
3419
3396
  releaseLock();