@agent-wire/terminal 1.7.5 → 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.
- package/README.md +447 -405
- package/dist/api.d.ts +72 -20
- package/dist/api.js +158 -47
- package/dist/api.js.map +1 -1
- package/dist/cli-projection.d.ts +8 -0
- package/dist/cli-projection.js +124 -0
- package/dist/cli-projection.js.map +1 -0
- package/dist/cli.js +43 -953
- package/dist/cli.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 +284 -139
- package/dist/mcp-server.js.map +1 -1
- 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/sync/preview.d.ts +4 -1
- package/dist/sync/preview.js +16 -4
- package/dist/sync/preview.js.map +1 -1
- package/dist/tool-projection.d.ts +10 -0
- package/dist/tool-projection.js +51 -0
- package/dist/tool-projection.js.map +1 -1
- package/dist/wait-cli.js +60 -26
- package/dist/wait-cli.js.map +1 -1
- package/package.json +60 -58
package/dist/mcp-server.js
CHANGED
|
@@ -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;
|
|
@@ -21,7 +23,7 @@ import {
|
|
|
21
23
|
// migration window.
|
|
22
24
|
loadDeviceRecord, saveDeviceRecord, deleteDeviceRecord, loadAuthPending, saveAuthPending, deleteAuthPending, getStableDeviceFingerprint, hasAnyLocalIdentityForEmail, acquireIdentityLock, releaseIdentityLock, } from './identity.js';
|
|
23
25
|
import { acquireLock, releaseLock, isWorkerRunning } from './lockfile.js';
|
|
24
|
-
import { walkDirectory, computeDiff, pushFiles, pullFiles, parseManifest, serializeManifest, updateManifest, paginateAll, } from './sync/index.js';
|
|
26
|
+
import { walkDirectory, computeDiff, pushFiles, pullFiles, parseManifest, serializeManifest, updateManifest, paginateAll, generatePreviewJson, } from './sync/index.js';
|
|
25
27
|
import { WIRE_HELP_POINTER } from './help-pointer.js';
|
|
26
28
|
import { pullLocalSubscriptions } from './local-pull.js';
|
|
27
29
|
const SURFACE_REFRESH_TTL_MS = 60_000;
|
|
@@ -82,9 +84,7 @@ function normalizeApiRootPath(path) {
|
|
|
82
84
|
return withSlash.replace(/^\/api\/v1(?=\/|$)/, '') || '/';
|
|
83
85
|
}
|
|
84
86
|
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);
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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(
|
|
658
|
+
const result = await refreshDynamicSurfaceTools(true);
|
|
686
659
|
console.error(`[wire-mcp] Surface discovery (${source}): ${result.surfaces.length} surface_config tools registered`);
|
|
687
|
-
|
|
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
|
|
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
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
-
'
|
|
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
|
],
|
|
@@ -1591,9 +1574,21 @@ export async function startMcpServer(options) {
|
|
|
1591
1574
|
}
|
|
1592
1575
|
});
|
|
1593
1576
|
// ── Tool: wire_corpora (split from wire_discover — corpus browser) ──
|
|
1594
|
-
server.tool('wire_corpora', 'Browse source corpora. List
|
|
1577
|
+
server.tool('wire_corpora', 'Browse or create source corpora. List corpora, list documents within a corpus, inspect health/revenue, or create a corpus. FREE except writes require approval.', {
|
|
1578
|
+
action: z.enum(['list', 'documents', 'create', 'health', 'revenue', 'bulk_ingest']).optional().describe('Action to perform. Defaults to documents when slug is present, otherwise list.'),
|
|
1595
1579
|
slug: z.string().optional().describe('Corpus slug to browse. Omit to list all corpora.'),
|
|
1580
|
+
title: z.string().optional().describe('Corpus title for create.'),
|
|
1581
|
+
description: z.string().optional().describe('Corpus description for create.'),
|
|
1582
|
+
material_class: z.string().optional().describe('Corpus material class for create.'),
|
|
1596
1583
|
visibility: z.enum(['public', 'private', 'all']).optional().describe('Filter by visibility (default: public)'),
|
|
1584
|
+
themes: z.array(z.string()).optional().describe('Optional corpus themes for create.'),
|
|
1585
|
+
default_author_share: z.number().optional().describe('Default author revenue share for create.'),
|
|
1586
|
+
default_steward_share: z.number().optional().describe('Default steward revenue share for create.'),
|
|
1587
|
+
split_justification: z.string().optional().describe('Revenue split justification for create.'),
|
|
1588
|
+
provenance_note: z.string().optional().describe('Provenance note for create.'),
|
|
1589
|
+
manifest: z.record(z.unknown()).optional().describe('Optional corpus manifest for create.'),
|
|
1590
|
+
days: z.number().optional().describe('Revenue lookback days for revenue action.'),
|
|
1591
|
+
documents: z.array(z.record(z.unknown())).optional().describe('Documents for bulk_ingest action.'),
|
|
1597
1592
|
limit: z.number().optional().describe('Max results'),
|
|
1598
1593
|
offset: z.number().optional().describe('Pagination offset'),
|
|
1599
1594
|
}, async (params) => {
|
|
@@ -1602,13 +1597,54 @@ export async function startMcpServer(options) {
|
|
|
1602
1597
|
return { content: [gate], isError: true };
|
|
1603
1598
|
try {
|
|
1604
1599
|
let result;
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1600
|
+
const action = params.action ?? (params.slug ? 'documents' : 'list');
|
|
1601
|
+
switch (action) {
|
|
1602
|
+
case 'list':
|
|
1603
|
+
result = await api.listCorpora();
|
|
1604
|
+
break;
|
|
1605
|
+
case 'documents':
|
|
1606
|
+
if (!params.slug)
|
|
1607
|
+
return { content: [{ type: 'text', text: 'Error: slug is required for documents action' }], isError: true };
|
|
1608
|
+
result = await api.getCorpusDocuments(params.slug, params.limit, params.offset);
|
|
1609
|
+
break;
|
|
1610
|
+
case 'create':
|
|
1611
|
+
if (!params.slug)
|
|
1612
|
+
return { content: [{ type: 'text', text: 'Error: slug is required for create action' }], isError: true };
|
|
1613
|
+
if (!params.title)
|
|
1614
|
+
return { content: [{ type: 'text', text: 'Error: title is required for create action' }], isError: true };
|
|
1615
|
+
result = await api.createCorpus({
|
|
1616
|
+
slug: params.slug,
|
|
1617
|
+
title: params.title,
|
|
1618
|
+
description: params.description,
|
|
1619
|
+
material_class: params.material_class,
|
|
1620
|
+
visibility: params.visibility === 'all' ? undefined : params.visibility,
|
|
1621
|
+
themes: params.themes,
|
|
1622
|
+
default_author_share: params.default_author_share,
|
|
1623
|
+
default_steward_share: params.default_steward_share,
|
|
1624
|
+
split_justification: params.split_justification,
|
|
1625
|
+
provenance_note: params.provenance_note,
|
|
1626
|
+
manifest: params.manifest,
|
|
1627
|
+
});
|
|
1628
|
+
break;
|
|
1629
|
+
case 'health':
|
|
1630
|
+
if (!params.slug)
|
|
1631
|
+
return { content: [{ type: 'text', text: 'Error: slug is required for health action' }], isError: true };
|
|
1632
|
+
result = await api.getCorpusHealth(params.slug);
|
|
1633
|
+
break;
|
|
1634
|
+
case 'revenue':
|
|
1635
|
+
if (!params.slug)
|
|
1636
|
+
return { content: [{ type: 'text', text: 'Error: slug is required for revenue action' }], isError: true };
|
|
1637
|
+
result = await api.getCorpusRevenue(params.slug, params.days);
|
|
1638
|
+
break;
|
|
1639
|
+
case 'bulk_ingest':
|
|
1640
|
+
if (!params.slug)
|
|
1641
|
+
return { content: [{ type: 'text', text: 'Error: slug is required for bulk_ingest action' }], isError: true };
|
|
1642
|
+
if (!params.documents)
|
|
1643
|
+
return { content: [{ type: 'text', text: 'Error: documents is required for bulk_ingest action' }], isError: true };
|
|
1644
|
+
result = await api.bulkIngestDocuments(params.slug, params.documents);
|
|
1645
|
+
break;
|
|
1610
1646
|
}
|
|
1611
|
-
const shaped = shapeCorpora(result, params.slug);
|
|
1647
|
+
const shaped = action === 'list' || action === 'documents' ? shapeCorpora(result, action === 'documents' ? params.slug : undefined) : result;
|
|
1612
1648
|
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
1613
1649
|
}
|
|
1614
1650
|
catch (err) {
|
|
@@ -1630,6 +1666,54 @@ export async function startMcpServer(options) {
|
|
|
1630
1666
|
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1631
1667
|
}
|
|
1632
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
|
+
});
|
|
1633
1717
|
// ── Tool: wire_retract ────────────────────────────────────────────────
|
|
1634
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.', {
|
|
1635
1719
|
contribution_id: z.string().describe('UUID of your contribution to retract'),
|
|
@@ -1717,6 +1801,8 @@ export async function startMcpServer(options) {
|
|
|
1717
1801
|
// ── Tool: wire_mesh (merged: wire_mesh_status + wire_mesh_board + wire_mesh_intent) ──
|
|
1718
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).', {
|
|
1719
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.'),
|
|
1720
1806
|
key: z.string().optional().describe('Board key (for board_write/board_delete)'),
|
|
1721
1807
|
value: z.unknown().optional().describe('Board value (for board_write)'),
|
|
1722
1808
|
ttl: z.number().optional().describe('TTL in seconds (for board_write: default 3600, max 86400; for intent_declare: default 300, max 3600)'),
|
|
@@ -1729,13 +1815,14 @@ export async function startMcpServer(options) {
|
|
|
1729
1815
|
if (gate)
|
|
1730
1816
|
return { content: [gate], isError: true };
|
|
1731
1817
|
try {
|
|
1818
|
+
const scope = { scope: params.scope, circle_id: params.circle_id };
|
|
1732
1819
|
switch (params.action) {
|
|
1733
1820
|
case 'status': {
|
|
1734
|
-
const result = await api.meshStatus();
|
|
1821
|
+
const result = await api.meshStatus(scope);
|
|
1735
1822
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1736
1823
|
}
|
|
1737
1824
|
case 'board_read': {
|
|
1738
|
-
const result = await api.meshBoardRead();
|
|
1825
|
+
const result = await api.meshBoardRead(scope);
|
|
1739
1826
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1740
1827
|
}
|
|
1741
1828
|
case 'board_write': {
|
|
@@ -1743,29 +1830,29 @@ export async function startMcpServer(options) {
|
|
|
1743
1830
|
return { content: [{ type: 'text', text: 'key is required for board_write' }], isError: true };
|
|
1744
1831
|
if (params.value === undefined)
|
|
1745
1832
|
return { content: [{ type: 'text', text: 'value is required for board_write' }], isError: true };
|
|
1746
|
-
const result = await api.meshBoardWrite(params.key, params.value, params.ttl);
|
|
1833
|
+
const result = await api.meshBoardWrite(params.key, params.value, params.ttl, scope);
|
|
1747
1834
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1748
1835
|
}
|
|
1749
1836
|
case 'board_delete': {
|
|
1750
1837
|
if (!params.key)
|
|
1751
1838
|
return { content: [{ type: 'text', text: 'key is required for board_delete' }], isError: true };
|
|
1752
|
-
const result = await api.meshBoardDelete(params.key);
|
|
1839
|
+
const result = await api.meshBoardDelete(params.key, scope);
|
|
1753
1840
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1754
1841
|
}
|
|
1755
1842
|
case 'intent_declare': {
|
|
1756
1843
|
if (!params.intent_action)
|
|
1757
1844
|
return { content: [{ type: 'text', text: 'intent_action is required for intent_declare' }], isError: true };
|
|
1758
|
-
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);
|
|
1759
1846
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1760
1847
|
}
|
|
1761
1848
|
case 'intent_release': {
|
|
1762
1849
|
if (!params.intent_id)
|
|
1763
1850
|
return { content: [{ type: 'text', text: 'intent_id is required for intent_release' }], isError: true };
|
|
1764
|
-
const result = await api.meshReleaseIntent(params.intent_id);
|
|
1851
|
+
const result = await api.meshReleaseIntent(params.intent_id, scope);
|
|
1765
1852
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1766
1853
|
}
|
|
1767
1854
|
case 'intent_list': {
|
|
1768
|
-
const result = await api.meshGetIntents();
|
|
1855
|
+
const result = await api.meshGetIntents(scope);
|
|
1769
1856
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1770
1857
|
}
|
|
1771
1858
|
default:
|
|
@@ -1783,6 +1870,7 @@ export async function startMcpServer(options) {
|
|
|
1783
1870
|
action: z.enum(['diff', 'push', 'pull', 'preview']).describe('Sync action to perform'),
|
|
1784
1871
|
publish: z.boolean().optional().describe('Legacy push only: publish new document versions instead of drafts'),
|
|
1785
1872
|
market_native_docs_ingest: z.enum(['off', 'auto', 'all']).optional().describe('Push only: route through /wire/sync and dispatch market-native docs ingest. auto sends docs without wire rear-matter or docs over the inline body budget; all sends every pushed doc. Oversized docs are dispatched as 8000-token windows with 2000-token overlap.'),
|
|
1873
|
+
include_content: z.boolean().optional().describe('Diff/preview only: include full local file bodies. Defaults false; previews normally return hash + sizeBytes only.'),
|
|
1786
1874
|
}, async (params) => {
|
|
1787
1875
|
const gate = await requireConfirmed();
|
|
1788
1876
|
if (gate)
|
|
@@ -1791,6 +1879,7 @@ export async function startMcpServer(options) {
|
|
|
1791
1879
|
const { dir, corpus, action } = params;
|
|
1792
1880
|
const publish = params.publish ?? false;
|
|
1793
1881
|
const marketNativeDocsIngest = (params.market_native_docs_ingest ?? 'off');
|
|
1882
|
+
const includeContent = params.include_content === true;
|
|
1794
1883
|
// ── Path traversal protection ──
|
|
1795
1884
|
if (!isAbsolute(dir)) {
|
|
1796
1885
|
return { content: [{ type: 'text', text: 'Error: dir must be an absolute path' }], isError: true };
|
|
@@ -1828,7 +1917,7 @@ export async function startMcpServer(options) {
|
|
|
1828
1917
|
const remoteDocs = await paginateAll(async (page, pageSize) => {
|
|
1829
1918
|
const offset = (page - 1) * pageSize;
|
|
1830
1919
|
const result = await api.getCorpusDocuments(corpus, pageSize, offset);
|
|
1831
|
-
return result.documents || [];
|
|
1920
|
+
return result.documents || result.items || [];
|
|
1832
1921
|
});
|
|
1833
1922
|
// 4. Compute diff
|
|
1834
1923
|
const diff = await computeDiff(localFiles, remoteDocs, manifest);
|
|
@@ -1843,11 +1932,15 @@ export async function startMcpServer(options) {
|
|
|
1843
1932
|
conflicts: diff.conflicts.length,
|
|
1844
1933
|
};
|
|
1845
1934
|
const output = {
|
|
1935
|
+
...generatePreviewJson({
|
|
1936
|
+
corpus_slug: corpus,
|
|
1937
|
+
local_dir: dir,
|
|
1938
|
+
diff,
|
|
1939
|
+
summary,
|
|
1940
|
+
}, { includeContent }),
|
|
1846
1941
|
corpus_slug: corpus,
|
|
1847
1942
|
local_dir: dir,
|
|
1848
1943
|
action,
|
|
1849
|
-
diff,
|
|
1850
|
-
summary,
|
|
1851
1944
|
};
|
|
1852
1945
|
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
1853
1946
|
}
|
|
@@ -1948,12 +2041,30 @@ export async function startMcpServer(options) {
|
|
|
1948
2041
|
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1949
2042
|
}
|
|
1950
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
|
+
});
|
|
1951
2061
|
// ── Tool: wire_market ──────────────────────────────────────────────────
|
|
1952
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.', {
|
|
1953
2063
|
action: z.enum(['stake', 'status', 'norms']).describe('Action to perform'),
|
|
1954
2064
|
contribution_id: z.string().optional().describe('Contribution UUID (required for stake, status)'),
|
|
1955
2065
|
side: z.enum(['agree', 'disagree']).optional().describe('Side to stake on (required for stake)'),
|
|
1956
|
-
amount:
|
|
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'),
|
|
1957
2068
|
domain: z.string().optional().describe('Domain filter for norms (optional)'),
|
|
1958
2069
|
}, async (params) => {
|
|
1959
2070
|
const gate = await requireConfirmed();
|
|
@@ -1967,7 +2078,7 @@ export async function startMcpServer(options) {
|
|
|
1967
2078
|
return { content: [{ type: 'text', text: 'Error: side is required for stake' }], isError: true };
|
|
1968
2079
|
if (!params.amount)
|
|
1969
2080
|
return { content: [{ type: 'text', text: 'Error: amount is required for stake' }], isError: true };
|
|
1970
|
-
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);
|
|
1971
2082
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1972
2083
|
}
|
|
1973
2084
|
if (params.action === 'status') {
|
|
@@ -2200,7 +2311,7 @@ export async function startMcpServer(options) {
|
|
|
2200
2311
|
server.tool('wire_wait', 'Block the live MCP session until a matching Wire coordination event arrives or the timeout elapses. Layer 1 uses SSE + cursor replay; durable wire_watch is not included.', {
|
|
2201
2312
|
triggers: z.array(z.string()).max(20).optional().describe('Event triggers to wait for, e.g. message.unread, task.assigned, task.moved, board.changed, circle.message. Supports prefix wildcards such as message.*. Omit to wait on any message.*, task.*, board.*, or circle.* event addressable to you.'),
|
|
2202
2313
|
filters: z.record(z.unknown()).optional().describe('Composable filters such as {from, column, key_prefix, assignee, created_by, target_type, task_id, task_handle, message_handle, from_handle, to_handle, circle, circle_id, written_by_thread}. Agent filters accept "me", agent UUID, or pseudo-ID when available. Handle filters match the corresponding event payload field exactly.'),
|
|
2203
|
-
timeout_seconds: z.number().int().min(
|
|
2314
|
+
timeout_seconds: z.number().int().min(-1).max(86400).optional().describe('Maximum seconds to block. Defaults to 30; max 86400 (24h). Use 0 for immediate replay/query; use -1 to park INDEFINITELY until an event (no re-arm cycles) — the SSE hair-trigger still delivers instantly.'),
|
|
2204
2315
|
cursor: z.string().optional().describe('Opaque next_cursor from a prior wire_wait or wire_events response. Legacy integer id cursors are accepted. If omitted, wait starts at the current high-water mark.'),
|
|
2205
2316
|
limit: z.number().int().min(1).max(200).optional().describe('Maximum events to return when triggered. Defaults to 50.'),
|
|
2206
2317
|
}, async (params) => {
|
|
@@ -2289,6 +2400,7 @@ export async function startMcpServer(options) {
|
|
|
2289
2400
|
action: z.enum(['supersede', 'propose', 'accept', 'reject']).describe('supersede = direct supersession authorized by target scope, propose = propose supersession (5-credit deposit), accept = approve a proposal, reject = deny a proposal'),
|
|
2290
2401
|
target_id: z.string().describe('Target contribution UUID (the one being superseded for supersede/propose, or the proposal UUID for accept/reject)'),
|
|
2291
2402
|
contribution_id: z.string().optional().describe('Your replacement contribution UUID (required for supersede and propose)'),
|
|
2403
|
+
allow_type_change: z.boolean().optional().describe('For action=supersede: allow a CROSS-TYPE supersession (successor type != target type). Without this a cross-type supersede 400s ("type-filtered consumers silently drop cross-type successors"). Pass true only when the type change is deliberate.'),
|
|
2292
2404
|
}, async (params) => {
|
|
2293
2405
|
const gate = await requireConfirmed();
|
|
2294
2406
|
if (gate)
|
|
@@ -2312,7 +2424,7 @@ export async function startMcpServer(options) {
|
|
|
2312
2424
|
if (!params.contribution_id) {
|
|
2313
2425
|
return { content: [{ type: 'text', text: 'contribution_id is required for supersede' }], isError: true };
|
|
2314
2426
|
}
|
|
2315
|
-
result = await api.executeSupersession(params.target_id, params.contribution_id);
|
|
2427
|
+
result = await api.executeSupersession(params.target_id, params.contribution_id, params.allow_type_change);
|
|
2316
2428
|
break;
|
|
2317
2429
|
default:
|
|
2318
2430
|
return { content: [{ type: 'text', text: 'Invalid action' }], isError: true };
|
|
@@ -2325,10 +2437,10 @@ export async function startMcpServer(options) {
|
|
|
2325
2437
|
});
|
|
2326
2438
|
// ── Tool: wire_correct ─────────────────────────────────────────────
|
|
2327
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.', {
|
|
2328
|
-
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'),
|
|
2329
2441
|
target_id: z.string().describe('For correct: contribution UUID being corrected. For endorse: correction contribution UUID.'),
|
|
2330
2442
|
correction_contribution_id: z.string().optional().describe('Your correction contribution UUID (required for correct)'),
|
|
2331
|
-
deposit_amount:
|
|
2443
|
+
deposit_amount: creditAmountSchema(1).optional().describe('Credits to deposit (positive integer, returned if accepted)'),
|
|
2332
2444
|
}, async (params) => {
|
|
2333
2445
|
const gate = await requireConfirmed();
|
|
2334
2446
|
if (gate)
|
|
@@ -2339,10 +2451,13 @@ export async function startMcpServer(options) {
|
|
|
2339
2451
|
if (!params.correction_contribution_id) {
|
|
2340
2452
|
return { content: [{ type: 'text', text: 'correction_contribution_id is required for correct' }], isError: true };
|
|
2341
2453
|
}
|
|
2342
|
-
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);
|
|
2343
2458
|
}
|
|
2344
2459
|
else {
|
|
2345
|
-
result = await api.endorseCorrection(params.target_id, params.deposit_amount);
|
|
2460
|
+
result = await api.endorseCorrection(params.target_id, params.deposit_amount ?? 0);
|
|
2346
2461
|
}
|
|
2347
2462
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2348
2463
|
}
|
|
@@ -3018,18 +3133,42 @@ export async function startMcpServer(options) {
|
|
|
3018
3133
|
}
|
|
3019
3134
|
});
|
|
3020
3135
|
// ── Tool: wire_feedback ──────────────────────────────────────────────
|
|
3021
|
-
|
|
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.', {
|
|
3022
3139
|
action: z.enum(['bug', 'suggestion']).describe('Type of feedback to submit'),
|
|
3023
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)'),
|
|
3024
3149
|
context: z.string().optional().describe('Additional detail or context'),
|
|
3025
3150
|
}, async (params) => {
|
|
3026
3151
|
const gate = requireIdentity();
|
|
3027
3152
|
if (gate)
|
|
3028
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
|
+
}
|
|
3029
3161
|
try {
|
|
3162
|
+
const description = params.context ? `${params.message}\n\n${params.context}` : params.message;
|
|
3030
3163
|
const result = await api.request('POST', `/wire/feedback/${params.action}`, {
|
|
3031
|
-
|
|
3032
|
-
...(params.
|
|
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 } : {}),
|
|
3033
3172
|
});
|
|
3034
3173
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
3035
3174
|
}
|
|
@@ -3197,10 +3336,16 @@ export async function startMcpServer(options) {
|
|
|
3197
3336
|
console.error('[wire-mcp] Background worker pending until wire_identify establishes a slot pin.');
|
|
3198
3337
|
}
|
|
3199
3338
|
// ── Connect transport ────────────────────────────────────────────────
|
|
3339
|
+
// Discover before the first tools/list. Without credentials, retry at pin.
|
|
3340
|
+
await discoverSurfacesAtPin('startup');
|
|
3200
3341
|
if (options?.transport === 'http') {
|
|
3201
3342
|
const port = options?.port ?? 3200;
|
|
3202
3343
|
const httpTransport = new StreamableHTTPServerTransport({
|
|
3203
|
-
|
|
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,
|
|
3204
3349
|
});
|
|
3205
3350
|
await server.connect(httpTransport);
|
|
3206
3351
|
const httpServer = createServer(async (req, res) => {
|