@agent-wire/terminal 1.7.6 → 1.7.9

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,22 @@
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';
13
+ import { resolveApiToken } from './api-token.js';
11
14
  import { loadSlotConfig, saveSlotConfig, migrateLegacyAgentsToSlots } from './slots.js';
12
- import { WireAPI } from './api.js';
15
+ import { WireAPI, WireApiRefusal, RegistrationError } from './api.js';
16
+ import { resumeToken } from './resume.js';
13
17
  import { shapeStatus, shapeRead, shapeInspect, shapeGraph, shapeCorpora, shapeMyContributions, shapeEarnings, shapeOpportunities, shapeRate, shapePatrolScan, shapeNotifications, shapeTasks, shapeMessages, } from './response-shaper.js';
14
18
  import { startBackgroundWorkLoop } from './background-worker.js';
15
- import { parseToolDefinition, dedupeToolDefinitions, fingerprintToolDefinitions, jsonSchemaToZodShape, } from './tool-projection.js';
19
+ import { parseToolDefinition, fetchCatalogDefinitionItems, invokeToolDefinition, dedupeToolDefinitions, withToolDefinitionAliases, fingerprintToolDefinitions, jsonSchemaToZodShape, } from './tool-projection.js';
16
20
  import {
17
21
  // Mission #1.5-rev2: agents/ cache fully retired. loadAgentIdentity /
18
22
  // saveAgentIdentity / deleteAgentIdentity are no longer imported here;
@@ -82,9 +86,7 @@ function normalizeApiRootPath(path) {
82
86
  return withSlash.replace(/^\/api\/v1(?=\/|$)/, '') || '/';
83
87
  }
84
88
  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);
89
+ return fetchCatalogDefinitionItems(api, 'surface');
88
90
  }
89
91
  async function refreshSubstrateSurfaces(api) {
90
92
  const items = await fetchSurfaceConfigItems(api);
@@ -152,12 +154,23 @@ function parseToolRefreshPollIntervalMs(items) {
152
154
  function toolsAvailableMsg(verificationStatus) {
153
155
  return verificationStatus === 'confirmed'
154
156
  ? '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.';
157
+ : '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
158
  }
157
159
  function looksLikeDeviceAuthFailure(error) {
158
160
  if (!error)
159
161
  return false;
160
- return /\b(401|410)\b|revoked|device authorization|device secret|unauthorized/i.test(error.message);
162
+ if (/\b410\b|wire_roster_deleted/i.test(error.message))
163
+ return false;
164
+ // Endpoint removal (410) does not revoke device authorization.
165
+ return /\b401\b|revoked|device authorization|device secret|unauthorized/i.test(error.message);
166
+ }
167
+ function looksLikeRetiredRoster(error) {
168
+ if (!error)
169
+ return false;
170
+ // The /wire/roster endpoint was intentionally retired (server returns 410 wire_roster_deleted,
171
+ // "use activity observations instead"). A 410 here is EXPECTED, not a fault — callers should
172
+ // degrade quietly rather than surface it as a failure. (patent-guy doorbell #4)
173
+ return /wire_roster_deleted|\b410\b/i.test(error.message);
161
174
  }
162
175
  function buildResumeResponse(params) {
163
176
  const resumedStatus = typeof params.verificationStatus === 'string'
@@ -202,7 +215,8 @@ export const __mcpSurfaceTestInternals = {
202
215
  looksLikeDeviceAuthFailure,
203
216
  };
204
217
  export async function startMcpServer(options) {
205
- const loadedConfig = loadConfig();
218
+ const loadedConfig = loadConfig({ allowInvalidTokenFile: true });
219
+ loadedConfig.apiUrl = process.env.WIRE_API_URL ?? loadedConfig.apiUrl;
206
220
  // MCP identity is slot-owned now. Keep node/base config, but deliberately
207
221
  // strip any legacy config/env agent token before constructing pre-pin APIs.
208
222
  const config = { ...loadedConfig, apiToken: '' };
@@ -215,6 +229,9 @@ export async function startMcpServer(options) {
215
229
  // tool handlers go through the `api` closure variable below — bootApi
216
230
  // is for identity-resolution operations only.
217
231
  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.
234
+ const discoveryApi = new WireAPI(loadedConfig, threadId);
218
235
  // `api` starts as bootApi (no agent token). Tool handlers MUST gate
219
236
  // through requireIdentity() before using it; that gate ensures the
220
237
  // closure has been promoted to the slot's authenticated WireAPI by
@@ -251,6 +268,12 @@ export async function startMcpServer(options) {
251
268
  function requireIdentity() {
252
269
  if (processSlot)
253
270
  return null;
271
+ try {
272
+ resolveApiToken(loadedConfig);
273
+ }
274
+ catch (error) {
275
+ return { type: 'text', text: error.message };
276
+ }
254
277
  return { type: 'text', text: WIRE_DOORWAY_AFFORDANCE };
255
278
  }
256
279
  // ── Session state for response shaping ──────────────────────────────
@@ -288,7 +311,8 @@ export async function startMcpServer(options) {
288
311
  verificationStatus: me.verification_status,
289
312
  };
290
313
  processSlot = { ...processSlot, identity: refreshedIdentity };
291
- saveSlotConfig(processSlot.name, refreshedIdentity);
314
+ if (loadedConfig.apiTokenFile === undefined)
315
+ saveSlotConfig(processSlot.name, refreshedIdentity);
292
316
  }
293
317
  }
294
318
  async function ensureShaperContext(options = {}) {
@@ -344,7 +368,9 @@ export async function startMcpServer(options) {
344
368
  const slotApi = new WireAPI({ ...config, apiToken: identity.apiToken }, threadId);
345
369
  processSlot = { name, identity };
346
370
  api = slotApi;
347
- saveSlotConfig(name, identity);
371
+ // A configured file remains the credential source; don't copy it into a slot.
372
+ if (identity.operatorEmail && loadedConfig.apiTokenFile === undefined)
373
+ saveSlotConfig(name, identity);
348
374
  console.error(`[wire-mcp] Pin established: ${name} (${identity.pseudoId}) [source=${source}]`);
349
375
  return identity;
350
376
  }
@@ -484,36 +510,11 @@ export async function startMcpServer(options) {
484
510
  dynamicSurfaceTools.set(surface.toolName, { ...registered, surface });
485
511
  }
486
512
  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
513
+ const hydrated = await fetchCatalogDefinitionItems(processSlot ? api : discoveryApi, 'tool_definition');
514
+ const definitions = withToolDefinitionAliases(dedupeToolDefinitions(hydrated
512
515
  .filter((item) => item !== null)
513
516
  .map((item) => parseToolDefinition(item))
514
- .filter((def) => def !== null));
515
- // Persist to disk cache (atomic write). Errors are non-fatal.
516
- void saveToolDefinitionsCache(definitions);
517
+ .filter((def) => def !== null)));
517
518
  return definitions;
518
519
  }
519
520
  function projectedToolDescription(def) {
@@ -546,40 +547,18 @@ export async function startMcpServer(options) {
546
547
  const registered = server.registerTool(def.toolName, {
547
548
  title: def.toolName,
548
549
  description: projectedToolDescription(def),
549
- inputSchema: jsonSchemaToZodShape(def.parameterSchema),
550
+ inputSchema: z.object(jsonSchemaToZodShape(def.parameterSchema)).strict(),
550
551
  _meta: {
551
552
  tool_definition_handle: def.configHandle,
552
553
  binding: def.binding,
553
554
  discovery: 'tool_definition',
554
555
  },
555
556
  }, async (params) => {
556
- const gate = def.gate === 'identity' ? requireIdentity() : await requireConfirmed();
557
+ const gate = def.toolName === 'wire_help' ? null : def.gate === 'identity' ? requireIdentity() : await requireConfirmed();
557
558
  if (gate)
558
559
  return { content: [gate], isError: true };
559
560
  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
- }
561
+ const result = await invokeToolDefinition(def, api, params);
583
562
  return {
584
563
  content: [{
585
564
  type: 'text',
@@ -588,6 +567,9 @@ export async function startMcpServer(options) {
588
567
  };
589
568
  }
590
569
  catch (err) {
570
+ if (err instanceof WireApiRefusal) {
571
+ return { content: [{ type: 'text', text: JSON.stringify(err.body, null, 2) }], isError: true };
572
+ }
591
573
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
592
574
  }
593
575
  });
@@ -611,10 +593,12 @@ export async function startMcpServer(options) {
611
593
  removed: [],
612
594
  };
613
595
  }
614
- const [surfaces, toolDefinitions] = await Promise.all([
615
- refreshSubstrateSurfaces(api),
616
- fetchToolDefinitions(),
596
+ // Reconcile atomically. A failed discovery read preserves session tools
597
+ // and the last good disk cache, including on a cold surface read.
598
+ const [toolDefinitions, surfaces] = await Promise.all([
599
+ fetchToolDefinitions(), refreshSubstrateSurfaces(processSlot ? api : discoveryApi),
617
600
  ]);
601
+ void saveToolDefinitionsCache(toolDefinitions);
618
602
  surfaceCache = { fetchedAt: Date.now(), surfaces };
619
603
  toolDefinitionCache = { fetchedAt: Date.now(), definitions: toolDefinitions };
620
604
  const removed = [];
@@ -682,13 +666,16 @@ export async function startMcpServer(options) {
682
666
  console.error(`[wire-mcp] tool-definitions-cache: warm-booted ${cachedDefs.length} projected tool(s) from disk`);
683
667
  }
684
668
  try {
685
- const result = await refreshDynamicSurfaceTools(false);
669
+ const result = await refreshDynamicSurfaceTools(true);
686
670
  console.error(`[wire-mcp] Surface discovery (${source}): ${result.surfaces.length} surface_config tools registered`);
687
- startToolRefreshPoller();
688
671
  }
689
672
  catch (err) {
690
673
  console.error(`[wire-mcp] Surface discovery failed (${source}): ${err.message}`);
691
674
  }
675
+ finally {
676
+ if (processSlot)
677
+ startToolRefreshPoller();
678
+ }
692
679
  }
693
680
  function notifyToolListChanged(reason) {
694
681
  if (!server.isConnected())
@@ -769,7 +756,7 @@ export async function startMcpServer(options) {
769
756
  }, null, 2);
770
757
  }
771
758
  // ── 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.', {
759
+ 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
760
  manifest_path: z.string().optional().describe('Optional manifest path. Defaults to ~/.wire-node/sync-manifest.json.'),
774
761
  dry_run: z.boolean().optional().describe('Preview target writes without modifying local files.'),
775
762
  init: z.boolean().optional().describe('Create a default manifest first if one does not exist.'),
@@ -789,6 +776,25 @@ export async function startMcpServer(options) {
789
776
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
790
777
  }
791
778
  });
779
+ async function beginDeviceAuthorization(params) {
780
+ const fingerprint = getStableDeviceFingerprint();
781
+ await bootApi.requestDeviceAuth(params.operator_email, fingerprint, params.agent_message);
782
+ saveAuthPending(params.operator_email, {
783
+ email: params.operator_email,
784
+ deviceFingerprint: fingerprint,
785
+ requestedAt: new Date().toISOString(),
786
+ createNew: params.create_new || undefined,
787
+ agentName: params.name,
788
+ });
789
+ return {
790
+ content: [{ type: 'text', text: JSON.stringify({
791
+ status: 'check_email',
792
+ message: `Device authorization code sent to ${params.operator_email}. Ask the operator for the 6-digit code from their email.`,
793
+ next_step: 'Call wire_identify again with the same name and email, plus the device_code parameter containing the 6-digit code.',
794
+ expires_in_minutes: 15,
795
+ }, null, 2) }],
796
+ };
797
+ }
792
798
  server.tool('wire_identify', 'Identify yourself to the Wire. Must be called before any other Wire tools. ' +
793
799
  'First call: provide name + operator_email. If this is a new device, you will receive a ' +
794
800
  '"check_email" response — ask the operator for the 6-digit code from their email, ' +
@@ -918,7 +924,7 @@ export async function startMcpServer(options) {
918
924
  catch { /* token dead */ }
919
925
  if (me) {
920
926
  // 1A: Verify cached identity matches requested name
921
- const serverName = me.name;
927
+ const serverName = (me.identity?.name ?? me.name);
922
928
  if (serverName && params.name && serverName.toLowerCase() !== params.name.toLowerCase()) {
923
929
  // Stale slot file — server-side name doesn't match what
924
930
  // the local file claims. Skip the slot file (it's
@@ -1039,13 +1045,24 @@ export async function startMcpServer(options) {
1039
1045
  client: process.env.MCP_CLIENT_NAME || 'unknown',
1040
1046
  session_started: new Date().toISOString(),
1041
1047
  };
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
- });
1048
+ let result;
1049
+ try {
1050
+ result = await bootApi.registerAgent({
1051
+ name: params.name,
1052
+ operator_email: params.operator_email,
1053
+ domains: ['all'],
1054
+ session_metadata: sessionMetadata,
1055
+ agent_message: params.agent_message,
1056
+ });
1057
+ }
1058
+ catch (err) {
1059
+ // An empty local cache says nothing about the operator's hub
1060
+ // identities. A conflict starts authorization on this new device.
1061
+ if (err instanceof RegistrationError && err.status === 409) {
1062
+ return await beginDeviceAuthorization({ ...params, create_new: false });
1063
+ }
1064
+ throw err;
1065
+ }
1049
1066
  if (!result.api_token || !result.pseudo_id || !result.agent_id) {
1050
1067
  const missing = [
1051
1068
  !result.api_token && 'api_token', !result.pseudo_id && 'pseudo_id', !result.agent_id && 'agent_id',
@@ -1091,35 +1108,7 @@ export async function startMcpServer(options) {
1091
1108
  }
1092
1109
  // EXISTING OPERATOR, NEW DEVICE: initiate device auth
1093
1110
  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
- };
1111
+ return await beginDeviceAuthorization(params);
1123
1112
  }
1124
1113
  // ── 5. Have device secret — resume/register without requiring roster ─
1125
1114
  if (deviceRecord) {
@@ -1224,14 +1213,21 @@ export async function startMcpServer(options) {
1224
1213
  lastSessionAt: null,
1225
1214
  lastAccessedContributionId: null,
1226
1215
  };
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) }] };
1216
+ const registered = {
1217
+ status: 'registered',
1218
+ identity: `${params.name} (pseudo-ID: ${result.pseudo_id})`,
1219
+ verification_status: result.verification_status,
1220
+ help: WIRE_HELP_POINTER,
1221
+ tools_available: toolsAvailableMsg(result.verification_status),
1222
+ };
1223
+ // The /wire/roster endpoint is intentionally retired (410 wire_roster_deleted), so
1224
+ // registering directly via /register is now the NORMAL path, not a fallback from a
1225
+ // fault — don't alarm the caller with a roster_warning for an expected 410. Surface a
1226
+ // warning only for an UNEXPECTED roster failure. (patent-guy doorbell #4)
1227
+ if (!looksLikeRetiredRoster(rosterError)) {
1228
+ registered.roster_warning = `Roster lookup failed (${rosterError.message}); registered directly via /register.`;
1229
+ }
1230
+ return { content: [{ type: 'text', text: JSON.stringify(registered, null, 2) }] };
1235
1231
  }
1236
1232
  if (looksLikeDeviceAuthFailure(resumeError) && looksLikeDeviceAuthFailure(rosterError)) {
1237
1233
  deleteDeviceRecord(params.operator_email);
@@ -1444,12 +1440,12 @@ export async function startMcpServer(options) {
1444
1440
  'Credits are internal platform tokens, not currency. No guaranteed dollar value. Credits do not expire while your account is active.',
1445
1441
  'All credits pool at the operator level (shared by all your agents).',
1446
1442
  'Contributions to the graph are permanent. Retraction makes them invisible but preserved for audit.',
1447
- 'Citation royalties flow through the Unified Flow Formula: 60% creator, 35% source chain, 2.5% Wire, 2.5% graph fund.',
1443
+ 'Citation royalties follow policy v2: 60% creator, 37.5% recursive sources, 2.5% Float plus integer dust; cited leaves keep their whole incoming allocation.',
1448
1444
  'Contributors are independent parties — the Wire does not direct, control, or edit contributions. All automated metadata is algorithmic, not editorial.',
1449
1445
  'Reputation is algorithmic and never manually adjusted.',
1450
1446
  'Pseudo-IDs protect agent privacy. Master identity mappings are never publicly disclosed.',
1451
1447
  '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.',
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.',
1453
1449
  'AI-generated content may contain errors — independently verify important information.',
1454
1450
  'Dispute resolution via binding arbitration (Delaware). 30-day opt-out window available.',
1455
1451
  ],
@@ -1595,6 +1591,11 @@ export async function startMcpServer(options) {
1595
1591
  action: z.enum(['list', 'documents', 'create', 'health', 'revenue', 'bulk_ingest']).optional().describe('Action to perform. Defaults to documents when slug is present, otherwise list.'),
1596
1592
  slug: z.string().optional().describe('Corpus slug to browse. Omit to list all corpora.'),
1597
1593
  title: z.string().optional().describe('Corpus title for create.'),
1594
+ 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.'),
1595
+ authorizing_party: z.string().optional().describe('Required for authorization: who authorized you.'),
1596
+ authorization_basis: z.string().optional().describe('Required for authorization: permission you received.'),
1597
+ rights_basis: z.string().optional().describe('Required for good_faith: why sharing is legitimate.'),
1598
+ author_identity: z.string().optional().describe('Author of the corpus material.'),
1598
1599
  description: z.string().optional().describe('Corpus description for create.'),
1599
1600
  material_class: z.string().optional().describe('Corpus material class for create.'),
1600
1601
  visibility: z.enum(['public', 'private', 'all']).optional().describe('Filter by visibility (default: public)'),
@@ -1629,7 +1630,14 @@ export async function startMcpServer(options) {
1629
1630
  return { content: [{ type: 'text', text: 'Error: slug is required for create action' }], isError: true };
1630
1631
  if (!params.title)
1631
1632
  return { content: [{ type: 'text', text: 'Error: title is required for create action' }], isError: true };
1633
+ if (!params.attestation_tier)
1634
+ 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 };
1632
1635
  result = await api.createCorpus({
1636
+ attestation_tier: params.attestation_tier,
1637
+ authorizing_party: params.authorizing_party,
1638
+ authorization_basis: params.authorization_basis,
1639
+ rights_basis: params.rights_basis,
1640
+ author_identity: params.author_identity,
1633
1641
  slug: params.slug,
1634
1642
  title: params.title,
1635
1643
  description: params.description,
@@ -1683,6 +1691,54 @@ export async function startMcpServer(options) {
1683
1691
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1684
1692
  }
1685
1693
  });
1694
+ // ── Tools: pyramid consume-head proxies (spec green playful/176/24) ──────
1695
+ // Thin HTTP proxies to the local AWN node's read-time pyramid routes. No new
1696
+ // logic, no LLM. pyramid_map orients; pyramid_hydrate pulls one node/region;
1697
+ // pyramid_ask runs cheap mechanical retrieval. JSON returned as-is.
1698
+ 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.', {
1699
+ slug: z.string().describe('Pyramid slug to map.'),
1700
+ }, async (params) => {
1701
+ const gate = requireIdentity();
1702
+ if (gate)
1703
+ return { content: [gate], isError: true };
1704
+ try {
1705
+ const result = await api.getPyramidMap(params.slug);
1706
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1707
+ }
1708
+ catch (err) {
1709
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1710
+ }
1711
+ });
1712
+ 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.", {
1713
+ slug: z.string().describe('Pyramid slug.'),
1714
+ node_id: z.string().describe('Node id to hydrate (from pyramid_map nav).'),
1715
+ }, async (params) => {
1716
+ const gate = requireIdentity();
1717
+ if (gate)
1718
+ return { content: [gate], isError: true };
1719
+ try {
1720
+ const result = await api.getPyramidDrill(params.slug, params.node_id);
1721
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1722
+ }
1723
+ catch (err) {
1724
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1725
+ }
1726
+ });
1727
+ 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.', {
1728
+ slug: z.string().describe('Pyramid slug to query.'),
1729
+ question: z.string().describe('Natural-language question to retrieve an answer for.'),
1730
+ }, async (params) => {
1731
+ const gate = requireIdentity();
1732
+ if (gate)
1733
+ return { content: [gate], isError: true };
1734
+ try {
1735
+ const result = await api.askPyramid(params.slug, params.question);
1736
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1737
+ }
1738
+ catch (err) {
1739
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1740
+ }
1741
+ });
1686
1742
  // ── Tool: wire_retract ────────────────────────────────────────────────
1687
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.', {
1688
1744
  contribution_id: z.string().describe('UUID of your contribution to retract'),
@@ -1770,6 +1826,8 @@ export async function startMcpServer(options) {
1770
1826
  // ── Tool: wire_mesh (merged: wire_mesh_status + wire_mesh_board + wire_mesh_intent) ──
1771
1827
  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).', {
1772
1828
  action: z.enum(['status', 'board_read', 'board_write', 'board_delete', 'intent_declare', 'intent_release', 'intent_list']).describe('Mesh action to perform'),
1829
+ scope: z.enum(['agent', 'fleet', 'circle']).optional().describe('Shared mesh scope; defaults to agent.'),
1830
+ circle_id: z.string().optional().describe('Circle UUID required for circle scope.'),
1773
1831
  key: z.string().optional().describe('Board key (for board_write/board_delete)'),
1774
1832
  value: z.unknown().optional().describe('Board value (for board_write)'),
1775
1833
  ttl: z.number().optional().describe('TTL in seconds (for board_write: default 3600, max 86400; for intent_declare: default 300, max 3600)'),
@@ -1782,13 +1840,14 @@ export async function startMcpServer(options) {
1782
1840
  if (gate)
1783
1841
  return { content: [gate], isError: true };
1784
1842
  try {
1843
+ const scope = { scope: params.scope, circle_id: params.circle_id };
1785
1844
  switch (params.action) {
1786
1845
  case 'status': {
1787
- const result = await api.meshStatus();
1846
+ const result = await api.meshStatus(scope);
1788
1847
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1789
1848
  }
1790
1849
  case 'board_read': {
1791
- const result = await api.meshBoardRead();
1850
+ const result = await api.meshBoardRead(scope);
1792
1851
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1793
1852
  }
1794
1853
  case 'board_write': {
@@ -1796,29 +1855,29 @@ export async function startMcpServer(options) {
1796
1855
  return { content: [{ type: 'text', text: 'key is required for board_write' }], isError: true };
1797
1856
  if (params.value === undefined)
1798
1857
  return { content: [{ type: 'text', text: 'value is required for board_write' }], isError: true };
1799
- const result = await api.meshBoardWrite(params.key, params.value, params.ttl);
1858
+ const result = await api.meshBoardWrite(params.key, params.value, params.ttl, scope);
1800
1859
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1801
1860
  }
1802
1861
  case 'board_delete': {
1803
1862
  if (!params.key)
1804
1863
  return { content: [{ type: 'text', text: 'key is required for board_delete' }], isError: true };
1805
- const result = await api.meshBoardDelete(params.key);
1864
+ const result = await api.meshBoardDelete(params.key, scope);
1806
1865
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1807
1866
  }
1808
1867
  case 'intent_declare': {
1809
1868
  if (!params.intent_action)
1810
1869
  return { content: [{ type: 'text', text: 'intent_action is required for intent_declare' }], isError: true };
1811
- const result = await api.meshDeclareIntent(params.intent_action, params.domain, params.meta, params.ttl);
1870
+ const result = await api.meshDeclareIntent(params.intent_action, params.domain, params.meta, params.ttl, scope);
1812
1871
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1813
1872
  }
1814
1873
  case 'intent_release': {
1815
1874
  if (!params.intent_id)
1816
1875
  return { content: [{ type: 'text', text: 'intent_id is required for intent_release' }], isError: true };
1817
- const result = await api.meshReleaseIntent(params.intent_id);
1876
+ const result = await api.meshReleaseIntent(params.intent_id, scope);
1818
1877
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1819
1878
  }
1820
1879
  case 'intent_list': {
1821
- const result = await api.meshGetIntents();
1880
+ const result = await api.meshGetIntents(scope);
1822
1881
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1823
1882
  }
1824
1883
  default:
@@ -2007,12 +2066,30 @@ export async function startMcpServer(options) {
2007
2066
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2008
2067
  }
2009
2068
  });
2069
+ 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.', {
2070
+ action: z.enum(['file', 'dispatch', 'report', 'settle', 'cancel', 'get', 'list']),
2071
+ claim_id: z.string().uuid().optional(), idempotency_key: z.string().min(1).max(256).optional(),
2072
+ claim_body: z.string().optional(), claim_type: z.string().optional(), domain: z.string().optional(),
2073
+ evidence_handles: z.array(z.string()).optional(), bond: creditAmountSchema(0).optional(),
2074
+ reward: creditAmountSchema(0).optional(), faculty_handle: z.string().optional(), offer_id: z.string().optional(),
2075
+ }, async (params) => {
2076
+ const gate = await requireConfirmed();
2077
+ if (gate)
2078
+ return { content: [gate], isError: true };
2079
+ try {
2080
+ return { content: [{ type: 'text', text: JSON.stringify(await api.claims(params), null, 2) }] };
2081
+ }
2082
+ catch (error) {
2083
+ return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
2084
+ }
2085
+ });
2010
2086
  // ── Tool: wire_market ──────────────────────────────────────────────────
2011
2087
  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.', {
2012
2088
  action: z.enum(['stake', 'status', 'norms']).describe('Action to perform'),
2013
2089
  contribution_id: z.string().optional().describe('Contribution UUID (required for stake, status)'),
2014
2090
  side: z.enum(['agree', 'disagree']).optional().describe('Side to stake on (required for stake)'),
2015
- amount: z.number().int().positive().optional().describe('Credits to stake (required for stake, must be positive integer)'),
2091
+ amount: creditAmountSchema(1).optional().describe('Credits to stake (required for stake, must be positive integer)'),
2092
+ request_id: z.string().uuid().optional().describe('Stable operation UUID for a stake; reuse on retry, change for an additional stake'),
2016
2093
  domain: z.string().optional().describe('Domain filter for norms (optional)'),
2017
2094
  }, async (params) => {
2018
2095
  const gate = await requireConfirmed();
@@ -2026,7 +2103,7 @@ export async function startMcpServer(options) {
2026
2103
  return { content: [{ type: 'text', text: 'Error: side is required for stake' }], isError: true };
2027
2104
  if (!params.amount)
2028
2105
  return { content: [{ type: 'text', text: 'Error: amount is required for stake' }], isError: true };
2029
- const result = await api.stakeMarket(params.contribution_id, params.side, params.amount);
2106
+ const result = await api.stakeMarket(params.contribution_id, params.side, params.amount, params.request_id);
2030
2107
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2031
2108
  }
2032
2109
  if (params.action === 'status') {
@@ -2385,10 +2462,10 @@ export async function startMcpServer(options) {
2385
2462
  });
2386
2463
  // ── Tool: wire_correct ─────────────────────────────────────────────
2387
2464
  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.', {
2388
- action: z.enum(['correct', 'endorse']).describe('correct = file a new correction, endorse = support an existing correction'),
2465
+ 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'),
2389
2466
  target_id: z.string().describe('For correct: contribution UUID being corrected. For endorse: correction contribution UUID.'),
2390
2467
  correction_contribution_id: z.string().optional().describe('Your correction contribution UUID (required for correct)'),
2391
- deposit_amount: z.number().int().min(1).describe('Credits to deposit (positive integer, returned if accepted)'),
2468
+ deposit_amount: creditAmountSchema(1).optional().describe('Credits to deposit (positive integer, returned if accepted)'),
2392
2469
  }, async (params) => {
2393
2470
  const gate = await requireConfirmed();
2394
2471
  if (gate)
@@ -2399,10 +2476,13 @@ export async function startMcpServer(options) {
2399
2476
  if (!params.correction_contribution_id) {
2400
2477
  return { content: [{ type: 'text', text: 'correction_contribution_id is required for correct' }], isError: true };
2401
2478
  }
2402
- result = await api.submitCorrection(params.target_id, params.correction_contribution_id, params.deposit_amount);
2479
+ result = await api.submitCorrection(params.target_id, params.correction_contribution_id, params.deposit_amount ?? 0);
2480
+ }
2481
+ else if (params.action === 'withdraw') {
2482
+ result = await api.withdrawCorrection(params.target_id);
2403
2483
  }
2404
2484
  else {
2405
- result = await api.endorseCorrection(params.target_id, params.deposit_amount);
2485
+ result = await api.endorseCorrection(params.target_id, params.deposit_amount ?? 0);
2406
2486
  }
2407
2487
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2408
2488
  }
@@ -3078,18 +3158,42 @@ export async function startMcpServer(options) {
3078
3158
  }
3079
3159
  });
3080
3160
  // ── Tool: wire_feedback ──────────────────────────────────────────────
3081
- server.tool('wire_feedback', 'Submit bug reports or feature suggestions to the Wire operator.', {
3161
+ const BUG_CATEGORIES = ['api_error', 'data_quality', 'economic', 'auth', 'other'];
3162
+ const SUGGESTION_CATEGORIES = ['feature', 'economic', 'ux', 'documentation', 'other'];
3163
+ 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.', {
3082
3164
  action: z.enum(['bug', 'suggestion']).describe('Type of feedback to submit'),
3083
3165
  message: z.string().describe('Description of the bug or suggestion'),
3166
+ category: z.enum(['api_error', 'data_quality', 'economic', 'auth', 'feature', 'ux', 'documentation', 'other']).optional()
3167
+ .describe('bug: api_error | data_quality | economic | auth | other. suggestion: feature | economic | ux | documentation | other. Defaults to other.'),
3168
+ severity: z.enum(['critical', 'major', 'minor', 'cosmetic']).optional().describe('Bugs only. Defaults to minor.'),
3169
+ title: z.string().optional().describe('Short title (suggestions)'),
3170
+ endpoint: z.string().optional().describe('The endpoint that misbehaved (bugs)'),
3171
+ steps_to_reproduce: z.string().optional().describe('How to trigger it (bugs)'),
3172
+ expected_behavior: z.string().optional().describe('What you expected (bugs)'),
3173
+ actual_behavior: z.string().optional().describe('What happened (bugs)'),
3084
3174
  context: z.string().optional().describe('Additional detail or context'),
3085
3175
  }, async (params) => {
3086
3176
  const gate = requireIdentity();
3087
3177
  if (gate)
3088
3178
  return { content: [gate], isError: true };
3179
+ const allowed = params.action === 'bug' ? BUG_CATEGORIES : SUGGESTION_CATEGORIES;
3180
+ if (params.category && !allowed.includes(params.category)) {
3181
+ return {
3182
+ content: [{ type: 'text', text: `Error: category "${params.category}" is not valid for a ${params.action}; use one of: ${allowed.join(', ')}` }],
3183
+ isError: true,
3184
+ };
3185
+ }
3089
3186
  try {
3187
+ const description = params.context ? `${params.message}\n\n${params.context}` : params.message;
3090
3188
  const result = await api.request('POST', `/wire/feedback/${params.action}`, {
3091
- message: params.message,
3092
- ...(params.context ? { context: params.context } : {}),
3189
+ description,
3190
+ ...(params.category ? { category: params.category } : {}),
3191
+ ...(params.action === 'bug' && params.severity ? { severity: params.severity } : {}),
3192
+ ...(params.action === 'suggestion' && params.title ? { title: params.title } : {}),
3193
+ ...(params.action === 'bug' && params.endpoint ? { endpoint: params.endpoint } : {}),
3194
+ ...(params.action === 'bug' && params.steps_to_reproduce ? { steps_to_reproduce: params.steps_to_reproduce } : {}),
3195
+ ...(params.action === 'bug' && params.expected_behavior ? { expected_behavior: params.expected_behavior } : {}),
3196
+ ...(params.action === 'bug' && params.actual_behavior ? { actual_behavior: params.actual_behavior } : {}),
3093
3197
  });
3094
3198
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
3095
3199
  }
@@ -3257,10 +3361,31 @@ export async function startMcpServer(options) {
3257
3361
  console.error('[wire-mcp] Background worker pending until wire_identify establishes a slot pin.');
3258
3362
  }
3259
3363
  // ── Connect transport ────────────────────────────────────────────────
3364
+ // 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');
3376
+ }
3377
+ await discoverSurfacesAtPin('startup');
3378
+ if (!server._registeredTools.wire_help) {
3379
+ 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)) }] }));
3380
+ }
3260
3381
  if (options?.transport === 'http') {
3261
3382
  const port = options?.port ?? 3200;
3262
3383
  const httpTransport = new StreamableHTTPServerTransport({
3263
- sessionIdGenerator: undefined, // stateless — identity is per-token, not per-session
3384
+ // This server retains its pinned identity across requests. The SDK's
3385
+ // stateless transport is single-use; a session keeps identify -> list ->
3386
+ // call usable on this same local process.
3387
+ sessionIdGenerator: () => randomUUID(),
3388
+ enableJsonResponse: true,
3264
3389
  });
3265
3390
  await server.connect(httpTransport);
3266
3391
  const httpServer = createServer(async (req, res) => {