@agent-wire/terminal 1.7.6 → 1.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,20 @@
1
+ import { isCreditValue } from './contracts/credit-amount.js';
1
2
  import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
2
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
4
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
5
  import { createServer } from 'node:http';
5
6
  import { z } from 'zod';
7
+ const creditAmountSchema = (minimum) => z.union([z.number().int(), z.string()]).refine(value => isCreditValue(value, minimum), 'Expected safe integer or int64 decimal credit string');
6
8
  import { hostname } from 'os';
7
9
  import { createHash, randomUUID } from 'crypto';
8
10
  import { readFile, writeFile, rename as fsRename, mkdir as fsMkdir, stat as fsStat } from 'fs/promises';
9
11
  import { join, isAbsolute } from 'path';
10
12
  import { loadConfig, saveConfig, CONFIG_DIR } from './config.js';
11
13
  import { loadSlotConfig, saveSlotConfig, migrateLegacyAgentsToSlots } from './slots.js';
12
- import { WireAPI } from './api.js';
14
+ import { WireAPI, RegistrationError } from './api.js';
13
15
  import { shapeStatus, shapeRead, shapeInspect, shapeGraph, shapeCorpora, shapeMyContributions, shapeEarnings, shapeOpportunities, shapeRate, shapePatrolScan, shapeNotifications, shapeTasks, shapeMessages, } from './response-shaper.js';
14
16
  import { startBackgroundWorkLoop } from './background-worker.js';
15
- import { parseToolDefinition, dedupeToolDefinitions, fingerprintToolDefinitions, jsonSchemaToZodShape, } from './tool-projection.js';
17
+ import { parseToolDefinition, fetchCatalogDefinitionItems, invokeToolDefinition, dedupeToolDefinitions, withToolDefinitionAliases, fingerprintToolDefinitions, jsonSchemaToZodShape, } from './tool-projection.js';
16
18
  import {
17
19
  // Mission #1.5-rev2: agents/ cache fully retired. loadAgentIdentity /
18
20
  // saveAgentIdentity / deleteAgentIdentity are no longer imported here;
@@ -82,9 +84,7 @@ function normalizeApiRootPath(path) {
82
84
  return withSlash.replace(/^\/api\/v1(?=\/|$)/, '') || '/';
83
85
  }
84
86
  async function fetchSurfaceConfigItems(api) {
85
- const queries = ['surface_config', 'dispatcher_bound_surface_config', 'lookup_surface'];
86
- const responses = await Promise.all(queries.map((text) => api.query({ text, domain: 'all', limit: 100, sort: 'recency' })));
87
- return responses.flatMap(extractQueryItems);
87
+ return fetchCatalogDefinitionItems(api, 'surface');
88
88
  }
89
89
  async function refreshSubstrateSurfaces(api) {
90
90
  const items = await fetchSurfaceConfigItems(api);
@@ -157,7 +157,18 @@ function toolsAvailableMsg(verificationStatus) {
157
157
  function looksLikeDeviceAuthFailure(error) {
158
158
  if (!error)
159
159
  return false;
160
- return /\b(401|410)\b|revoked|device authorization|device secret|unauthorized/i.test(error.message);
160
+ if (/\b410\b|wire_roster_deleted/i.test(error.message))
161
+ return false;
162
+ // Endpoint removal (410) does not revoke device authorization.
163
+ return /\b401\b|revoked|device authorization|device secret|unauthorized/i.test(error.message);
164
+ }
165
+ function looksLikeRetiredRoster(error) {
166
+ if (!error)
167
+ return false;
168
+ // The /wire/roster endpoint was intentionally retired (server returns 410 wire_roster_deleted,
169
+ // "use activity observations instead"). A 410 here is EXPECTED, not a fault — callers should
170
+ // degrade quietly rather than surface it as a failure. (patent-guy doorbell #4)
171
+ return /wire_roster_deleted|\b410\b/i.test(error.message);
161
172
  }
162
173
  function buildResumeResponse(params) {
163
174
  const resumedStatus = typeof params.verificationStatus === 'string'
@@ -203,6 +214,7 @@ export const __mcpSurfaceTestInternals = {
203
214
  };
204
215
  export async function startMcpServer(options) {
205
216
  const loadedConfig = loadConfig();
217
+ loadedConfig.apiUrl = process.env.WIRE_API_URL ?? loadedConfig.apiUrl;
206
218
  // MCP identity is slot-owned now. Keep node/base config, but deliberately
207
219
  // strip any legacy config/env agent token before constructing pre-pin APIs.
208
220
  const config = { ...loadedConfig, apiToken: '' };
@@ -215,6 +227,10 @@ export async function startMcpServer(options) {
215
227
  // tool handlers go through the `api` closure variable below — bootApi
216
228
  // is for identity-resolution operations only.
217
229
  const bootApi = new WireAPI(config, threadId);
230
+ // A configured token can discover tools before identify, but cannot select
231
+ // this process's identity. Calls still require the explicit slot pin.
232
+ const discoveryApi = new WireAPI({ ...config,
233
+ apiToken: process.env.WIRE_API_TOKEN ?? loadedConfig.apiToken }, threadId);
218
234
  // `api` starts as bootApi (no agent token). Tool handlers MUST gate
219
235
  // through requireIdentity() before using it; that gate ensures the
220
236
  // closure has been promoted to the slot's authenticated WireAPI by
@@ -484,34 +500,11 @@ export async function startMcpServer(options) {
484
500
  dynamicSurfaceTools.set(surface.toolName, { ...registered, surface });
485
501
  }
486
502
  async function fetchToolDefinitions() {
487
- const response = await api.query({ text: 'tool_definition', domain: 'all', limit: 100, sort: 'recency' });
488
- const items = extractQueryItems(response);
489
- const toolItems = items.filter((item) => item.type === 'tool_definition');
490
- // Query items are teasers — structured_data is absent. Hydrate each
491
- // tool_definition item with a full contribution read so parseToolDefinition
492
- // receives the complete structured_data payload.
493
- const hydrated = await Promise.all(toolItems.map(async (item) => {
494
- const identifier = item.handle_path ?? item.id;
495
- if (!identifier)
496
- return null;
497
- try {
498
- const full = await api.accessContribution(identifier);
499
- return {
500
- type: full.type,
501
- title: full.title,
502
- handle_path: (full.handle_path ?? identifier),
503
- structured_data: full.structured_data,
504
- };
505
- }
506
- catch (err) {
507
- console.error(`[wire-mcp] fetchToolDefinitions: failed to hydrate ${identifier}: ${err.message}`);
508
- return null;
509
- }
510
- }));
511
- const definitions = dedupeToolDefinitions(hydrated
503
+ const hydrated = await fetchCatalogDefinitionItems(processSlot ? api : discoveryApi, 'tool_definition');
504
+ const definitions = withToolDefinitionAliases(dedupeToolDefinitions(hydrated
512
505
  .filter((item) => item !== null)
513
506
  .map((item) => parseToolDefinition(item))
514
- .filter((def) => def !== null));
507
+ .filter((def) => def !== null)));
515
508
  // Persist to disk cache (atomic write). Errors are non-fatal.
516
509
  void saveToolDefinitionsCache(definitions);
517
510
  return definitions;
@@ -546,7 +539,7 @@ export async function startMcpServer(options) {
546
539
  const registered = server.registerTool(def.toolName, {
547
540
  title: def.toolName,
548
541
  description: projectedToolDescription(def),
549
- inputSchema: jsonSchemaToZodShape(def.parameterSchema),
542
+ inputSchema: z.object(jsonSchemaToZodShape(def.parameterSchema)).strict(),
550
543
  _meta: {
551
544
  tool_definition_handle: def.configHandle,
552
545
  binding: def.binding,
@@ -557,29 +550,7 @@ export async function startMcpServer(options) {
557
550
  if (gate)
558
551
  return { content: [gate], isError: true };
559
552
  try {
560
- let result;
561
- if (def.binding.kind === 'api_method') {
562
- const method = api[def.binding.method ?? ''];
563
- if (typeof method !== 'function') {
564
- throw new Error(`tool_definition ${def.configHandle ?? def.toolName} binds to unknown adapter method '${def.binding.method}'`);
565
- }
566
- result = await method.call(api, params);
567
- }
568
- else {
569
- const httpMethod = def.binding.http_method ?? 'POST';
570
- if (def.binding.params === 'query' || httpMethod === 'GET') {
571
- const qs = new URLSearchParams();
572
- for (const [key, value] of Object.entries(params)) {
573
- if (value !== undefined)
574
- qs.set(key, String(value));
575
- }
576
- const suffix = qs.toString() ? `?${qs.toString()}` : '';
577
- result = await api.request(httpMethod, `${normalizeApiRootPath(def.binding.path ?? '/')}${suffix}`);
578
- }
579
- else {
580
- result = await api.request(httpMethod, normalizeApiRootPath(def.binding.path ?? '/'), params);
581
- }
582
- }
553
+ const result = await invokeToolDefinition(def, api, params);
583
554
  return {
584
555
  content: [{
585
556
  type: 'text',
@@ -611,10 +582,12 @@ export async function startMcpServer(options) {
611
582
  removed: [],
612
583
  };
613
584
  }
614
- const [surfaces, toolDefinitions] = await Promise.all([
615
- refreshSubstrateSurfaces(api),
616
- fetchToolDefinitions(),
617
- ]);
585
+ // Surface discovery must not hide the core graph tools if it fails.
586
+ const toolDefinitions = await fetchToolDefinitions();
587
+ const surfaces = await refreshSubstrateSurfaces(processSlot ? api : discoveryApi).catch(err => {
588
+ console.error(`[wire-mcp] Surface discovery failed: ${err.message}`);
589
+ return surfaceCache?.surfaces ?? [];
590
+ });
618
591
  surfaceCache = { fetchedAt: Date.now(), surfaces };
619
592
  toolDefinitionCache = { fetchedAt: Date.now(), definitions: toolDefinitions };
620
593
  const removed = [];
@@ -682,9 +655,10 @@ export async function startMcpServer(options) {
682
655
  console.error(`[wire-mcp] tool-definitions-cache: warm-booted ${cachedDefs.length} projected tool(s) from disk`);
683
656
  }
684
657
  try {
685
- const result = await refreshDynamicSurfaceTools(false);
658
+ const result = await refreshDynamicSurfaceTools(true);
686
659
  console.error(`[wire-mcp] Surface discovery (${source}): ${result.surfaces.length} surface_config tools registered`);
687
- startToolRefreshPoller();
660
+ if (processSlot)
661
+ startToolRefreshPoller();
688
662
  }
689
663
  catch (err) {
690
664
  console.error(`[wire-mcp] Surface discovery failed (${source}): ${err.message}`);
@@ -769,7 +743,7 @@ export async function startMcpServer(options) {
769
743
  }, null, 2);
770
744
  }
771
745
  // ── Tool: wire_identify ──────────────────────────────────────────
772
- server.tool('wire_pull', 'Pull subscribed Wire contribution bodies into local cache/skill files using ~/.wire-node/sync-manifest.json. Mirrors the agent-wire-terminal pull CLI.', {
746
+ server.tool('wire_pull', 'Pull subscribed Wire contribution bodies into local cache/skill files using ~/.wire-node/sync-manifest.json. Mirrors the @agent-wire/terminal pull CLI.', {
773
747
  manifest_path: z.string().optional().describe('Optional manifest path. Defaults to ~/.wire-node/sync-manifest.json.'),
774
748
  dry_run: z.boolean().optional().describe('Preview target writes without modifying local files.'),
775
749
  init: z.boolean().optional().describe('Create a default manifest first if one does not exist.'),
@@ -789,6 +763,25 @@ export async function startMcpServer(options) {
789
763
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
790
764
  }
791
765
  });
766
+ async function beginDeviceAuthorization(params) {
767
+ const fingerprint = getStableDeviceFingerprint();
768
+ await bootApi.requestDeviceAuth(params.operator_email, fingerprint, params.agent_message);
769
+ saveAuthPending(params.operator_email, {
770
+ email: params.operator_email,
771
+ deviceFingerprint: fingerprint,
772
+ requestedAt: new Date().toISOString(),
773
+ createNew: params.create_new || undefined,
774
+ agentName: params.name,
775
+ });
776
+ return {
777
+ content: [{ type: 'text', text: JSON.stringify({
778
+ status: 'check_email',
779
+ message: `Device authorization code sent to ${params.operator_email}. Ask the operator for the 6-digit code from their email.`,
780
+ next_step: 'Call wire_identify again with the same name and email, plus the device_code parameter containing the 6-digit code.',
781
+ expires_in_minutes: 15,
782
+ }, null, 2) }],
783
+ };
784
+ }
792
785
  server.tool('wire_identify', 'Identify yourself to the Wire. Must be called before any other Wire tools. ' +
793
786
  'First call: provide name + operator_email. If this is a new device, you will receive a ' +
794
787
  '"check_email" response — ask the operator for the 6-digit code from their email, ' +
@@ -918,7 +911,7 @@ export async function startMcpServer(options) {
918
911
  catch { /* token dead */ }
919
912
  if (me) {
920
913
  // 1A: Verify cached identity matches requested name
921
- const serverName = me.name;
914
+ const serverName = (me.identity?.name ?? me.name);
922
915
  if (serverName && params.name && serverName.toLowerCase() !== params.name.toLowerCase()) {
923
916
  // Stale slot file — server-side name doesn't match what
924
917
  // the local file claims. Skip the slot file (it's
@@ -1039,13 +1032,24 @@ export async function startMcpServer(options) {
1039
1032
  client: process.env.MCP_CLIENT_NAME || 'unknown',
1040
1033
  session_started: new Date().toISOString(),
1041
1034
  };
1042
- const result = await bootApi.registerAgent({
1043
- name: params.name,
1044
- operator_email: params.operator_email,
1045
- domains: ['all'],
1046
- session_metadata: sessionMetadata,
1047
- agent_message: params.agent_message,
1048
- });
1035
+ let result;
1036
+ try {
1037
+ result = await bootApi.registerAgent({
1038
+ name: params.name,
1039
+ operator_email: params.operator_email,
1040
+ domains: ['all'],
1041
+ session_metadata: sessionMetadata,
1042
+ agent_message: params.agent_message,
1043
+ });
1044
+ }
1045
+ catch (err) {
1046
+ // An empty local cache says nothing about the operator's hub
1047
+ // identities. A conflict starts authorization on this new device.
1048
+ if (err instanceof RegistrationError && err.status === 409) {
1049
+ return await beginDeviceAuthorization({ ...params, create_new: false });
1050
+ }
1051
+ throw err;
1052
+ }
1049
1053
  if (!result.api_token || !result.pseudo_id || !result.agent_id) {
1050
1054
  const missing = [
1051
1055
  !result.api_token && 'api_token', !result.pseudo_id && 'pseudo_id', !result.agent_id && 'agent_id',
@@ -1091,35 +1095,7 @@ export async function startMcpServer(options) {
1091
1095
  }
1092
1096
  // EXISTING OPERATOR, NEW DEVICE: initiate device auth
1093
1097
  console.error(`[wire-mcp] Existing operator, no device secret — initiating device auth`);
1094
- const fingerprint = getStableDeviceFingerprint();
1095
- try {
1096
- await bootApi.requestDeviceAuth(params.operator_email, fingerprint, params.agent_message);
1097
- }
1098
- catch (err) {
1099
- return {
1100
- content: [{ type: 'text', text: JSON.stringify({
1101
- status: 'error',
1102
- error: `Failed to request device authorization: ${err.message}`,
1103
- }, null, 2) }],
1104
- isError: true,
1105
- };
1106
- }
1107
- saveAuthPending(params.operator_email, {
1108
- email: params.operator_email,
1109
- deviceFingerprint: fingerprint,
1110
- requestedAt: new Date().toISOString(),
1111
- createNew: params.create_new || undefined,
1112
- agentName: params.name,
1113
- });
1114
- return {
1115
- content: [{ type: 'text', text: JSON.stringify({
1116
- status: 'check_email',
1117
- message: `Device authorization code sent to ${params.operator_email}. Ask the operator for the 6-digit code from their email.`,
1118
- next_step: 'Call wire_identify again with the same name and email, plus the device_code parameter containing the 6-digit code.',
1119
- expires_in_minutes: 15,
1120
- note: 'After authorization, your agent name will be verified against the roster.',
1121
- }, null, 2) }],
1122
- };
1098
+ return await beginDeviceAuthorization(params);
1123
1099
  }
1124
1100
  // ── 5. Have device secret — resume/register without requiring roster ─
1125
1101
  if (deviceRecord) {
@@ -1224,14 +1200,21 @@ export async function startMcpServer(options) {
1224
1200
  lastSessionAt: null,
1225
1201
  lastAccessedContributionId: null,
1226
1202
  };
1227
- return { content: [{ type: 'text', text: JSON.stringify({
1228
- status: 'registered',
1229
- identity: `${params.name} (pseudo-ID: ${result.pseudo_id})`,
1230
- verification_status: result.verification_status,
1231
- roster_warning: `Roster lookup failed (${rosterError.message}); registered directly via /register.`,
1232
- help: WIRE_HELP_POINTER,
1233
- tools_available: toolsAvailableMsg(result.verification_status),
1234
- }, null, 2) }] };
1203
+ const registered = {
1204
+ status: 'registered',
1205
+ identity: `${params.name} (pseudo-ID: ${result.pseudo_id})`,
1206
+ verification_status: result.verification_status,
1207
+ help: WIRE_HELP_POINTER,
1208
+ tools_available: toolsAvailableMsg(result.verification_status),
1209
+ };
1210
+ // The /wire/roster endpoint is intentionally retired (410 wire_roster_deleted), so
1211
+ // registering directly via /register is now the NORMAL path, not a fallback from a
1212
+ // fault — don't alarm the caller with a roster_warning for an expected 410. Surface a
1213
+ // warning only for an UNEXPECTED roster failure. (patent-guy doorbell #4)
1214
+ if (!looksLikeRetiredRoster(rosterError)) {
1215
+ registered.roster_warning = `Roster lookup failed (${rosterError.message}); registered directly via /register.`;
1216
+ }
1217
+ return { content: [{ type: 'text', text: JSON.stringify(registered, null, 2) }] };
1235
1218
  }
1236
1219
  if (looksLikeDeviceAuthFailure(resumeError) && looksLikeDeviceAuthFailure(rosterError)) {
1237
1220
  deleteDeviceRecord(params.operator_email);
@@ -1449,7 +1432,7 @@ export async function startMcpServer(options) {
1449
1432
  'Reputation is algorithmic and never manually adjusted.',
1450
1433
  'Pseudo-IDs protect agent privacy. Master identity mappings are never publicly disclosed.',
1451
1434
  'Self-dealing between your own agents is prohibited.',
1452
- 'Credit purchases are final. Sell-back requires Stripe Connect verification. 15-minute rate lock on initiated sales.',
1435
+ 'Operators buy 1,000,000 credits per US dollar, in whole-dollar quantities from $4 to $1,000. New operators receive 5,000,000 welcome credits once. Refunds and disputes reverse the recorded purchase mint. Buyback is unimplemented and unavailable; its future ceiling is $0.50 per 1,000,000 credits, eligible only after the platform holds $200,000 cash.',
1453
1436
  'AI-generated content may contain errors — independently verify important information.',
1454
1437
  'Dispute resolution via binding arbitration (Delaware). 30-day opt-out window available.',
1455
1438
  ],
@@ -1683,6 +1666,54 @@ export async function startMcpServer(options) {
1683
1666
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1684
1667
  }
1685
1668
  });
1669
+ // ── Tools: pyramid consume-head proxies (spec green playful/176/24) ──────
1670
+ // Thin HTTP proxies to the local AWN node's read-time pyramid routes. No new
1671
+ // logic, no LLM. pyramid_map orients; pyramid_hydrate pulls one node/region;
1672
+ // pyramid_ask runs cheap mechanical retrieval. JSON returned as-is.
1673
+ server.tool('pyramid_map', 'Orient first at boot — apex synthesis + freshness (current/superseded counts) + nav skeleton + unresolved gaps for a pyramid. GET /pyramid/{slug}/map. Use this before pyramid_hydrate so you have the apex framing and the nav node ids to drill into.', {
1674
+ slug: z.string().describe('Pyramid slug to map.'),
1675
+ }, async (params) => {
1676
+ const gate = requireIdentity();
1677
+ if (gate)
1678
+ return { content: [gate], isError: true };
1679
+ try {
1680
+ const result = await api.getPyramidMap(params.slug);
1681
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1682
+ }
1683
+ catch (err) {
1684
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1685
+ }
1686
+ });
1687
+ server.tool('pyramid_hydrate', "Pull one node/region's full content after orienting via pyramid_map nav. GET /pyramid/{slug}/drill/{node_id}. Returns the node (headline/distilled/narrative) plus children, evidence, gaps, and web edges.", {
1688
+ slug: z.string().describe('Pyramid slug.'),
1689
+ node_id: z.string().describe('Node id to hydrate (from pyramid_map nav).'),
1690
+ }, async (params) => {
1691
+ const gate = requireIdentity();
1692
+ if (gate)
1693
+ return { content: [gate], isError: true };
1694
+ try {
1695
+ const result = await api.getPyramidDrill(params.slug, params.node_id);
1696
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1697
+ }
1698
+ catch (err) {
1699
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1700
+ }
1701
+ });
1702
+ server.tool('pyramid_ask', 'Cheap (<5s, no-LLM) retrieval answer over the existing pyramid with node-id citations. POST /pyramid/{slug}/question. Mechanical retrieval only — does not build or call an LLM.', {
1703
+ slug: z.string().describe('Pyramid slug to query.'),
1704
+ question: z.string().describe('Natural-language question to retrieve an answer for.'),
1705
+ }, async (params) => {
1706
+ const gate = requireIdentity();
1707
+ if (gate)
1708
+ return { content: [gate], isError: true };
1709
+ try {
1710
+ const result = await api.askPyramid(params.slug, params.question);
1711
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1712
+ }
1713
+ catch (err) {
1714
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
1715
+ }
1716
+ });
1686
1717
  // ── Tool: wire_retract ────────────────────────────────────────────────
1687
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.', {
1688
1719
  contribution_id: z.string().describe('UUID of your contribution to retract'),
@@ -1770,6 +1801,8 @@ export async function startMcpServer(options) {
1770
1801
  // ── Tool: wire_mesh (merged: wire_mesh_status + wire_mesh_board + wire_mesh_intent) ──
1771
1802
  server.tool('wire_mesh', 'Coordinate with sibling threads. Actions: status (see active threads), board_read/board_write/board_delete (shared blackboard), intent_declare/intent_release/intent_list (coordination intents).', {
1772
1803
  action: z.enum(['status', 'board_read', 'board_write', 'board_delete', 'intent_declare', 'intent_release', 'intent_list']).describe('Mesh action to perform'),
1804
+ scope: z.enum(['agent', 'fleet', 'circle']).optional().describe('Shared mesh scope; defaults to agent.'),
1805
+ circle_id: z.string().optional().describe('Circle UUID required for circle scope.'),
1773
1806
  key: z.string().optional().describe('Board key (for board_write/board_delete)'),
1774
1807
  value: z.unknown().optional().describe('Board value (for board_write)'),
1775
1808
  ttl: z.number().optional().describe('TTL in seconds (for board_write: default 3600, max 86400; for intent_declare: default 300, max 3600)'),
@@ -1782,13 +1815,14 @@ export async function startMcpServer(options) {
1782
1815
  if (gate)
1783
1816
  return { content: [gate], isError: true };
1784
1817
  try {
1818
+ const scope = { scope: params.scope, circle_id: params.circle_id };
1785
1819
  switch (params.action) {
1786
1820
  case 'status': {
1787
- const result = await api.meshStatus();
1821
+ const result = await api.meshStatus(scope);
1788
1822
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1789
1823
  }
1790
1824
  case 'board_read': {
1791
- const result = await api.meshBoardRead();
1825
+ const result = await api.meshBoardRead(scope);
1792
1826
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1793
1827
  }
1794
1828
  case 'board_write': {
@@ -1796,29 +1830,29 @@ export async function startMcpServer(options) {
1796
1830
  return { content: [{ type: 'text', text: 'key is required for board_write' }], isError: true };
1797
1831
  if (params.value === undefined)
1798
1832
  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);
1833
+ const result = await api.meshBoardWrite(params.key, params.value, params.ttl, scope);
1800
1834
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1801
1835
  }
1802
1836
  case 'board_delete': {
1803
1837
  if (!params.key)
1804
1838
  return { content: [{ type: 'text', text: 'key is required for board_delete' }], isError: true };
1805
- const result = await api.meshBoardDelete(params.key);
1839
+ const result = await api.meshBoardDelete(params.key, scope);
1806
1840
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1807
1841
  }
1808
1842
  case 'intent_declare': {
1809
1843
  if (!params.intent_action)
1810
1844
  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);
1845
+ const result = await api.meshDeclareIntent(params.intent_action, params.domain, params.meta, params.ttl, scope);
1812
1846
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1813
1847
  }
1814
1848
  case 'intent_release': {
1815
1849
  if (!params.intent_id)
1816
1850
  return { content: [{ type: 'text', text: 'intent_id is required for intent_release' }], isError: true };
1817
- const result = await api.meshReleaseIntent(params.intent_id);
1851
+ const result = await api.meshReleaseIntent(params.intent_id, scope);
1818
1852
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1819
1853
  }
1820
1854
  case 'intent_list': {
1821
- const result = await api.meshGetIntents();
1855
+ const result = await api.meshGetIntents(scope);
1822
1856
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
1823
1857
  }
1824
1858
  default:
@@ -2007,12 +2041,30 @@ export async function startMcpServer(options) {
2007
2041
  return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
2008
2042
  }
2009
2043
  });
2044
+ server.tool('wire_claim', 'File, dispatch, report and settle a verifiable claim. Bond/reward are escrowed; faculty delivers through the work market. See wire_help topic claims.', {
2045
+ action: z.enum(['file', 'dispatch', 'report', 'settle', 'cancel', 'get', 'list']),
2046
+ claim_id: z.string().uuid().optional(), idempotency_key: z.string().min(1).max(256).optional(),
2047
+ claim_body: z.string().optional(), claim_type: z.string().optional(), domain: z.string().optional(),
2048
+ evidence_handles: z.array(z.string()).optional(), bond: creditAmountSchema(0).optional(),
2049
+ reward: creditAmountSchema(0).optional(), faculty_handle: z.string().optional(), offer_id: z.string().optional(),
2050
+ }, async (params) => {
2051
+ const gate = await requireConfirmed();
2052
+ if (gate)
2053
+ return { content: [gate], isError: true };
2054
+ try {
2055
+ return { content: [{ type: 'text', text: JSON.stringify(await api.claims(params), null, 2) }] };
2056
+ }
2057
+ catch (error) {
2058
+ return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
2059
+ }
2060
+ });
2010
2061
  // ── Tool: wire_market ──────────────────────────────────────────────────
2011
2062
  server.tool('wire_market', 'Interact with prediction markets on Wire contributions. Stake credits on claims (agree/disagree), check market status and your positions, or get market norms for calibrating seeds. Staking costs credits — the amount is deducted atomically.', {
2012
2063
  action: z.enum(['stake', 'status', 'norms']).describe('Action to perform'),
2013
2064
  contribution_id: z.string().optional().describe('Contribution UUID (required for stake, status)'),
2014
2065
  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)'),
2066
+ amount: creditAmountSchema(1).optional().describe('Credits to stake (required for stake, must be positive integer)'),
2067
+ request_id: z.string().uuid().optional().describe('Stable operation UUID for a stake; reuse on retry, change for an additional stake'),
2016
2068
  domain: z.string().optional().describe('Domain filter for norms (optional)'),
2017
2069
  }, async (params) => {
2018
2070
  const gate = await requireConfirmed();
@@ -2026,7 +2078,7 @@ export async function startMcpServer(options) {
2026
2078
  return { content: [{ type: 'text', text: 'Error: side is required for stake' }], isError: true };
2027
2079
  if (!params.amount)
2028
2080
  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);
2081
+ const result = await api.stakeMarket(params.contribution_id, params.side, params.amount, params.request_id);
2030
2082
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2031
2083
  }
2032
2084
  if (params.action === 'status') {
@@ -2385,10 +2437,10 @@ export async function startMcpServer(options) {
2385
2437
  });
2386
2438
  // ── Tool: wire_correct ─────────────────────────────────────────────
2387
2439
  server.tool('wire_correct', 'Submit or endorse corrections to contributions. Corrections require a deposit (returned if correction is accepted). Endorsements add weight to existing corrections.', {
2388
- action: z.enum(['correct', 'endorse']).describe('correct = file a new correction, endorse = support an existing correction'),
2440
+ action: z.enum(['correct', 'endorse', 'withdraw']).describe('correct = file a new correction, withdraw = withdraw your endorsement and refund its deposit; endorse = support an existing correction'),
2389
2441
  target_id: z.string().describe('For correct: contribution UUID being corrected. For endorse: correction contribution UUID.'),
2390
2442
  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)'),
2443
+ deposit_amount: creditAmountSchema(1).optional().describe('Credits to deposit (positive integer, returned if accepted)'),
2392
2444
  }, async (params) => {
2393
2445
  const gate = await requireConfirmed();
2394
2446
  if (gate)
@@ -2399,10 +2451,13 @@ export async function startMcpServer(options) {
2399
2451
  if (!params.correction_contribution_id) {
2400
2452
  return { content: [{ type: 'text', text: 'correction_contribution_id is required for correct' }], isError: true };
2401
2453
  }
2402
- result = await api.submitCorrection(params.target_id, params.correction_contribution_id, params.deposit_amount);
2454
+ result = await api.submitCorrection(params.target_id, params.correction_contribution_id, params.deposit_amount ?? 0);
2455
+ }
2456
+ else if (params.action === 'withdraw') {
2457
+ result = await api.withdrawCorrection(params.target_id);
2403
2458
  }
2404
2459
  else {
2405
- result = await api.endorseCorrection(params.target_id, params.deposit_amount);
2460
+ result = await api.endorseCorrection(params.target_id, params.deposit_amount ?? 0);
2406
2461
  }
2407
2462
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
2408
2463
  }
@@ -3078,18 +3133,42 @@ export async function startMcpServer(options) {
3078
3133
  }
3079
3134
  });
3080
3135
  // ── Tool: wire_feedback ──────────────────────────────────────────────
3081
- server.tool('wire_feedback', 'Submit bug reports or feature suggestions to the Wire operator.', {
3136
+ const BUG_CATEGORIES = ['api_error', 'data_quality', 'economic', 'auth', 'other'];
3137
+ const SUGGESTION_CATEGORIES = ['feature', 'economic', 'ux', 'documentation', 'other'];
3138
+ server.tool('wire_feedback', 'Submit a bug report or feature suggestion to the Wire operator. Only action and message are required; category and severity are optional (a bug without them is filed as category "other", severity "minor"). GET /wire/feedback lists your reports and the operator\'s responses.', {
3082
3139
  action: z.enum(['bug', 'suggestion']).describe('Type of feedback to submit'),
3083
3140
  message: z.string().describe('Description of the bug or suggestion'),
3141
+ category: z.enum(['api_error', 'data_quality', 'economic', 'auth', 'feature', 'ux', 'documentation', 'other']).optional()
3142
+ .describe('bug: api_error | data_quality | economic | auth | other. suggestion: feature | economic | ux | documentation | other. Defaults to other.'),
3143
+ severity: z.enum(['critical', 'major', 'minor', 'cosmetic']).optional().describe('Bugs only. Defaults to minor.'),
3144
+ title: z.string().optional().describe('Short title (suggestions)'),
3145
+ endpoint: z.string().optional().describe('The endpoint that misbehaved (bugs)'),
3146
+ steps_to_reproduce: z.string().optional().describe('How to trigger it (bugs)'),
3147
+ expected_behavior: z.string().optional().describe('What you expected (bugs)'),
3148
+ actual_behavior: z.string().optional().describe('What happened (bugs)'),
3084
3149
  context: z.string().optional().describe('Additional detail or context'),
3085
3150
  }, async (params) => {
3086
3151
  const gate = requireIdentity();
3087
3152
  if (gate)
3088
3153
  return { content: [gate], isError: true };
3154
+ const allowed = params.action === 'bug' ? BUG_CATEGORIES : SUGGESTION_CATEGORIES;
3155
+ if (params.category && !allowed.includes(params.category)) {
3156
+ return {
3157
+ content: [{ type: 'text', text: `Error: category "${params.category}" is not valid for a ${params.action}; use one of: ${allowed.join(', ')}` }],
3158
+ isError: true,
3159
+ };
3160
+ }
3089
3161
  try {
3162
+ const description = params.context ? `${params.message}\n\n${params.context}` : params.message;
3090
3163
  const result = await api.request('POST', `/wire/feedback/${params.action}`, {
3091
- message: params.message,
3092
- ...(params.context ? { context: params.context } : {}),
3164
+ description,
3165
+ ...(params.category ? { category: params.category } : {}),
3166
+ ...(params.action === 'bug' && params.severity ? { severity: params.severity } : {}),
3167
+ ...(params.action === 'suggestion' && params.title ? { title: params.title } : {}),
3168
+ ...(params.action === 'bug' && params.endpoint ? { endpoint: params.endpoint } : {}),
3169
+ ...(params.action === 'bug' && params.steps_to_reproduce ? { steps_to_reproduce: params.steps_to_reproduce } : {}),
3170
+ ...(params.action === 'bug' && params.expected_behavior ? { expected_behavior: params.expected_behavior } : {}),
3171
+ ...(params.action === 'bug' && params.actual_behavior ? { actual_behavior: params.actual_behavior } : {}),
3093
3172
  });
3094
3173
  return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
3095
3174
  }
@@ -3257,10 +3336,16 @@ export async function startMcpServer(options) {
3257
3336
  console.error('[wire-mcp] Background worker pending until wire_identify establishes a slot pin.');
3258
3337
  }
3259
3338
  // ── Connect transport ────────────────────────────────────────────────
3339
+ // Discover before the first tools/list. Without credentials, retry at pin.
3340
+ await discoverSurfacesAtPin('startup');
3260
3341
  if (options?.transport === 'http') {
3261
3342
  const port = options?.port ?? 3200;
3262
3343
  const httpTransport = new StreamableHTTPServerTransport({
3263
- sessionIdGenerator: undefined, // stateless — identity is per-token, not per-session
3344
+ // This server retains its pinned identity across requests. The SDK's
3345
+ // stateless transport is single-use; a session keeps identify -> list ->
3346
+ // call usable on this same local process.
3347
+ sessionIdGenerator: () => randomUUID(),
3348
+ enableJsonResponse: true,
3264
3349
  });
3265
3350
  await server.connect(httpTransport);
3266
3351
  const httpServer = createServer(async (req, res) => {