@agent-wire/terminal 1.7.7 → 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.
Files changed (44) hide show
  1. package/README.md +519 -435
  2. package/dist/api-token.d.ts +9 -0
  3. package/dist/api-token.js +46 -0
  4. package/dist/api-token.js.map +1 -0
  5. package/dist/api.d.ts +50 -10
  6. package/dist/api.js +78 -31
  7. package/dist/api.js.map +1 -1
  8. package/dist/b107-copy.d.ts +68 -0
  9. package/dist/b107-copy.js +69 -0
  10. package/dist/b107-copy.js.map +1 -0
  11. package/dist/catalog-follow.d.ts +32 -0
  12. package/dist/catalog-follow.js +184 -0
  13. package/dist/catalog-follow.js.map +1 -0
  14. package/dist/cli-projection.js +20 -5
  15. package/dist/cli-projection.js.map +1 -1
  16. package/dist/cli.js +32 -19
  17. package/dist/cli.js.map +1 -1
  18. package/dist/config.d.ts +14 -1
  19. package/dist/config.js +54 -9
  20. package/dist/config.js.map +1 -1
  21. package/dist/mcp-server.d.ts +1 -6
  22. package/dist/mcp-server.js +161 -144
  23. package/dist/mcp-server.js.map +1 -1
  24. package/dist/read-tool-input.d.ts +15 -0
  25. package/dist/read-tool-input.js +15 -0
  26. package/dist/read-tool-input.js.map +1 -0
  27. package/dist/response-shaper.js +76 -15
  28. package/dist/response-shaper.js.map +1 -1
  29. package/dist/resume.d.ts +7 -0
  30. package/dist/resume.js +15 -0
  31. package/dist/resume.js.map +1 -0
  32. package/dist/slots.d.ts +1 -0
  33. package/dist/slots.js +1 -1
  34. package/dist/slots.js.map +1 -1
  35. package/dist/sync/diff.js +7 -1
  36. package/dist/sync/diff.js.map +1 -1
  37. package/dist/sync/pull.d.ts +1 -0
  38. package/dist/sync/pull.js +17 -9
  39. package/dist/sync/pull.js.map +1 -1
  40. package/dist/sync/types.d.ts +2 -1
  41. package/dist/tool-projection.d.ts +19 -2
  42. package/dist/tool-projection.js +544 -8
  43. package/dist/tool-projection.js.map +1 -1
  44. package/package.json +58 -58
@@ -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';
@@ -10,11 +12,14 @@ import { createHash, randomUUID } from 'crypto';
10
12
  import { readFile, writeFile, rename as fsRename, mkdir as fsMkdir, stat as fsStat } from 'fs/promises';
11
13
  import { join, isAbsolute } from 'path';
12
14
  import { loadConfig, saveConfig, CONFIG_DIR } from './config.js';
13
- import { loadSlotConfig, saveSlotConfig, migrateLegacyAgentsToSlots } from './slots.js';
14
- import { WireAPI, RegistrationError } from './api.js';
15
+ import { resolveApiToken } from './api-token.js';
16
+ import { CatalogIdentityMismatch, startCatalogFollow, waitForCatalogReady } from './catalog-follow.js';
17
+ import { loadSlotConfig, saveSlotConfig, slotPath, migrateLegacyAgentsToSlots } from './slots.js';
18
+ import { WireAPI, WireApiRefusal, RegistrationError } from './api.js';
19
+ import { resumeToken } from './resume.js';
15
20
  import { shapeStatus, shapeRead, shapeInspect, shapeGraph, shapeCorpora, shapeMyContributions, shapeEarnings, shapeOpportunities, shapeRate, shapePatrolScan, shapeNotifications, shapeTasks, shapeMessages, } from './response-shaper.js';
16
21
  import { startBackgroundWorkLoop } from './background-worker.js';
17
- 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';
18
23
  import {
19
24
  // Mission #1.5-rev2: agents/ cache fully retired. loadAgentIdentity /
20
25
  // saveAgentIdentity / deleteAgentIdentity are no longer imported here;
@@ -27,7 +32,6 @@ import { walkDirectory, computeDiff, pushFiles, pullFiles, parseManifest, serial
27
32
  import { WIRE_HELP_POINTER } from './help-pointer.js';
28
33
  import { pullLocalSubscriptions } from './local-pull.js';
29
34
  const SURFACE_REFRESH_TTL_MS = 60_000;
30
- const DEFAULT_TOOL_REFRESH_POLL_INTERVAL_MS = 90_000;
31
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"}).';
32
36
  function normalizeSurfaceName(name) {
33
37
  return name.trim().toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
@@ -72,10 +76,6 @@ function dedupeSurfaces(surfaces) {
72
76
  }
73
77
  return Array.from(byName.values()).sort((a, b) => a.surface.localeCompare(b.surface));
74
78
  }
75
- function extractQueryItems(response) {
76
- const items = response.items ?? response.results ?? response.contributions;
77
- return Array.isArray(items) ? items : [];
78
- }
79
79
  function normalizeApiRootPath(path) {
80
80
  const trimmed = path.trim();
81
81
  if (!trimmed)
@@ -83,11 +83,11 @@ function normalizeApiRootPath(path) {
83
83
  const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
84
84
  return withSlash.replace(/^\/api\/v1(?=\/|$)/, '') || '/';
85
85
  }
86
- async function fetchSurfaceConfigItems(api) {
87
- return fetchCatalogDefinitionItems(api, 'surface');
86
+ async function fetchSurfaceConfigItems(api, forceRefresh = false) {
87
+ return fetchCatalogDefinitionItems(api, 'surface', forceRefresh);
88
88
  }
89
- async function refreshSubstrateSurfaces(api) {
90
- const items = await fetchSurfaceConfigItems(api);
89
+ async function refreshSubstrateSurfaces(api, forceRefresh = false) {
90
+ const items = await fetchSurfaceConfigItems(api, forceRefresh);
91
91
  return dedupeSurfaces(items
92
92
  .filter((item) => item.type === 'surface_config')
93
93
  .map(parseSurfaceConfig)
@@ -106,53 +106,10 @@ function fingerprintSubstrateSurfaces(surfaces) {
106
106
  .sort((a, b) => a.toolName.localeCompare(b.toolName));
107
107
  return createHash('sha256').update(JSON.stringify(projected)).digest('hex');
108
108
  }
109
- function numericStructuredValue(data) {
110
- for (const key of ['poll_interval_ms', 'interval_ms', 'value_ms', 'value']) {
111
- const value = data[key];
112
- if (typeof value === 'number' && Number.isFinite(value))
113
- return value;
114
- if (typeof value === 'string' && value.trim()) {
115
- const parsed = Number(value);
116
- if (Number.isFinite(parsed))
117
- return parsed;
118
- }
119
- }
120
- for (const key of ['poll_interval_seconds', 'interval_seconds', 'value_seconds']) {
121
- const value = data[key];
122
- if (typeof value === 'number' && Number.isFinite(value))
123
- return value * 1000;
124
- if (typeof value === 'string' && value.trim()) {
125
- const parsed = Number(value);
126
- if (Number.isFinite(parsed))
127
- return parsed * 1000;
128
- }
129
- }
130
- return null;
131
- }
132
- function parseToolRefreshPollIntervalMs(items) {
133
- const latest = items
134
- .filter((item) => item.type === 'economic_parameter')
135
- .filter((item) => {
136
- const data = item.structured_data ?? {};
137
- const name = data.parameter_name ?? data.name ?? data.key;
138
- return (name === 'refresh_mcp_tools_poll_interval_ms' ||
139
- name === 'refresh_mcp_tools_poll_interval_seconds' ||
140
- name === 'mcp_tool_refresh_poll_interval_ms' ||
141
- name === 'mcp_tool_refresh_poll_interval_seconds');
142
- })
143
- .at(-1);
144
- if (!latest)
145
- return { pollIntervalMs: DEFAULT_TOOL_REFRESH_POLL_INTERVAL_MS, source: 'default' };
146
- const value = numericStructuredValue(latest.structured_data ?? {});
147
- if (value === null || value <= 0) {
148
- throw new Error(`Invalid refresh-MCP-tools economic_parameter ${latest.handle_path ?? '(unhandled)'}: expected a positive interval value`);
149
- }
150
- return { pollIntervalMs: Math.round(value), source: 'economic_parameter', handle: latest.handle_path };
151
- }
152
109
  function toolsAvailableMsg(verificationStatus) {
153
110
  return verificationStatus === 'confirmed'
154
111
  ? 'All tools available.'
155
- : 'Pending verification: wire_identify, wire_query, wire_graph, wire_inspect, wire_status, wire_corpora, wire_legal, wire_pulse, wire_opportunities, wire_roster available now. Credit-affecting tools (wire_contribute, wire_rate, wire_flag, wire_correct, wire_messages, wire_market, wire_games, wire_supersede) require operator approval — check your operator\'s email.';
112
+ : 'Pending verification: wire_identify, wire_query, wire_graph, wire_inspect, wire_status, wire_corpora, wire_legal, wire_pulse, wire_opportunities available now. Credit-affecting tools (wire_contribute, wire_rate, wire_flag, wire_correct, wire_messages, wire_market, wire_games, wire_supersede) require operator approval — check your operator\'s email.';
156
113
  }
157
114
  function looksLikeDeviceAuthFailure(error) {
158
115
  if (!error)
@@ -181,6 +138,7 @@ function buildResumeResponse(params) {
181
138
  verification_status: resumedStatus,
182
139
  help: WIRE_HELP_POINTER,
183
140
  tools_available: toolsAvailableMsg(resumedStatus),
141
+ ...(params.welcomeGrant ? { welcome_grant: params.welcomeGrant } : {}),
184
142
  };
185
143
  if (params.sinceLastSession && typeof params.sinceLastSession === 'object') {
186
144
  const sls = params.sinceLastSession;
@@ -208,12 +166,11 @@ export const __mcpSurfaceTestInternals = {
208
166
  normalizeApiRootPath,
209
167
  SURFACE_REFRESH_TTL_MS,
210
168
  fingerprintSubstrateSurfaces,
211
- parseToolRefreshPollIntervalMs,
212
169
  buildResumeResponse,
213
170
  looksLikeDeviceAuthFailure,
214
171
  };
215
172
  export async function startMcpServer(options) {
216
- const loadedConfig = loadConfig();
173
+ const loadedConfig = loadConfig({ allowInvalidTokenFile: true, ownIdentityOnly: true });
217
174
  loadedConfig.apiUrl = process.env.WIRE_API_URL ?? loadedConfig.apiUrl;
218
175
  // MCP identity is slot-owned now. Keep node/base config, but deliberately
219
176
  // strip any legacy config/env agent token before constructing pre-pin APIs.
@@ -227,10 +184,8 @@ export async function startMcpServer(options) {
227
184
  // tool handlers go through the `api` closure variable below — bootApi
228
185
  // is for identity-resolution operations only.
229
186
  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);
187
+ // This contains only an explicit process credential, never the shared saved token.
188
+ const discoveryApi = new WireAPI(loadedConfig, threadId);
234
189
  // `api` starts as bootApi (no agent token). Tool handlers MUST gate
235
190
  // through requireIdentity() before using it; that gate ensures the
236
191
  // closure has been promoted to the slot's authenticated WireAPI by
@@ -245,7 +200,8 @@ export async function startMcpServer(options) {
245
200
  let identifyingName = null;
246
201
  let worker = null;
247
202
  let workerStartAttempted = false;
248
- let toolRefreshPollerStarted = false;
203
+ let catalogFollower;
204
+ let missingCatalogIdentityLogged = false;
249
205
  // ── Migration on first boot ──────────────────────────────────────
250
206
  // One-time copy from ~/.wire-node/agents/<name>.json to
251
207
  // ~/.wire-node/slots/<slot>.json. Idempotent.
@@ -267,6 +223,12 @@ export async function startMcpServer(options) {
267
223
  function requireIdentity() {
268
224
  if (processSlot)
269
225
  return null;
226
+ try {
227
+ resolveApiToken(loadedConfig);
228
+ }
229
+ catch (error) {
230
+ return { type: 'text', text: error.message };
231
+ }
270
232
  return { type: 'text', text: WIRE_DOORWAY_AFFORDANCE };
271
233
  }
272
234
  // ── Session state for response shaping ──────────────────────────────
@@ -304,7 +266,8 @@ export async function startMcpServer(options) {
304
266
  verificationStatus: me.verification_status,
305
267
  };
306
268
  processSlot = { ...processSlot, identity: refreshedIdentity };
307
- saveSlotConfig(processSlot.name, refreshedIdentity);
269
+ if (loadedConfig.apiTokenFile === undefined)
270
+ saveSlotConfig(processSlot.name, refreshedIdentity);
308
271
  }
309
272
  }
310
273
  async function ensureShaperContext(options = {}) {
@@ -354,13 +317,21 @@ export async function startMcpServer(options) {
354
317
  // Defensive — the handler-entry guard should have prevented this.
355
318
  throw new Error(`Pin contract violation: tried to establish slot "${name}" while pinned to "${processSlot.name}"`);
356
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
+ }
357
326
  // Build the API client bound to this slot's token. We build a fresh
358
327
  // config object rather than mutating the closure-shared one — the
359
328
  // disk-side config.apiToken stays untouched.
360
329
  const slotApi = new WireAPI({ ...config, apiToken: identity.apiToken }, threadId);
361
330
  processSlot = { name, identity };
362
331
  api = slotApi;
363
- saveSlotConfig(name, identity);
332
+ // A configured file remains the credential source; don't copy it into a slot.
333
+ if (identity.operatorEmail && loadedConfig.apiTokenFile === undefined)
334
+ saveSlotConfig(name, identity);
364
335
  console.error(`[wire-mcp] Pin established: ${name} (${identity.pseudoId}) [source=${source}]`);
365
336
  return identity;
366
337
  }
@@ -371,8 +342,10 @@ export async function startMcpServer(options) {
371
342
  if (!config.nodeId) {
372
343
  console.error('[wire-mcp] No node ID found, auto-registering after slot pin...');
373
344
  try {
374
- const node = await api.registerNode(config.nodeName, config.capabilities);
345
+ const node = await api.registerNode(config.nodeName, config.capabilities, { node_handle: config.nodeHandle });
375
346
  config.nodeId = node.node_id;
347
+ if (node.registration_notice)
348
+ console.error(node.registration_notice);
376
349
  saveConfig(config);
377
350
  console.error(`[wire-mcp] Auto-registered node: ${node.node_id} (${config.nodeName})`);
378
351
  }
@@ -499,19 +472,17 @@ export async function startMcpServer(options) {
499
472
  });
500
473
  dynamicSurfaceTools.set(surface.toolName, { ...registered, surface });
501
474
  }
502
- async function fetchToolDefinitions() {
503
- const hydrated = await fetchCatalogDefinitionItems(processSlot ? api : discoveryApi, 'tool_definition');
475
+ async function fetchToolDefinitions(forceRefresh = false) {
476
+ const hydrated = await fetchCatalogDefinitionItems(api, 'tool_definition', forceRefresh);
504
477
  const definitions = withToolDefinitionAliases(dedupeToolDefinitions(hydrated
505
478
  .filter((item) => item !== null)
506
479
  .map((item) => parseToolDefinition(item))
507
480
  .filter((def) => def !== null)));
508
- // Persist to disk cache (atomic write). Errors are non-fatal.
509
- void saveToolDefinitionsCache(definitions);
510
481
  return definitions;
511
482
  }
512
483
  function projectedToolDescription(def) {
513
484
  const handle = def.configHandle ? ` Definition: ${def.configHandle}.` : '';
514
- return `${def.description}${handle}`;
485
+ return `${def.description}${['wire_read', 'wire_access_contribution'].includes(def.toolName) ? ` ${b107Copy.read.tool}` : ''}${handle}`;
515
486
  }
516
487
  function registerProjectedCoreTool(def) {
517
488
  const fingerprint = fingerprintToolDefinitions([def]);
@@ -539,14 +510,23 @@ export async function startMcpServer(options) {
539
510
  const registered = server.registerTool(def.toolName, {
540
511
  title: def.toolName,
541
512
  description: projectedToolDescription(def),
542
- 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(),
543
516
  _meta: {
544
517
  tool_definition_handle: def.configHandle,
545
518
  binding: def.binding,
546
519
  discovery: 'tool_definition',
547
520
  },
548
521
  }, async (params) => {
549
- const gate = def.gate === 'identity' ? requireIdentity() : await requireConfirmed();
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
+ }
529
+ const gate = def.toolName === 'wire_help' ? null : def.gate === 'identity' ? requireIdentity() : await requireConfirmed();
550
530
  if (gate)
551
531
  return { content: [gate], isError: true };
552
532
  try {
@@ -559,12 +539,17 @@ export async function startMcpServer(options) {
559
539
  };
560
540
  }
561
541
  catch (err) {
542
+ if (err instanceof WireApiRefusal) {
543
+ return { content: [{ type: 'text', text: JSON.stringify(err.body, null, 2) }], isError: true };
544
+ }
562
545
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
563
546
  }
564
547
  });
565
548
  projectedCoreTools.set(def.toolName, { remove: registered.remove.bind(registered), fingerprint, definition: def });
566
549
  }
567
- 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.
568
553
  if (!forceRefresh &&
569
554
  surfaceCache &&
570
555
  toolDefinitionCache &&
@@ -582,12 +567,12 @@ export async function startMcpServer(options) {
582
567
  removed: [],
583
568
  };
584
569
  }
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
- });
570
+ // Reconcile atomically. A failed discovery read preserves session tools
571
+ // and the last good disk cache, including on a cold surface read.
572
+ const [toolDefinitions, surfaces] = await Promise.all([
573
+ fetchToolDefinitions(forceCatalogRefresh), refreshSubstrateSurfaces(api, forceCatalogRefresh),
574
+ ]);
575
+ void saveToolDefinitionsCache(toolDefinitions);
591
576
  surfaceCache = { fetchedAt: Date.now(), surfaces };
592
577
  toolDefinitionCache = { fetchedAt: Date.now(), definitions: toolDefinitions };
593
578
  const removed = [];
@@ -654,11 +639,45 @@ export async function startMcpServer(options) {
654
639
  }
655
640
  console.error(`[wire-mcp] tool-definitions-cache: warm-booted ${cachedDefs.length} projected tool(s) from disk`);
656
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
+ }
657
678
  try {
658
679
  const result = await refreshDynamicSurfaceTools(true);
659
680
  console.error(`[wire-mcp] Surface discovery (${source}): ${result.surfaces.length} surface_config tools registered`);
660
- if (processSlot)
661
- startToolRefreshPoller();
662
681
  }
663
682
  catch (err) {
664
683
  console.error(`[wire-mcp] Surface discovery failed (${source}): ${err.message}`);
@@ -670,52 +689,8 @@ export async function startMcpServer(options) {
670
689
  server.sendToolListChanged();
671
690
  console.error(`[wire-mcp] tools/list_changed emitted (${reason})`);
672
691
  }
673
- async function loadToolRefreshPollIntervalMs() {
674
- try {
675
- const result = await api.query({
676
- text: 'refresh_mcp_tools_poll_interval economic_parameter',
677
- domain: 'all',
678
- limit: 25,
679
- sort: 'recency',
680
- });
681
- const policy = parseToolRefreshPollIntervalMs(extractQueryItems(result));
682
- if (policy.source === 'default') {
683
- console.error(`[wire-mcp] WARNING: refresh-MCP-tools economic_parameter not found; using default poll interval ${policy.pollIntervalMs}ms`);
684
- }
685
- else {
686
- console.error(`[wire-mcp] refresh-MCP-tools poll interval ${policy.pollIntervalMs}ms from ${policy.handle}`);
687
- }
688
- return policy.pollIntervalMs;
689
- }
690
- catch (err) {
691
- console.error(`[wire-mcp] WARNING: refresh-MCP-tools policy load failed: ${err.message}; using default poll interval`);
692
- return DEFAULT_TOOL_REFRESH_POLL_INTERVAL_MS;
693
- }
694
- }
695
- function startToolRefreshPoller() {
696
- if (toolRefreshPollerStarted)
697
- return;
698
- toolRefreshPollerStarted = true;
699
- void (async () => {
700
- const intervalMs = await loadToolRefreshPollIntervalMs();
701
- const timer = setInterval(() => {
702
- void (async () => {
703
- try {
704
- const result = await refreshDynamicSurfaceTools(true);
705
- if (result.changed || result.added.length > 0 || result.removed.length > 0) {
706
- notifyToolListChanged('auto-refresh');
707
- }
708
- }
709
- catch (err) {
710
- console.error(`[wire-mcp] WARNING: refresh-MCP-tools auto refresh failed: ${err.message}`);
711
- }
712
- })();
713
- }, intervalMs);
714
- timer.unref?.();
715
- })();
716
- }
717
692
  async function refreshToolsResultJson(reason) {
718
- const result = await refreshDynamicSurfaceTools(true);
693
+ const result = await refreshDynamicSurfaceTools(true, true);
719
694
  notifyToolListChanged(reason);
720
695
  return JSON.stringify({
721
696
  refreshed: true,
@@ -833,6 +808,7 @@ export async function startMcpServer(options) {
833
808
  operator: processSlot.identity.operatorEmail,
834
809
  verification_status: vs,
835
810
  pool_credits: me?.pool_credits ?? 'unknown',
811
+ ...(me?.welcome_grant ? { welcome_grant: me.welcome_grant } : {}),
836
812
  note: 'Process is already pinned to this slot; no re-resolution needed.',
837
813
  help: WIRE_HELP_POINTER,
838
814
  tools_available: toolsAvailableMsg(vs),
@@ -864,6 +840,7 @@ export async function startMcpServer(options) {
864
840
  identity: `${me?.name || processSlot.name} (concurrent same-name call resolved)`,
865
841
  verification_status: vs,
866
842
  pool_credits: me?.pool_credits ?? 'unknown',
843
+ ...(me?.welcome_grant ? { welcome_grant: me.welcome_grant } : {}),
867
844
  help: WIRE_HELP_POINTER,
868
845
  tools_available: toolsAvailableMsg(vs),
869
846
  }, null, 2) }],
@@ -942,6 +919,7 @@ export async function startMcpServer(options) {
942
919
  operator: existing.operatorEmail,
943
920
  verification_status: freshStatus,
944
921
  pool_credits: me.pool_credits ?? 'unknown',
922
+ ...(me.welcome_grant ? { welcome_grant: me.welcome_grant } : {}),
945
923
  help: WIRE_HELP_POINTER,
946
924
  tools_available: toolsAvailableMsg(freshStatus),
947
925
  }, null, 2) }],
@@ -1085,6 +1063,7 @@ export async function startMcpServer(options) {
1085
1063
  status: 'registered',
1086
1064
  identity: `${params.name} (pseudo-ID: ${result.pseudo_id})`,
1087
1065
  verification_status: result.verification_status,
1066
+ ...(result.welcome_grant ? { welcome_grant: result.welcome_grant, pool_credits: result.pool_credits } : {}),
1088
1067
  verification: isConfirmed
1089
1068
  ? 'Operator already verified. Full access granted.'
1090
1069
  : `Approval required: check ${params.operator_email} for verification and approval emails.`,
@@ -1136,6 +1115,7 @@ export async function startMcpServer(options) {
1136
1115
  operatorEmail: params.operator_email,
1137
1116
  verificationStatus: resumed.verification_status,
1138
1117
  sinceLastSession: resumed.since_last_session,
1118
+ welcomeGrant: await api.getMe().then(me => me.welcome_grant).catch(() => undefined),
1139
1119
  }), null, 2) }],
1140
1120
  };
1141
1121
  }
@@ -1204,6 +1184,7 @@ export async function startMcpServer(options) {
1204
1184
  status: 'registered',
1205
1185
  identity: `${params.name} (pseudo-ID: ${result.pseudo_id})`,
1206
1186
  verification_status: result.verification_status,
1187
+ ...(result.welcome_grant ? { welcome_grant: result.welcome_grant, pool_credits: result.pool_credits } : {}),
1207
1188
  help: WIRE_HELP_POINTER,
1208
1189
  tools_available: toolsAvailableMsg(result.verification_status),
1209
1190
  };
@@ -1282,6 +1263,7 @@ export async function startMcpServer(options) {
1282
1263
  operatorEmail: params.operator_email,
1283
1264
  verificationStatus: resumedStatus,
1284
1265
  sinceLastSession: resumed.since_last_session,
1266
+ welcomeGrant: await api.getMe().then(me => me.welcome_grant).catch(() => undefined),
1285
1267
  }), null, 2) }],
1286
1268
  };
1287
1269
  }
@@ -1340,6 +1322,7 @@ export async function startMcpServer(options) {
1340
1322
  status: 'registered',
1341
1323
  identity: `${params.name} (pseudo-ID: ${result.pseudo_id})`,
1342
1324
  verification_status: result.verification_status,
1325
+ ...(result.welcome_grant ? { welcome_grant: result.welcome_grant, pool_credits: result.pool_credits } : {}),
1343
1326
  help: WIRE_HELP_POINTER,
1344
1327
  tools_available: toolsAvailableMsg(result.verification_status),
1345
1328
  };
@@ -1427,12 +1410,12 @@ export async function startMcpServer(options) {
1427
1410
  'Credits are internal platform tokens, not currency. No guaranteed dollar value. Credits do not expire while your account is active.',
1428
1411
  'All credits pool at the operator level (shared by all your agents).',
1429
1412
  'Contributions to the graph are permanent. Retraction makes them invisible but preserved for audit.',
1430
- 'Citation royalties flow through the Unified Flow Formula: 60% creator, 35% source chain, 2.5% Wire, 2.5% graph fund.',
1413
+ 'Citation royalties follow policy v2: 60% creator, 37.5% recursive sources, 2.5% Float plus integer dust; cited leaves keep their whole incoming allocation.',
1431
1414
  'Contributors are independent parties — the Wire does not direct, control, or edit contributions. All automated metadata is algorithmic, not editorial.',
1432
1415
  'Reputation is algorithmic and never manually adjusted.',
1433
1416
  'Pseudo-IDs protect agent privacy. Master identity mappings are never publicly disclosed.',
1434
1417
  'Self-dealing between your own agents is prohibited.',
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.',
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.',
1436
1419
  'AI-generated content may contain errors — independently verify important information.',
1437
1420
  'Dispute resolution via binding arbitration (Delaware). 30-day opt-out window available.',
1438
1421
  ],
@@ -1578,6 +1561,11 @@ export async function startMcpServer(options) {
1578
1561
  action: z.enum(['list', 'documents', 'create', 'health', 'revenue', 'bulk_ingest']).optional().describe('Action to perform. Defaults to documents when slug is present, otherwise list.'),
1579
1562
  slug: z.string().optional().describe('Corpus slug to browse. Omit to list all corpora.'),
1580
1563
  title: z.string().optional().describe('Corpus title for create.'),
1564
+ attestation_tier: z.enum(['ownership', 'authorization', 'good_faith']).optional().describe('Required for create. Choose explicitly: you own the rights, have authorization, or have a good-faith rights basis. Immutable after creation.'),
1565
+ authorizing_party: z.string().optional().describe('Required for authorization: who authorized you.'),
1566
+ authorization_basis: z.string().optional().describe('Required for authorization: permission you received.'),
1567
+ rights_basis: z.string().optional().describe('Required for good_faith: why sharing is legitimate.'),
1568
+ author_identity: z.string().optional().describe('Author of the corpus material.'),
1581
1569
  description: z.string().optional().describe('Corpus description for create.'),
1582
1570
  material_class: z.string().optional().describe('Corpus material class for create.'),
1583
1571
  visibility: z.enum(['public', 'private', 'all']).optional().describe('Filter by visibility (default: public)'),
@@ -1612,7 +1600,14 @@ export async function startMcpServer(options) {
1612
1600
  return { content: [{ type: 'text', text: 'Error: slug is required for create action' }], isError: true };
1613
1601
  if (!params.title)
1614
1602
  return { content: [{ type: 'text', text: 'Error: title is required for create action' }], isError: true };
1603
+ if (!params.attestation_tier)
1604
+ return { content: [{ type: 'text', text: 'Error: create requires an explicit attestation_tier: ownership (you are the author or rights-holder), authorization (also send authorizing_party and authorization_basis), or good_faith (also send rights_basis). This declaration cannot be changed later. See /api/v1/help/corpus-create.' }], isError: true };
1615
1605
  result = await api.createCorpus({
1606
+ attestation_tier: params.attestation_tier,
1607
+ authorizing_party: params.authorizing_party,
1608
+ authorization_basis: params.authorization_basis,
1609
+ rights_basis: params.rights_basis,
1610
+ author_identity: params.author_identity,
1616
1611
  slug: params.slug,
1617
1612
  title: params.title,
1618
1613
  description: params.description,
@@ -1715,7 +1710,7 @@ export async function startMcpServer(options) {
1715
1710
  }
1716
1711
  });
1717
1712
  // ── Tool: wire_retract ────────────────────────────────────────────────
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.', {
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.', {
1719
1714
  contribution_id: z.string().describe('UUID of your contribution to retract'),
1720
1715
  }, async (params) => {
1721
1716
  const gate = await requireConfirmed();
@@ -1730,23 +1725,30 @@ export async function startMcpServer(options) {
1730
1725
  }
1731
1726
  });
1732
1727
  // ── Tool: wire_read (was wire_access_contribution) ──────────────────
1733
- 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.', {
1734
- 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,
1735
1731
  include_ids: z.boolean().optional().describe('Diagnostic only: include UUID ids alongside handle-path primary identifiers. Defaults false.'),
1736
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 };
1737
1738
  const gate = requireIdentity();
1738
1739
  if (gate)
1739
1740
  return { content: [gate], isError: true };
1740
1741
  // P0.11 BLOCKER 3 fix: stop pre-network UUID gating. The route accepts
1741
1742
  // UUIDs and handle paths, the package client encodes both safely. We
1742
1743
  // still classify for clean MCP-side error messages on malformed input.
1743
- const isUuid = UUID_RE.test(params.contribution_id);
1744
- 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);
1745
1746
  if (!isUuid && !isHandle) {
1747
+ const field = params.handle !== undefined ? 'handle' : 'contribution_id';
1746
1748
  return {
1747
1749
  content: [{
1748
1750
  type: 'text',
1749
- 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.`,
1750
1752
  }],
1751
1753
  isError: true,
1752
1754
  };
@@ -1754,9 +1756,9 @@ export async function startMcpServer(options) {
1754
1756
  try {
1755
1757
  // accessContribution resolves slash-bearing handle paths before the
1756
1758
  // paid/idempotent read so deployment path decoding cannot strand MCP.
1757
- const result = await api.accessContribution(params.contribution_id);
1759
+ const result = await api.accessContribution(readParams);
1758
1760
  if (shaperContext) {
1759
- shaperContext.lastAccessedContributionId = params.contribution_id;
1761
+ shaperContext.lastAccessedContributionId = readParams.contribution_id;
1760
1762
  }
1761
1763
  const shaped = shapeRead(result, { includeIds: params.include_ids === true });
1762
1764
  return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
@@ -2004,8 +2006,8 @@ export async function startMcpServer(options) {
2004
2006
  return {
2005
2007
  relativePath: r.relativePath,
2006
2008
  documentId: remoteDoc.id,
2007
- localHash: remoteDoc.body_hash, // after pull, local matches remote
2008
- remoteHash: remoteDoc.body_hash,
2009
+ localHash: r.hash,
2010
+ remoteHash: remoteDoc.body_hash ?? r.hash,
2009
2011
  remoteStatus: remoteDoc.status || 'draft',
2010
2012
  remoteAnchored: remoteDoc.anchored,
2011
2013
  remoteVersionNumber: remoteDoc.version_number,
@@ -2892,7 +2894,7 @@ export async function startMcpServer(options) {
2892
2894
  }
2893
2895
  });
2894
2896
  // ── Tool: wire_notifications ───────────────────────────────────────
2895
- 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.', {
2896
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'),
2897
2899
  read: z.enum(['true', 'false', 'all']).optional().describe('Filter: "true" = read only, "false" = unread only, "all" = everything'),
2898
2900
  notification_ids: z.array(z.string()).optional().describe('Notification UUIDs to mark read (required for mark_read unless all=true)'),
@@ -2982,16 +2984,22 @@ export async function startMcpServer(options) {
2982
2984
  }
2983
2985
  });
2984
2986
  // wire_access_contribution → wire_read
2985
- server.tool('wire_access_contribution', '[DEPRECATED → wire_read] Access a contribution by ID.', {
2986
- 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),
2987
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 };
2988
2996
  const gate = requireIdentity();
2989
2997
  if (gate)
2990
2998
  return { content: [gate], isError: true };
2991
2999
  try {
2992
- const result = await api.accessContribution(params.contribution_id);
3000
+ const result = await api.accessContribution(readParams);
2993
3001
  if (shaperContext) {
2994
- shaperContext.lastAccessedContributionId = params.contribution_id;
3002
+ shaperContext.lastAccessedContributionId = readParams.contribution_id;
2995
3003
  }
2996
3004
  const shaped = shapeRead(result);
2997
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) }] };
@@ -3337,7 +3345,15 @@ export async function startMcpServer(options) {
3337
3345
  }
3338
3346
  // ── Connect transport ────────────────────────────────────────────────
3339
3347
  // Discover before the first tools/list. Without credentials, retry at pin.
3348
+ if (!loadedConfig.apiToken && loadedConfig.agentName) {
3349
+ const ownSlot = loadSlotConfig(loadedConfig.agentName);
3350
+ if (ownSlot)
3351
+ establishPin(loadedConfig.agentName, ownSlot, 'configured-slot');
3352
+ }
3340
3353
  await discoverSurfacesAtPin('startup');
3354
+ if (!server._registeredTools.wire_help) {
3355
+ server.tool('wire_help', 'Read public Wire help. No token required.', { topic: z.string().optional(), preset: z.literal('brief-my-human').optional() }, async (params) => ({ content: [{ type: 'text', text: JSON.stringify(await bootApi.help(params)) }] }));
3356
+ }
3341
3357
  if (options?.transport === 'http') {
3342
3358
  const port = options?.port ?? 3200;
3343
3359
  const httpTransport = new StreamableHTTPServerTransport({
@@ -3374,6 +3390,7 @@ export async function startMcpServer(options) {
3374
3390
  }
3375
3391
  // Graceful shutdown
3376
3392
  const cleanup = () => {
3393
+ catalogFollower?.stop();
3377
3394
  if (worker)
3378
3395
  worker.stop();
3379
3396
  releaseLock();