@agent-wire/terminal 1.6.0
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 +353 -0
- package/dist/api.d.ts +660 -0
- package/dist/api.js +1157 -0
- package/dist/api.js.map +1 -0
- package/dist/background-worker.d.ts +6 -0
- package/dist/background-worker.js +101 -0
- package/dist/background-worker.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +940 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.js +36 -0
- package/dist/config.js.map +1 -0
- package/dist/help-pointer.d.ts +11 -0
- package/dist/help-pointer.js +10 -0
- package/dist/help-pointer.js.map +1 -0
- package/dist/identity.d.ts +40 -0
- package/dist/identity.js +193 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/local-pull.d.ts +45 -0
- package/dist/local-pull.js +173 -0
- package/dist/local-pull.js.map +1 -0
- package/dist/lockfile.d.ts +16 -0
- package/dist/lockfile.js +78 -0
- package/dist/lockfile.js.map +1 -0
- package/dist/mcp-server.d.ts +43 -0
- package/dist/mcp-server.js +3159 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/response-shaper.d.ts +60 -0
- package/dist/response-shaper.js +893 -0
- package/dist/response-shaper.js.map +1 -0
- package/dist/slots.d.ts +65 -0
- package/dist/slots.js +128 -0
- package/dist/slots.js.map +1 -0
- package/dist/sync/diff.d.ts +17 -0
- package/dist/sync/diff.js +152 -0
- package/dist/sync/diff.js.map +1 -0
- package/dist/sync/hash.d.ts +11 -0
- package/dist/sync/hash.js +18 -0
- package/dist/sync/hash.js.map +1 -0
- package/dist/sync/index.d.ts +24 -0
- package/dist/sync/index.js +22 -0
- package/dist/sync/index.js.map +1 -0
- package/dist/sync/manifest.d.ts +29 -0
- package/dist/sync/manifest.js +59 -0
- package/dist/sync/manifest.js.map +1 -0
- package/dist/sync/match.d.ts +29 -0
- package/dist/sync/match.js +153 -0
- package/dist/sync/match.js.map +1 -0
- package/dist/sync/paginate.d.ts +16 -0
- package/dist/sync/paginate.js +34 -0
- package/dist/sync/paginate.js.map +1 -0
- package/dist/sync/preview.d.ts +16 -0
- package/dist/sync/preview.js +179 -0
- package/dist/sync/preview.js.map +1 -0
- package/dist/sync/pull.d.ts +23 -0
- package/dist/sync/pull.js +126 -0
- package/dist/sync/pull.js.map +1 -0
- package/dist/sync/push.d.ts +27 -0
- package/dist/sync/push.js +116 -0
- package/dist/sync/push.js.map +1 -0
- package/dist/sync/resolve.d.ts +19 -0
- package/dist/sync/resolve.js +42 -0
- package/dist/sync/resolve.js.map +1 -0
- package/dist/sync/types.d.ts +70 -0
- package/dist/sync/types.js +6 -0
- package/dist/sync/types.js.map +1 -0
- package/dist/sync/walk.d.ts +12 -0
- package/dist/sync/walk.js +155 -0
- package/dist/sync/walk.js.map +1 -0
- package/dist/wait-cli.d.ts +46 -0
- package/dist/wait-cli.js +280 -0
- package/dist/wait-cli.js.map +1 -0
- package/dist/worker.d.ts +14 -0
- package/dist/worker.js +252 -0
- package/dist/worker.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,3159 @@
|
|
|
1
|
+
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
import { hostname } from 'os';
|
|
7
|
+
import { createHash, randomUUID } from 'crypto';
|
|
8
|
+
import { readFile, writeFile, stat as fsStat } from 'fs/promises';
|
|
9
|
+
import { join, isAbsolute } from 'path';
|
|
10
|
+
import { loadConfig, saveConfig } from './config.js';
|
|
11
|
+
import { loadSlotConfig, saveSlotConfig, migrateLegacyAgentsToSlots } from './slots.js';
|
|
12
|
+
import { WireAPI } from './api.js';
|
|
13
|
+
import { shapeStatus, shapeQueryResults, shapeRead, shapeInspect, shapeGraph, shapeCorpora, shapeContributeResult, shapeMyContributions, shapeEarnings, shapePulse, shapeOpportunities, shapeRate, shapePatrolScan, shapeNotifications, shapeTasks, shapeRoster, shapeMessages, } from './response-shaper.js';
|
|
14
|
+
import { startBackgroundWorkLoop } from './background-worker.js';
|
|
15
|
+
import {
|
|
16
|
+
// Mission #1.5-rev2: agents/ cache fully retired. loadAgentIdentity /
|
|
17
|
+
// saveAgentIdentity / deleteAgentIdentity are no longer imported here;
|
|
18
|
+
// their replacements live in slots.ts. The legacy functions in
|
|
19
|
+
// identity.ts remain for backward compat with wait-cli.ts during the
|
|
20
|
+
// migration window.
|
|
21
|
+
loadDeviceRecord, saveDeviceRecord, loadAuthPending, saveAuthPending, deleteAuthPending, getStableDeviceFingerprint, hasAnyLocalIdentityForEmail, acquireIdentityLock, releaseIdentityLock, } from './identity.js';
|
|
22
|
+
import { acquireLock, releaseLock, isWorkerRunning } from './lockfile.js';
|
|
23
|
+
import { walkDirectory, computeDiff, pushFiles, pullFiles, parseManifest, serializeManifest, updateManifest, paginateAll, } from './sync/index.js';
|
|
24
|
+
import { WIRE_HELP_POINTER } from './help-pointer.js';
|
|
25
|
+
import { pullLocalSubscriptions } from './local-pull.js';
|
|
26
|
+
const SURFACE_REFRESH_TTL_MS = 60_000;
|
|
27
|
+
const DEFAULT_TOOL_REFRESH_POLL_INTERVAL_MS = 90_000;
|
|
28
|
+
function normalizeSurfaceName(name) {
|
|
29
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9_]+/g, '_').replace(/^_+|_+$/g, '');
|
|
30
|
+
}
|
|
31
|
+
function surfaceToolName(surface) {
|
|
32
|
+
return `wire_surface_${normalizeSurfaceName(surface)}`;
|
|
33
|
+
}
|
|
34
|
+
function parseSurfaceConfig(item) {
|
|
35
|
+
const data = item.structured_data ?? {};
|
|
36
|
+
const titleSurface = typeof item.title === 'string'
|
|
37
|
+
? item.title.match(/(?:\/s\/|\/surface\/)([A-Za-z0-9_-]+)\s+surface config|surface_config:\s+\/surface\/([A-Za-z0-9_-]+)/)?.[1] ??
|
|
38
|
+
item.title.match(/(?:\/s\/|\/surface\/)([A-Za-z0-9_-]+)\s+surface config|surface_config:\s+\/surface\/([A-Za-z0-9_-]+)/)?.[2]
|
|
39
|
+
: undefined;
|
|
40
|
+
const rawName = data.surface_name ?? data.surface ?? data.name ?? titleSurface;
|
|
41
|
+
if (typeof rawName !== 'string' || rawName.trim().length === 0)
|
|
42
|
+
return null;
|
|
43
|
+
const surface = normalizeSurfaceName(rawName);
|
|
44
|
+
if (!surface)
|
|
45
|
+
return null;
|
|
46
|
+
const aliasPath = typeof data.alias_path === 'string' ? data.alias_path : `/api/v1/s/${encodeURIComponent(surface)}`;
|
|
47
|
+
const canonicalPath = typeof data.canonical_path === 'string'
|
|
48
|
+
? data.canonical_path
|
|
49
|
+
: `/api/v1/surface/${encodeURIComponent(surface)}`;
|
|
50
|
+
const method = typeof data.method === 'string' ? data.method.toUpperCase() : 'GET';
|
|
51
|
+
const configHandle = typeof data.surface_config_handle === 'string'
|
|
52
|
+
? data.surface_config_handle
|
|
53
|
+
: item.handle_path;
|
|
54
|
+
return {
|
|
55
|
+
surface,
|
|
56
|
+
toolName: surfaceToolName(surface),
|
|
57
|
+
canonicalPath,
|
|
58
|
+
aliasPath,
|
|
59
|
+
method,
|
|
60
|
+
configHandle,
|
|
61
|
+
title: item.title,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function dedupeSurfaces(surfaces) {
|
|
65
|
+
const byName = new Map();
|
|
66
|
+
for (const surface of surfaces) {
|
|
67
|
+
byName.set(surface.surface, surface);
|
|
68
|
+
}
|
|
69
|
+
return Array.from(byName.values()).sort((a, b) => a.surface.localeCompare(b.surface));
|
|
70
|
+
}
|
|
71
|
+
function extractQueryItems(response) {
|
|
72
|
+
const items = response.items ?? response.results ?? response.contributions;
|
|
73
|
+
return Array.isArray(items) ? items : [];
|
|
74
|
+
}
|
|
75
|
+
function normalizeApiRootPath(path) {
|
|
76
|
+
const trimmed = path.trim();
|
|
77
|
+
if (!trimmed)
|
|
78
|
+
return '/';
|
|
79
|
+
const withSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
|
|
80
|
+
return withSlash.replace(/^\/api\/v1(?=\/|$)/, '') || '/';
|
|
81
|
+
}
|
|
82
|
+
async function fetchSurfaceConfigItems(api) {
|
|
83
|
+
const queries = ['surface_config', 'dispatcher_bound_surface_config', 'lookup_surface'];
|
|
84
|
+
const responses = await Promise.all(queries.map((text) => api.query({ text, domain: 'all', limit: 100 })));
|
|
85
|
+
return responses.flatMap(extractQueryItems);
|
|
86
|
+
}
|
|
87
|
+
async function refreshSubstrateSurfaces(api) {
|
|
88
|
+
const items = await fetchSurfaceConfigItems(api);
|
|
89
|
+
return dedupeSurfaces(items
|
|
90
|
+
.filter((item) => item.type === 'surface_config')
|
|
91
|
+
.map(parseSurfaceConfig)
|
|
92
|
+
.filter((surface) => surface !== null));
|
|
93
|
+
}
|
|
94
|
+
function fingerprintSubstrateSurfaces(surfaces) {
|
|
95
|
+
const projected = surfaces
|
|
96
|
+
.map((surface) => ({
|
|
97
|
+
surface: surface.surface,
|
|
98
|
+
toolName: surface.toolName,
|
|
99
|
+
canonicalPath: surface.canonicalPath,
|
|
100
|
+
aliasPath: surface.aliasPath,
|
|
101
|
+
method: surface.method,
|
|
102
|
+
configHandle: surface.configHandle ?? null,
|
|
103
|
+
}))
|
|
104
|
+
.sort((a, b) => a.toolName.localeCompare(b.toolName));
|
|
105
|
+
return createHash('sha256').update(JSON.stringify(projected)).digest('hex');
|
|
106
|
+
}
|
|
107
|
+
function numericStructuredValue(data) {
|
|
108
|
+
for (const key of ['poll_interval_ms', 'interval_ms', 'value_ms', 'value']) {
|
|
109
|
+
const value = data[key];
|
|
110
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
111
|
+
return value;
|
|
112
|
+
if (typeof value === 'string' && value.trim()) {
|
|
113
|
+
const parsed = Number(value);
|
|
114
|
+
if (Number.isFinite(parsed))
|
|
115
|
+
return parsed;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const key of ['poll_interval_seconds', 'interval_seconds', 'value_seconds']) {
|
|
119
|
+
const value = data[key];
|
|
120
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
121
|
+
return value * 1000;
|
|
122
|
+
if (typeof value === 'string' && value.trim()) {
|
|
123
|
+
const parsed = Number(value);
|
|
124
|
+
if (Number.isFinite(parsed))
|
|
125
|
+
return parsed * 1000;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
function parseToolRefreshPollIntervalMs(items) {
|
|
131
|
+
const latest = items
|
|
132
|
+
.filter((item) => item.type === 'economic_parameter')
|
|
133
|
+
.filter((item) => {
|
|
134
|
+
const data = item.structured_data ?? {};
|
|
135
|
+
const name = data.parameter_name ?? data.name ?? data.key;
|
|
136
|
+
return (name === 'refresh_mcp_tools_poll_interval_ms' ||
|
|
137
|
+
name === 'refresh_mcp_tools_poll_interval_seconds' ||
|
|
138
|
+
name === 'mcp_tool_refresh_poll_interval_ms' ||
|
|
139
|
+
name === 'mcp_tool_refresh_poll_interval_seconds');
|
|
140
|
+
})
|
|
141
|
+
.at(-1);
|
|
142
|
+
if (!latest)
|
|
143
|
+
return { pollIntervalMs: DEFAULT_TOOL_REFRESH_POLL_INTERVAL_MS, source: 'default' };
|
|
144
|
+
const value = numericStructuredValue(latest.structured_data ?? {});
|
|
145
|
+
if (value === null || value <= 0) {
|
|
146
|
+
throw new Error(`Invalid refresh-MCP-tools economic_parameter ${latest.handle_path ?? '(unhandled)'}: expected a positive interval value`);
|
|
147
|
+
}
|
|
148
|
+
return { pollIntervalMs: Math.round(value), source: 'economic_parameter', handle: latest.handle_path };
|
|
149
|
+
}
|
|
150
|
+
export const __mcpSurfaceTestInternals = {
|
|
151
|
+
normalizeSurfaceName,
|
|
152
|
+
surfaceToolName,
|
|
153
|
+
parseSurfaceConfig,
|
|
154
|
+
dedupeSurfaces,
|
|
155
|
+
normalizeApiRootPath,
|
|
156
|
+
SURFACE_REFRESH_TTL_MS,
|
|
157
|
+
fingerprintSubstrateSurfaces,
|
|
158
|
+
parseToolRefreshPollIntervalMs,
|
|
159
|
+
};
|
|
160
|
+
export async function startMcpServer(options) {
|
|
161
|
+
const loadedConfig = loadConfig();
|
|
162
|
+
// MCP identity is slot-owned now. Keep node/base config, but deliberately
|
|
163
|
+
// strip any legacy config/env agent token before constructing pre-pin APIs.
|
|
164
|
+
const config = { ...loadedConfig, apiToken: '' };
|
|
165
|
+
let processSlot = null;
|
|
166
|
+
// ── Mesh: generate per-process thread ID ───────────────────────
|
|
167
|
+
const threadId = randomUUID();
|
|
168
|
+
console.error(`[wire-mcp] Thread ID: ${threadId.slice(0, 8)}...`);
|
|
169
|
+
// Pre-pin WireAPI for unauthenticated device-auth / register / resume
|
|
170
|
+
// server calls. Carries node-level config but no agent token. Post-pin
|
|
171
|
+
// tool handlers go through the `api` closure variable below — bootApi
|
|
172
|
+
// is for identity-resolution operations only.
|
|
173
|
+
const bootApi = new WireAPI(config, threadId);
|
|
174
|
+
// `api` starts as bootApi (no agent token). Tool handlers MUST gate
|
|
175
|
+
// through requireIdentity() before using it; that gate ensures the
|
|
176
|
+
// closure has been promoted to the slot's authenticated WireAPI by
|
|
177
|
+
// establishPin. Keeping `api` non-nullable means every handler can
|
|
178
|
+
// call api.X() without per-site null narrowing — the gate IS the
|
|
179
|
+
// null check.
|
|
180
|
+
let api = bootApi;
|
|
181
|
+
// Per-name in-flight identify Promise. Same-name retries de-dupe;
|
|
182
|
+
// cross-name attempts in flight will also be serialized through
|
|
183
|
+
// this map (and rejected by the first-call-wins guard).
|
|
184
|
+
const identifyInProgressByName = new Map();
|
|
185
|
+
let identifyingName = null;
|
|
186
|
+
let worker = null;
|
|
187
|
+
let workerStartAttempted = false;
|
|
188
|
+
let toolRefreshPollerStarted = false;
|
|
189
|
+
// ── Migration on first boot ──────────────────────────────────────
|
|
190
|
+
// One-time copy from ~/.wire-node/agents/<name>.json to
|
|
191
|
+
// ~/.wire-node/slots/<slot>.json. Idempotent.
|
|
192
|
+
try {
|
|
193
|
+
const migrated = migrateLegacyAgentsToSlots();
|
|
194
|
+
if (migrated > 0) {
|
|
195
|
+
console.error(`[wire-mcp] Migrated ${migrated} legacy agents/ entries → slots/`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
console.error(`[wire-mcp] Migration warn (non-fatal): ${err.message}`);
|
|
200
|
+
}
|
|
201
|
+
// ── Auto-register node ───────────────────────────────────────────
|
|
202
|
+
// Deferred to first wire_identify (the slot's apiToken is needed; we
|
|
203
|
+
// no longer have a pre-resolved token at boot time per Mission
|
|
204
|
+
// #1.5-rev2). Worker loop is enabled inside wire_identify when the
|
|
205
|
+
// pin is established and config.nodeId is missing.
|
|
206
|
+
// ── Helper: identity gate ──────────────────────────────────────────
|
|
207
|
+
function requireIdentity() {
|
|
208
|
+
if (processSlot)
|
|
209
|
+
return null;
|
|
210
|
+
return { type: 'text', text: 'Call wire_identify first to establish your identity.' };
|
|
211
|
+
}
|
|
212
|
+
// ── Session state for response shaping ──────────────────────────────
|
|
213
|
+
let shaperContext = null;
|
|
214
|
+
function getShaperContext() {
|
|
215
|
+
return shaperContext ?? {
|
|
216
|
+
agentName: 'unknown',
|
|
217
|
+
agentHandle: null,
|
|
218
|
+
verificationStatus: 'pending',
|
|
219
|
+
firstIdentifyInSession: false,
|
|
220
|
+
lastSessionAt: null,
|
|
221
|
+
lastAccessedContributionId: null,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function normalizeVerificationStatus(value) {
|
|
225
|
+
return value === 'confirmed' || value === 'rejected' ? value : 'pending';
|
|
226
|
+
}
|
|
227
|
+
function syncShaperContextFromMe(me, options = {}) {
|
|
228
|
+
const previous = shaperContext;
|
|
229
|
+
const identity = (me.identity && typeof me.identity === 'object' ? me.identity : {});
|
|
230
|
+
const agentName = identity.name || me.name || previous?.agentName || 'unknown';
|
|
231
|
+
const agentHandle = identity.handle_path || me.handle_path || me.handle || previous?.agentHandle || null;
|
|
232
|
+
const lastSessionAt = me.last_session_at || previous?.lastSessionAt || null;
|
|
233
|
+
shaperContext = {
|
|
234
|
+
agentName,
|
|
235
|
+
agentHandle,
|
|
236
|
+
verificationStatus: normalizeVerificationStatus(me.verification_status),
|
|
237
|
+
firstIdentifyInSession: options.firstIdentifyInSession ?? previous?.firstIdentifyInSession ?? true,
|
|
238
|
+
lastSessionAt,
|
|
239
|
+
lastAccessedContributionId: previous?.lastAccessedContributionId ?? null,
|
|
240
|
+
};
|
|
241
|
+
if (processSlot && typeof me.verification_status === 'string' && processSlot.identity.verificationStatus !== me.verification_status) {
|
|
242
|
+
const refreshedIdentity = {
|
|
243
|
+
...processSlot.identity,
|
|
244
|
+
verificationStatus: me.verification_status,
|
|
245
|
+
};
|
|
246
|
+
processSlot = { ...processSlot, identity: refreshedIdentity };
|
|
247
|
+
saveSlotConfig(processSlot.name, refreshedIdentity);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
async function ensureShaperContext(options = {}) {
|
|
251
|
+
if (shaperContext && !options.force)
|
|
252
|
+
return;
|
|
253
|
+
if (!processSlot)
|
|
254
|
+
return;
|
|
255
|
+
try {
|
|
256
|
+
const me = await api.getMe();
|
|
257
|
+
if (me)
|
|
258
|
+
syncShaperContextFromMe(me);
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// Non-fatal — shaper will work without context
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// No env-token fast-path in Mission #1.5-rev2. Shaper context is
|
|
265
|
+
// populated lazily after the first wire_identify pin is established.
|
|
266
|
+
// ── Helper: confirmed identity gate (for credit-affecting tools) ──
|
|
267
|
+
async function requireConfirmed() {
|
|
268
|
+
const idGate = requireIdentity();
|
|
269
|
+
if (idGate)
|
|
270
|
+
return idGate;
|
|
271
|
+
// Agent approval happens out-of-band by operator email. Always refresh
|
|
272
|
+
// from /me before local credit-tool gating so stale pending state clears.
|
|
273
|
+
await ensureShaperContext({ force: true });
|
|
274
|
+
if (!shaperContext || shaperContext.verificationStatus !== 'confirmed') {
|
|
275
|
+
const status = shaperContext?.verificationStatus ?? 'pending';
|
|
276
|
+
return { type: 'text', text: `This tool requires a confirmed agent. Your verification status is "${status}". Ask your operator to check their email for approval.` };
|
|
277
|
+
}
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
// ── Helper: UUID format validator ─────────────────────────────────
|
|
281
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
282
|
+
// ── Helper: establish the process pin (Mission #1.5-rev2) ──────────
|
|
283
|
+
// Single point of identity assignment. Asserts the pin is either unset
|
|
284
|
+
// or matches the requested name (caller MUST have already passed the
|
|
285
|
+
// first-call-wins guard at handler entry). Builds the WireAPI bound
|
|
286
|
+
// to the slot's apiToken, persists the slot file, and returns the
|
|
287
|
+
// canonical SlotConfig that callers can use for response shaping.
|
|
288
|
+
//
|
|
289
|
+
// Audit-flag #1 (Jerry, dm-083476): only this helper writes slot
|
|
290
|
+
// state. Tool handlers other than wire_identify must not call
|
|
291
|
+
// saveSlotConfig directly.
|
|
292
|
+
function establishPin(name, identity, source) {
|
|
293
|
+
if (processSlot && processSlot.name.toLowerCase() !== name.toLowerCase()) {
|
|
294
|
+
// Defensive — the handler-entry guard should have prevented this.
|
|
295
|
+
throw new Error(`Pin contract violation: tried to establish slot "${name}" while pinned to "${processSlot.name}"`);
|
|
296
|
+
}
|
|
297
|
+
// Build the API client bound to this slot's token. We build a fresh
|
|
298
|
+
// config object rather than mutating the closure-shared one — the
|
|
299
|
+
// disk-side config.apiToken stays untouched.
|
|
300
|
+
const slotApi = new WireAPI({ ...config, apiToken: identity.apiToken }, threadId);
|
|
301
|
+
processSlot = { name, identity };
|
|
302
|
+
api = slotApi;
|
|
303
|
+
saveSlotConfig(name, identity);
|
|
304
|
+
console.error(`[wire-mcp] Pin established: ${name} (${identity.pseudoId}) [source=${source}]`);
|
|
305
|
+
return identity;
|
|
306
|
+
}
|
|
307
|
+
async function ensureNodeAndMaybeStartWorker() {
|
|
308
|
+
if (options?.noWorker || worker || workerStartAttempted || !processSlot)
|
|
309
|
+
return;
|
|
310
|
+
workerStartAttempted = true;
|
|
311
|
+
if (!config.nodeId) {
|
|
312
|
+
console.error('[wire-mcp] No node ID found, auto-registering after slot pin...');
|
|
313
|
+
try {
|
|
314
|
+
const node = await api.registerNode(config.nodeName, config.capabilities);
|
|
315
|
+
config.nodeId = node.node_id;
|
|
316
|
+
saveConfig(config);
|
|
317
|
+
console.error(`[wire-mcp] Auto-registered node: ${node.node_id} (${config.nodeName})`);
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
console.error(`[wire-mcp] Auto-registration failed: ${err.message}`);
|
|
321
|
+
console.error('[wire-mcp] Continuing without node registration. Background work loop will be inactive.');
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const existingPid = isWorkerRunning();
|
|
326
|
+
if (existingPid) {
|
|
327
|
+
console.error(`[wire-mcp] A Wire worker node is already running on this machine (PID ${existingPid}). Starting in MCP-only mode.`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
if (acquireLock()) {
|
|
331
|
+
worker = startBackgroundWorkLoop(config, api);
|
|
332
|
+
console.error('[wire-mcp] Background worker started.');
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
console.error('[wire-mcp] Could not acquire daemon lock. Starting in MCP-only mode.');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// Create MCP server
|
|
339
|
+
const server = new McpServer({
|
|
340
|
+
name: 'wire',
|
|
341
|
+
version: '1.0.0',
|
|
342
|
+
});
|
|
343
|
+
const dynamicSurfaceTools = new Map();
|
|
344
|
+
let surfaceCache = null;
|
|
345
|
+
let activeToolSetFingerprint = null;
|
|
346
|
+
function surfaceToolDescription(surface) {
|
|
347
|
+
const handle = surface.configHandle ? ` Config: ${surface.configHandle}.` : '';
|
|
348
|
+
return `Invoke the graph-discovered Wire surface '${surface.surface}' via ${surface.aliasPath}.${handle}`;
|
|
349
|
+
}
|
|
350
|
+
function registerDynamicSurfaceTool(surface) {
|
|
351
|
+
const existing = dynamicSurfaceTools.get(surface.toolName);
|
|
352
|
+
if (existing) {
|
|
353
|
+
if (JSON.stringify(existing.surface) === JSON.stringify(surface))
|
|
354
|
+
return;
|
|
355
|
+
existing.remove();
|
|
356
|
+
dynamicSurfaceTools.delete(surface.toolName);
|
|
357
|
+
}
|
|
358
|
+
const registered = server.registerTool(surface.toolName, {
|
|
359
|
+
title: `Wire surface: ${surface.surface}`,
|
|
360
|
+
description: surfaceToolDescription(surface),
|
|
361
|
+
inputSchema: {
|
|
362
|
+
query: z
|
|
363
|
+
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
|
|
364
|
+
.optional()
|
|
365
|
+
.describe('Optional query parameters appended to the discovered surface path'),
|
|
366
|
+
body: z.unknown().optional().describe('Optional JSON body for non-GET surfaces'),
|
|
367
|
+
},
|
|
368
|
+
_meta: {
|
|
369
|
+
surface: surface.surface,
|
|
370
|
+
surface_config_handle: surface.configHandle,
|
|
371
|
+
canonical_path: surface.canonicalPath,
|
|
372
|
+
alias_path: surface.aliasPath,
|
|
373
|
+
discovery: 'surface_config',
|
|
374
|
+
},
|
|
375
|
+
}, async ({ query, body }) => {
|
|
376
|
+
const gate = requireIdentity();
|
|
377
|
+
if (gate)
|
|
378
|
+
return { content: [gate], isError: true };
|
|
379
|
+
const qs = new URLSearchParams();
|
|
380
|
+
if (query) {
|
|
381
|
+
for (const [key, value] of Object.entries(query)) {
|
|
382
|
+
qs.set(key, String(value));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : '';
|
|
386
|
+
const path = `${normalizeApiRootPath(surface.aliasPath)}${suffix}`;
|
|
387
|
+
const method = body === undefined ? surface.method : surface.method === 'GET' ? 'POST' : surface.method;
|
|
388
|
+
try {
|
|
389
|
+
const result = await api.request(method, path, body);
|
|
390
|
+
return {
|
|
391
|
+
content: [{
|
|
392
|
+
type: 'text',
|
|
393
|
+
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
|
|
394
|
+
}],
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
dynamicSurfaceTools.set(surface.toolName, { ...registered, surface });
|
|
402
|
+
}
|
|
403
|
+
async function refreshDynamicSurfaceTools(forceRefresh) {
|
|
404
|
+
if (!forceRefresh &&
|
|
405
|
+
surfaceCache &&
|
|
406
|
+
Date.now() - surfaceCache.fetchedAt < SURFACE_REFRESH_TTL_MS) {
|
|
407
|
+
const fingerprint = fingerprintSubstrateSurfaces(surfaceCache.surfaces);
|
|
408
|
+
return {
|
|
409
|
+
changed: false,
|
|
410
|
+
fingerprint,
|
|
411
|
+
previousFingerprint: activeToolSetFingerprint,
|
|
412
|
+
surfaces: surfaceCache.surfaces,
|
|
413
|
+
added: [],
|
|
414
|
+
removed: [],
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const surfaces = await refreshSubstrateSurfaces(api);
|
|
418
|
+
surfaceCache = { fetchedAt: Date.now(), surfaces };
|
|
419
|
+
const nextNames = new Set(surfaces.map((surface) => surface.toolName));
|
|
420
|
+
const previousNames = new Set(dynamicSurfaceTools.keys());
|
|
421
|
+
const removed = [];
|
|
422
|
+
for (const [toolName, registered] of dynamicSurfaceTools) {
|
|
423
|
+
if (!nextNames.has(toolName)) {
|
|
424
|
+
registered.remove();
|
|
425
|
+
dynamicSurfaceTools.delete(toolName);
|
|
426
|
+
removed.push(toolName);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const added = [];
|
|
430
|
+
for (const surface of surfaces) {
|
|
431
|
+
if (!previousNames.has(surface.toolName))
|
|
432
|
+
added.push(surface.toolName);
|
|
433
|
+
registerDynamicSurfaceTool(surface);
|
|
434
|
+
}
|
|
435
|
+
const fingerprint = fingerprintSubstrateSurfaces(surfaces);
|
|
436
|
+
const previousFingerprint = activeToolSetFingerprint;
|
|
437
|
+
activeToolSetFingerprint = fingerprint;
|
|
438
|
+
return {
|
|
439
|
+
changed: previousFingerprint !== null && previousFingerprint !== fingerprint,
|
|
440
|
+
fingerprint,
|
|
441
|
+
previousFingerprint,
|
|
442
|
+
surfaces,
|
|
443
|
+
added,
|
|
444
|
+
removed,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
async function discoverSurfacesAtPin(source) {
|
|
448
|
+
try {
|
|
449
|
+
const result = await refreshDynamicSurfaceTools(false);
|
|
450
|
+
console.error(`[wire-mcp] Surface discovery (${source}): ${result.surfaces.length} surface_config tools registered`);
|
|
451
|
+
startToolRefreshPoller();
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
console.error(`[wire-mcp] Surface discovery failed (${source}): ${err.message}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function notifyToolListChanged(reason) {
|
|
458
|
+
if (!server.isConnected())
|
|
459
|
+
return;
|
|
460
|
+
server.sendToolListChanged();
|
|
461
|
+
console.error(`[wire-mcp] tools/list_changed emitted (${reason})`);
|
|
462
|
+
}
|
|
463
|
+
async function loadToolRefreshPollIntervalMs() {
|
|
464
|
+
try {
|
|
465
|
+
const result = await api.query({
|
|
466
|
+
text: 'refresh_mcp_tools_poll_interval economic_parameter',
|
|
467
|
+
domain: 'all',
|
|
468
|
+
limit: 25,
|
|
469
|
+
});
|
|
470
|
+
const policy = parseToolRefreshPollIntervalMs(extractQueryItems(result));
|
|
471
|
+
if (policy.source === 'default') {
|
|
472
|
+
console.error(`[wire-mcp] WARNING: refresh-MCP-tools economic_parameter not found; using default poll interval ${policy.pollIntervalMs}ms`);
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
console.error(`[wire-mcp] refresh-MCP-tools poll interval ${policy.pollIntervalMs}ms from ${policy.handle}`);
|
|
476
|
+
}
|
|
477
|
+
return policy.pollIntervalMs;
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
console.error(`[wire-mcp] WARNING: refresh-MCP-tools policy load failed: ${err.message}; using default poll interval`);
|
|
481
|
+
return DEFAULT_TOOL_REFRESH_POLL_INTERVAL_MS;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
function startToolRefreshPoller() {
|
|
485
|
+
if (toolRefreshPollerStarted)
|
|
486
|
+
return;
|
|
487
|
+
toolRefreshPollerStarted = true;
|
|
488
|
+
void (async () => {
|
|
489
|
+
const intervalMs = await loadToolRefreshPollIntervalMs();
|
|
490
|
+
const timer = setInterval(() => {
|
|
491
|
+
void (async () => {
|
|
492
|
+
try {
|
|
493
|
+
const result = await refreshDynamicSurfaceTools(true);
|
|
494
|
+
if (result.changed || result.added.length > 0 || result.removed.length > 0) {
|
|
495
|
+
notifyToolListChanged('auto-refresh');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
catch (err) {
|
|
499
|
+
console.error(`[wire-mcp] WARNING: refresh-MCP-tools auto refresh failed: ${err.message}`);
|
|
500
|
+
}
|
|
501
|
+
})();
|
|
502
|
+
}, intervalMs);
|
|
503
|
+
timer.unref?.();
|
|
504
|
+
})();
|
|
505
|
+
}
|
|
506
|
+
async function refreshToolsResultJson(reason) {
|
|
507
|
+
const result = await refreshDynamicSurfaceTools(true);
|
|
508
|
+
notifyToolListChanged(reason);
|
|
509
|
+
return JSON.stringify({
|
|
510
|
+
refreshed: true,
|
|
511
|
+
notification: 'notifications/tools/list_changed',
|
|
512
|
+
changed: result.changed,
|
|
513
|
+
fingerprint: result.fingerprint,
|
|
514
|
+
previous_fingerprint: result.previousFingerprint,
|
|
515
|
+
added: result.added,
|
|
516
|
+
removed: result.removed,
|
|
517
|
+
count: result.surfaces.length,
|
|
518
|
+
tools: result.surfaces.map((surface) => ({
|
|
519
|
+
surface: surface.surface,
|
|
520
|
+
tool: surface.toolName,
|
|
521
|
+
config_handle: surface.configHandle,
|
|
522
|
+
alias_path: surface.aliasPath,
|
|
523
|
+
canonical_path: surface.canonicalPath,
|
|
524
|
+
method: surface.method,
|
|
525
|
+
})),
|
|
526
|
+
}, null, 2);
|
|
527
|
+
}
|
|
528
|
+
// ── Tool: wire_identify ──────────────────────────────────────────
|
|
529
|
+
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.', {
|
|
530
|
+
manifest_path: z.string().optional().describe('Optional manifest path. Defaults to ~/.wire-node/sync-manifest.json.'),
|
|
531
|
+
dry_run: z.boolean().optional().describe('Preview target writes without modifying local files.'),
|
|
532
|
+
init: z.boolean().optional().describe('Create a default manifest first if one does not exist.'),
|
|
533
|
+
}, async (params) => {
|
|
534
|
+
const gate = requireIdentity();
|
|
535
|
+
if (gate)
|
|
536
|
+
return { content: [gate], isError: true };
|
|
537
|
+
try {
|
|
538
|
+
const report = await pullLocalSubscriptions(api, {
|
|
539
|
+
manifestPath: params.manifest_path,
|
|
540
|
+
dryRun: params.dry_run,
|
|
541
|
+
init: params.init,
|
|
542
|
+
});
|
|
543
|
+
return { content: [{ type: 'text', text: JSON.stringify(report, null, 2) }], isError: report.failed > 0 };
|
|
544
|
+
}
|
|
545
|
+
catch (err) {
|
|
546
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
server.tool('wire_identify', 'Identify yourself to the Wire. Must be called before any other Wire tools. ' +
|
|
550
|
+
'First call: provide name + operator_email. If this is a new device, you will receive a ' +
|
|
551
|
+
'"check_email" response — ask the operator for the 6-digit code from their email, ' +
|
|
552
|
+
'then call wire_identify again with the same name, email, and the device_code parameter.', {
|
|
553
|
+
name: z.string().describe('Your chosen agent name (e.g. "cursor-claude", "research-bot"). This is your persistent identity.'),
|
|
554
|
+
operator_email: z.string().email().describe('Email of the human who operates you.'),
|
|
555
|
+
device_code: z.string().optional().describe('6-digit code from operator email. Only needed for device authorization on new devices.'),
|
|
556
|
+
create_new: z.boolean().optional().describe('Set true to explicitly create a new agent identity. Without this, unrecognized names return the roster instead of auto-creating.'),
|
|
557
|
+
agent_message: z.string().optional().describe('Context message included in any emails sent to the operator during this flow.'),
|
|
558
|
+
}, async (params) => {
|
|
559
|
+
// ── Helper: tools_available message based on verification status ──
|
|
560
|
+
const toolsAvailableMsg = (verificationStatus) => verificationStatus === 'confirmed'
|
|
561
|
+
? 'All tools available.'
|
|
562
|
+
: 'Pending verification: wire_identify, wire_query, wire_graph, wire_inspect, wire_status, wire_corpora, wire_legal, wire_pulse, wire_opportunities, wire_roster available now. Credit-affecting tools (wire_contribute, wire_rate, wire_flag, wire_correct, wire_messages, wire_market, wire_games, wire_supersede) require operator approval — check your operator\'s email.';
|
|
563
|
+
const nameKey = params.name.trim().toLowerCase();
|
|
564
|
+
// ── Mission #1.5-rev2: first-call-wins pin guard ──────────
|
|
565
|
+
// If this MCP process is already pinned and the request is for
|
|
566
|
+
// a DIFFERENT name, refuse. This is the structural anti-collision
|
|
567
|
+
// rule — the act of logging in IS the binding mechanism, and the
|
|
568
|
+
// binding is one-shot per process.
|
|
569
|
+
if (processSlot && processSlot.name.toLowerCase() !== nameKey) {
|
|
570
|
+
return {
|
|
571
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
572
|
+
status: 'error',
|
|
573
|
+
error: `This MCP process is already pinned to slot "${processSlot.name}". wire_identify(name="${params.name}") refused. To use a different slot, spawn a separate Claude Code session — each session runs its own MCP process.`,
|
|
574
|
+
}, null, 2) }],
|
|
575
|
+
isError: true,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
if (!processSlot && identifyingName && identifyingName !== nameKey) {
|
|
579
|
+
return {
|
|
580
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
581
|
+
status: 'error',
|
|
582
|
+
error: `This MCP process is already establishing slot "${identifyingName}". wire_identify(name="${params.name}") refused. To use a different slot, spawn a separate Claude Code session — each session runs its own MCP process.`,
|
|
583
|
+
}, null, 2) }],
|
|
584
|
+
isError: true,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
// Same-name re-call when already pinned: fast noop / cheap revalidate.
|
|
588
|
+
if (processSlot && processSlot.name.toLowerCase() === nameKey) {
|
|
589
|
+
let me = null;
|
|
590
|
+
try {
|
|
591
|
+
me = await api.getMe();
|
|
592
|
+
}
|
|
593
|
+
catch { /* fall through */ }
|
|
594
|
+
if (me)
|
|
595
|
+
syncShaperContextFromMe(me, { firstIdentifyInSession: false });
|
|
596
|
+
const vs = me?.verification_status ?? processSlot.identity.verificationStatus ?? 'unknown';
|
|
597
|
+
return {
|
|
598
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
599
|
+
status: 'resumed',
|
|
600
|
+
identity: `${me?.name || processSlot.name} (pseudo-ID: ${processSlot.identity.pseudoId})`,
|
|
601
|
+
operator: processSlot.identity.operatorEmail,
|
|
602
|
+
verification_status: vs,
|
|
603
|
+
pool_credits: me?.pool_credits ?? 'unknown',
|
|
604
|
+
note: 'Process is already pinned to this slot; no re-resolution needed.',
|
|
605
|
+
help: WIRE_HELP_POINTER,
|
|
606
|
+
tools_available: toolsAvailableMsg(vs),
|
|
607
|
+
}, null, 2) }],
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
// ── In-process per-name mutex ─────────────────────
|
|
611
|
+
// Concurrent wire_identify(X) from another concurrent caller in
|
|
612
|
+
// THIS process awaits the in-flight one. Because of the first-
|
|
613
|
+
// call-wins guard above, by definition concurrent calls must be
|
|
614
|
+
// for the same name (different name was already rejected).
|
|
615
|
+
const inFlightForThisName = identifyInProgressByName.get(nameKey);
|
|
616
|
+
if (inFlightForThisName) {
|
|
617
|
+
await inFlightForThisName;
|
|
618
|
+
// Recheck pin state after awaiting — the in-flight call should
|
|
619
|
+
// have established the pin if it succeeded.
|
|
620
|
+
if (processSlot && processSlot.name.toLowerCase() === nameKey) {
|
|
621
|
+
let me = null;
|
|
622
|
+
try {
|
|
623
|
+
me = await api.getMe();
|
|
624
|
+
}
|
|
625
|
+
catch { /* fall through */ }
|
|
626
|
+
if (me)
|
|
627
|
+
syncShaperContextFromMe(me, { firstIdentifyInSession: false });
|
|
628
|
+
const vs = me?.verification_status ?? processSlot.identity.verificationStatus ?? 'unknown';
|
|
629
|
+
return {
|
|
630
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
631
|
+
status: 'resumed',
|
|
632
|
+
identity: `${me?.name || processSlot.name} (concurrent same-name call resolved)`,
|
|
633
|
+
verification_status: vs,
|
|
634
|
+
pool_credits: me?.pool_credits ?? 'unknown',
|
|
635
|
+
help: WIRE_HELP_POINTER,
|
|
636
|
+
tools_available: toolsAvailableMsg(vs),
|
|
637
|
+
}, null, 2) }],
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// ── 0. Cross-process lock ─────────────────────────────────
|
|
642
|
+
const lockAcquired = acquireIdentityLock(params.name);
|
|
643
|
+
if (!lockAcquired) {
|
|
644
|
+
return {
|
|
645
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
646
|
+
status: 'error',
|
|
647
|
+
error: 'Another wire_identify call is in progress for this agent name. Wait a moment and retry.',
|
|
648
|
+
}, null, 2) }],
|
|
649
|
+
isError: true,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
if (!identifyingName)
|
|
653
|
+
identifyingName = nameKey;
|
|
654
|
+
let resolveIdentify;
|
|
655
|
+
const thisIdentifyPromise = new Promise(r => { resolveIdentify = r; });
|
|
656
|
+
identifyInProgressByName.set(nameKey, thisIdentifyPromise);
|
|
657
|
+
try {
|
|
658
|
+
// ── 2. Check local slot file ─────────────────────────────
|
|
659
|
+
const existing = loadSlotConfig(params.name);
|
|
660
|
+
if (existing) {
|
|
661
|
+
// D10 fix: verify operator_email matches
|
|
662
|
+
if (existing.operatorEmail.toLowerCase() !== params.operator_email.toLowerCase()) {
|
|
663
|
+
return {
|
|
664
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
665
|
+
status: 'error',
|
|
666
|
+
error: 'Operator email mismatch. This agent identity belongs to a different operator. Use the correct email or a different agent name.',
|
|
667
|
+
}, null, 2) }],
|
|
668
|
+
isError: true,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
// Build a transient WireAPI to validate the cached token —
|
|
672
|
+
// we have NOT yet established the pin, so we don't touch the
|
|
673
|
+
// closure-shared `api`.
|
|
674
|
+
const probeApi = new WireAPI({ ...config, apiToken: existing.apiToken }, threadId);
|
|
675
|
+
let me = null;
|
|
676
|
+
try {
|
|
677
|
+
me = await probeApi.getMe();
|
|
678
|
+
}
|
|
679
|
+
catch { /* token dead */ }
|
|
680
|
+
if (me) {
|
|
681
|
+
// 1A: Verify cached identity matches requested name
|
|
682
|
+
const serverName = me.name;
|
|
683
|
+
if (serverName && params.name && serverName.toLowerCase() !== params.name.toLowerCase()) {
|
|
684
|
+
// Stale slot file — server-side name doesn't match what
|
|
685
|
+
// the local file claims. Skip the slot file (it's
|
|
686
|
+
// mis-keyed) and fall through to roster flow. We
|
|
687
|
+
// intentionally do NOT delete the slot file here —
|
|
688
|
+
// overwrite happens on successful re-resolution below
|
|
689
|
+
// via establishPin.
|
|
690
|
+
console.error(`[wire-mcp] Stale slot: server name "${serverName}" != requested "${params.name}", falling through to re-resolve`);
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
const canonicalName = serverName || params.name;
|
|
694
|
+
// 1E: Always use fresh verification_status from server
|
|
695
|
+
const freshStatus = me.verification_status ?? existing.verificationStatus ?? 'unknown';
|
|
696
|
+
const refreshedIdentity = {
|
|
697
|
+
...existing,
|
|
698
|
+
verificationStatus: typeof me.verification_status === 'string'
|
|
699
|
+
? me.verification_status
|
|
700
|
+
: existing.verificationStatus,
|
|
701
|
+
};
|
|
702
|
+
establishPin(params.name, refreshedIdentity, 'slot-cache');
|
|
703
|
+
await discoverSurfacesAtPin('slot-cache');
|
|
704
|
+
await ensureNodeAndMaybeStartWorker();
|
|
705
|
+
syncShaperContextFromMe(me, { firstIdentifyInSession: false });
|
|
706
|
+
return {
|
|
707
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
708
|
+
status: 'resumed',
|
|
709
|
+
identity: `${canonicalName} (pseudo-ID: ${existing.pseudoId})`,
|
|
710
|
+
operator: existing.operatorEmail,
|
|
711
|
+
verification_status: freshStatus,
|
|
712
|
+
pool_credits: me.pool_credits ?? 'unknown',
|
|
713
|
+
help: WIRE_HELP_POINTER,
|
|
714
|
+
tools_available: toolsAvailableMsg(freshStatus),
|
|
715
|
+
}, null, 2) }],
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
// Token is dead — leave the slot file in place; it will be
|
|
720
|
+
// overwritten by establishPin once the roster/register flow
|
|
721
|
+
// produces a fresh token. (We don't delete because the slot
|
|
722
|
+
// file is the canonical source of truth; mid-flow truncation
|
|
723
|
+
// would orphan callers.)
|
|
724
|
+
console.error(`[wire-mcp] Local identity for ${params.name} has dead token, falling through to re-resolve...`);
|
|
725
|
+
}
|
|
726
|
+
// ── 3. Device code provided? (continuing device auth) ─────
|
|
727
|
+
if (params.device_code) {
|
|
728
|
+
const pending = loadAuthPending(params.operator_email);
|
|
729
|
+
if (!pending) {
|
|
730
|
+
return {
|
|
731
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
732
|
+
status: 'error',
|
|
733
|
+
error: 'No pending device authorization found. Call wire_identify without device_code first to initiate the flow.',
|
|
734
|
+
}, null, 2) }],
|
|
735
|
+
isError: true,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
// Check if pending file is expired (codes expire in 15 min, we use 20 min buffer)
|
|
739
|
+
if (Date.now() - new Date(pending.requestedAt).getTime() > 20 * 60 * 1000) {
|
|
740
|
+
deleteAuthPending(params.operator_email);
|
|
741
|
+
return {
|
|
742
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
743
|
+
status: 'error',
|
|
744
|
+
error: 'Device authorization code has expired. Call wire_identify without device_code to request a new one.',
|
|
745
|
+
}, null, 2) }],
|
|
746
|
+
isError: true,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
let verifyResult;
|
|
750
|
+
try {
|
|
751
|
+
verifyResult = await bootApi.verifyDeviceCode(params.operator_email, pending.deviceFingerprint, params.device_code);
|
|
752
|
+
}
|
|
753
|
+
catch (err) {
|
|
754
|
+
return {
|
|
755
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
756
|
+
status: 'error',
|
|
757
|
+
error: `Device verification failed: ${err.message}`,
|
|
758
|
+
}, null, 2) }],
|
|
759
|
+
isError: true,
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
if (verifyResult.status !== 'authorized') {
|
|
763
|
+
return {
|
|
764
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
765
|
+
status: 'error',
|
|
766
|
+
error: `Device authorization failed: ${verifyResult.message || 'Unknown error'}`,
|
|
767
|
+
hint: verifyResult.status === 'invalid_code' ? 'Check the code and try again.' : 'Request a new code by calling wire_identify without device_code.',
|
|
768
|
+
}, null, 2) }],
|
|
769
|
+
isError: true,
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
// Save device secret
|
|
773
|
+
const pendingCreateNew = pending.createNew;
|
|
774
|
+
const pendingAgentName = pending.agentName;
|
|
775
|
+
saveDeviceRecord(params.operator_email, {
|
|
776
|
+
deviceSecret: verifyResult.device_secret,
|
|
777
|
+
deviceId: verifyResult.device_id,
|
|
778
|
+
operatorId: verifyResult.operator_id,
|
|
779
|
+
authorizedAt: new Date().toISOString(),
|
|
780
|
+
});
|
|
781
|
+
deleteAuthPending(params.operator_email);
|
|
782
|
+
console.error(`[wire-mcp] Device authorized for ${params.operator_email}`);
|
|
783
|
+
// 1C: If create_new was persisted through auth and name matches, set flag for roster flow
|
|
784
|
+
if (pendingCreateNew && pendingAgentName && pendingAgentName.toLowerCase() === params.name.toLowerCase()) {
|
|
785
|
+
// Override params.create_new so roster flow goes straight to registration
|
|
786
|
+
params.create_new = true;
|
|
787
|
+
}
|
|
788
|
+
// Fall through to roster flow (step 5)
|
|
789
|
+
}
|
|
790
|
+
// ── 4. Check local device secret ──────────────────────────
|
|
791
|
+
const deviceRecord = loadDeviceRecord(params.operator_email);
|
|
792
|
+
if (!deviceRecord && !params.device_code) {
|
|
793
|
+
// No device secret — determine if first-time or existing operator
|
|
794
|
+
if (!hasAnyLocalIdentityForEmail(params.operator_email)) {
|
|
795
|
+
// FIRST-TIME OPERATOR: register directly via POST /register
|
|
796
|
+
console.error(`[wire-mcp] First-time operator — registering directly`);
|
|
797
|
+
const sessionMetadata = {
|
|
798
|
+
hostname: hostname(),
|
|
799
|
+
pid: process.pid,
|
|
800
|
+
client: process.env.MCP_CLIENT_NAME || 'unknown',
|
|
801
|
+
session_started: new Date().toISOString(),
|
|
802
|
+
};
|
|
803
|
+
const result = await bootApi.registerAgent({
|
|
804
|
+
name: params.name,
|
|
805
|
+
operator_email: params.operator_email,
|
|
806
|
+
domains: ['all'],
|
|
807
|
+
session_metadata: sessionMetadata,
|
|
808
|
+
agent_message: params.agent_message,
|
|
809
|
+
});
|
|
810
|
+
if (!result.api_token || !result.pseudo_id || !result.agent_id) {
|
|
811
|
+
const missing = [
|
|
812
|
+
!result.api_token && 'api_token', !result.pseudo_id && 'pseudo_id', !result.agent_id && 'agent_id',
|
|
813
|
+
].filter(Boolean);
|
|
814
|
+
return {
|
|
815
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
816
|
+
status: 'error',
|
|
817
|
+
error: `Registration response missing: ${missing.join(', ')}. Try again.`,
|
|
818
|
+
}, null, 2) }],
|
|
819
|
+
isError: true,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
establishPin(params.name, {
|
|
823
|
+
apiToken: result.api_token,
|
|
824
|
+
agentId: result.agent_id,
|
|
825
|
+
pseudoId: result.pseudo_id,
|
|
826
|
+
operatorEmail: params.operator_email,
|
|
827
|
+
verificationStatus: result.verification_status,
|
|
828
|
+
}, 'first-time-register');
|
|
829
|
+
await discoverSurfacesAtPin('first-time-register');
|
|
830
|
+
await ensureNodeAndMaybeStartWorker();
|
|
831
|
+
shaperContext = {
|
|
832
|
+
agentName: params.name,
|
|
833
|
+
agentHandle: null,
|
|
834
|
+
verificationStatus: normalizeVerificationStatus(result.verification_status),
|
|
835
|
+
firstIdentifyInSession: true,
|
|
836
|
+
lastSessionAt: null,
|
|
837
|
+
lastAccessedContributionId: null,
|
|
838
|
+
};
|
|
839
|
+
const isConfirmed = result.verification_status === 'confirmed';
|
|
840
|
+
return {
|
|
841
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
842
|
+
status: 'registered',
|
|
843
|
+
identity: `${params.name} (pseudo-ID: ${result.pseudo_id})`,
|
|
844
|
+
verification_status: result.verification_status,
|
|
845
|
+
verification: isConfirmed
|
|
846
|
+
? 'Operator already verified. Full access granted.'
|
|
847
|
+
: `Approval required: check ${params.operator_email} for verification and approval emails.`,
|
|
848
|
+
help: WIRE_HELP_POINTER,
|
|
849
|
+
tools_available: toolsAvailableMsg(result.verification_status),
|
|
850
|
+
}, null, 2) }],
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
// EXISTING OPERATOR, NEW DEVICE: initiate device auth
|
|
854
|
+
console.error(`[wire-mcp] Existing operator, no device secret — initiating device auth`);
|
|
855
|
+
const fingerprint = getStableDeviceFingerprint();
|
|
856
|
+
try {
|
|
857
|
+
await bootApi.requestDeviceAuth(params.operator_email, fingerprint, params.agent_message);
|
|
858
|
+
}
|
|
859
|
+
catch (err) {
|
|
860
|
+
return {
|
|
861
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
862
|
+
status: 'error',
|
|
863
|
+
error: `Failed to request device authorization: ${err.message}`,
|
|
864
|
+
}, null, 2) }],
|
|
865
|
+
isError: true,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
saveAuthPending(params.operator_email, {
|
|
869
|
+
email: params.operator_email,
|
|
870
|
+
deviceFingerprint: fingerprint,
|
|
871
|
+
requestedAt: new Date().toISOString(),
|
|
872
|
+
createNew: params.create_new || undefined,
|
|
873
|
+
agentName: params.name,
|
|
874
|
+
});
|
|
875
|
+
return {
|
|
876
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
877
|
+
status: 'check_email',
|
|
878
|
+
message: `Device authorization code sent to ${params.operator_email}. Ask the operator for the 6-digit code from their email.`,
|
|
879
|
+
next_step: 'Call wire_identify again with the same name and email, plus the device_code parameter containing the 6-digit code.',
|
|
880
|
+
expires_in_minutes: 15,
|
|
881
|
+
note: 'After authorization, your agent name will be verified against the roster.',
|
|
882
|
+
}, null, 2) }],
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
// ── 5. Have device secret — get roster ────────────────────
|
|
886
|
+
if (deviceRecord) {
|
|
887
|
+
const harness = process.env.MCP_CLIENT_NAME || undefined;
|
|
888
|
+
let roster;
|
|
889
|
+
try {
|
|
890
|
+
roster = await bootApi.getRosterWithDeviceSecret(deviceRecord.deviceSecret, harness, params.name);
|
|
891
|
+
}
|
|
892
|
+
catch (err) {
|
|
893
|
+
console.error(`[wire-mcp] Roster fetch failed: ${err.message}`);
|
|
894
|
+
// Device secret might be revoked — clear it and fall back to device auth
|
|
895
|
+
return {
|
|
896
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
897
|
+
status: 'error',
|
|
898
|
+
error: `Failed to fetch roster: ${err.message}. Your device authorization may have been revoked. Try again to re-authorize.`,
|
|
899
|
+
}, null, 2) }],
|
|
900
|
+
isError: true,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
const existingAgents = (roster.existing_agents || []);
|
|
904
|
+
const suggested = (roster.suggested || []);
|
|
905
|
+
// Case A: Name matches an existing agent → resume
|
|
906
|
+
const match = existingAgents.find((a) => a.name.toLowerCase() === params.name.toLowerCase());
|
|
907
|
+
if (match) {
|
|
908
|
+
const nonce = randomUUID();
|
|
909
|
+
let resumed;
|
|
910
|
+
try {
|
|
911
|
+
resumed = await bootApi.resumeAgent(params.name, deviceRecord.deviceSecret, nonce);
|
|
912
|
+
}
|
|
913
|
+
catch (err) {
|
|
914
|
+
return {
|
|
915
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
916
|
+
status: 'error',
|
|
917
|
+
error: `Failed to resume agent: ${err.message}`,
|
|
918
|
+
}, null, 2) }],
|
|
919
|
+
isError: true,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
establishPin(params.name, {
|
|
923
|
+
apiToken: resumed.api_token,
|
|
924
|
+
agentId: resumed.agent_id,
|
|
925
|
+
pseudoId: resumed.pseudo_id,
|
|
926
|
+
operatorEmail: params.operator_email,
|
|
927
|
+
verificationStatus: resumed.verification_status,
|
|
928
|
+
}, 'roster-resume');
|
|
929
|
+
await discoverSurfacesAtPin('roster-resume');
|
|
930
|
+
await ensureNodeAndMaybeStartWorker();
|
|
931
|
+
const canonicalName = resumed.name || params.name;
|
|
932
|
+
shaperContext = {
|
|
933
|
+
agentName: canonicalName,
|
|
934
|
+
agentHandle: null,
|
|
935
|
+
verificationStatus: normalizeVerificationStatus(resumed.verification_status),
|
|
936
|
+
firstIdentifyInSession: false,
|
|
937
|
+
lastSessionAt: null,
|
|
938
|
+
lastAccessedContributionId: null,
|
|
939
|
+
};
|
|
940
|
+
const resumedStatus = resumed.verification_status ?? 'unknown';
|
|
941
|
+
console.error(`[wire-mcp] Resumed via roster: ${canonicalName} (${resumed.pseudo_id})`);
|
|
942
|
+
const resumeResponse = {
|
|
943
|
+
status: 'resumed',
|
|
944
|
+
identity: `${params.name} (pseudo-ID: ${resumed.pseudo_id})`,
|
|
945
|
+
operator: params.operator_email,
|
|
946
|
+
verification_status: resumed.verification_status ?? 'unknown',
|
|
947
|
+
help: WIRE_HELP_POINTER,
|
|
948
|
+
tools_available: toolsAvailableMsg(resumedStatus),
|
|
949
|
+
};
|
|
950
|
+
// Include "while you were away" summary if available
|
|
951
|
+
if (resumed.since_last_session) {
|
|
952
|
+
const sls = resumed.since_last_session;
|
|
953
|
+
const parts = [];
|
|
954
|
+
if (sls.new_messages > 0)
|
|
955
|
+
parts.push(`${sls.new_messages} new message${sls.new_messages !== 1 ? 's' : ''}`);
|
|
956
|
+
if (sls.new_tasks > 0)
|
|
957
|
+
parts.push(`${sls.new_tasks} new task${sls.new_tasks !== 1 ? 's' : ''}`);
|
|
958
|
+
if (sls.new_contributions > 0)
|
|
959
|
+
parts.push(`${sls.new_contributions} new contribution${sls.new_contributions !== 1 ? 's' : ''}`);
|
|
960
|
+
if (sls.credits_earned > 0)
|
|
961
|
+
parts.push(`${sls.credits_earned} credits earned`);
|
|
962
|
+
if (parts.length > 0) {
|
|
963
|
+
resumeResponse.while_you_were_away = `While you were away: ${parts.join(', ')}.`;
|
|
964
|
+
}
|
|
965
|
+
resumeResponse.since_last_session = sls;
|
|
966
|
+
}
|
|
967
|
+
return {
|
|
968
|
+
content: [{ type: 'text', text: JSON.stringify(resumeResponse, null, 2) }],
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
// Case B: Name doesn't match + create_new → register
|
|
972
|
+
if (params.create_new) {
|
|
973
|
+
if (roster.can_create_new === false) {
|
|
974
|
+
return {
|
|
975
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
976
|
+
status: 'error',
|
|
977
|
+
error: 'Agent limit reached for this operator (max 50 active agents). Archive unused agents first.',
|
|
978
|
+
}, null, 2) }],
|
|
979
|
+
isError: true,
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
const sessionMetadata = {
|
|
983
|
+
hostname: hostname(),
|
|
984
|
+
pid: process.pid,
|
|
985
|
+
client: process.env.MCP_CLIENT_NAME || 'unknown',
|
|
986
|
+
session_started: new Date().toISOString(),
|
|
987
|
+
};
|
|
988
|
+
const result = await bootApi.registerAgent({
|
|
989
|
+
name: params.name,
|
|
990
|
+
operator_email: params.operator_email,
|
|
991
|
+
domains: ['all'],
|
|
992
|
+
session_metadata: sessionMetadata,
|
|
993
|
+
agent_message: params.agent_message,
|
|
994
|
+
});
|
|
995
|
+
if (!result.api_token || !result.pseudo_id || !result.agent_id) {
|
|
996
|
+
return {
|
|
997
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
998
|
+
status: 'error', error: 'Registration response missing critical fields.',
|
|
999
|
+
}, null, 2) }],
|
|
1000
|
+
isError: true,
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
establishPin(params.name, {
|
|
1004
|
+
apiToken: result.api_token,
|
|
1005
|
+
agentId: result.agent_id,
|
|
1006
|
+
pseudoId: result.pseudo_id,
|
|
1007
|
+
operatorEmail: params.operator_email,
|
|
1008
|
+
verificationStatus: result.verification_status,
|
|
1009
|
+
}, 'roster-create-new');
|
|
1010
|
+
await discoverSurfacesAtPin('roster-create-new');
|
|
1011
|
+
await ensureNodeAndMaybeStartWorker();
|
|
1012
|
+
shaperContext = {
|
|
1013
|
+
agentName: params.name,
|
|
1014
|
+
agentHandle: null,
|
|
1015
|
+
verificationStatus: normalizeVerificationStatus(result.verification_status),
|
|
1016
|
+
firstIdentifyInSession: true,
|
|
1017
|
+
lastSessionAt: null,
|
|
1018
|
+
lastAccessedContributionId: null,
|
|
1019
|
+
};
|
|
1020
|
+
// Warn on high-confidence near-match
|
|
1021
|
+
const nearMatch = suggested.find((s) => s.confidence > 0.8);
|
|
1022
|
+
const response = {
|
|
1023
|
+
status: 'registered',
|
|
1024
|
+
identity: `${params.name} (pseudo-ID: ${result.pseudo_id})`,
|
|
1025
|
+
verification_status: result.verification_status,
|
|
1026
|
+
help: WIRE_HELP_POINTER,
|
|
1027
|
+
tools_available: toolsAvailableMsg(result.verification_status),
|
|
1028
|
+
};
|
|
1029
|
+
if (nearMatch) {
|
|
1030
|
+
response.warning = `Note: existing agent "${nearMatch.name}" is similar (${Math.round(nearMatch.confidence * 100)}% match). If you meant to resume that agent, call wire_identify with that name instead.`;
|
|
1031
|
+
}
|
|
1032
|
+
return { content: [{ type: 'text', text: JSON.stringify(response, null, 2) }] };
|
|
1033
|
+
}
|
|
1034
|
+
// Case C: No match, no create_new → return roster + hint (1B: show roster, NOT check_email)
|
|
1035
|
+
// Build fuzzy did_you_mean from existing agents (simple substring/prefix similarity)
|
|
1036
|
+
const didYouMean = existingAgents
|
|
1037
|
+
.map((a) => {
|
|
1038
|
+
const aName = a.name.toLowerCase();
|
|
1039
|
+
const pName = params.name.toLowerCase();
|
|
1040
|
+
// Simple similarity: shared prefix length / max length
|
|
1041
|
+
let shared = 0;
|
|
1042
|
+
for (let i = 0; i < Math.min(aName.length, pName.length); i++) {
|
|
1043
|
+
if (aName[i] === pName[i])
|
|
1044
|
+
shared++;
|
|
1045
|
+
else
|
|
1046
|
+
break;
|
|
1047
|
+
}
|
|
1048
|
+
const similarity = shared / Math.max(aName.length, pName.length);
|
|
1049
|
+
// Also check substring containment
|
|
1050
|
+
const containsBonus = aName.includes(pName) || pName.includes(aName) ? 0.5 : 0;
|
|
1051
|
+
return { name: a.name, score: similarity + containsBonus };
|
|
1052
|
+
})
|
|
1053
|
+
.filter(x => x.score > 0)
|
|
1054
|
+
.sort((a, b) => b.score - a.score)
|
|
1055
|
+
.slice(0, 3)
|
|
1056
|
+
.map(x => x.name);
|
|
1057
|
+
return {
|
|
1058
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1059
|
+
status: 'name_not_found',
|
|
1060
|
+
message: `No agent named "${params.name}" in your roster.`,
|
|
1061
|
+
did_you_mean: didYouMean.length > 0 ? didYouMean : undefined,
|
|
1062
|
+
roster_count: existingAgents.length,
|
|
1063
|
+
existing_agents: existingAgents.map((a) => ({
|
|
1064
|
+
name: a.name, pseudo_id: a.pseudo_id, role: a.role,
|
|
1065
|
+
last_active: a.last_active, contributions: a.contribution_count,
|
|
1066
|
+
})),
|
|
1067
|
+
suggested,
|
|
1068
|
+
hint: `To resume an existing agent, call wire_identify with that agent's name. To create "${params.name}" as a new agent, call again with create_new: true.`,
|
|
1069
|
+
can_create_new: roster.can_create_new,
|
|
1070
|
+
help: WIRE_HELP_POINTER,
|
|
1071
|
+
}, null, 2) }],
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
// Should not reach here — device_code path saves deviceRecord and loops back
|
|
1075
|
+
return {
|
|
1076
|
+
content: [{ type: 'text', text: JSON.stringify({
|
|
1077
|
+
status: 'error', error: 'Unexpected state in identity flow. Please retry.',
|
|
1078
|
+
}, null, 2) }],
|
|
1079
|
+
isError: true,
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
catch (err) {
|
|
1083
|
+
return {
|
|
1084
|
+
content: [{ type: 'text', text: `Identity error: ${err.message}` }],
|
|
1085
|
+
isError: true,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
finally {
|
|
1089
|
+
resolveIdentify();
|
|
1090
|
+
if (identifyInProgressByName.get(nameKey) === thisIdentifyPromise) {
|
|
1091
|
+
identifyInProgressByName.delete(nameKey);
|
|
1092
|
+
}
|
|
1093
|
+
if (!processSlot && identifyingName === nameKey) {
|
|
1094
|
+
identifyingName = null;
|
|
1095
|
+
}
|
|
1096
|
+
releaseIdentityLock(params.name);
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
// ── Tool: wire_legal ──────────────────────────────────────────────────
|
|
1100
|
+
server.tool('wire_legal', 'Read the Wire\'s Terms of Service and Privacy Policy. No identity required — you can (and should) read these before registering.', {
|
|
1101
|
+
document: z.enum(['terms', 'privacy', 'both']).optional().default('both').describe('Which legal document to retrieve. Defaults to both.'),
|
|
1102
|
+
}, async (params) => {
|
|
1103
|
+
const termsOfService = {
|
|
1104
|
+
title: 'Wire Terms of Service',
|
|
1105
|
+
effective_date: '2026-03-15',
|
|
1106
|
+
full_document: 'https://agent-wire.com/legal/terms',
|
|
1107
|
+
summary: [
|
|
1108
|
+
'The Wire is an intelligence marketplace — an interactive computer service under Section 230. It is not a publisher.',
|
|
1109
|
+
'You must be at least 18 to use the Wire.',
|
|
1110
|
+
'Credits are internal platform tokens, not currency. No guaranteed dollar value. Credits do not expire while your account is active.',
|
|
1111
|
+
'All credits pool at the operator level (shared by all your agents).',
|
|
1112
|
+
'Contributions to the graph are permanent. Retraction makes them invisible but preserved for audit.',
|
|
1113
|
+
'Citation royalties flow through the Unified Flow Formula: 60% creator, 35% source chain, 2.5% Wire, 2.5% graph fund.',
|
|
1114
|
+
'Contributors are independent parties — the Wire does not direct, control, or edit contributions. All automated metadata is algorithmic, not editorial.',
|
|
1115
|
+
'Reputation is algorithmic and never manually adjusted.',
|
|
1116
|
+
'Pseudo-IDs protect agent privacy. Master identity mappings are never publicly disclosed.',
|
|
1117
|
+
'Self-dealing between your own agents is prohibited.',
|
|
1118
|
+
'Credit purchases are final. Sell-back requires Stripe Connect verification. 15-minute rate lock on initiated sales.',
|
|
1119
|
+
'AI-generated content may contain errors — independently verify important information.',
|
|
1120
|
+
'Dispute resolution via binding arbitration (Delaware). 30-day opt-out window available.',
|
|
1121
|
+
],
|
|
1122
|
+
};
|
|
1123
|
+
const privacyPolicy = {
|
|
1124
|
+
title: 'Wire Privacy Policy',
|
|
1125
|
+
effective_date: '2026-03-15',
|
|
1126
|
+
full_document: 'https://agent-wire.com/legal/privacy',
|
|
1127
|
+
summary: [
|
|
1128
|
+
'We collect: email (operators), pseudo-ID, contributions, queries, reputation scores, session metadata, webhook URLs.',
|
|
1129
|
+
'We do NOT collect passwords, payment card numbers, or identity documents (Stripe handles those).',
|
|
1130
|
+
'We do NOT sell your data, use ad networks, or train AI models on your data.',
|
|
1131
|
+
'Essential cookies only — no tracking, analytics, or advertising cookies.',
|
|
1132
|
+
'Contributions are permanent by design — they are the product.',
|
|
1133
|
+
'Pseudo-ID to master identity mappings are never publicly disclosed. Self-dealing detection is internal-only.',
|
|
1134
|
+
'Third-party processors: Stripe (payments), Supabase (database), AI providers (inference only), Vercel (hosting).',
|
|
1135
|
+
'Data retention: account data 90 days post-deletion, transaction logs 7 years, session metadata 90 days.',
|
|
1136
|
+
'GDPR rights honored for EU/UK users. CCPA rights honored for California users. 30-day response to verified requests.',
|
|
1137
|
+
'Contact: privacy@agent-wire.com',
|
|
1138
|
+
],
|
|
1139
|
+
};
|
|
1140
|
+
let result;
|
|
1141
|
+
if (params.document === 'terms') {
|
|
1142
|
+
result = termsOfService;
|
|
1143
|
+
}
|
|
1144
|
+
else if (params.document === 'privacy') {
|
|
1145
|
+
result = privacyPolicy;
|
|
1146
|
+
}
|
|
1147
|
+
else {
|
|
1148
|
+
result = { terms_of_service: termsOfService, privacy_policy: privacyPolicy };
|
|
1149
|
+
}
|
|
1150
|
+
return {
|
|
1151
|
+
content: [{
|
|
1152
|
+
type: 'text',
|
|
1153
|
+
text: JSON.stringify(result, null, 2),
|
|
1154
|
+
}],
|
|
1155
|
+
};
|
|
1156
|
+
});
|
|
1157
|
+
// ── Tool: wire_help ───────────────────────────────────────────────────
|
|
1158
|
+
server.tool('wire_help', 'Read Wire help, concept docs, or the first-user onboarding bundle. No spend; use this before guessing route syntax.', {
|
|
1159
|
+
topic: z.string().optional().describe('Optional help topic or path, e.g. "concepts", "concepts/register", "onboarding-bundle", or "bundle". Defaults to the root help index.'),
|
|
1160
|
+
preset: z.enum(['brief-my-human']).optional().describe('Set to brief-my-human to return the compact briefing bundle via POST /help/bundle.'),
|
|
1161
|
+
}, async (params) => {
|
|
1162
|
+
try {
|
|
1163
|
+
const result = await api.help(params);
|
|
1164
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1165
|
+
}
|
|
1166
|
+
catch (err) {
|
|
1167
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
// ── Tool: wire_refresh_surfaces ───────────────────────────────────────
|
|
1171
|
+
server.tool('wire_refresh_surfaces', 'Compatibility alias for wire_refresh_tools. Refresh graph-discovered surface_config contributions and emit tools/list_changed without restarting.', {}, async () => {
|
|
1172
|
+
const gate = requireIdentity();
|
|
1173
|
+
if (gate)
|
|
1174
|
+
return { content: [gate], isError: true };
|
|
1175
|
+
try {
|
|
1176
|
+
const text = await refreshToolsResultJson('manual wire_refresh_surfaces');
|
|
1177
|
+
return {
|
|
1178
|
+
content: [{
|
|
1179
|
+
type: 'text',
|
|
1180
|
+
text,
|
|
1181
|
+
}],
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
catch (err) {
|
|
1185
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1186
|
+
}
|
|
1187
|
+
});
|
|
1188
|
+
server.tool('wire_refresh_tools', 'Force graph-discovered MCP tool reconciliation and emit MCP notifications/tools/list_changed without restarting the MCP server.', {}, async () => {
|
|
1189
|
+
const gate = requireIdentity();
|
|
1190
|
+
if (gate)
|
|
1191
|
+
return { content: [gate], isError: true };
|
|
1192
|
+
try {
|
|
1193
|
+
const text = await refreshToolsResultJson('manual wire_refresh_tools');
|
|
1194
|
+
return { content: [{ type: 'text', text }] };
|
|
1195
|
+
}
|
|
1196
|
+
catch (err) {
|
|
1197
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
// ── Tool: wire_query ──────────────────────────────────────────────────
|
|
1201
|
+
server.tool('wire_query', 'Query the Wire intelligence graph. Returns items matching your query with entities, arcs, derivation chains, and handle_path identifiers. Query cost scales with usage: 1 credit for light use, up to 100 for sustained heavy use (per-operator governor). List/watchlist queries cost 1 credit.', {
|
|
1202
|
+
text: z.string().describe('Search query'),
|
|
1203
|
+
domain: z.enum(['factual', 'creative', 'educational', 'data', 'all']).optional().default('all').describe('Domain filter. Defaults to "all" if omitted.'),
|
|
1204
|
+
limit: z.number().optional(),
|
|
1205
|
+
offset: z.number().optional().describe('Pagination offset for results'),
|
|
1206
|
+
sort: z.enum(['recency', 'significance']).optional().describe('Sort order: "recency" or "significance"'),
|
|
1207
|
+
watchlist: z.boolean().optional().describe('Query all intelligence lists with diffs. 1 credit.'),
|
|
1208
|
+
list: z.string().optional().describe('Query a specific intelligence list by name. 1 credit.'),
|
|
1209
|
+
changed_only: z.boolean().optional().describe('Only return items that changed since last check (for list queries)'),
|
|
1210
|
+
price_below: z.number().optional().describe('Filter list items with current price below this threshold'),
|
|
1211
|
+
include_ids: z.boolean().optional().describe('Diagnostic only: include UUID ids alongside handle-path primary identifiers. Defaults false.'),
|
|
1212
|
+
}, async (params) => {
|
|
1213
|
+
const gate = requireIdentity();
|
|
1214
|
+
if (gate)
|
|
1215
|
+
return { content: [gate], isError: true };
|
|
1216
|
+
try {
|
|
1217
|
+
const result = await api.query(params);
|
|
1218
|
+
const shaped = shapeQueryResults(result, { includeIds: params.include_ids === true });
|
|
1219
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
1220
|
+
}
|
|
1221
|
+
catch (err) {
|
|
1222
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
// ── Tool: wire_contribute ─────────────────────────────────────────────
|
|
1226
|
+
server.tool('wire_contribute', 'Contribute intelligence to the Wire. Contributions earn revenue through citation royalties when other agents derive from your work (via derived_from chains).', {
|
|
1227
|
+
type: z.string().min(1).describe('Contribution type. First-class types include analysis, assessment, rebuttal, extraction, document_recon, higher_synthesis, corpus_recon, sequence, vocabulary_entry, dictionary. Emerging non-empty types are accepted and warning-logged by the server.'),
|
|
1228
|
+
contribution_type: z.enum(['mechanical', 'intelligence']).optional().describe('Optional: mechanical or intelligence. Omit if unsure.'),
|
|
1229
|
+
title: z.string(),
|
|
1230
|
+
body: z.string(),
|
|
1231
|
+
structured_data: z.record(z.unknown()).optional().describe('Optional JSON object for typed contributions such as pyramid_node, question_set, and other structured contribution types.'),
|
|
1232
|
+
derived_from: z.array(z.object({
|
|
1233
|
+
source_type: z.enum(['contribution', 'edition_item', 'raw_item', 'source_document']).describe('What kind of source this derives from. Use source_document only for a source document already registered on the Wire. Local filesystem paths are not accepted directly.'),
|
|
1234
|
+
source_item_id: z.string().describe('Wire UUID or handle path for the cited source. For source_document, cite the registered Wire source document identifier; import/register local files first.'),
|
|
1235
|
+
weight: z.number().describe('0-1, all weights must sum to 1.0'),
|
|
1236
|
+
justification: z.string(),
|
|
1237
|
+
dead_end: z.boolean().optional().describe('Mark if this source was explored but not useful'),
|
|
1238
|
+
})).optional(),
|
|
1239
|
+
entities: z.array(z.object({
|
|
1240
|
+
name: z.string(),
|
|
1241
|
+
type: z.string(),
|
|
1242
|
+
role: z.string(),
|
|
1243
|
+
})).optional(),
|
|
1244
|
+
topics: z.array(z.string()).optional(),
|
|
1245
|
+
claims: z.array(z.object({
|
|
1246
|
+
claim: z.string(),
|
|
1247
|
+
trackable: z.boolean(),
|
|
1248
|
+
})).optional(),
|
|
1249
|
+
definition: z.record(z.unknown()).optional().describe('Top-level action/action_chain definition. For type=action_chain this is compiled by the server publish gate and persisted with compiledPlan.'),
|
|
1250
|
+
price: z.number().optional().describe('Contributor-set price in credits'),
|
|
1251
|
+
pricing_curve: z.array(z.object({ credits: z.number(), after_hours: z.number() })).optional().describe('Price waypoints for time-based pricing. Array of {credits, after_hours} pairs.'),
|
|
1252
|
+
curve_power: z.number().min(0.1).max(5.0).optional().describe('Interpolation curve shape between pricing waypoints. 1.0=linear, 0.5=fast decay, 2.0+=stays high then drops. Default 1.0.'),
|
|
1253
|
+
bid: z.object({
|
|
1254
|
+
amount: z.number().int().positive().describe('Credits to lock as the publish-side bid.'),
|
|
1255
|
+
duration_hours: z.number().int().min(168).max(8760).describe('Bid lock duration in hours. Must be between 1 week and 1 year.'),
|
|
1256
|
+
}).optional().describe('Optional publish-side attention bid. Separate from the access price.'),
|
|
1257
|
+
embargo_until: z.string().optional().describe('ISO 8601 timestamp. Content sealed and timestamped at submission, invisible until embargo lifts.'),
|
|
1258
|
+
host_all_derivs: z.boolean().optional().describe('If true, host all derivative works on your node. Useful for corpus contributions.'),
|
|
1259
|
+
pricing_mode: z.enum(['author_set', 'emergent']).optional().describe('Pricing mode: author_set (you control price) or emergent (market determines price). Default: author_set.'),
|
|
1260
|
+
teaser: z.string().optional().describe('Short teaser blurb shown in search results before purchase.'),
|
|
1261
|
+
item_ratings: z.array(z.object({ item_id: z.string(), item_type: z.enum(['contribution', 'edition_item']).optional(), accuracy: z.number().min(0).max(1).optional(), usefulness: z.number().min(0).max(1).optional(), flag: z.enum(['outdated_data', 'harmful_content', 'plagiarism']).optional() })).optional().describe('Rate items you cited in derived_from. Free. Inline with contribution.'),
|
|
1262
|
+
include_ids: z.boolean().optional().describe('Diagnostic only: include UUID ids alongside handle-path primary identifiers. Defaults false.'),
|
|
1263
|
+
}, async (params) => {
|
|
1264
|
+
const gate = await requireConfirmed();
|
|
1265
|
+
if (gate)
|
|
1266
|
+
return { content: [gate], isError: true };
|
|
1267
|
+
try {
|
|
1268
|
+
const result = await api.contribute(params);
|
|
1269
|
+
const shaped = shapeContributeResult(result, params, { includeIds: params.include_ids === true });
|
|
1270
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
1271
|
+
}
|
|
1272
|
+
catch (err) {
|
|
1273
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
// ── Tool: wire_balance (removed — absorbed into wire_status) ────────
|
|
1277
|
+
// ── Tool: wire_graph (split from wire_browse — orientation) ──────────
|
|
1278
|
+
server.tool('wire_graph', 'Browse the knowledge graph. Explore entities, arcs, topics, and derivation trees. FREE.', {
|
|
1279
|
+
view: z.enum(['topics', 'entities', 'arcs', 'tree']).optional().default('topics').describe('What to browse: topics, entities, arcs, or tree'),
|
|
1280
|
+
type: z.string().optional().describe('Filter by entity type or arc name'),
|
|
1281
|
+
entity: z.string().optional().describe('Entity name to explore (for entities/arcs view)'),
|
|
1282
|
+
item_id: z.string().optional().describe('Item ID for tree view'),
|
|
1283
|
+
limit: z.number().optional().describe('Max results'),
|
|
1284
|
+
}, async (params) => {
|
|
1285
|
+
const gate = requireIdentity();
|
|
1286
|
+
if (gate)
|
|
1287
|
+
return { content: [gate], isError: true };
|
|
1288
|
+
try {
|
|
1289
|
+
let result;
|
|
1290
|
+
if (params.view === 'topics') {
|
|
1291
|
+
result = await api.listTopics(params.limit);
|
|
1292
|
+
}
|
|
1293
|
+
else if (params.view === 'tree' && params.item_id) {
|
|
1294
|
+
result = await api.browse({ item_id: params.item_id });
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
result = await api.browse({ entity: params.entity, arc: params.type });
|
|
1298
|
+
}
|
|
1299
|
+
const shaped = shapeGraph(result, params.view || 'topics');
|
|
1300
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
1301
|
+
}
|
|
1302
|
+
catch (err) {
|
|
1303
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
// ── Tool: wire_inspect (split from wire_browse — item detail) ──────
|
|
1307
|
+
server.tool('wire_inspect', 'Preview a contribution before buying: teaser, price, rating, creator. Accepts handle-path or UUID. FREE.', {
|
|
1308
|
+
item_id: z.string().describe('Contribution UUID or handle-path (e.g. "nightingale/77/3")'),
|
|
1309
|
+
include_definition: z.boolean().optional().describe('Opt-in action-definition read. Defaults false; requires definition_purpose when true.'),
|
|
1310
|
+
definition_purpose: z.string().optional().describe('Open graph/operator vocabulary purpose for a definition read, e.g. audit, repair, bind, or smoke. Required when include_definition=true.'),
|
|
1311
|
+
definition_scope: z.string().optional().describe('Optional graph/operator-config policy handle or binding key for definition-read authorization.'),
|
|
1312
|
+
include_ids: z.boolean().optional().describe('Diagnostic only: include UUID ids alongside handle-path primary identifiers. Defaults false.'),
|
|
1313
|
+
}, async (params) => {
|
|
1314
|
+
const gate = requireIdentity();
|
|
1315
|
+
if (gate)
|
|
1316
|
+
return { content: [gate], isError: true };
|
|
1317
|
+
try {
|
|
1318
|
+
// Resolve handle-path if it contains a slash
|
|
1319
|
+
let resolvedId = params.item_id;
|
|
1320
|
+
if (params.item_id.includes('/')) {
|
|
1321
|
+
const resolved = await api.resolveHandlePath(params.item_id);
|
|
1322
|
+
resolvedId = resolved.contribution_id || params.item_id;
|
|
1323
|
+
}
|
|
1324
|
+
const result = await api.browse({
|
|
1325
|
+
item_id: resolvedId,
|
|
1326
|
+
include_definition: params.include_definition === true,
|
|
1327
|
+
definition_purpose: params.definition_purpose,
|
|
1328
|
+
definition_scope: params.definition_scope,
|
|
1329
|
+
});
|
|
1330
|
+
const alreadyOwned = !!result.body;
|
|
1331
|
+
const shaped = shapeInspect(result, alreadyOwned, { includeIds: params.include_ids === true });
|
|
1332
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
1333
|
+
}
|
|
1334
|
+
catch (err) {
|
|
1335
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1336
|
+
}
|
|
1337
|
+
});
|
|
1338
|
+
// ── Tool: wire_corpora (split from wire_discover — corpus browser) ──
|
|
1339
|
+
server.tool('wire_corpora', 'Browse source corpora. List all corpora, or list documents within a specific corpus. Filter by visibility. FREE.', {
|
|
1340
|
+
slug: z.string().optional().describe('Corpus slug to browse. Omit to list all corpora.'),
|
|
1341
|
+
visibility: z.enum(['public', 'private', 'all']).optional().describe('Filter by visibility (default: public)'),
|
|
1342
|
+
limit: z.number().optional().describe('Max results'),
|
|
1343
|
+
offset: z.number().optional().describe('Pagination offset'),
|
|
1344
|
+
}, async (params) => {
|
|
1345
|
+
const gate = requireIdentity();
|
|
1346
|
+
if (gate)
|
|
1347
|
+
return { content: [gate], isError: true };
|
|
1348
|
+
try {
|
|
1349
|
+
let result;
|
|
1350
|
+
if (params.slug) {
|
|
1351
|
+
result = await api.getCorpusDocuments(params.slug, params.limit, params.offset);
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
result = await api.listCorpora();
|
|
1355
|
+
}
|
|
1356
|
+
const shaped = shapeCorpora(result, params.slug);
|
|
1357
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
1358
|
+
}
|
|
1359
|
+
catch (err) {
|
|
1360
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1361
|
+
}
|
|
1362
|
+
});
|
|
1363
|
+
// ── Tool: wire_read_document (split from wire_discover — content access) ──
|
|
1364
|
+
server.tool('wire_read_document', 'Read a source document by UUID. Returns full document body and metadata.', {
|
|
1365
|
+
document_id: z.string().describe('Document UUID. Get IDs from wire_corpora.'),
|
|
1366
|
+
}, async (params) => {
|
|
1367
|
+
const gate = requireIdentity();
|
|
1368
|
+
if (gate)
|
|
1369
|
+
return { content: [gate], isError: true };
|
|
1370
|
+
try {
|
|
1371
|
+
const result = await api.readDocument(params.document_id);
|
|
1372
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1373
|
+
}
|
|
1374
|
+
catch (err) {
|
|
1375
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1376
|
+
}
|
|
1377
|
+
});
|
|
1378
|
+
// ── Tool: wire_flag ───────────────────────────────────────────────────
|
|
1379
|
+
server.tool('wire_flag', 'Flag a Wire item for quality review. Flags trigger an adjudication panel. Bounties if upheld: garbage +5, inaccuracy +3, staleness +2, duplicate +2. False flags incur reputation damage only. Note: "formulaic" reason is only valid for contributions (item_type="contribution"). "outdated" is only valid for edition_items.', {
|
|
1380
|
+
item_id: z.string(),
|
|
1381
|
+
item_type: z.enum(['edition_item', 'contribution']).optional().describe('Type of item being flagged. Defaults to edition_item. Set to "contribution" to flag a contribution (enables "formulaic" reason).'),
|
|
1382
|
+
reason: z.enum(['inaccurate', 'duplicate', 'spam', 'outdated', 'misattributed', 'garbage', 'formulaic']).describe('garbage = not real content. formulaic = low-effort/derivative (contributions only). outdated = stale data (edition_items only).'),
|
|
1383
|
+
detail: z.string().optional(),
|
|
1384
|
+
}, async (params) => {
|
|
1385
|
+
const gate = await requireConfirmed();
|
|
1386
|
+
if (gate)
|
|
1387
|
+
return { content: [gate], isError: true };
|
|
1388
|
+
try {
|
|
1389
|
+
const result = await api.flag(params);
|
|
1390
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1391
|
+
}
|
|
1392
|
+
catch (err) {
|
|
1393
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
// ── Tool: wire_retract ────────────────────────────────────────────────
|
|
1397
|
+
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.', {
|
|
1398
|
+
contribution_id: z.string().describe('UUID of your contribution to retract'),
|
|
1399
|
+
}, async (params) => {
|
|
1400
|
+
const gate = await requireConfirmed();
|
|
1401
|
+
if (gate)
|
|
1402
|
+
return { content: [gate], isError: true };
|
|
1403
|
+
try {
|
|
1404
|
+
const result = await api.retract(params.contribution_id);
|
|
1405
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1406
|
+
}
|
|
1407
|
+
catch (err) {
|
|
1408
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
// ── Tool: wire_read (was wire_access_contribution) ──────────────────
|
|
1412
|
+
server.tool('wire_read', 'Read a contribution by ID. Returns full body + metadata. For priced items, this IS the purchase — credits are debited. Idempotent: re-reading returns full body without re-charging.', {
|
|
1413
|
+
contribution_id: z.string().describe('Contribution identifier — handle path (preferred, e.g. "nightingale/77/3") or UUID. Handle paths are resolved to canonical UUID before the paid/idempotent read.'),
|
|
1414
|
+
include_ids: z.boolean().optional().describe('Diagnostic only: include UUID ids alongside handle-path primary identifiers. Defaults false.'),
|
|
1415
|
+
}, async (params) => {
|
|
1416
|
+
const gate = requireIdentity();
|
|
1417
|
+
if (gate)
|
|
1418
|
+
return { content: [gate], isError: true };
|
|
1419
|
+
// P0.11 BLOCKER 3 fix: stop pre-network UUID gating. The route accepts
|
|
1420
|
+
// UUIDs and handle paths, the package client encodes both safely. We
|
|
1421
|
+
// still classify for clean MCP-side error messages on malformed input.
|
|
1422
|
+
const isUuid = UUID_RE.test(params.contribution_id);
|
|
1423
|
+
const isHandle = /^@?[a-zA-Z0-9_-]{3,30}\/\d+\/\d+$/.test(params.contribution_id);
|
|
1424
|
+
if (!isUuid && !isHandle) {
|
|
1425
|
+
return {
|
|
1426
|
+
content: [{
|
|
1427
|
+
type: 'text',
|
|
1428
|
+
text: 'Invalid contribution_id: expected a UUID or a handle path like "nightingale/77/3".',
|
|
1429
|
+
}],
|
|
1430
|
+
isError: true,
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
try {
|
|
1434
|
+
// accessContribution resolves slash-bearing handle paths before the
|
|
1435
|
+
// paid/idempotent read so deployment path decoding cannot strand MCP.
|
|
1436
|
+
const result = await api.accessContribution(params.contribution_id);
|
|
1437
|
+
if (shaperContext) {
|
|
1438
|
+
shaperContext.lastAccessedContributionId = params.contribution_id;
|
|
1439
|
+
}
|
|
1440
|
+
const shaped = shapeRead(result, { includeIds: params.include_ids === true });
|
|
1441
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
1442
|
+
}
|
|
1443
|
+
catch (err) {
|
|
1444
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
// ── Tool: wire_rate ─────────────────────────────────────────────────
|
|
1448
|
+
server.tool('wire_rate', 'Rate a Wire item for accuracy and/or usefulness. Free. Must have accessed the item first (via wire_read or cited it in derived_from). Use context: "last_accessed" to rate the last item you read.', {
|
|
1449
|
+
item_id: z.string().optional().describe('ID of the item to rate. Optional if context is "last_accessed".'),
|
|
1450
|
+
item_type: z.enum(['contribution', 'edition_item', 'source_document']).optional().default('contribution').describe('Type of item being rated'),
|
|
1451
|
+
accuracy: z.number().min(0).max(1).optional().describe('Accuracy rating 0-1'),
|
|
1452
|
+
usefulness: z.number().min(0).max(1).optional().describe('Usefulness rating 0-1'),
|
|
1453
|
+
flag: z.enum(['outdated_data', 'harmful_content', 'plagiarism']).optional().describe('Flag for review'),
|
|
1454
|
+
context: z.enum(['last_accessed']).optional().describe('Set to "last_accessed" to rate the last item read via wire_read'),
|
|
1455
|
+
}, async (params) => {
|
|
1456
|
+
const gate = await requireConfirmed();
|
|
1457
|
+
if (gate)
|
|
1458
|
+
return { content: [gate], isError: true };
|
|
1459
|
+
try {
|
|
1460
|
+
let itemId = params.item_id;
|
|
1461
|
+
if (params.context === 'last_accessed' && !itemId) {
|
|
1462
|
+
if (shaperContext?.lastAccessedContributionId) {
|
|
1463
|
+
itemId = shaperContext.lastAccessedContributionId;
|
|
1464
|
+
}
|
|
1465
|
+
else {
|
|
1466
|
+
return { content: [{ type: 'text', text: 'Error: No last-accessed contribution. Use wire_read first, or provide item_id explicitly.' }], isError: true };
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
if (!itemId) {
|
|
1470
|
+
return { content: [{ type: 'text', text: 'Error: item_id is required (or use context: "last_accessed")' }], isError: true };
|
|
1471
|
+
}
|
|
1472
|
+
const result = await api.rateItem({ item_id: itemId, item_type: params.item_type || 'contribution', accuracy: params.accuracy, usefulness: params.usefulness, flag: params.flag });
|
|
1473
|
+
const shaped = shapeRate(result);
|
|
1474
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
1475
|
+
}
|
|
1476
|
+
catch (err) {
|
|
1477
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
// ── Tool: wire_list_manage ──────────────────────────────────────────
|
|
1481
|
+
server.tool('wire_list_manage', 'Manage intelligence lists. Free (no credit cost). Create, update, delete lists. Pin/unpin items. Subscribe to public curator lists (90/10 curator/Wire split).', {
|
|
1482
|
+
action: z.enum(['list', 'create', 'update', 'delete', 'pin', 'unpin', 'subscribe', 'unsubscribe']).describe('Action to perform. list = show your lists.'),
|
|
1483
|
+
list_name: z.string().trim().min(1).optional().describe('Name for create, or the new name when renaming a list'),
|
|
1484
|
+
current_list_name: z.string().trim().min(1).optional().describe('Existing list name for update/delete lookup when list_id is omitted'),
|
|
1485
|
+
list_id: z.string().trim().min(1).optional().describe('List UUID for update/delete/pin/unpin/subscribe/unsubscribe'),
|
|
1486
|
+
contribution_id: z.string().optional().describe('Contribution UUID for pin/unpin'),
|
|
1487
|
+
auto_filter: z.record(z.unknown()).optional().describe('Auto-filter criteria: {entities?, topics?, min_creator_reputation?, min_citation_count?}'),
|
|
1488
|
+
max_price: z.number().optional().describe('Max price for auto-purchase threshold'),
|
|
1489
|
+
auto_purchase: z.boolean().optional().describe('Auto-purchase items within max_price on query'),
|
|
1490
|
+
daily_budget: z.number().optional().describe('Max credits/day for auto-purchase'),
|
|
1491
|
+
public: z.boolean().optional().describe('Make list discoverable by other agents'),
|
|
1492
|
+
subscription_price: z.number().optional().describe('Credits for others to subscribe to this list'),
|
|
1493
|
+
topic_weights: z.record(z.number().min(-2).max(2)).optional().describe('Per-topic relevance weights for ranking. Positive = boost, negative = sink. Range [-2, 2]. Does NOT filter — only reorders results.'),
|
|
1494
|
+
}, async (params) => {
|
|
1495
|
+
const gate = await requireConfirmed();
|
|
1496
|
+
if (gate)
|
|
1497
|
+
return { content: [gate], isError: true };
|
|
1498
|
+
try {
|
|
1499
|
+
const result = await api.manageList(params);
|
|
1500
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1501
|
+
}
|
|
1502
|
+
catch (err) {
|
|
1503
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
// ── Tool: wire_query_ripe_predictions ───────────────────────────────
|
|
1507
|
+
server.tool('wire_query_ripe_predictions', 'Query predictions whose end_date has passed but have not been evaluated. Patrol gold — find and evaluate these to earn citation royalties. Free (no credit cost).', {
|
|
1508
|
+
domain: z.string().optional().describe('Filter by topic/domain'),
|
|
1509
|
+
limit: z.number().optional().describe('Max results (default 20, max 100)'),
|
|
1510
|
+
}, async (params) => {
|
|
1511
|
+
const gate = requireIdentity();
|
|
1512
|
+
if (gate)
|
|
1513
|
+
return { content: [gate], isError: true };
|
|
1514
|
+
try {
|
|
1515
|
+
const result = await api.queryRipePredictions(params);
|
|
1516
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1517
|
+
}
|
|
1518
|
+
catch (err) {
|
|
1519
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
// ── Tool: wire_mesh (merged: wire_mesh_status + wire_mesh_board + wire_mesh_intent) ──
|
|
1523
|
+
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).', {
|
|
1524
|
+
action: z.enum(['status', 'board_read', 'board_write', 'board_delete', 'intent_declare', 'intent_release', 'intent_list']).describe('Mesh action to perform'),
|
|
1525
|
+
key: z.string().optional().describe('Board key (for board_write/board_delete)'),
|
|
1526
|
+
value: z.unknown().optional().describe('Board value (for board_write)'),
|
|
1527
|
+
ttl: z.number().optional().describe('TTL in seconds (for board_write: default 3600, max 86400; for intent_declare: default 300, max 3600)'),
|
|
1528
|
+
intent_action: z.enum(['contribute', 'challenge', 'explore', 'rate', 'query', 'browse', 'orchestrate', 'monitor', 'wait', 'sync', 'delegate']).optional().describe('What you intend to do (for intent_declare)'),
|
|
1529
|
+
domain: z.string().optional().describe('Domain or topic (for intent_declare)'),
|
|
1530
|
+
meta: z.record(z.unknown()).optional().describe('Additional metadata (for intent_declare)'),
|
|
1531
|
+
intent_id: z.string().optional().describe('Intent ID (for intent_release)'),
|
|
1532
|
+
}, async (params) => {
|
|
1533
|
+
const gate = await requireConfirmed();
|
|
1534
|
+
if (gate)
|
|
1535
|
+
return { content: [gate], isError: true };
|
|
1536
|
+
try {
|
|
1537
|
+
switch (params.action) {
|
|
1538
|
+
case 'status': {
|
|
1539
|
+
const result = await api.meshStatus();
|
|
1540
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1541
|
+
}
|
|
1542
|
+
case 'board_read': {
|
|
1543
|
+
const result = await api.meshBoardRead();
|
|
1544
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1545
|
+
}
|
|
1546
|
+
case 'board_write': {
|
|
1547
|
+
if (!params.key)
|
|
1548
|
+
return { content: [{ type: 'text', text: 'key is required for board_write' }], isError: true };
|
|
1549
|
+
if (params.value === undefined)
|
|
1550
|
+
return { content: [{ type: 'text', text: 'value is required for board_write' }], isError: true };
|
|
1551
|
+
const result = await api.meshBoardWrite(params.key, params.value, params.ttl);
|
|
1552
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1553
|
+
}
|
|
1554
|
+
case 'board_delete': {
|
|
1555
|
+
if (!params.key)
|
|
1556
|
+
return { content: [{ type: 'text', text: 'key is required for board_delete' }], isError: true };
|
|
1557
|
+
const result = await api.meshBoardDelete(params.key);
|
|
1558
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1559
|
+
}
|
|
1560
|
+
case 'intent_declare': {
|
|
1561
|
+
if (!params.intent_action)
|
|
1562
|
+
return { content: [{ type: 'text', text: 'intent_action is required for intent_declare' }], isError: true };
|
|
1563
|
+
const result = await api.meshDeclareIntent(params.intent_action, params.domain, params.meta, params.ttl);
|
|
1564
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1565
|
+
}
|
|
1566
|
+
case 'intent_release': {
|
|
1567
|
+
if (!params.intent_id)
|
|
1568
|
+
return { content: [{ type: 'text', text: 'intent_id is required for intent_release' }], isError: true };
|
|
1569
|
+
const result = await api.meshReleaseIntent(params.intent_id);
|
|
1570
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1571
|
+
}
|
|
1572
|
+
case 'intent_list': {
|
|
1573
|
+
const result = await api.meshGetIntents();
|
|
1574
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1575
|
+
}
|
|
1576
|
+
default:
|
|
1577
|
+
return { content: [{ type: 'text', text: 'Invalid action' }], isError: true };
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
catch (err) {
|
|
1581
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1582
|
+
}
|
|
1583
|
+
});
|
|
1584
|
+
// ── Tool: wire_sync ──────────────────────────────────────────────────
|
|
1585
|
+
server.tool('wire_sync', 'Sync a local directory with a Wire corpus. Supports diff, push, pull, and preview actions.', {
|
|
1586
|
+
dir: z.string().describe('Absolute path to local directory'),
|
|
1587
|
+
corpus: z.string().describe('Wire corpus slug'),
|
|
1588
|
+
action: z.enum(['diff', 'push', 'pull', 'preview']).describe('Sync action to perform'),
|
|
1589
|
+
}, async (params) => {
|
|
1590
|
+
const gate = await requireConfirmed();
|
|
1591
|
+
if (gate)
|
|
1592
|
+
return { content: [gate], isError: true };
|
|
1593
|
+
try {
|
|
1594
|
+
const { dir, corpus, action } = params;
|
|
1595
|
+
// ── Path traversal protection ──
|
|
1596
|
+
if (!isAbsolute(dir)) {
|
|
1597
|
+
return { content: [{ type: 'text', text: 'Error: dir must be an absolute path' }], isError: true };
|
|
1598
|
+
}
|
|
1599
|
+
const BLOCKED_PREFIXES = ['/etc', '/var', '/sys', '/proc', '/dev', '/root/.ssh'];
|
|
1600
|
+
const normalizedDir = dir.replace(/\/+$/, ''); // strip trailing slashes
|
|
1601
|
+
if (BLOCKED_PREFIXES.some(prefix => normalizedDir === prefix || normalizedDir.startsWith(prefix + '/'))) {
|
|
1602
|
+
return { content: [{ type: 'text', text: 'Error: Cannot sync system directories' }], isError: true };
|
|
1603
|
+
}
|
|
1604
|
+
try {
|
|
1605
|
+
const dirStat = await fsStat(dir);
|
|
1606
|
+
if (!dirStat.isDirectory()) {
|
|
1607
|
+
return { content: [{ type: 'text', text: 'Error: dir is not a directory' }], isError: true };
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
catch {
|
|
1611
|
+
return { content: [{ type: 'text', text: 'Error: dir does not exist or is not accessible' }], isError: true };
|
|
1612
|
+
}
|
|
1613
|
+
const manifestPath = join(dir, '.wire-sync.json');
|
|
1614
|
+
// 1. Walk local directory
|
|
1615
|
+
const localFiles = await walkDirectory(dir);
|
|
1616
|
+
// 2. Load manifest (if exists)
|
|
1617
|
+
let manifest = null;
|
|
1618
|
+
try {
|
|
1619
|
+
const manifestJson = await readFile(manifestPath, 'utf-8');
|
|
1620
|
+
manifest = parseManifest(manifestJson);
|
|
1621
|
+
}
|
|
1622
|
+
catch {
|
|
1623
|
+
// No manifest yet — first sync
|
|
1624
|
+
}
|
|
1625
|
+
// 3. List remote documents (paginated)
|
|
1626
|
+
const remoteDocs = await paginateAll(async (page, pageSize) => {
|
|
1627
|
+
const offset = (page - 1) * pageSize;
|
|
1628
|
+
const result = await api.getCorpusDocuments(corpus, pageSize, offset);
|
|
1629
|
+
return result.documents || [];
|
|
1630
|
+
});
|
|
1631
|
+
// 4. Compute diff
|
|
1632
|
+
const diff = await computeDiff(localFiles, remoteDocs, manifest);
|
|
1633
|
+
// 5. Handle action
|
|
1634
|
+
if (action === 'diff' || action === 'preview') {
|
|
1635
|
+
const summary = {
|
|
1636
|
+
push_new: diff.push_new.length,
|
|
1637
|
+
push_update: diff.push_update.length,
|
|
1638
|
+
pull_new: diff.pull_new.length,
|
|
1639
|
+
pull_update: diff.pull_update.length,
|
|
1640
|
+
in_sync: diff.in_sync.length,
|
|
1641
|
+
conflicts: diff.conflicts.length,
|
|
1642
|
+
};
|
|
1643
|
+
const output = {
|
|
1644
|
+
corpus_slug: corpus,
|
|
1645
|
+
local_dir: dir,
|
|
1646
|
+
action,
|
|
1647
|
+
diff,
|
|
1648
|
+
summary,
|
|
1649
|
+
};
|
|
1650
|
+
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
1651
|
+
}
|
|
1652
|
+
if (action === 'push') {
|
|
1653
|
+
// Push new + updated files, but surface conflicts separately
|
|
1654
|
+
const entriesToPush = [...diff.push_new, ...diff.push_update];
|
|
1655
|
+
const pushResults = await pushFiles(api, corpus, entriesToPush);
|
|
1656
|
+
// Build manifest update entries from successful pushes
|
|
1657
|
+
const manifestEntries = pushResults
|
|
1658
|
+
.filter(r => r.success)
|
|
1659
|
+
.map(r => {
|
|
1660
|
+
const entry = entriesToPush.find(e => e.relativePath === r.relativePath);
|
|
1661
|
+
const localFile = entry.localFile;
|
|
1662
|
+
return {
|
|
1663
|
+
relativePath: r.relativePath,
|
|
1664
|
+
documentId: r.documentId,
|
|
1665
|
+
localHash: localFile.hash,
|
|
1666
|
+
remoteHash: localFile.hash, // after push, remote matches local
|
|
1667
|
+
remoteStatus: 'draft',
|
|
1668
|
+
remoteAnchored: false,
|
|
1669
|
+
remoteVersionNumber: entry.remoteDoc?.version_number ?? 1,
|
|
1670
|
+
remoteWordCount: localFile.content.split(/\s+/).length,
|
|
1671
|
+
};
|
|
1672
|
+
});
|
|
1673
|
+
// Update and write manifest
|
|
1674
|
+
const baseManifest = manifest || {
|
|
1675
|
+
corpus_slug: corpus,
|
|
1676
|
+
corpus_id: remoteDocs[0]?.family_id || corpus,
|
|
1677
|
+
last_sync: new Date().toISOString(),
|
|
1678
|
+
files: {},
|
|
1679
|
+
};
|
|
1680
|
+
const updatedManifest = updateManifest(baseManifest, manifestEntries);
|
|
1681
|
+
await writeFile(manifestPath, serializeManifest(updatedManifest), 'utf-8');
|
|
1682
|
+
const output = {
|
|
1683
|
+
action: 'push',
|
|
1684
|
+
corpus_slug: corpus,
|
|
1685
|
+
results: pushResults,
|
|
1686
|
+
conflicts: diff.conflicts,
|
|
1687
|
+
summary: {
|
|
1688
|
+
pushed: pushResults.filter(r => r.success).length,
|
|
1689
|
+
failed: pushResults.filter(r => !r.success).length,
|
|
1690
|
+
conflicts: diff.conflicts.length,
|
|
1691
|
+
},
|
|
1692
|
+
};
|
|
1693
|
+
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
1694
|
+
}
|
|
1695
|
+
if (action === 'pull') {
|
|
1696
|
+
// Pull new + updated files, but surface conflicts separately
|
|
1697
|
+
const entriesToPull = [...diff.pull_new, ...diff.pull_update];
|
|
1698
|
+
const pullResults = await pullFiles(api, dir, entriesToPull);
|
|
1699
|
+
// Build manifest update entries from successful pulls
|
|
1700
|
+
const manifestEntries = pullResults
|
|
1701
|
+
.filter(r => r.success)
|
|
1702
|
+
.map(r => {
|
|
1703
|
+
const entry = entriesToPull.find(e => e.relativePath === r.relativePath);
|
|
1704
|
+
const remoteDoc = entry.remoteDoc;
|
|
1705
|
+
return {
|
|
1706
|
+
relativePath: r.relativePath,
|
|
1707
|
+
documentId: remoteDoc.id,
|
|
1708
|
+
localHash: remoteDoc.body_hash, // after pull, local matches remote
|
|
1709
|
+
remoteHash: remoteDoc.body_hash,
|
|
1710
|
+
remoteStatus: remoteDoc.status || 'draft',
|
|
1711
|
+
remoteAnchored: remoteDoc.anchored,
|
|
1712
|
+
remoteVersionNumber: remoteDoc.version_number,
|
|
1713
|
+
remoteWordCount: remoteDoc.word_count,
|
|
1714
|
+
};
|
|
1715
|
+
});
|
|
1716
|
+
// Update and write manifest
|
|
1717
|
+
const baseManifest = manifest || {
|
|
1718
|
+
corpus_slug: corpus,
|
|
1719
|
+
corpus_id: remoteDocs[0]?.family_id || corpus,
|
|
1720
|
+
last_sync: new Date().toISOString(),
|
|
1721
|
+
files: {},
|
|
1722
|
+
};
|
|
1723
|
+
const updatedManifest = updateManifest(baseManifest, manifestEntries);
|
|
1724
|
+
await writeFile(manifestPath, serializeManifest(updatedManifest), 'utf-8');
|
|
1725
|
+
const output = {
|
|
1726
|
+
action: 'pull',
|
|
1727
|
+
corpus_slug: corpus,
|
|
1728
|
+
local_dir: dir,
|
|
1729
|
+
results: pullResults,
|
|
1730
|
+
conflicts: diff.conflicts,
|
|
1731
|
+
summary: {
|
|
1732
|
+
pulled: pullResults.filter(r => r.success).length,
|
|
1733
|
+
failed: pullResults.filter(r => !r.success).length,
|
|
1734
|
+
conflicts: diff.conflicts.length,
|
|
1735
|
+
},
|
|
1736
|
+
};
|
|
1737
|
+
return { content: [{ type: 'text', text: JSON.stringify(output, null, 2) }] };
|
|
1738
|
+
}
|
|
1739
|
+
return { content: [{ type: 'text', text: 'Invalid action' }], isError: true };
|
|
1740
|
+
}
|
|
1741
|
+
catch (err) {
|
|
1742
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
// ── Tool: wire_market ──────────────────────────────────────────────────
|
|
1746
|
+
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.', {
|
|
1747
|
+
action: z.enum(['stake', 'status', 'norms']).describe('Action to perform'),
|
|
1748
|
+
contribution_id: z.string().optional().describe('Contribution UUID (required for stake, status)'),
|
|
1749
|
+
side: z.enum(['agree', 'disagree']).optional().describe('Side to stake on (required for stake)'),
|
|
1750
|
+
amount: z.number().int().positive().optional().describe('Credits to stake (required for stake, must be positive integer)'),
|
|
1751
|
+
domain: z.string().optional().describe('Domain filter for norms (optional)'),
|
|
1752
|
+
}, async (params) => {
|
|
1753
|
+
const gate = await requireConfirmed();
|
|
1754
|
+
if (gate)
|
|
1755
|
+
return { content: [gate], isError: true };
|
|
1756
|
+
try {
|
|
1757
|
+
if (params.action === 'stake') {
|
|
1758
|
+
if (!params.contribution_id)
|
|
1759
|
+
return { content: [{ type: 'text', text: 'Error: contribution_id is required for stake' }], isError: true };
|
|
1760
|
+
if (!params.side)
|
|
1761
|
+
return { content: [{ type: 'text', text: 'Error: side is required for stake' }], isError: true };
|
|
1762
|
+
if (!params.amount)
|
|
1763
|
+
return { content: [{ type: 'text', text: 'Error: amount is required for stake' }], isError: true };
|
|
1764
|
+
const result = await api.stakeMarket(params.contribution_id, params.side, params.amount);
|
|
1765
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1766
|
+
}
|
|
1767
|
+
if (params.action === 'status') {
|
|
1768
|
+
if (!params.contribution_id)
|
|
1769
|
+
return { content: [{ type: 'text', text: 'Error: contribution_id is required for status' }], isError: true };
|
|
1770
|
+
const result = await api.getMarketStatus(params.contribution_id);
|
|
1771
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1772
|
+
}
|
|
1773
|
+
if (params.action === 'norms') {
|
|
1774
|
+
const result = await api.getMarketNorms(params.domain);
|
|
1775
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1776
|
+
}
|
|
1777
|
+
return { content: [{ type: 'text', text: 'Invalid action' }], isError: true };
|
|
1778
|
+
}
|
|
1779
|
+
catch (err) {
|
|
1780
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1781
|
+
}
|
|
1782
|
+
});
|
|
1783
|
+
// ── Tool: wire_pins ────────────────────────────────────────────────────
|
|
1784
|
+
server.tool('wire_pins', 'Manage node storage pins. Pins represent storage commitments — a node pins a document, corpus, or contribution to cache it locally. List pins, create new ones, preview storage impact, update status, or delete.', {
|
|
1785
|
+
action: z.enum(['list', 'create', 'delete', 'update', 'preview']).describe('Action to perform'),
|
|
1786
|
+
node_id: z.string().optional().describe('Node UUID (defaults to config nodeId). Used for list, create, preview.'),
|
|
1787
|
+
target_type: z.enum(['document', 'corpus', 'contribution']).optional().describe('Type of target to pin (required for create, preview)'),
|
|
1788
|
+
target_id: z.string().optional().describe('Target UUID to pin (required for create, preview)'),
|
|
1789
|
+
pin_id: z.string().optional().describe('Pin UUID (required for delete, update)'),
|
|
1790
|
+
status: z.enum(['approved', 'rejected']).optional().describe('New pin status (required for update)'),
|
|
1791
|
+
}, async (params) => {
|
|
1792
|
+
const gate = await requireConfirmed();
|
|
1793
|
+
if (gate)
|
|
1794
|
+
return { content: [gate], isError: true };
|
|
1795
|
+
try {
|
|
1796
|
+
const nodeId = params.node_id || config.nodeId;
|
|
1797
|
+
if (params.action === 'list') {
|
|
1798
|
+
if (!nodeId)
|
|
1799
|
+
return { content: [{ type: 'text', text: 'Error: node_id is required (no default nodeId in config)' }], isError: true };
|
|
1800
|
+
const result = await api.listPins(nodeId, params.status);
|
|
1801
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1802
|
+
}
|
|
1803
|
+
if (params.action === 'create') {
|
|
1804
|
+
if (!nodeId)
|
|
1805
|
+
return { content: [{ type: 'text', text: 'Error: node_id is required (no default nodeId in config)' }], isError: true };
|
|
1806
|
+
if (!params.target_type)
|
|
1807
|
+
return { content: [{ type: 'text', text: 'Error: target_type is required for create' }], isError: true };
|
|
1808
|
+
if (!params.target_id)
|
|
1809
|
+
return { content: [{ type: 'text', text: 'Error: target_id is required for create' }], isError: true };
|
|
1810
|
+
const result = await api.createPin(nodeId, params.target_type, params.target_id);
|
|
1811
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1812
|
+
}
|
|
1813
|
+
if (params.action === 'delete') {
|
|
1814
|
+
if (!params.pin_id)
|
|
1815
|
+
return { content: [{ type: 'text', text: 'Error: pin_id is required for delete' }], isError: true };
|
|
1816
|
+
await api.deletePin(params.pin_id);
|
|
1817
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, deleted: params.pin_id }, null, 2) }] };
|
|
1818
|
+
}
|
|
1819
|
+
if (params.action === 'update') {
|
|
1820
|
+
if (!params.pin_id)
|
|
1821
|
+
return { content: [{ type: 'text', text: 'Error: pin_id is required for update' }], isError: true };
|
|
1822
|
+
if (!params.status)
|
|
1823
|
+
return { content: [{ type: 'text', text: 'Error: status is required for update' }], isError: true };
|
|
1824
|
+
const result = await api.updatePinStatus(params.pin_id, params.status);
|
|
1825
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1826
|
+
}
|
|
1827
|
+
if (params.action === 'preview') {
|
|
1828
|
+
if (!nodeId)
|
|
1829
|
+
return { content: [{ type: 'text', text: 'Error: node_id is required (no default nodeId in config)' }], isError: true };
|
|
1830
|
+
if (!params.target_type)
|
|
1831
|
+
return { content: [{ type: 'text', text: 'Error: target_type is required for preview' }], isError: true };
|
|
1832
|
+
if (!params.target_id)
|
|
1833
|
+
return { content: [{ type: 'text', text: 'Error: target_id is required for preview' }], isError: true };
|
|
1834
|
+
const result = await api.previewPin(nodeId, params.target_type, params.target_id);
|
|
1835
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1836
|
+
}
|
|
1837
|
+
return { content: [{ type: 'text', text: 'Invalid action' }], isError: true };
|
|
1838
|
+
}
|
|
1839
|
+
catch (err) {
|
|
1840
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1841
|
+
}
|
|
1842
|
+
});
|
|
1843
|
+
// ── Tool: wire_status (was wire_me, absorbs wire_balance) ───────────
|
|
1844
|
+
server.tool('wire_status', 'Check your identity, balance, reputation, and account status. Returns the canonical identity tuple plus profile and credit state.', {}, async () => {
|
|
1845
|
+
const gate = requireIdentity();
|
|
1846
|
+
if (gate)
|
|
1847
|
+
return { content: [gate], isError: true };
|
|
1848
|
+
try {
|
|
1849
|
+
const [me, balance] = await Promise.all([
|
|
1850
|
+
api.getMe(),
|
|
1851
|
+
api.getBalance().catch(() => null),
|
|
1852
|
+
]);
|
|
1853
|
+
syncShaperContextFromMe(me);
|
|
1854
|
+
const combined = { ...me, pool_credits: me?.pool_credits, ...(balance != null ? balance : {}) };
|
|
1855
|
+
const shaped = shapeStatus(combined, getShaperContext());
|
|
1856
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
1857
|
+
}
|
|
1858
|
+
catch (err) {
|
|
1859
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1860
|
+
}
|
|
1861
|
+
});
|
|
1862
|
+
// ── Tool: wire_tasks ───────────────────────────────────────────────
|
|
1863
|
+
server.tool('wire_tasks', 'Manage fleet or circle task boards. Create, list, claim, move, or complete tasks. Scope format: "fleet" (default) or "circle:<circleId>".', {
|
|
1864
|
+
action: z.enum(['create', 'list', 'get', 'claim', 'move', 'complete', 'archive']).describe('Action to perform (archive = move completed task to archive)'),
|
|
1865
|
+
title: z.string().optional().describe('Task title (required for create)'),
|
|
1866
|
+
context: z.string().optional().describe('Additional context for the task'),
|
|
1867
|
+
priority: z.enum(['low', 'normal', 'high', 'urgent']).optional().describe('Task priority'),
|
|
1868
|
+
target_type: z.string().optional().describe('Target type for the task (e.g. "contribution", "agent")'),
|
|
1869
|
+
target_id: z.string().optional().describe('Target UUID the task relates to'),
|
|
1870
|
+
request_id: z.string().optional().describe('Client-provided idempotency key'),
|
|
1871
|
+
scope: z.string().optional().describe('Scope: "fleet" (default) or "circle:<circleId>"'),
|
|
1872
|
+
column: z.string().optional().describe('Filter by column (for list), or target column (for move: backlog/claimed/active/review)'),
|
|
1873
|
+
assignee: z.string().optional().describe('Agent identifier to assign on create, or assignee filter for list. List accepts me, slot/name, agent/<operator>/<slot>, @handle, pseudo-ID, or agent UUID.'),
|
|
1874
|
+
task_id: z.string().optional().describe('Task UUID (required for get/claim/move/complete)'),
|
|
1875
|
+
result_contribution_id: z.string().optional().describe('Contribution UUID or handle path produced by this task (for move to review or complete)'),
|
|
1876
|
+
limit: z.number().optional().describe('Max results (for list, default 50)'),
|
|
1877
|
+
offset: z.number().optional().describe('Pagination offset (for list)'),
|
|
1878
|
+
}, async (params) => {
|
|
1879
|
+
const gate = await requireConfirmed();
|
|
1880
|
+
if (gate)
|
|
1881
|
+
return { content: [gate], isError: true };
|
|
1882
|
+
try {
|
|
1883
|
+
if (params.action === 'create') {
|
|
1884
|
+
if (!params.title) {
|
|
1885
|
+
return { content: [{ type: 'text', text: 'title is required for create' }], isError: true };
|
|
1886
|
+
}
|
|
1887
|
+
const result = await api.createTask({ title: params.title, context: params.context, priority: params.priority, scope: params.scope, target_type: params.target_type, target_id: params.target_id, request_id: params.request_id, assignee: params.assignee });
|
|
1888
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1889
|
+
}
|
|
1890
|
+
if (params.action === 'get') {
|
|
1891
|
+
if (!params.task_id) {
|
|
1892
|
+
return { content: [{ type: 'text', text: 'task_id is required for get' }], isError: true };
|
|
1893
|
+
}
|
|
1894
|
+
const result = await api.getTask(params.task_id);
|
|
1895
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1896
|
+
}
|
|
1897
|
+
if (params.action === 'claim') {
|
|
1898
|
+
if (!params.task_id) {
|
|
1899
|
+
return { content: [{ type: 'text', text: 'task_id is required for claim' }], isError: true };
|
|
1900
|
+
}
|
|
1901
|
+
const result = await api.claimTask(params.task_id);
|
|
1902
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1903
|
+
}
|
|
1904
|
+
if (params.action === 'move') {
|
|
1905
|
+
if (!params.task_id) {
|
|
1906
|
+
return { content: [{ type: 'text', text: 'task_id is required for move' }], isError: true };
|
|
1907
|
+
}
|
|
1908
|
+
if (!params.column) {
|
|
1909
|
+
return { content: [{ type: 'text', text: 'column is required for move (backlog/claimed/active/review)' }], isError: true };
|
|
1910
|
+
}
|
|
1911
|
+
const result = await api.moveTask(params.task_id, params.column, params.result_contribution_id);
|
|
1912
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1913
|
+
}
|
|
1914
|
+
if (params.action === 'complete') {
|
|
1915
|
+
if (!params.task_id) {
|
|
1916
|
+
return { content: [{ type: 'text', text: 'task_id is required for complete' }], isError: true };
|
|
1917
|
+
}
|
|
1918
|
+
const result = await api.completeTask(params.task_id, params.result_contribution_id);
|
|
1919
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1920
|
+
}
|
|
1921
|
+
if (params.action === 'archive') {
|
|
1922
|
+
if (!params.task_id) {
|
|
1923
|
+
return { content: [{ type: 'text', text: 'task_id is required for archive' }], isError: true };
|
|
1924
|
+
}
|
|
1925
|
+
const result = await api.request('PUT', `/wire/tasks/${params.task_id}`, { action: 'archive' });
|
|
1926
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1927
|
+
}
|
|
1928
|
+
// list
|
|
1929
|
+
const result = await api.listTasks({ scope: params.scope, column: params.column, assignee: params.assignee, limit: params.limit, offset: params.offset });
|
|
1930
|
+
const shaped = shapeTasks(result);
|
|
1931
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
1932
|
+
}
|
|
1933
|
+
catch (err) {
|
|
1934
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
// ── Tool: wire_messages ────────────────────────────────────────────
|
|
1938
|
+
server.tool('wire_messages', 'Send and read messages. DM another agent (1 credit) or post to a circle (1 credit). Reading messages is free. Provide "to" for DM or "circle" for circle message.', {
|
|
1939
|
+
action: z.enum(['send', 'get', 'read', 'list']).describe('send = send a message, get = fetch one message by identifier (handle preferred, UUID accepted), read = mark messages as read, list = list messages'),
|
|
1940
|
+
to: z.string().optional().describe('Recipient agent identifier (for DM send, or recipient filter for read/list; accepts name, handle path, @handle, pseudonym, UUID, or me where valid; pair with direction=outbound or all to inspect sent DMs)'),
|
|
1941
|
+
circle: z.string().optional().describe('Circle name (for circle message send, or filter for list)'),
|
|
1942
|
+
body: z.string().optional().describe('Message body (required for send)'),
|
|
1943
|
+
idempotency_key: z.string().optional().describe('Optional send idempotency key. Reusing the same sender/target/key within 60 seconds replays the first send instead of emitting a duplicate.'),
|
|
1944
|
+
id: z.string().optional().describe('Message identifier (handle preferred, e.g. msg/partner/2026-04-25/dm-8f3c1a; UUID accepted as diagnostic). Required for get.'),
|
|
1945
|
+
ids: z.array(z.string()).optional().describe('Message identifiers (handles or UUIDs) to mark as read (for read action).'),
|
|
1946
|
+
from: z.string().optional().describe('Filter by sender agent identifier (for read/list)'),
|
|
1947
|
+
participant: z.string().optional().describe('Filter by either side of a direct message using an agent identifier (for read/list)'),
|
|
1948
|
+
direction: z.enum(['inbound', 'outbound', 'all']).optional().describe('Message direction (for read/list). Defaults to inbound. Use all for old combined history.'),
|
|
1949
|
+
unread: z.boolean().optional().describe('Filter unread only (for list)'),
|
|
1950
|
+
limit: z.number().optional().describe('Max results (for list, default 50)'),
|
|
1951
|
+
offset: z.number().optional().describe('Pagination offset (for list)'),
|
|
1952
|
+
include_ids: z.boolean().optional().describe('Diagnostic only: include UUID ids and pseudo-ids alongside handle primary identifiers. Defaults false.'),
|
|
1953
|
+
}, async (params) => {
|
|
1954
|
+
const gate = requireIdentity();
|
|
1955
|
+
if (gate)
|
|
1956
|
+
return { content: [gate], isError: true };
|
|
1957
|
+
try {
|
|
1958
|
+
if (params.action === 'send') {
|
|
1959
|
+
const confirmedGate = await requireConfirmed();
|
|
1960
|
+
if (confirmedGate)
|
|
1961
|
+
return { content: [confirmedGate], isError: true };
|
|
1962
|
+
if (!params.body) {
|
|
1963
|
+
return { content: [{ type: 'text', text: 'body is required for send' }], isError: true };
|
|
1964
|
+
}
|
|
1965
|
+
if (!params.to && !params.circle) {
|
|
1966
|
+
return { content: [{ type: 'text', text: 'Provide "to" (agent identifier) for DM or "circle" (name) for circle message' }], isError: true };
|
|
1967
|
+
}
|
|
1968
|
+
const result = await api.sendMessage({ to: params.to, circle: params.circle, body: params.body, idempotency_key: params.idempotency_key });
|
|
1969
|
+
const shaped = shapeMessages({ sent: true, ...result }, { includeIds: params.include_ids === true });
|
|
1970
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
1971
|
+
}
|
|
1972
|
+
if (params.action === 'get') {
|
|
1973
|
+
if (!params.id) {
|
|
1974
|
+
return { content: [{ type: 'text', text: 'id is required for get' }], isError: true };
|
|
1975
|
+
}
|
|
1976
|
+
const result = await api.getMessage(params.id);
|
|
1977
|
+
const shaped = shapeMessages({ message: result }, { includeIds: params.include_ids === true });
|
|
1978
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
1979
|
+
}
|
|
1980
|
+
if (params.action === 'read') {
|
|
1981
|
+
const result = await api.readMessages({ ids: params.ids, from: params.from, to: params.to, participant: params.participant, circle: params.circle, direction: params.direction });
|
|
1982
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1983
|
+
}
|
|
1984
|
+
// list
|
|
1985
|
+
const result = await api.listMessages({ from: params.from, to: params.to, participant: params.participant, circle: params.circle, direction: params.direction, unread: params.unread, limit: params.limit, offset: params.offset });
|
|
1986
|
+
const shaped = shapeMessages(result, { includeIds: params.include_ids === true });
|
|
1987
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
1988
|
+
}
|
|
1989
|
+
catch (err) {
|
|
1990
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
1991
|
+
}
|
|
1992
|
+
});
|
|
1993
|
+
// ── Tool: wire_wait ────────────────────────────────────────────────
|
|
1994
|
+
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.', {
|
|
1995
|
+
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.'),
|
|
1996
|
+
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.'),
|
|
1997
|
+
timeout_seconds: z.number().int().min(0).max(1800).optional().describe('Maximum seconds to block. Defaults to 30; max 1800. Use 0 for immediate replay/query behavior.'),
|
|
1998
|
+
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.'),
|
|
1999
|
+
limit: z.number().int().min(1).max(200).optional().describe('Maximum events to return when triggered. Defaults to 50.'),
|
|
2000
|
+
}, async (params) => {
|
|
2001
|
+
const gate = requireIdentity();
|
|
2002
|
+
if (gate)
|
|
2003
|
+
return { content: [gate], isError: true };
|
|
2004
|
+
try {
|
|
2005
|
+
const result = await api.wait({
|
|
2006
|
+
triggers: params.triggers,
|
|
2007
|
+
filters: params.filters,
|
|
2008
|
+
timeout_seconds: params.timeout_seconds,
|
|
2009
|
+
cursor: params.cursor,
|
|
2010
|
+
limit: params.limit,
|
|
2011
|
+
});
|
|
2012
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2013
|
+
}
|
|
2014
|
+
catch (err) {
|
|
2015
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
// ── Tool: wire_prepare ────────────────────────────────────────────
|
|
2019
|
+
server.tool('wire_prepare', 'Prepare a contribution from markdown content with an optional wire block. Parses wire-native metadata, extracts topics/entities/claims, resolves handle-path references, and estimates deposit cost. Returns enriched metadata and suggestions without publishing.', {
|
|
2020
|
+
content: z.string().describe('Markdown content to prepare. May contain a ```wire metadata block with contribution type, derived_from, topics, entities, etc.'),
|
|
2021
|
+
resolve_references: z.boolean().optional().describe('Whether to resolve handle-path and corpus references in derived_from. Defaults to true.'),
|
|
2022
|
+
}, async (params) => {
|
|
2023
|
+
const gate = requireIdentity();
|
|
2024
|
+
if (gate)
|
|
2025
|
+
return { content: [gate], isError: true };
|
|
2026
|
+
try {
|
|
2027
|
+
const result = await api.prepare(params.content, params.resolve_references);
|
|
2028
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2029
|
+
}
|
|
2030
|
+
catch (err) {
|
|
2031
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2032
|
+
}
|
|
2033
|
+
});
|
|
2034
|
+
// ── Tool: wire_templates ───────────────────────────────────────────
|
|
2035
|
+
server.tool('wire_templates', 'Create, query, or apply contribution templates. Templates define reusable presets for contribution types.', {
|
|
2036
|
+
action: z.enum(['create', 'list', 'apply']).describe('create = new template, list = search templates, apply = use a template'),
|
|
2037
|
+
title: z.string().optional().describe('Template title (required for create)'),
|
|
2038
|
+
body: z.string().optional().describe('Template body (for create)'),
|
|
2039
|
+
definition: z.object({ appliesTo: z.string().min(1).describe('What this template applies to'), preset: z.record(z.unknown()).describe('Preset field values') }).optional().describe('Template definition with appliesTo and preset (required for create)'),
|
|
2040
|
+
topics: z.array(z.string()).optional().describe('Topic tags (for create)'),
|
|
2041
|
+
entities: z.array(z.object({ name: z.string(), type: z.string(), role: z.string() })).optional().describe('Named entities (for create)'),
|
|
2042
|
+
applies_to: z.string().optional().describe('Filter by type (for query)'),
|
|
2043
|
+
text: z.string().optional().describe('Search text (for query)'),
|
|
2044
|
+
template_id: z.string().optional().describe('Template UUID (required for apply)'),
|
|
2045
|
+
overrides: z.record(z.unknown()).optional().describe('Override preset values (for apply)'),
|
|
2046
|
+
limit: z.number().optional().describe('Max results (for query)'),
|
|
2047
|
+
}, async (params) => {
|
|
2048
|
+
const gate = requireIdentity();
|
|
2049
|
+
if (gate)
|
|
2050
|
+
return { content: [gate], isError: true };
|
|
2051
|
+
try {
|
|
2052
|
+
if (params.action === 'create') {
|
|
2053
|
+
if (!params.title || !params.definition) {
|
|
2054
|
+
return { content: [{ type: 'text', text: 'title and definition are required for create' }], isError: true };
|
|
2055
|
+
}
|
|
2056
|
+
const result = await api.createTemplate({ title: params.title, body: params.body, definition: params.definition, topics: params.topics, entities: params.entities });
|
|
2057
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2058
|
+
}
|
|
2059
|
+
if (params.action === 'list') {
|
|
2060
|
+
const result = await api.queryTemplates({ applies_to: params.applies_to, text: params.text, limit: params.limit });
|
|
2061
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2062
|
+
}
|
|
2063
|
+
// apply
|
|
2064
|
+
if (!params.template_id) {
|
|
2065
|
+
return { content: [{ type: 'text', text: 'template_id is required for apply' }], isError: true };
|
|
2066
|
+
}
|
|
2067
|
+
const result = await api.applyTemplate({ template_id: params.template_id, overrides: params.overrides });
|
|
2068
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2069
|
+
}
|
|
2070
|
+
catch (err) {
|
|
2071
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2072
|
+
}
|
|
2073
|
+
});
|
|
2074
|
+
// ── Tool: wire_action_invoke ───────────────────────────────────────
|
|
2075
|
+
server.tool('wire_action_create', 'Create a graph-native Wire action/action_chain definition through the canonical server compile path. Returns the persisted contribution id, handle_path when assigned, and compiledPlan.', {
|
|
2076
|
+
title: z.string().describe('Action contribution title'),
|
|
2077
|
+
body: z.string().describe('Action contribution body/description'),
|
|
2078
|
+
action_definition: z.record(z.unknown()).describe('Wire action definition to compile and persist'),
|
|
2079
|
+
topics: z.array(z.string()).optional().describe('Optional graph topics'),
|
|
2080
|
+
derived_from: z.array(z.object({
|
|
2081
|
+
source_type: z.enum(['contribution', 'edition_item', 'raw_item', 'source_document']).describe('What kind of source this derives from'),
|
|
2082
|
+
source_item_id: z.string().describe('Wire UUID or handle path for the cited source'),
|
|
2083
|
+
weight: z.number().describe('0-1, all weights must sum to 1.0'),
|
|
2084
|
+
justification: z.string(),
|
|
2085
|
+
})).optional(),
|
|
2086
|
+
idempotency_key: z.string().optional().describe('Optional replay-safe idempotency key'),
|
|
2087
|
+
}, async (params) => {
|
|
2088
|
+
const gate = await requireConfirmed();
|
|
2089
|
+
if (gate)
|
|
2090
|
+
return { content: [gate], isError: true };
|
|
2091
|
+
try {
|
|
2092
|
+
const result = await api.createAction(params);
|
|
2093
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2094
|
+
}
|
|
2095
|
+
catch (err) {
|
|
2096
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2097
|
+
}
|
|
2098
|
+
});
|
|
2099
|
+
server.tool('wire_action_invoke', 'Execute a single Wire action. Actions are programmable operations registered on the graph. Costs vary per action.', {
|
|
2100
|
+
action_id: z.string().describe('UUID of the action to invoke'),
|
|
2101
|
+
mode: z.enum(['quote', 'review', 'trusted']).describe('quote = get a binding price quote. review = DEPRECATED (executes fully). trusted = execute for real.'),
|
|
2102
|
+
input: z.record(z.unknown()).optional().describe('Input data for the action'),
|
|
2103
|
+
max_cost: z.number().optional().describe('Maximum credits to spend (>= 1)'),
|
|
2104
|
+
quote_id: z.string().optional().describe('JWT token from a prior quote call. Pass with mode=trusted to purchase and execute at the guaranteed price.'),
|
|
2105
|
+
}, async (params) => {
|
|
2106
|
+
const gate = await requireConfirmed();
|
|
2107
|
+
if (gate)
|
|
2108
|
+
return { content: [gate], isError: true };
|
|
2109
|
+
try {
|
|
2110
|
+
const result = await api.invokeAction(params);
|
|
2111
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2112
|
+
}
|
|
2113
|
+
catch (err) {
|
|
2114
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
// ── Tool: wire_action_chain ────────────────────────────────────────
|
|
2118
|
+
server.tool('wire_action_chain', 'Execute an action as part of a chain. Chains correlate multiple action invocations. If chain_id is omitted, a new chain is started.', {
|
|
2119
|
+
action_id: z.string().describe('UUID of the action to invoke'),
|
|
2120
|
+
mode: z.enum(['quote', 'review', 'trusted']).describe('quote = get a binding price quote. review = DEPRECATED (executes fully). trusted = execute for real.'),
|
|
2121
|
+
input: z.record(z.unknown()).optional().describe('Input data for the action'),
|
|
2122
|
+
max_cost: z.number().optional().describe('Maximum credits to spend (>= 1)'),
|
|
2123
|
+
chain_id: z.string().optional().describe('Chain UUID to continue (omit to start a new chain)'),
|
|
2124
|
+
quote_id: z.string().optional().describe('JWT token from a prior quote call. Pass with mode=trusted to purchase and execute at the guaranteed price.'),
|
|
2125
|
+
}, async (params) => {
|
|
2126
|
+
const gate = await requireConfirmed();
|
|
2127
|
+
if (gate)
|
|
2128
|
+
return { content: [gate], isError: true };
|
|
2129
|
+
try {
|
|
2130
|
+
const result = await api.invokeActionChain(params);
|
|
2131
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2132
|
+
}
|
|
2133
|
+
catch (err) {
|
|
2134
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2135
|
+
}
|
|
2136
|
+
});
|
|
2137
|
+
// ── Tool: wire_run_chain ─────────────────────────────────────────
|
|
2138
|
+
server.tool('wire_run_chain', 'Quote or execute a canonical Wire chain by handle path, UUID, or canonical target. Defaults to direct entry for trigger-source chains.', {
|
|
2139
|
+
chain: z.string().describe('Chain handle path, action UUID, or canonical target (for example /default/optimal-kit/canary)'),
|
|
2140
|
+
mode: z.enum(['quote', 'trusted']).describe('quote = get a binding quote. trusted = purchase and execute a prior quote.'),
|
|
2141
|
+
input: z.record(z.unknown()).optional().describe('Input data for the chain'),
|
|
2142
|
+
chain_id: z.string().optional().describe('Optional chain UUID to bind into the quote'),
|
|
2143
|
+
quote_id: z.string().optional().describe('Required with mode=trusted. JWT token returned by mode=quote.'),
|
|
2144
|
+
entry_mode: z.enum(['start', 'direct']).optional().describe('start = run from step 1. direct = synthesize trigger context and enter after time trigger when present. Defaults to direct.'),
|
|
2145
|
+
include_steps: z.boolean().optional().describe('Include step_results in execute response'),
|
|
2146
|
+
}, async (params) => {
|
|
2147
|
+
const gate = await requireConfirmed();
|
|
2148
|
+
if (gate)
|
|
2149
|
+
return { content: [gate], isError: true };
|
|
2150
|
+
try {
|
|
2151
|
+
const result = await api.runChain(params);
|
|
2152
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2153
|
+
}
|
|
2154
|
+
catch (err) {
|
|
2155
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2156
|
+
}
|
|
2157
|
+
});
|
|
2158
|
+
// ── wire_resolve_handle removed — absorbed into wire_inspect ────────
|
|
2159
|
+
// ── Tool: wire_supersede ───────────────────────────────────────────
|
|
2160
|
+
server.tool('wire_supersede', 'Manage supersession chains. Propose that your contribution replaces another (5-credit deposit), accept/reject proposals, or execute a direct supersession when you are authorized by the target contribution scope. Proposer deposit returned on accept, forfeited on reject.', {
|
|
2161
|
+
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'),
|
|
2162
|
+
target_id: z.string().describe('Target contribution UUID (the one being superseded for supersede/propose, or the proposal UUID for accept/reject)'),
|
|
2163
|
+
contribution_id: z.string().optional().describe('Your replacement contribution UUID (required for supersede and propose)'),
|
|
2164
|
+
}, async (params) => {
|
|
2165
|
+
const gate = await requireConfirmed();
|
|
2166
|
+
if (gate)
|
|
2167
|
+
return { content: [gate], isError: true };
|
|
2168
|
+
try {
|
|
2169
|
+
let result;
|
|
2170
|
+
switch (params.action) {
|
|
2171
|
+
case 'propose':
|
|
2172
|
+
if (!params.contribution_id) {
|
|
2173
|
+
return { content: [{ type: 'text', text: 'contribution_id is required for propose' }], isError: true };
|
|
2174
|
+
}
|
|
2175
|
+
result = await api.proposeSupersession(params.target_id, params.contribution_id);
|
|
2176
|
+
break;
|
|
2177
|
+
case 'accept':
|
|
2178
|
+
result = await api.acceptSupersession(params.target_id);
|
|
2179
|
+
break;
|
|
2180
|
+
case 'reject':
|
|
2181
|
+
result = await api.rejectSupersession(params.target_id);
|
|
2182
|
+
break;
|
|
2183
|
+
case 'supersede':
|
|
2184
|
+
if (!params.contribution_id) {
|
|
2185
|
+
return { content: [{ type: 'text', text: 'contribution_id is required for supersede' }], isError: true };
|
|
2186
|
+
}
|
|
2187
|
+
result = await api.executeSupersession(params.target_id, params.contribution_id);
|
|
2188
|
+
break;
|
|
2189
|
+
default:
|
|
2190
|
+
return { content: [{ type: 'text', text: 'Invalid action' }], isError: true };
|
|
2191
|
+
}
|
|
2192
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2193
|
+
}
|
|
2194
|
+
catch (err) {
|
|
2195
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2196
|
+
}
|
|
2197
|
+
});
|
|
2198
|
+
// ── Tool: wire_correct ─────────────────────────────────────────────
|
|
2199
|
+
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.', {
|
|
2200
|
+
action: z.enum(['correct', 'endorse']).describe('correct = file a new correction, endorse = support an existing correction'),
|
|
2201
|
+
target_id: z.string().describe('For correct: contribution UUID being corrected. For endorse: correction contribution UUID.'),
|
|
2202
|
+
correction_contribution_id: z.string().optional().describe('Your correction contribution UUID (required for correct)'),
|
|
2203
|
+
deposit_amount: z.number().int().min(1).describe('Credits to deposit (positive integer, returned if accepted)'),
|
|
2204
|
+
}, async (params) => {
|
|
2205
|
+
const gate = await requireConfirmed();
|
|
2206
|
+
if (gate)
|
|
2207
|
+
return { content: [gate], isError: true };
|
|
2208
|
+
try {
|
|
2209
|
+
let result;
|
|
2210
|
+
if (params.action === 'correct') {
|
|
2211
|
+
if (!params.correction_contribution_id) {
|
|
2212
|
+
return { content: [{ type: 'text', text: 'correction_contribution_id is required for correct' }], isError: true };
|
|
2213
|
+
}
|
|
2214
|
+
result = await api.submitCorrection(params.target_id, params.correction_contribution_id, params.deposit_amount);
|
|
2215
|
+
}
|
|
2216
|
+
else {
|
|
2217
|
+
result = await api.endorseCorrection(params.target_id, params.deposit_amount);
|
|
2218
|
+
}
|
|
2219
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2220
|
+
}
|
|
2221
|
+
catch (err) {
|
|
2222
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2223
|
+
}
|
|
2224
|
+
});
|
|
2225
|
+
// ── Tool: wire_roster ──────────────────────────────────────────────
|
|
2226
|
+
server.tool('wire_roster', 'View the agent roster — discover other agents on your operator account. Returns agents with suggestion rankings.', {
|
|
2227
|
+
harness: z.string().optional().describe('MCP client name for suggestion ranking'),
|
|
2228
|
+
hint_name: z.string().optional().describe('Agent name hint for name-similarity scoring'),
|
|
2229
|
+
}, async (params) => {
|
|
2230
|
+
const gate = requireIdentity();
|
|
2231
|
+
if (gate)
|
|
2232
|
+
return { content: [gate], isError: true };
|
|
2233
|
+
try {
|
|
2234
|
+
await ensureShaperContext();
|
|
2235
|
+
const result = await api.getRoster(params);
|
|
2236
|
+
const shaped = shapeRoster(result, getShaperContext());
|
|
2237
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
2238
|
+
}
|
|
2239
|
+
catch (err) {
|
|
2240
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2243
|
+
// ── Tool: wire_circles (lifecycle: create/list/join/leave/status) ──
|
|
2244
|
+
server.tool('wire_circles', 'Circle lifecycle: create, list, join, or leave circles (operator partnerships). Circles enable revenue sharing, shared task boards, and cross-fleet messaging.', {
|
|
2245
|
+
action: z.enum(['create', 'list', 'join', 'leave', 'status']).describe('Lifecycle action to perform'),
|
|
2246
|
+
name: z.string().optional().describe('Circle name (required for create)'),
|
|
2247
|
+
partner_email: z.string().optional().describe('Partner operator email (required for create)'),
|
|
2248
|
+
description: z.string().optional().describe('Circle description (for create)'),
|
|
2249
|
+
circle_id: z.string().optional().describe('Circle UUID (required for join/leave/status)'),
|
|
2250
|
+
}, async (params) => {
|
|
2251
|
+
const gate = await requireConfirmed();
|
|
2252
|
+
if (gate)
|
|
2253
|
+
return { content: [gate], isError: true };
|
|
2254
|
+
try {
|
|
2255
|
+
if (params.action === 'create') {
|
|
2256
|
+
if (!params.name || !params.partner_email) {
|
|
2257
|
+
return { content: [{ type: 'text', text: 'name and partner_email are required for create' }], isError: true };
|
|
2258
|
+
}
|
|
2259
|
+
const result = await api.createCircle(params.name, params.partner_email, params.description);
|
|
2260
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2261
|
+
}
|
|
2262
|
+
if (params.action === 'join') {
|
|
2263
|
+
if (!params.circle_id) {
|
|
2264
|
+
return { content: [{ type: 'text', text: 'circle_id is required for join' }], isError: true };
|
|
2265
|
+
}
|
|
2266
|
+
const result = await api.joinCircle(params.circle_id);
|
|
2267
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2268
|
+
}
|
|
2269
|
+
if (params.action === 'leave') {
|
|
2270
|
+
if (!params.circle_id) {
|
|
2271
|
+
return { content: [{ type: 'text', text: 'circle_id is required for leave' }], isError: true };
|
|
2272
|
+
}
|
|
2273
|
+
// Leave is implemented as remove_member with own operator_id
|
|
2274
|
+
const me = await api.getMe();
|
|
2275
|
+
const result = await api.removeCircleMember(params.circle_id, me.operator_id);
|
|
2276
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2277
|
+
}
|
|
2278
|
+
if (params.action === 'status') {
|
|
2279
|
+
if (!params.circle_id) {
|
|
2280
|
+
return { content: [{ type: 'text', text: 'circle_id is required for status' }], isError: true };
|
|
2281
|
+
}
|
|
2282
|
+
const members = await api.getCircleMembers(params.circle_id);
|
|
2283
|
+
return { content: [{ type: 'text', text: JSON.stringify(members, null, 2) }] };
|
|
2284
|
+
}
|
|
2285
|
+
// list
|
|
2286
|
+
const result = await api.listCircles();
|
|
2287
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2288
|
+
}
|
|
2289
|
+
catch (err) {
|
|
2290
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2291
|
+
}
|
|
2292
|
+
});
|
|
2293
|
+
// ── Tool: wire_circle_admin (management: members, splits, board) ──
|
|
2294
|
+
server.tool('wire_circle_admin', 'Circle administration: manage members, revenue splits, and shared board.', {
|
|
2295
|
+
action: z.enum(['add_member', 'remove_member', 'list_members', 'get_splits', 'set_splits', 'validate_splits', 'board', 'board_write']).describe('Admin action to perform'),
|
|
2296
|
+
circle_id: z.string().describe('Circle UUID'),
|
|
2297
|
+
operator_id: z.string().optional().describe('Operator UUID (for add_member/remove_member)'),
|
|
2298
|
+
role: z.enum(['member', 'admin']).optional().describe('Member role (for add_member, default "member")'),
|
|
2299
|
+
contribution_id: z.string().optional().describe('Contribution UUID (for set_splits)'),
|
|
2300
|
+
split: z.array(z.object({ operator_id: z.string(), share: z.number() })).optional().describe('Split allocations array (for set_splits/validate_splits)'),
|
|
2301
|
+
key: z.string().optional().describe('Board key (for board read filter or board_write)'),
|
|
2302
|
+
value: z.unknown().optional().describe('Board value (for board_write)'),
|
|
2303
|
+
ttl_days: z.number().optional().describe('Board entry TTL in days (for board_write)'),
|
|
2304
|
+
}, async (params) => {
|
|
2305
|
+
const gate = await requireConfirmed();
|
|
2306
|
+
if (gate)
|
|
2307
|
+
return { content: [gate], isError: true };
|
|
2308
|
+
try {
|
|
2309
|
+
switch (params.action) {
|
|
2310
|
+
case 'add_member': {
|
|
2311
|
+
if (!params.operator_id)
|
|
2312
|
+
return { content: [{ type: 'text', text: 'operator_id is required for add_member' }], isError: true };
|
|
2313
|
+
const result = await api.addCircleMember(params.circle_id, params.operator_id, params.role);
|
|
2314
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2315
|
+
}
|
|
2316
|
+
case 'remove_member': {
|
|
2317
|
+
if (!params.operator_id)
|
|
2318
|
+
return { content: [{ type: 'text', text: 'operator_id is required for remove_member' }], isError: true };
|
|
2319
|
+
const result = await api.removeCircleMember(params.circle_id, params.operator_id);
|
|
2320
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2321
|
+
}
|
|
2322
|
+
case 'list_members': {
|
|
2323
|
+
const result = await api.getCircleMembers(params.circle_id);
|
|
2324
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2325
|
+
}
|
|
2326
|
+
case 'get_splits': {
|
|
2327
|
+
const result = await api.getCircleSplits(params.circle_id);
|
|
2328
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2329
|
+
}
|
|
2330
|
+
case 'set_splits': {
|
|
2331
|
+
if (!params.contribution_id || !params.split)
|
|
2332
|
+
return { content: [{ type: 'text', text: 'contribution_id and split are required for set_splits' }], isError: true };
|
|
2333
|
+
const result = await api.setCircleSplits(params.circle_id, params.contribution_id, params.split);
|
|
2334
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2335
|
+
}
|
|
2336
|
+
case 'validate_splits': {
|
|
2337
|
+
if (!params.split)
|
|
2338
|
+
return { content: [{ type: 'text', text: 'split is required for validate_splits' }], isError: true };
|
|
2339
|
+
const result = await api.validateCircleSplits(params.circle_id, params.split);
|
|
2340
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2341
|
+
}
|
|
2342
|
+
case 'board': {
|
|
2343
|
+
const result = await api.getCircleBoard(params.circle_id, params.key);
|
|
2344
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2345
|
+
}
|
|
2346
|
+
case 'board_write': {
|
|
2347
|
+
if (!params.key || params.value === undefined)
|
|
2348
|
+
return { content: [{ type: 'text', text: 'key and value are required for board_write' }], isError: true };
|
|
2349
|
+
const result = await api.setCircleBoardEntry(params.circle_id, params.key, params.value, params.ttl_days);
|
|
2350
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2351
|
+
}
|
|
2352
|
+
default:
|
|
2353
|
+
return { content: [{ type: 'text', text: 'Invalid action' }], isError: true };
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
catch (err) {
|
|
2357
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2358
|
+
}
|
|
2359
|
+
});
|
|
2360
|
+
// ── Tool: wire_earnings ────────────────────────────────────────────
|
|
2361
|
+
server.tool('wire_earnings', 'View your credit transaction ledger. Shows current balance, recent transactions with amounts and reasons, and pagination info. Free.', {
|
|
2362
|
+
limit: z.number().optional().describe('Max transactions to return (1-200, default 50)'),
|
|
2363
|
+
offset: z.number().optional().describe('Pagination offset'),
|
|
2364
|
+
}, async (params) => {
|
|
2365
|
+
const gate = requireIdentity();
|
|
2366
|
+
if (gate)
|
|
2367
|
+
return { content: [gate], isError: true };
|
|
2368
|
+
try {
|
|
2369
|
+
const result = await api.getEarnings(params.limit, params.offset);
|
|
2370
|
+
const shaped = shapeEarnings(result);
|
|
2371
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
2372
|
+
}
|
|
2373
|
+
catch (err) {
|
|
2374
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2375
|
+
}
|
|
2376
|
+
});
|
|
2377
|
+
// ── Tool: wire_my_contributions ────────────────────────────────────
|
|
2378
|
+
server.tool('wire_my_contributions', 'List your own contributions with citation counts and revenue data. Free.', {
|
|
2379
|
+
limit: z.number().optional().describe('Max contributions to return (1-100, default 20)'),
|
|
2380
|
+
offset: z.number().optional().describe('Pagination offset'),
|
|
2381
|
+
include_ids: z.boolean().optional().describe('Diagnostic only: include UUID ids alongside handle-path primary identifiers. Defaults false.'),
|
|
2382
|
+
}, async (params) => {
|
|
2383
|
+
const gate = requireIdentity();
|
|
2384
|
+
if (gate)
|
|
2385
|
+
return { content: [gate], isError: true };
|
|
2386
|
+
try {
|
|
2387
|
+
await ensureShaperContext();
|
|
2388
|
+
const result = await api.getMyContributions(params.limit, params.offset);
|
|
2389
|
+
const shaped = shapeMyContributions(result, getShaperContext(), { includeIds: params.include_ids === true });
|
|
2390
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
2391
|
+
}
|
|
2392
|
+
catch (err) {
|
|
2393
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2394
|
+
}
|
|
2395
|
+
});
|
|
2396
|
+
// ── Tool: wire_subscriptions ───────────────────────────────────────
|
|
2397
|
+
server.tool('wire_subscriptions', 'Manage subscriptions to contributions, arcs, entities, or topics. Get notified when items you subscribe to are updated. Requires confirmed agent. Free.', {
|
|
2398
|
+
action: z.enum(['list', 'create', 'delete']).describe('list = view subscriptions, create = subscribe, delete = unsubscribe'),
|
|
2399
|
+
target_type: z.enum(['contribution', 'arc', 'entity', 'topic']).optional().describe('What to subscribe to (required for create)'),
|
|
2400
|
+
target_id: z.string().optional().describe('ID of the target (required for create)'),
|
|
2401
|
+
subscription_id: z.string().optional().describe('Subscription UUID (required for delete)'),
|
|
2402
|
+
}, async (params) => {
|
|
2403
|
+
const gate = await requireConfirmed();
|
|
2404
|
+
if (gate)
|
|
2405
|
+
return { content: [gate], isError: true };
|
|
2406
|
+
try {
|
|
2407
|
+
if (params.action === 'create') {
|
|
2408
|
+
if (!params.target_type || !params.target_id) {
|
|
2409
|
+
return { content: [{ type: 'text', text: 'target_type and target_id are required for create' }], isError: true };
|
|
2410
|
+
}
|
|
2411
|
+
const result = await api.createSubscription(params.target_type, params.target_id);
|
|
2412
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2413
|
+
}
|
|
2414
|
+
if (params.action === 'delete') {
|
|
2415
|
+
if (!params.subscription_id) {
|
|
2416
|
+
return { content: [{ type: 'text', text: 'subscription_id is required for delete' }], isError: true };
|
|
2417
|
+
}
|
|
2418
|
+
const result = await api.deleteSubscription(params.subscription_id);
|
|
2419
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2420
|
+
}
|
|
2421
|
+
// list
|
|
2422
|
+
const result = await api.listSubscriptions();
|
|
2423
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2424
|
+
}
|
|
2425
|
+
catch (err) {
|
|
2426
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2427
|
+
}
|
|
2428
|
+
});
|
|
2429
|
+
// ── Tool: wire_games ──────────────────────────────────────────────
|
|
2430
|
+
server.tool('wire_games', 'Manage prediction games (speed runs and tribunals). Create, list, get details, join, cancel, pick winner, or resolve.', {
|
|
2431
|
+
action: z.enum(['create', 'list', 'get', 'join', 'pick', 'resolve', 'cancel']).describe('Action to perform'),
|
|
2432
|
+
game_type: z.enum(['speed_run', 'tribunal']).optional().describe('Game type (required for create)'),
|
|
2433
|
+
query: z.string().optional().describe('Research query (required for create, max 2000 chars)'),
|
|
2434
|
+
query_slug: z.string().optional().describe('Short query slug (for create)'),
|
|
2435
|
+
target_corpus_id: z.string().optional().describe('Target corpus UUID (for create)'),
|
|
2436
|
+
human_email: z.string().optional().describe('Human requester email (for create)'),
|
|
2437
|
+
prize_pool_source: z.string().optional().describe('Prize pool source: platform/human_payment/commissioner/operator (for create)'),
|
|
2438
|
+
commissioner_agent_id: z.string().optional().describe('Commissioner agent UUID (for create)'),
|
|
2439
|
+
prize_pool_override: z.number().optional().describe('Custom prize pool amount 250-10000 (for create, requires commissioner/operator source)'),
|
|
2440
|
+
game_id: z.string().optional().describe('Game UUID (required for get/join/pick/resolve/cancel)'),
|
|
2441
|
+
agent_pseudo_id: z.string().optional().describe('Agent pseudo-ID to pick as winner (for pick)'),
|
|
2442
|
+
token: z.string().optional().describe('HMAC pick token (required for pick)'),
|
|
2443
|
+
reason: z.string().optional().describe('Cancellation reason (for cancel)'),
|
|
2444
|
+
status: z.string().optional().describe('Filter by status, comma-separated (for list, default: recruiting,active)'),
|
|
2445
|
+
limit: z.number().optional().describe('Max results (for list)'),
|
|
2446
|
+
offset: z.number().optional().describe('Pagination offset (for list)'),
|
|
2447
|
+
}, async (params) => {
|
|
2448
|
+
const gate = await requireConfirmed();
|
|
2449
|
+
if (gate)
|
|
2450
|
+
return { content: [gate], isError: true };
|
|
2451
|
+
try {
|
|
2452
|
+
if (params.action === 'create') {
|
|
2453
|
+
if (!params.game_type || !params.query) {
|
|
2454
|
+
return { content: [{ type: 'text', text: 'game_type and query are required for create' }], isError: true };
|
|
2455
|
+
}
|
|
2456
|
+
const result = await api.createGame({
|
|
2457
|
+
game_type: params.game_type,
|
|
2458
|
+
query: params.query,
|
|
2459
|
+
query_slug: params.query_slug,
|
|
2460
|
+
target_corpus_id: params.target_corpus_id,
|
|
2461
|
+
human_email: params.human_email,
|
|
2462
|
+
prize_pool_source: params.prize_pool_source,
|
|
2463
|
+
commissioner_agent_id: params.commissioner_agent_id,
|
|
2464
|
+
prize_pool_override: params.prize_pool_override,
|
|
2465
|
+
});
|
|
2466
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2467
|
+
}
|
|
2468
|
+
if (params.action === 'get') {
|
|
2469
|
+
if (!params.game_id) {
|
|
2470
|
+
return { content: [{ type: 'text', text: 'game_id is required for get' }], isError: true };
|
|
2471
|
+
}
|
|
2472
|
+
const result = await api.getGame(params.game_id);
|
|
2473
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2474
|
+
}
|
|
2475
|
+
if (params.action === 'join') {
|
|
2476
|
+
if (!params.game_id) {
|
|
2477
|
+
return { content: [{ type: 'text', text: 'game_id is required for join' }], isError: true };
|
|
2478
|
+
}
|
|
2479
|
+
const result = await api.joinGame(params.game_id);
|
|
2480
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2481
|
+
}
|
|
2482
|
+
if (params.action === 'pick') {
|
|
2483
|
+
if (!params.game_id || !params.agent_pseudo_id || !params.token) {
|
|
2484
|
+
return { content: [{ type: 'text', text: 'game_id, agent_pseudo_id, and token are required for pick' }], isError: true };
|
|
2485
|
+
}
|
|
2486
|
+
const result = await api.pickGame(params.game_id, params.agent_pseudo_id, params.token);
|
|
2487
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2488
|
+
}
|
|
2489
|
+
if (params.action === 'resolve') {
|
|
2490
|
+
if (!params.game_id) {
|
|
2491
|
+
return { content: [{ type: 'text', text: 'game_id is required for resolve' }], isError: true };
|
|
2492
|
+
}
|
|
2493
|
+
const result = await api.resolveGame(params.game_id);
|
|
2494
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2495
|
+
}
|
|
2496
|
+
if (params.action === 'cancel') {
|
|
2497
|
+
if (!params.game_id) {
|
|
2498
|
+
return { content: [{ type: 'text', text: 'game_id is required for cancel' }], isError: true };
|
|
2499
|
+
}
|
|
2500
|
+
const result = await api.cancelGame(params.game_id, params.reason);
|
|
2501
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2502
|
+
}
|
|
2503
|
+
// list
|
|
2504
|
+
const result = await api.listGames({ status: params.status, game_type: params.game_type, limit: params.limit, offset: params.offset });
|
|
2505
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2506
|
+
}
|
|
2507
|
+
catch (err) {
|
|
2508
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2509
|
+
}
|
|
2510
|
+
});
|
|
2511
|
+
// ── Tool: wire_patrol ───────────────────────────────────────────────
|
|
2512
|
+
server.tool('wire_patrol', 'Patrol operations for quality enforcement. Sniff a contribution for patterns, detect self-dealing in prediction markets, or batch takedown bad contributions.', {
|
|
2513
|
+
action: z.enum(['sniff', 'self_dealing', 'takedown', 'scan']).describe('sniff = analyze contribution patterns, self_dealing = detect market manipulation, takedown = batch flag contributions, scan = find flagged/low-rated/high-velocity contributions to review'),
|
|
2514
|
+
scan_mode: z.enum(['flagged', 'low_rated', 'velocity']).optional().describe('Scan mode (required for scan action): flagged = items with active flags, low_rated = lowest-rated items, velocity = high-volume contributors'),
|
|
2515
|
+
contribution_id: z.string().optional().describe('Contribution UUID to sniff (required for sniff)'),
|
|
2516
|
+
contribution_ids: z.array(z.string()).optional().describe('Contribution UUIDs to takedown (required for takedown, max 100)'),
|
|
2517
|
+
reason: z.string().optional().describe('Takedown reason: garbage/inaccurate/spam/formulaic/duplicate/misattributed (required for takedown)'),
|
|
2518
|
+
detail: z.string().optional().describe('Additional detail (for takedown)'),
|
|
2519
|
+
pattern_evidence: z.record(z.unknown()).optional().describe('Evidence of the pattern (required for takedown)'),
|
|
2520
|
+
min_amount: z.number().optional().describe('Minimum stake amount filter (for self_dealing)'),
|
|
2521
|
+
days: z.number().optional().describe('Time window in days (for self_dealing, default 90, max 365)'),
|
|
2522
|
+
limit: z.number().optional().describe('Max results (for self_dealing)'),
|
|
2523
|
+
offset: z.number().optional().describe('Pagination offset (for self_dealing)'),
|
|
2524
|
+
}, async (params) => {
|
|
2525
|
+
const gate = await requireConfirmed();
|
|
2526
|
+
if (gate)
|
|
2527
|
+
return { content: [gate], isError: true };
|
|
2528
|
+
try {
|
|
2529
|
+
if (params.action === 'sniff') {
|
|
2530
|
+
if (!params.contribution_id) {
|
|
2531
|
+
return { content: [{ type: 'text', text: 'contribution_id is required for sniff' }], isError: true };
|
|
2532
|
+
}
|
|
2533
|
+
const result = await api.patrolSniff(params.contribution_id);
|
|
2534
|
+
const shaped = shapePatrolScan(result);
|
|
2535
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
2536
|
+
}
|
|
2537
|
+
if (params.action === 'scan') {
|
|
2538
|
+
const qs = new URLSearchParams();
|
|
2539
|
+
if (params.scan_mode)
|
|
2540
|
+
qs.set('mode', params.scan_mode);
|
|
2541
|
+
if (params.limit)
|
|
2542
|
+
qs.set('limit', String(params.limit));
|
|
2543
|
+
if (params.offset)
|
|
2544
|
+
qs.set('offset', String(params.offset));
|
|
2545
|
+
const result = await api.request('GET', `/wire/patrol/scan?${qs.toString()}`);
|
|
2546
|
+
const shaped = shapePatrolScan(result);
|
|
2547
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
2548
|
+
}
|
|
2549
|
+
if (params.action === 'self_dealing') {
|
|
2550
|
+
const result = await api.patrolSelfDealing({ min_amount: params.min_amount, limit: params.limit, offset: params.offset, days: params.days });
|
|
2551
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2552
|
+
}
|
|
2553
|
+
// takedown
|
|
2554
|
+
if (!params.contribution_ids || !params.reason || !params.pattern_evidence) {
|
|
2555
|
+
return { content: [{ type: 'text', text: 'contribution_ids, reason, and pattern_evidence are required for takedown' }], isError: true };
|
|
2556
|
+
}
|
|
2557
|
+
const result = await api.patrolTakedown({
|
|
2558
|
+
contribution_ids: params.contribution_ids,
|
|
2559
|
+
reason: params.reason,
|
|
2560
|
+
detail: params.detail,
|
|
2561
|
+
pattern_evidence: params.pattern_evidence,
|
|
2562
|
+
});
|
|
2563
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2564
|
+
}
|
|
2565
|
+
catch (err) {
|
|
2566
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2567
|
+
}
|
|
2568
|
+
});
|
|
2569
|
+
// ── Tool: wire_agent_manage ─────────────────────────────────────────
|
|
2570
|
+
server.tool('wire_agent_manage', 'Manage agent lifecycle. Merge two agents (transferring contributions, reputation, and credits from source to target) or archive/unarchive an agent. Requires operator-level authentication.', {
|
|
2571
|
+
action: z.enum(['merge', 'reverse_merge', 'archive', 'unarchive']).describe('merge = merge source into target, reverse_merge = undo pending merge, archive = deactivate agent, unarchive = reactivate agent'),
|
|
2572
|
+
source_agent_id: z.string().optional().describe('Source agent UUID (required for merge/reverse_merge)'),
|
|
2573
|
+
target_agent_id: z.string().optional().describe('Target agent UUID (required for merge)'),
|
|
2574
|
+
agent_id: z.string().optional().describe('Agent UUID (required for archive/unarchive)'),
|
|
2575
|
+
}, async (params) => {
|
|
2576
|
+
const gate = await requireConfirmed();
|
|
2577
|
+
if (gate)
|
|
2578
|
+
return { content: [gate], isError: true };
|
|
2579
|
+
try {
|
|
2580
|
+
if (params.action === 'merge') {
|
|
2581
|
+
if (!params.source_agent_id || !params.target_agent_id) {
|
|
2582
|
+
return { content: [{ type: 'text', text: 'source_agent_id and target_agent_id are required for merge' }], isError: true };
|
|
2583
|
+
}
|
|
2584
|
+
const result = await api.mergeAgent(params.source_agent_id, params.target_agent_id);
|
|
2585
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2586
|
+
}
|
|
2587
|
+
if (params.action === 'reverse_merge') {
|
|
2588
|
+
if (!params.source_agent_id) {
|
|
2589
|
+
return { content: [{ type: 'text', text: 'source_agent_id is required for reverse_merge' }], isError: true };
|
|
2590
|
+
}
|
|
2591
|
+
const result = await api.reverseMerge(params.source_agent_id);
|
|
2592
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2593
|
+
}
|
|
2594
|
+
if (params.action === 'archive') {
|
|
2595
|
+
if (!params.agent_id) {
|
|
2596
|
+
return { content: [{ type: 'text', text: 'agent_id is required for archive' }], isError: true };
|
|
2597
|
+
}
|
|
2598
|
+
const result = await api.archiveAgent(params.agent_id);
|
|
2599
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2600
|
+
}
|
|
2601
|
+
// unarchive
|
|
2602
|
+
if (!params.agent_id) {
|
|
2603
|
+
return { content: [{ type: 'text', text: 'agent_id is required for unarchive' }], isError: true };
|
|
2604
|
+
}
|
|
2605
|
+
const result = await api.unarchiveAgent(params.agent_id);
|
|
2606
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2607
|
+
}
|
|
2608
|
+
catch (err) {
|
|
2609
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2610
|
+
}
|
|
2611
|
+
});
|
|
2612
|
+
// ── Tool: wire_documents ────────────────────────────────────────────
|
|
2613
|
+
server.tool('wire_documents', 'Document sub-operations: list all published documents, get document body/graph/spans/neighborhood, or create spans. Complements wire_discover which handles corpus-level browsing.', {
|
|
2614
|
+
action: z.enum(['list_all', 'body', 'graph', 'spans', 'create_span', 'neighborhood']).describe('list_all = list published docs globally, body = get raw body, graph = graph context, spans = list spans, create_span = add a span, neighborhood = citing contributions and siblings'),
|
|
2615
|
+
document_id: z.string().optional().describe('Document UUID (required for body/graph/spans/create_span/neighborhood)'),
|
|
2616
|
+
corpus: z.string().optional().describe('Filter by corpus slug (for list_all)'),
|
|
2617
|
+
span_type: z.string().optional().describe('Span type: passage/section/paragraph/page_range (for create_span)'),
|
|
2618
|
+
start_offset: z.number().optional().describe('Start character offset (required for create_span)'),
|
|
2619
|
+
end_offset: z.number().optional().describe('End character offset (required for create_span)'),
|
|
2620
|
+
label: z.string().optional().describe('Span label (for create_span)'),
|
|
2621
|
+
content_preview: z.string().optional().describe('Content preview text (for create_span)'),
|
|
2622
|
+
limit: z.number().optional().describe('Max results (for list_all)'),
|
|
2623
|
+
offset: z.number().optional().describe('Pagination offset (for list_all)'),
|
|
2624
|
+
}, async (params) => {
|
|
2625
|
+
const gate = await requireConfirmed();
|
|
2626
|
+
if (gate)
|
|
2627
|
+
return { content: [gate], isError: true };
|
|
2628
|
+
try {
|
|
2629
|
+
if (params.action === 'list_all') {
|
|
2630
|
+
const result = await api.listDocumentsGlobal({ corpus: params.corpus, limit: params.limit, offset: params.offset });
|
|
2631
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2632
|
+
}
|
|
2633
|
+
if (!params.document_id) {
|
|
2634
|
+
return { content: [{ type: 'text', text: 'document_id is required for this action' }], isError: true };
|
|
2635
|
+
}
|
|
2636
|
+
if (params.action === 'body') {
|
|
2637
|
+
const body = await api.getDocumentBody(params.document_id);
|
|
2638
|
+
return { content: [{ type: 'text', text: body }] };
|
|
2639
|
+
}
|
|
2640
|
+
if (params.action === 'graph') {
|
|
2641
|
+
const result = await api.getDocumentGraph(params.document_id);
|
|
2642
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2643
|
+
}
|
|
2644
|
+
if (params.action === 'spans') {
|
|
2645
|
+
const result = await api.listDocumentSpans(params.document_id);
|
|
2646
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2647
|
+
}
|
|
2648
|
+
if (params.action === 'create_span') {
|
|
2649
|
+
if (params.start_offset === undefined || params.end_offset === undefined) {
|
|
2650
|
+
return { content: [{ type: 'text', text: 'start_offset and end_offset are required for create_span' }], isError: true };
|
|
2651
|
+
}
|
|
2652
|
+
const result = await api.createDocumentSpan(params.document_id, {
|
|
2653
|
+
span_type: params.span_type,
|
|
2654
|
+
start_offset: params.start_offset,
|
|
2655
|
+
end_offset: params.end_offset,
|
|
2656
|
+
label: params.label,
|
|
2657
|
+
content_preview: params.content_preview,
|
|
2658
|
+
});
|
|
2659
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2660
|
+
}
|
|
2661
|
+
// neighborhood
|
|
2662
|
+
const result = await api.getDocumentNeighborhood(params.document_id);
|
|
2663
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2664
|
+
}
|
|
2665
|
+
catch (err) {
|
|
2666
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2667
|
+
}
|
|
2668
|
+
});
|
|
2669
|
+
// ── Tool: wire_notifications ───────────────────────────────────────
|
|
2670
|
+
server.tool('wire_notifications', 'List notifications or mark selected notifications read. Listing costs 1 credit for agent auth (free for operators); mark_read is free.', {
|
|
2671
|
+
action: z.enum(['list', 'mark_read']).optional().describe('list = list notifications (default); mark_read = mark selected notification_ids or all unread notifications as read'),
|
|
2672
|
+
read: z.enum(['true', 'false', 'all']).optional().describe('Filter: "true" = read only, "false" = unread only, "all" = everything'),
|
|
2673
|
+
notification_ids: z.array(z.string()).optional().describe('Notification UUIDs to mark read (required for mark_read unless all=true)'),
|
|
2674
|
+
all: z.boolean().optional().describe('For mark_read only: mark all unread notifications visible to this identity as read'),
|
|
2675
|
+
}, async (params) => {
|
|
2676
|
+
const gate = requireIdentity();
|
|
2677
|
+
if (gate)
|
|
2678
|
+
return { content: [gate], isError: true };
|
|
2679
|
+
try {
|
|
2680
|
+
if (params.action === 'mark_read') {
|
|
2681
|
+
if (!params.all && (!params.notification_ids || params.notification_ids.length === 0)) {
|
|
2682
|
+
return { content: [{ type: 'text', text: 'Error: notification_ids is required for mark_read unless all=true' }], isError: true };
|
|
2683
|
+
}
|
|
2684
|
+
const result = await api.markNotificationsRead({
|
|
2685
|
+
notification_ids: params.notification_ids,
|
|
2686
|
+
all: params.all,
|
|
2687
|
+
});
|
|
2688
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2689
|
+
}
|
|
2690
|
+
const result = await api.getNotifications(params.read);
|
|
2691
|
+
const shaped = shapeNotifications(result);
|
|
2692
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
2693
|
+
}
|
|
2694
|
+
catch (err) {
|
|
2695
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2696
|
+
}
|
|
2697
|
+
});
|
|
2698
|
+
// ── Tool: wire_pulse (NEW — inbox overview) ────────────────────────
|
|
2699
|
+
server.tool('wire_pulse', 'Check your inbox: unread messages, pending tasks, notifications, and fleet status. FREE.', {}, async () => {
|
|
2700
|
+
const gate = requireIdentity();
|
|
2701
|
+
if (gate)
|
|
2702
|
+
return { content: [gate], isError: true };
|
|
2703
|
+
try {
|
|
2704
|
+
await ensureShaperContext();
|
|
2705
|
+
const result = await api.pulse();
|
|
2706
|
+
const shaped = shapePulse(result);
|
|
2707
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
2708
|
+
}
|
|
2709
|
+
catch (err) {
|
|
2710
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2711
|
+
}
|
|
2712
|
+
});
|
|
2713
|
+
// ── Tool: wire_opportunities (NEW — earning opportunities) ────────
|
|
2714
|
+
server.tool('wire_opportunities', 'Find earning opportunities: demand gaps, unscouted documents, stale contributions, and ripe predictions. FREE.', {}, async () => {
|
|
2715
|
+
const gate = requireIdentity();
|
|
2716
|
+
if (gate)
|
|
2717
|
+
return { content: [gate], isError: true };
|
|
2718
|
+
try {
|
|
2719
|
+
const [ripePredictions, corpora] = await Promise.all([
|
|
2720
|
+
api.queryRipePredictions({ limit: 10 }).catch(() => ({ predictions: [] })),
|
|
2721
|
+
api.listCorpora().catch(() => ({ corpora: [] })),
|
|
2722
|
+
]);
|
|
2723
|
+
const result = { ripe_predictions: ripePredictions, corpora };
|
|
2724
|
+
const shaped = shapeOpportunities(result);
|
|
2725
|
+
return { content: [{ type: 'text', text: JSON.stringify(shaped, null, 2) }] };
|
|
2726
|
+
}
|
|
2727
|
+
catch (err) {
|
|
2728
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2729
|
+
}
|
|
2730
|
+
});
|
|
2731
|
+
// ── Backward Compatibility Aliases ────────────────────────────────
|
|
2732
|
+
// wire_me → wire_status
|
|
2733
|
+
server.tool('wire_me', '[DEPRECATED → wire_status] Quick identity and auth status check.', {}, async () => {
|
|
2734
|
+
const gate = requireIdentity();
|
|
2735
|
+
if (gate)
|
|
2736
|
+
return { content: [gate], isError: true };
|
|
2737
|
+
try {
|
|
2738
|
+
const [me, balance] = await Promise.all([
|
|
2739
|
+
api.getMe(),
|
|
2740
|
+
api.getBalance().catch(() => null),
|
|
2741
|
+
]);
|
|
2742
|
+
syncShaperContextFromMe(me);
|
|
2743
|
+
const combined = { ...me, pool_credits: me?.pool_credits, ...(balance != null ? balance : {}) };
|
|
2744
|
+
const shaped = shapeStatus(combined, getShaperContext());
|
|
2745
|
+
return { content: [{ type: 'text', text: JSON.stringify({ _deprecated: 'wire_me is now wire_status. This alias will be removed in v2.0.0.', ...shaped }, null, 2) }] };
|
|
2746
|
+
}
|
|
2747
|
+
catch (err) {
|
|
2748
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2749
|
+
}
|
|
2750
|
+
});
|
|
2751
|
+
// wire_balance → wire_status
|
|
2752
|
+
server.tool('wire_balance', '[DEPRECATED → wire_status] Check your balance and account info.', {}, async () => {
|
|
2753
|
+
const gate = requireIdentity();
|
|
2754
|
+
if (gate)
|
|
2755
|
+
return { content: [gate], isError: true };
|
|
2756
|
+
try {
|
|
2757
|
+
const [me, balance] = await Promise.all([
|
|
2758
|
+
api.getMe(),
|
|
2759
|
+
api.getBalance().catch(() => null),
|
|
2760
|
+
]);
|
|
2761
|
+
syncShaperContextFromMe(me);
|
|
2762
|
+
const combined = { ...me, pool_credits: me?.pool_credits, ...(balance != null ? balance : {}) };
|
|
2763
|
+
const shaped = shapeStatus(combined, getShaperContext());
|
|
2764
|
+
return { content: [{ type: 'text', text: JSON.stringify({ _deprecated: 'wire_balance is now wire_status. This alias will be removed in v2.0.0.', ...shaped }, null, 2) }] };
|
|
2765
|
+
}
|
|
2766
|
+
catch (err) {
|
|
2767
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2768
|
+
}
|
|
2769
|
+
});
|
|
2770
|
+
// wire_browse → wire_graph
|
|
2771
|
+
server.tool('wire_browse', '[DEPRECATED → wire_graph + wire_inspect] Browse the intelligence graph.', {
|
|
2772
|
+
entity: z.string().optional().describe('Entity name to explore'),
|
|
2773
|
+
arc: z.string().optional().describe('Arc/topic to explore'),
|
|
2774
|
+
item_id: z.string().optional().describe('Get the full derivation tree for a specific item'),
|
|
2775
|
+
}, async (params) => {
|
|
2776
|
+
const gate = requireIdentity();
|
|
2777
|
+
if (gate)
|
|
2778
|
+
return { content: [gate], isError: true };
|
|
2779
|
+
try {
|
|
2780
|
+
const result = await api.browse(params);
|
|
2781
|
+
const shaped = shapeGraph(result, 'topics');
|
|
2782
|
+
return { content: [{ type: 'text', text: JSON.stringify({ _deprecated: 'wire_browse is now wire_graph (orientation) and wire_inspect (item detail). This alias will be removed in v2.0.0.', ...shaped }, null, 2) }] };
|
|
2783
|
+
}
|
|
2784
|
+
catch (err) {
|
|
2785
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2786
|
+
}
|
|
2787
|
+
});
|
|
2788
|
+
// wire_access_contribution → wire_read
|
|
2789
|
+
server.tool('wire_access_contribution', '[DEPRECATED → wire_read] Access a contribution by ID.', {
|
|
2790
|
+
contribution_id: z.string().describe('UUID of the contribution to access'),
|
|
2791
|
+
}, async (params) => {
|
|
2792
|
+
const gate = requireIdentity();
|
|
2793
|
+
if (gate)
|
|
2794
|
+
return { content: [gate], isError: true };
|
|
2795
|
+
try {
|
|
2796
|
+
const result = await api.accessContribution(params.contribution_id);
|
|
2797
|
+
if (shaperContext) {
|
|
2798
|
+
shaperContext.lastAccessedContributionId = params.contribution_id;
|
|
2799
|
+
}
|
|
2800
|
+
const shaped = shapeRead(result);
|
|
2801
|
+
return { content: [{ type: 'text', text: JSON.stringify({ _deprecated: 'wire_access_contribution is now wire_read. This alias will be removed in v2.0.0.', ...shaped }, null, 2) }] };
|
|
2802
|
+
}
|
|
2803
|
+
catch (err) {
|
|
2804
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
// wire_resolve_handle → wire_inspect
|
|
2808
|
+
server.tool('wire_resolve_handle', '[DEPRECATED → wire_inspect] Resolve a handle path to a contribution.', {
|
|
2809
|
+
handle_path: z.string().describe('Handle path to resolve (e.g. "nightingale/77/3")'),
|
|
2810
|
+
}, async (params) => {
|
|
2811
|
+
const gate = requireIdentity();
|
|
2812
|
+
if (gate)
|
|
2813
|
+
return { content: [gate], isError: true };
|
|
2814
|
+
try {
|
|
2815
|
+
const result = await api.resolveHandlePath(params.handle_path);
|
|
2816
|
+
const shaped = shapeInspect(result, false);
|
|
2817
|
+
return { content: [{ type: 'text', text: JSON.stringify({ _deprecated: 'wire_resolve_handle is now wire_inspect. This alias will be removed in v2.0.0.', ...shaped }, null, 2) }] };
|
|
2818
|
+
}
|
|
2819
|
+
catch (err) {
|
|
2820
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2821
|
+
}
|
|
2822
|
+
});
|
|
2823
|
+
// wire_discover → wire_corpora
|
|
2824
|
+
server.tool('wire_discover', '[DEPRECATED → wire_corpora + wire_read_document] Browse source material.', {
|
|
2825
|
+
action: z.enum(['corpora', 'documents', 'read', 'topics']).describe('Action'),
|
|
2826
|
+
corpus_slug: z.string().optional(),
|
|
2827
|
+
document_id: z.string().optional(),
|
|
2828
|
+
limit: z.number().optional(),
|
|
2829
|
+
offset: z.number().optional(),
|
|
2830
|
+
}, async (params) => {
|
|
2831
|
+
const gate = requireIdentity();
|
|
2832
|
+
if (gate)
|
|
2833
|
+
return { content: [gate], isError: true };
|
|
2834
|
+
try {
|
|
2835
|
+
let result;
|
|
2836
|
+
switch (params.action) {
|
|
2837
|
+
case 'corpora':
|
|
2838
|
+
result = await api.listCorpora();
|
|
2839
|
+
break;
|
|
2840
|
+
case 'documents':
|
|
2841
|
+
if (!params.corpus_slug)
|
|
2842
|
+
return { content: [{ type: 'text', text: 'corpus_slug required for documents' }], isError: true };
|
|
2843
|
+
result = await api.getCorpusDocuments(params.corpus_slug, params.limit, params.offset);
|
|
2844
|
+
break;
|
|
2845
|
+
case 'read':
|
|
2846
|
+
if (!params.document_id)
|
|
2847
|
+
return { content: [{ type: 'text', text: 'document_id required for read' }], isError: true };
|
|
2848
|
+
result = await api.readDocument(params.document_id);
|
|
2849
|
+
break;
|
|
2850
|
+
case 'topics':
|
|
2851
|
+
result = await api.listTopics(params.limit, params.offset);
|
|
2852
|
+
break;
|
|
2853
|
+
}
|
|
2854
|
+
return { content: [{ type: 'text', text: JSON.stringify({ _deprecated: 'wire_discover is now wire_corpora (corpus browser) and wire_read_document (content access). This alias will be removed in v2.0.0.', ...result }, null, 2) }] };
|
|
2855
|
+
}
|
|
2856
|
+
catch (err) {
|
|
2857
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2858
|
+
}
|
|
2859
|
+
});
|
|
2860
|
+
// wire_mesh_status → wire_mesh
|
|
2861
|
+
server.tool('wire_mesh_status', '[DEPRECATED → wire_mesh action="status"] Check mesh status.', {}, async () => {
|
|
2862
|
+
const gate = await requireConfirmed();
|
|
2863
|
+
if (gate)
|
|
2864
|
+
return { content: [gate], isError: true };
|
|
2865
|
+
try {
|
|
2866
|
+
const result = await api.meshStatus();
|
|
2867
|
+
return { content: [{ type: 'text', text: JSON.stringify({ _deprecated: 'wire_mesh_status is now wire_mesh with action="status". This alias will be removed in v2.0.0.', ...result }, null, 2) }] };
|
|
2868
|
+
}
|
|
2869
|
+
catch (err) {
|
|
2870
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2871
|
+
}
|
|
2872
|
+
});
|
|
2873
|
+
// wire_mesh_board → wire_mesh
|
|
2874
|
+
server.tool('wire_mesh_board', '[DEPRECATED → wire_mesh action="board_*"] Read or write the shared blackboard.', {
|
|
2875
|
+
action: z.enum(['read', 'write', 'delete']).describe('Action'),
|
|
2876
|
+
key: z.string().optional(),
|
|
2877
|
+
value: z.unknown().optional(),
|
|
2878
|
+
ttl: z.number().optional(),
|
|
2879
|
+
}, async (params) => {
|
|
2880
|
+
const gate = await requireConfirmed();
|
|
2881
|
+
if (gate)
|
|
2882
|
+
return { content: [gate], isError: true };
|
|
2883
|
+
try {
|
|
2884
|
+
let result;
|
|
2885
|
+
if (params.action === 'read') {
|
|
2886
|
+
result = await api.meshBoardRead();
|
|
2887
|
+
}
|
|
2888
|
+
else if (params.action === 'write') {
|
|
2889
|
+
if (!params.key)
|
|
2890
|
+
return { content: [{ type: 'text', text: 'key is required for write' }], isError: true };
|
|
2891
|
+
if (params.value === undefined)
|
|
2892
|
+
return { content: [{ type: 'text', text: 'value is required for write' }], isError: true };
|
|
2893
|
+
result = await api.meshBoardWrite(params.key, params.value, params.ttl);
|
|
2894
|
+
}
|
|
2895
|
+
else {
|
|
2896
|
+
if (!params.key)
|
|
2897
|
+
return { content: [{ type: 'text', text: 'key is required for delete' }], isError: true };
|
|
2898
|
+
result = await api.meshBoardDelete(params.key);
|
|
2899
|
+
}
|
|
2900
|
+
return { content: [{ type: 'text', text: JSON.stringify({ _deprecated: 'wire_mesh_board is now wire_mesh with action="board_read/board_write/board_delete". This alias will be removed in v2.0.0.', ...result }, null, 2) }] };
|
|
2901
|
+
}
|
|
2902
|
+
catch (err) {
|
|
2903
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2904
|
+
}
|
|
2905
|
+
});
|
|
2906
|
+
// wire_mesh_intent → wire_mesh
|
|
2907
|
+
server.tool('wire_mesh_intent', '[DEPRECATED → wire_mesh action="intent_*"] Declare or release intents.', {
|
|
2908
|
+
action: z.enum(['declare', 'release', 'list']).describe('Action'),
|
|
2909
|
+
intent_action: z.enum(['contribute', 'challenge', 'explore', 'rate', 'query', 'browse', 'orchestrate', 'monitor', 'wait', 'sync', 'delegate']).optional(),
|
|
2910
|
+
domain: z.string().optional(),
|
|
2911
|
+
meta: z.record(z.unknown()).optional(),
|
|
2912
|
+
ttl: z.number().optional(),
|
|
2913
|
+
intent_id: z.string().optional(),
|
|
2914
|
+
}, async (params) => {
|
|
2915
|
+
const gate = await requireConfirmed();
|
|
2916
|
+
if (gate)
|
|
2917
|
+
return { content: [gate], isError: true };
|
|
2918
|
+
try {
|
|
2919
|
+
let result;
|
|
2920
|
+
if (params.action === 'list') {
|
|
2921
|
+
result = await api.meshGetIntents();
|
|
2922
|
+
}
|
|
2923
|
+
else if (params.action === 'declare') {
|
|
2924
|
+
if (!params.intent_action)
|
|
2925
|
+
return { content: [{ type: 'text', text: 'intent_action is required for declare' }], isError: true };
|
|
2926
|
+
result = await api.meshDeclareIntent(params.intent_action, params.domain, params.meta, params.ttl);
|
|
2927
|
+
}
|
|
2928
|
+
else {
|
|
2929
|
+
if (!params.intent_id)
|
|
2930
|
+
return { content: [{ type: 'text', text: 'intent_id is required for release' }], isError: true };
|
|
2931
|
+
result = await api.meshReleaseIntent(params.intent_id);
|
|
2932
|
+
}
|
|
2933
|
+
return { content: [{ type: 'text', text: JSON.stringify({ _deprecated: 'wire_mesh_intent is now wire_mesh with action="intent_declare/intent_release/intent_list". This alias will be removed in v2.0.0.', ...result }, null, 2) }] };
|
|
2934
|
+
}
|
|
2935
|
+
catch (err) {
|
|
2936
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2937
|
+
}
|
|
2938
|
+
});
|
|
2939
|
+
// ── Tool: wire_feedback ──────────────────────────────────────────────
|
|
2940
|
+
server.tool('wire_feedback', 'Submit bug reports or feature suggestions to the Wire operator.', {
|
|
2941
|
+
action: z.enum(['bug', 'suggestion']).describe('Type of feedback to submit'),
|
|
2942
|
+
message: z.string().describe('Description of the bug or suggestion'),
|
|
2943
|
+
context: z.string().optional().describe('Additional detail or context'),
|
|
2944
|
+
}, async (params) => {
|
|
2945
|
+
const gate = requireIdentity();
|
|
2946
|
+
if (gate)
|
|
2947
|
+
return { content: [gate], isError: true };
|
|
2948
|
+
try {
|
|
2949
|
+
const result = await api.request('POST', `/wire/feedback/${params.action}`, {
|
|
2950
|
+
message: params.message,
|
|
2951
|
+
...(params.context ? { context: params.context } : {}),
|
|
2952
|
+
});
|
|
2953
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2954
|
+
}
|
|
2955
|
+
catch (err) {
|
|
2956
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2957
|
+
}
|
|
2958
|
+
});
|
|
2959
|
+
// ── Tool: wire_requests ─────────────────────────────────────────────
|
|
2960
|
+
server.tool('wire_requests', 'View and manage pending requests (e.g. circle join requests, contribution access requests).', {
|
|
2961
|
+
action: z.enum(['list', 'pending']).describe('Action: list = all requests, pending = only pending'),
|
|
2962
|
+
request_id: z.string().uuid().optional().describe('Specific request UUID to view'),
|
|
2963
|
+
}, async (params) => {
|
|
2964
|
+
const gate = requireIdentity();
|
|
2965
|
+
if (gate)
|
|
2966
|
+
return { content: [gate], isError: true };
|
|
2967
|
+
try {
|
|
2968
|
+
const path = params.action === 'pending'
|
|
2969
|
+
? '/wire/requests/pending'
|
|
2970
|
+
: '/wire/requests';
|
|
2971
|
+
const url = params.request_id ? `${path}?request_id=${encodeURIComponent(params.request_id)}` : path;
|
|
2972
|
+
const result = await api.request('GET', url);
|
|
2973
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2974
|
+
}
|
|
2975
|
+
catch (err) {
|
|
2976
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
2977
|
+
}
|
|
2978
|
+
});
|
|
2979
|
+
// ── Tool: wire_handles ──────────────────────────────────────────────
|
|
2980
|
+
server.tool('wire_handles', 'Check handle availability or view your current handle.', {
|
|
2981
|
+
action: z.enum(['check', 'mine']).describe('check = check handle availability, mine = view your current handle'),
|
|
2982
|
+
handle: z.string().optional().describe('Handle to check availability for (required for "check" action)'),
|
|
2983
|
+
}, async (params) => {
|
|
2984
|
+
const gate = requireIdentity();
|
|
2985
|
+
if (gate)
|
|
2986
|
+
return { content: [gate], isError: true };
|
|
2987
|
+
try {
|
|
2988
|
+
if (params.action === 'check') {
|
|
2989
|
+
if (!params.handle)
|
|
2990
|
+
return { content: [{ type: 'text', text: 'Error: handle is required for check action' }], isError: true };
|
|
2991
|
+
const result = await api.request('GET', `/wire/handles/check?handle=${encodeURIComponent(params.handle)}`);
|
|
2992
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2993
|
+
}
|
|
2994
|
+
// mine
|
|
2995
|
+
const result = await api.request('GET', '/wire/handles');
|
|
2996
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
2997
|
+
}
|
|
2998
|
+
catch (err) {
|
|
2999
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
3000
|
+
}
|
|
3001
|
+
});
|
|
3002
|
+
// ── Tool: wire_event_subscriptions ──────────────────────────────────
|
|
3003
|
+
server.tool('wire_event_subscriptions', 'Manage webhook event subscriptions for real-time notifications. Credit-affecting: requires confirmed agent status.', {
|
|
3004
|
+
action: z.enum(['list', 'create', 'delete']).describe('Action to perform'),
|
|
3005
|
+
subscription_id: z.string().uuid().optional().describe('Subscription UUID (required for delete)'),
|
|
3006
|
+
event_type: z.string().optional().describe('Event type to subscribe to (required for create)'),
|
|
3007
|
+
webhook_url: z.string().optional().describe('Webhook URL for notifications (required for create)'),
|
|
3008
|
+
}, async (params) => {
|
|
3009
|
+
const gate = requireIdentity();
|
|
3010
|
+
if (gate)
|
|
3011
|
+
return { content: [gate], isError: true };
|
|
3012
|
+
try {
|
|
3013
|
+
if (params.action === 'list') {
|
|
3014
|
+
const result = await api.request('GET', '/wire/event-subscriptions');
|
|
3015
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
3016
|
+
}
|
|
3017
|
+
if (params.action === 'create') {
|
|
3018
|
+
if (!params.event_type)
|
|
3019
|
+
return { content: [{ type: 'text', text: 'Error: event_type is required for create' }], isError: true };
|
|
3020
|
+
if (!params.webhook_url)
|
|
3021
|
+
return { content: [{ type: 'text', text: 'Error: webhook_url is required for create' }], isError: true };
|
|
3022
|
+
const result = await api.request('POST', '/wire/event-subscriptions', {
|
|
3023
|
+
event_type: params.event_type,
|
|
3024
|
+
webhook_url: params.webhook_url,
|
|
3025
|
+
});
|
|
3026
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
3027
|
+
}
|
|
3028
|
+
if (params.action === 'delete') {
|
|
3029
|
+
if (!params.subscription_id)
|
|
3030
|
+
return { content: [{ type: 'text', text: 'Error: subscription_id is required for delete' }], isError: true };
|
|
3031
|
+
await api.request('DELETE', `/wire/event-subscriptions?id=${encodeURIComponent(params.subscription_id)}`);
|
|
3032
|
+
return { content: [{ type: 'text', text: JSON.stringify({ success: true, deleted: params.subscription_id }, null, 2) }] };
|
|
3033
|
+
}
|
|
3034
|
+
return { content: [{ type: 'text', text: 'Invalid action' }], isError: true };
|
|
3035
|
+
}
|
|
3036
|
+
catch (err) {
|
|
3037
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
3038
|
+
}
|
|
3039
|
+
});
|
|
3040
|
+
// ── Tool: wire_pearl_dive ───────────────────────────────────────────
|
|
3041
|
+
server.tool('wire_pearl_dive', 'Discover surprising or underexplored connections in the knowledge graph.', {
|
|
3042
|
+
topic: z.string().optional().describe('Topic to explore for hidden connections'),
|
|
3043
|
+
depth: z.number().optional().default(2).describe('Search depth (default 2)'),
|
|
3044
|
+
}, async (params) => {
|
|
3045
|
+
const gate = requireIdentity();
|
|
3046
|
+
if (gate)
|
|
3047
|
+
return { content: [gate], isError: true };
|
|
3048
|
+
try {
|
|
3049
|
+
const qs = new URLSearchParams();
|
|
3050
|
+
if (params.topic)
|
|
3051
|
+
qs.set('topic', params.topic);
|
|
3052
|
+
if (params.depth !== undefined)
|
|
3053
|
+
qs.set('depth', String(params.depth));
|
|
3054
|
+
const queryString = qs.toString();
|
|
3055
|
+
const result = await api.request('GET', `/wire/discover/pearl-dive${queryString ? '?' + queryString : ''}`);
|
|
3056
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
3057
|
+
}
|
|
3058
|
+
catch (err) {
|
|
3059
|
+
return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true };
|
|
3060
|
+
}
|
|
3061
|
+
});
|
|
3062
|
+
// ── Resources ─────────────────────────────────────────────────────────
|
|
3063
|
+
server.resource('entities-list', 'wire://entities', async (uri) => {
|
|
3064
|
+
try {
|
|
3065
|
+
const result = await api.browse({});
|
|
3066
|
+
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] };
|
|
3067
|
+
}
|
|
3068
|
+
catch (err) {
|
|
3069
|
+
return { contents: [{ uri: uri.href, mimeType: 'text/plain', text: `Error: ${err.message}` }] };
|
|
3070
|
+
}
|
|
3071
|
+
});
|
|
3072
|
+
server.resource('entity-detail', new ResourceTemplate('wire://entities/{name}', { list: undefined }), async (uri, variables) => {
|
|
3073
|
+
try {
|
|
3074
|
+
const result = await api.browse({ entity: variables.name });
|
|
3075
|
+
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] };
|
|
3076
|
+
}
|
|
3077
|
+
catch (err) {
|
|
3078
|
+
return { contents: [{ uri: uri.href, mimeType: 'text/plain', text: `Error: ${err.message}` }] };
|
|
3079
|
+
}
|
|
3080
|
+
});
|
|
3081
|
+
server.resource('entity-derivatives', new ResourceTemplate('wire://entities/{name}/derivatives', { list: undefined }), async (uri, variables) => {
|
|
3082
|
+
try {
|
|
3083
|
+
const result = await api.browse({ entity: variables.name, arc: 'derivatives' });
|
|
3084
|
+
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] };
|
|
3085
|
+
}
|
|
3086
|
+
catch (err) {
|
|
3087
|
+
return { contents: [{ uri: uri.href, mimeType: 'text/plain', text: `Error: ${err.message}` }] };
|
|
3088
|
+
}
|
|
3089
|
+
});
|
|
3090
|
+
server.resource('item-detail', new ResourceTemplate('wire://items/{id}', { list: undefined }), async (uri, variables) => {
|
|
3091
|
+
try {
|
|
3092
|
+
const result = await api.browse({ item_id: variables.id });
|
|
3093
|
+
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] };
|
|
3094
|
+
}
|
|
3095
|
+
catch (err) {
|
|
3096
|
+
return { contents: [{ uri: uri.href, mimeType: 'text/plain', text: `Error: ${err.message}` }] };
|
|
3097
|
+
}
|
|
3098
|
+
});
|
|
3099
|
+
server.resource('item-tree', new ResourceTemplate('wire://items/{id}/tree', { list: undefined }), async (uri, variables) => {
|
|
3100
|
+
try {
|
|
3101
|
+
const result = await api.browse({ item_id: variables.id, arc: 'tree' });
|
|
3102
|
+
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(result, null, 2) }] };
|
|
3103
|
+
}
|
|
3104
|
+
catch (err) {
|
|
3105
|
+
return { contents: [{ uri: uri.href, mimeType: 'text/plain', text: `Error: ${err.message}` }] };
|
|
3106
|
+
}
|
|
3107
|
+
});
|
|
3108
|
+
// ── Start background work loop (conditionally) ────────────────────────
|
|
3109
|
+
// Mission #1.5-rev2: background worker no longer auto-starts at MCP
|
|
3110
|
+
// boot. It starts from ensureNodeAndMaybeStartWorker() after the first
|
|
3111
|
+
// successful wire_identify pin, when an agent token exists.
|
|
3112
|
+
if (options?.noWorker) {
|
|
3113
|
+
console.error('[wire-mcp] Starting in MCP-only mode (--no-worker).');
|
|
3114
|
+
}
|
|
3115
|
+
else {
|
|
3116
|
+
console.error('[wire-mcp] Background worker pending until wire_identify establishes a slot pin.');
|
|
3117
|
+
}
|
|
3118
|
+
// ── Connect transport ────────────────────────────────────────────────
|
|
3119
|
+
if (options?.transport === 'http') {
|
|
3120
|
+
const port = options?.port ?? 3200;
|
|
3121
|
+
const httpTransport = new StreamableHTTPServerTransport({
|
|
3122
|
+
sessionIdGenerator: undefined, // stateless — identity is per-token, not per-session
|
|
3123
|
+
});
|
|
3124
|
+
await server.connect(httpTransport);
|
|
3125
|
+
const httpServer = createServer(async (req, res) => {
|
|
3126
|
+
// CORS for browser-based / remote clients
|
|
3127
|
+
const origin = req.headers.origin || '';
|
|
3128
|
+
const isLocalhost = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin);
|
|
3129
|
+
res.setHeader('Access-Control-Allow-Origin', isLocalhost ? origin : 'http://localhost');
|
|
3130
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
3131
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id, mcp-protocol-version');
|
|
3132
|
+
res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id');
|
|
3133
|
+
if (req.method === 'OPTIONS') {
|
|
3134
|
+
res.writeHead(204);
|
|
3135
|
+
res.end();
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
3138
|
+
await httpTransport.handleRequest(req, res);
|
|
3139
|
+
});
|
|
3140
|
+
httpServer.listen(port, () => {
|
|
3141
|
+
console.error(`[wire-mcp] MCP server started on http://localhost:${port}/`);
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
3144
|
+
else {
|
|
3145
|
+
const transport = new StdioServerTransport();
|
|
3146
|
+
await server.connect(transport);
|
|
3147
|
+
console.error('[wire-mcp] MCP server started on stdio');
|
|
3148
|
+
}
|
|
3149
|
+
// Graceful shutdown
|
|
3150
|
+
const cleanup = () => {
|
|
3151
|
+
if (worker)
|
|
3152
|
+
worker.stop();
|
|
3153
|
+
releaseLock();
|
|
3154
|
+
process.exit(0);
|
|
3155
|
+
};
|
|
3156
|
+
process.on('SIGINT', cleanup);
|
|
3157
|
+
process.on('SIGTERM', cleanup);
|
|
3158
|
+
}
|
|
3159
|
+
//# sourceMappingURL=mcp-server.js.map
|