@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.
- package/README.md +492 -403
- package/dist/api-token.d.ts +7 -0
- package/dist/api-token.js +42 -0
- package/dist/api-token.js.map +1 -0
- package/dist/api.d.ts +84 -21
- package/dist/api.js +179 -55
- package/dist/api.js.map +1 -1
- package/dist/cli-projection.d.ts +8 -0
- package/dist/cli-projection.js +139 -0
- package/dist/cli-projection.js.map +1 -0
- package/dist/cli.js +62 -961
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +4 -1
- package/dist/config.js +20 -9
- package/dist/config.js.map +1 -1
- package/dist/contracts/credit-amount.d.ts +19 -0
- package/dist/contracts/credit-amount.js +87 -0
- package/dist/contracts/credit-amount.js.map +1 -0
- package/dist/local-pull.js +1 -1
- package/dist/local-pull.js.map +1 -1
- package/dist/mcp-server.js +258 -133
- package/dist/mcp-server.js.map +1 -1
- package/dist/response-shaper.js +54 -14
- package/dist/response-shaper.js.map +1 -1
- package/dist/resume.d.ts +7 -0
- package/dist/resume.js +15 -0
- package/dist/resume.js.map +1 -0
- package/dist/sync/paginate.d.ts +2 -16
- package/dist/sync/paginate.js +2 -33
- package/dist/sync/paginate.js.map +1 -1
- package/dist/tool-projection.d.ts +10 -0
- package/dist/tool-projection.js +54 -0
- package/dist/tool-projection.js.map +1 -1
- package/dist/wait-cli.js +46 -21
- package/dist/wait-cli.js.map +1 -1
- package/package.json +4 -2
package/dist/mcp-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
488
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
|
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
|
-
'
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
3092
|
-
...(params.
|
|
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
|
-
|
|
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) => {
|