eacn3 0.1.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/dist/index.d.ts +7 -0
- package/dist/index.js +574 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +640 -0
- package/dist/server.js.map +1 -0
- package/dist/src/models.d.ts +161 -0
- package/dist/src/models.js +14 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/network-client.d.ts +96 -0
- package/dist/src/network-client.js +228 -0
- package/dist/src/network-client.js.map +1 -0
- package/dist/src/state.d.ts +33 -0
- package/dist/src/state.js +113 -0
- package/dist/src/state.js.map +1 -0
- package/dist/src/ws-manager.d.ts +30 -0
- package/dist/src/ws-manager.js +150 -0
- package/dist/src/ws-manager.js.map +1 -0
- package/openclaw.plugin.json +21 -0
- package/package.json +52 -0
- package/scripts/cli.cjs +332 -0
- package/scripts/postinstall.cjs +62 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EACN MCP Server — exposes 32 tools via stdio transport.
|
|
3
|
+
*
|
|
4
|
+
* All intelligence lives in Skills (host LLM). This server is just
|
|
5
|
+
* state management + network API wrapper. No adapter, no registry —
|
|
6
|
+
* everything is inline.
|
|
7
|
+
*/
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import * as state from "./src/state.js";
|
|
12
|
+
import * as net from "./src/network-client.js";
|
|
13
|
+
import * as ws from "./src/ws-manager.js";
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Helper: MCP text result
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
function ok(data) {
|
|
18
|
+
return { content: [{ type: "text", text: JSON.stringify(data) }] };
|
|
19
|
+
}
|
|
20
|
+
function err(message) {
|
|
21
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: message }) }] };
|
|
22
|
+
}
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Heartbeat background interval
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
let heartbeatInterval = null;
|
|
27
|
+
function startHeartbeat() {
|
|
28
|
+
if (heartbeatInterval)
|
|
29
|
+
return;
|
|
30
|
+
heartbeatInterval = setInterval(async () => {
|
|
31
|
+
try {
|
|
32
|
+
await net.heartbeat();
|
|
33
|
+
}
|
|
34
|
+
catch { /* silent */ }
|
|
35
|
+
}, 60_000);
|
|
36
|
+
}
|
|
37
|
+
function stopHeartbeat() {
|
|
38
|
+
if (heartbeatInterval) {
|
|
39
|
+
clearInterval(heartbeatInterval);
|
|
40
|
+
heartbeatInterval = null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// MCP Server
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
const server = new McpServer({ name: "eacn", version: "0.1.0" });
|
|
47
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
48
|
+
// Server Management (4)
|
|
49
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
50
|
+
// #1 eacn_connect
|
|
51
|
+
server.tool("eacn_connect", "Connect to EACN network. Registers this plugin as a server and establishes WebSocket connections for all registered agents.", {
|
|
52
|
+
network_endpoint: z.string().optional().describe("Network URL. Defaults to https://network.eacn.dev"),
|
|
53
|
+
}, async (params) => {
|
|
54
|
+
const endpoint = params.network_endpoint ?? "https://network.eacn.dev";
|
|
55
|
+
const s = state.getState();
|
|
56
|
+
s.network_endpoint = endpoint;
|
|
57
|
+
// Register as server
|
|
58
|
+
const res = await net.registerServer("0.1.0", "plugin://local", "plugin-user");
|
|
59
|
+
s.server_card = {
|
|
60
|
+
server_id: res.server_id,
|
|
61
|
+
version: "0.1.0",
|
|
62
|
+
endpoint: "plugin://local",
|
|
63
|
+
owner: "plugin-user",
|
|
64
|
+
status: "online",
|
|
65
|
+
};
|
|
66
|
+
state.save();
|
|
67
|
+
// Start background heartbeat
|
|
68
|
+
startHeartbeat();
|
|
69
|
+
// Reconnect WS for all existing agents
|
|
70
|
+
for (const agentId of Object.keys(s.agents)) {
|
|
71
|
+
ws.connect(agentId);
|
|
72
|
+
}
|
|
73
|
+
return ok({
|
|
74
|
+
connected: true,
|
|
75
|
+
server_id: res.server_id,
|
|
76
|
+
network_endpoint: endpoint,
|
|
77
|
+
agents_online: Object.keys(s.agents).length,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
// #2 eacn_disconnect
|
|
81
|
+
server.tool("eacn_disconnect", "Disconnect from EACN network. Unregisters server and closes all WebSocket connections.", {}, async () => {
|
|
82
|
+
stopHeartbeat();
|
|
83
|
+
ws.disconnectAll();
|
|
84
|
+
try {
|
|
85
|
+
await net.unregisterServer();
|
|
86
|
+
}
|
|
87
|
+
catch { /* may already be gone */ }
|
|
88
|
+
const s = state.getState();
|
|
89
|
+
s.server_card = null;
|
|
90
|
+
s.agents = {};
|
|
91
|
+
state.save();
|
|
92
|
+
return ok({ disconnected: true });
|
|
93
|
+
});
|
|
94
|
+
// #3 eacn_heartbeat
|
|
95
|
+
server.tool("eacn_heartbeat", "Send heartbeat to network. Background interval auto-sends every 60s; this is for manual trigger.", {}, async () => {
|
|
96
|
+
const res = await net.heartbeat();
|
|
97
|
+
return ok(res);
|
|
98
|
+
});
|
|
99
|
+
// #4 eacn_server_info
|
|
100
|
+
server.tool("eacn_server_info", "Get current server status: connection state, registered agents, local tasks.", {}, async () => {
|
|
101
|
+
const s = state.getState();
|
|
102
|
+
if (!s.server_card)
|
|
103
|
+
return err("Not connected");
|
|
104
|
+
let remote;
|
|
105
|
+
try {
|
|
106
|
+
remote = await net.getServer(s.server_card.server_id);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
remote = null;
|
|
110
|
+
}
|
|
111
|
+
return ok({
|
|
112
|
+
server_card: s.server_card,
|
|
113
|
+
network_endpoint: s.network_endpoint,
|
|
114
|
+
agents_count: Object.keys(s.agents).length,
|
|
115
|
+
agents: Object.keys(s.agents),
|
|
116
|
+
tasks_count: Object.keys(s.local_tasks).length,
|
|
117
|
+
remote_status: remote?.status ?? "unknown",
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
121
|
+
// Agent Management (7)
|
|
122
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
123
|
+
// #5 eacn_register_agent
|
|
124
|
+
// Inlines: adapter (AgentCard assembly) + registry (validate + persist + DHT)
|
|
125
|
+
server.tool("eacn_register_agent", "Register an Agent on the network. Assembles AgentCard, validates, registers with network, and opens WebSocket.", {
|
|
126
|
+
name: z.string().describe("Agent display name"),
|
|
127
|
+
description: z.string().describe("What this Agent does"),
|
|
128
|
+
domains: z.array(z.string()).describe("Capability domains (e.g. ['translation', 'coding'])"),
|
|
129
|
+
skills: z.array(z.object({
|
|
130
|
+
id: z.string().optional(),
|
|
131
|
+
name: z.string(),
|
|
132
|
+
description: z.string(),
|
|
133
|
+
tags: z.array(z.string()).optional(),
|
|
134
|
+
parameters: z.record(z.string(), z.unknown()).optional(),
|
|
135
|
+
})).optional().describe("Agent skills"),
|
|
136
|
+
capabilities: z.object({
|
|
137
|
+
max_concurrent_tasks: z.number().describe("Max tasks this Agent can handle simultaneously (0 = unlimited)"),
|
|
138
|
+
concurrent: z.boolean().describe("Whether this Agent supports concurrent execution"),
|
|
139
|
+
}).optional().describe("Agent capacity limits"),
|
|
140
|
+
agent_type: z.enum(["executor", "planner"]).optional().describe("Defaults to executor"),
|
|
141
|
+
agent_id: z.string().optional().describe("Custom agent ID. Auto-generated if omitted."),
|
|
142
|
+
}, async (params) => {
|
|
143
|
+
const s = state.getState();
|
|
144
|
+
if (!s.server_card)
|
|
145
|
+
return err("Not connected. Call eacn_connect first.");
|
|
146
|
+
// Validate
|
|
147
|
+
if (!params.name.trim())
|
|
148
|
+
return err("name cannot be empty");
|
|
149
|
+
if (params.domains.length === 0)
|
|
150
|
+
return err("domains cannot be empty");
|
|
151
|
+
const agentId = params.agent_id ?? `agent-${Date.now().toString(36)}`;
|
|
152
|
+
const sid = s.server_card.server_id;
|
|
153
|
+
// Assemble AgentCard (what adapter used to do)
|
|
154
|
+
const card = {
|
|
155
|
+
agent_id: agentId,
|
|
156
|
+
name: params.name,
|
|
157
|
+
agent_type: params.agent_type ?? "executor",
|
|
158
|
+
domains: params.domains,
|
|
159
|
+
skills: params.skills ?? [],
|
|
160
|
+
capabilities: params.capabilities,
|
|
161
|
+
url: `plugin://local/agents/${agentId}`,
|
|
162
|
+
server_id: sid,
|
|
163
|
+
network_id: "",
|
|
164
|
+
description: params.description,
|
|
165
|
+
};
|
|
166
|
+
// Register with network (what registry used to do)
|
|
167
|
+
const res = await net.registerAgent(card);
|
|
168
|
+
// Persist locally
|
|
169
|
+
state.addAgent(card);
|
|
170
|
+
// Open WebSocket for event push
|
|
171
|
+
ws.connect(agentId);
|
|
172
|
+
return ok({
|
|
173
|
+
registered: true,
|
|
174
|
+
agent_id: agentId,
|
|
175
|
+
seeds: res.seeds,
|
|
176
|
+
domains: params.domains,
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
// #6 eacn_get_agent
|
|
180
|
+
server.tool("eacn_get_agent", "Get any Agent's details (AgentCard) by ID.", {
|
|
181
|
+
agent_id: z.string(),
|
|
182
|
+
}, async (params) => {
|
|
183
|
+
// Check local first
|
|
184
|
+
const local = state.getAgent(params.agent_id);
|
|
185
|
+
if (local)
|
|
186
|
+
return ok(local);
|
|
187
|
+
// Fetch from network
|
|
188
|
+
const remote = await net.getAgentInfo(params.agent_id);
|
|
189
|
+
return ok(remote);
|
|
190
|
+
});
|
|
191
|
+
// #7 eacn_update_agent
|
|
192
|
+
server.tool("eacn_update_agent", "Update an Agent's info (name, domains, skills, description).", {
|
|
193
|
+
agent_id: z.string(),
|
|
194
|
+
name: z.string().optional(),
|
|
195
|
+
domains: z.array(z.string()).optional(),
|
|
196
|
+
skills: z.array(z.object({
|
|
197
|
+
id: z.string().optional(),
|
|
198
|
+
name: z.string(),
|
|
199
|
+
description: z.string(),
|
|
200
|
+
tags: z.array(z.string()).optional(),
|
|
201
|
+
parameters: z.record(z.string(), z.unknown()).optional(),
|
|
202
|
+
})).optional(),
|
|
203
|
+
description: z.string().optional(),
|
|
204
|
+
}, async (params) => {
|
|
205
|
+
const { agent_id, ...updates } = params;
|
|
206
|
+
const res = await net.updateAgent(agent_id, updates);
|
|
207
|
+
// Update local state
|
|
208
|
+
const local = state.getAgent(agent_id);
|
|
209
|
+
if (local) {
|
|
210
|
+
if (updates.name !== undefined)
|
|
211
|
+
local.name = updates.name;
|
|
212
|
+
if (updates.domains !== undefined)
|
|
213
|
+
local.domains = updates.domains;
|
|
214
|
+
if (updates.skills !== undefined)
|
|
215
|
+
local.skills = updates.skills;
|
|
216
|
+
if (updates.description !== undefined)
|
|
217
|
+
local.description = updates.description;
|
|
218
|
+
state.addAgent(local); // re-save
|
|
219
|
+
}
|
|
220
|
+
return ok({ updated: true, agent_id, ...res });
|
|
221
|
+
});
|
|
222
|
+
// #8 eacn_unregister_agent
|
|
223
|
+
server.tool("eacn_unregister_agent", "Unregister an Agent from the network.", {
|
|
224
|
+
agent_id: z.string(),
|
|
225
|
+
}, async (params) => {
|
|
226
|
+
const res = await net.unregisterAgent(params.agent_id);
|
|
227
|
+
ws.disconnect(params.agent_id);
|
|
228
|
+
state.removeAgent(params.agent_id);
|
|
229
|
+
return ok({ unregistered: true, agent_id: params.agent_id, ...res });
|
|
230
|
+
});
|
|
231
|
+
// #9 eacn_list_my_agents
|
|
232
|
+
server.tool("eacn_list_my_agents", "List all Agents registered under this server.", {}, async () => {
|
|
233
|
+
const agents = state.listAgents();
|
|
234
|
+
return ok({
|
|
235
|
+
count: agents.length,
|
|
236
|
+
agents: agents.map((a) => ({
|
|
237
|
+
agent_id: a.agent_id,
|
|
238
|
+
name: a.name,
|
|
239
|
+
agent_type: a.agent_type,
|
|
240
|
+
domains: a.domains,
|
|
241
|
+
ws_connected: ws.isConnected(a.agent_id),
|
|
242
|
+
})),
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
// #10 eacn_discover_agents
|
|
246
|
+
server.tool("eacn_discover_agents", "Discover Agents by domain. Searches network via Gossip → DHT → Bootstrap fallback.", {
|
|
247
|
+
domain: z.string(),
|
|
248
|
+
requester_id: z.string().optional(),
|
|
249
|
+
}, async (params) => {
|
|
250
|
+
const res = await net.discoverAgents(params.domain, params.requester_id);
|
|
251
|
+
return ok(res);
|
|
252
|
+
});
|
|
253
|
+
// #11 eacn_list_agents
|
|
254
|
+
server.tool("eacn_list_agents", "List Agents from the network. Filter by domain or server_id.", {
|
|
255
|
+
domain: z.string().optional(),
|
|
256
|
+
server_id: z.string().optional(),
|
|
257
|
+
limit: z.number().optional(),
|
|
258
|
+
offset: z.number().optional(),
|
|
259
|
+
}, async (params) => {
|
|
260
|
+
const agents = await net.listAgentsRemote(params);
|
|
261
|
+
return ok({ count: agents.length, agents });
|
|
262
|
+
});
|
|
263
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
264
|
+
// Task Query (4)
|
|
265
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
266
|
+
// #12 eacn_get_task
|
|
267
|
+
server.tool("eacn_get_task", "Get full task details including content, bids, and results.", {
|
|
268
|
+
task_id: z.string(),
|
|
269
|
+
}, async (params) => {
|
|
270
|
+
const task = await net.getTask(params.task_id);
|
|
271
|
+
return ok(task);
|
|
272
|
+
});
|
|
273
|
+
// #13 eacn_get_task_status
|
|
274
|
+
server.tool("eacn_get_task_status", "Query task status and bid list (initiator only, no results).", {
|
|
275
|
+
task_id: z.string(),
|
|
276
|
+
agent_id: z.string().describe("Initiator agent ID"),
|
|
277
|
+
}, async (params) => {
|
|
278
|
+
const status = await net.getTaskStatus(params.task_id, params.agent_id);
|
|
279
|
+
return ok(status);
|
|
280
|
+
});
|
|
281
|
+
// #14 eacn_list_open_tasks
|
|
282
|
+
server.tool("eacn_list_open_tasks", "List tasks open for bidding. Optionally filter by domains.", {
|
|
283
|
+
domains: z.string().optional().describe("Comma-separated domain filter"),
|
|
284
|
+
limit: z.number().optional(),
|
|
285
|
+
offset: z.number().optional(),
|
|
286
|
+
}, async (params) => {
|
|
287
|
+
const tasks = await net.getOpenTasks(params);
|
|
288
|
+
return ok({ count: tasks.length, tasks });
|
|
289
|
+
});
|
|
290
|
+
// #15 eacn_list_tasks
|
|
291
|
+
server.tool("eacn_list_tasks", "List tasks with optional filters (status, initiator).", {
|
|
292
|
+
status: z.string().optional(),
|
|
293
|
+
initiator_id: z.string().optional(),
|
|
294
|
+
limit: z.number().optional(),
|
|
295
|
+
offset: z.number().optional(),
|
|
296
|
+
}, async (params) => {
|
|
297
|
+
const tasks = await net.listTasks(params);
|
|
298
|
+
return ok({ count: tasks.length, tasks });
|
|
299
|
+
});
|
|
300
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
301
|
+
// Task Operations — Initiator (7)
|
|
302
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
303
|
+
// #16 eacn_create_task
|
|
304
|
+
// Inlines matcher: check local agents before hitting network
|
|
305
|
+
server.tool("eacn_create_task", "Create a new task. Checks local agents first, then broadcasts to network.", {
|
|
306
|
+
description: z.string(),
|
|
307
|
+
budget: z.number(),
|
|
308
|
+
domains: z.array(z.string()).optional(),
|
|
309
|
+
deadline: z.string().optional().describe("ISO 8601 deadline"),
|
|
310
|
+
max_concurrent_bidders: z.number().optional(),
|
|
311
|
+
max_depth: z.number().optional().describe("Max subtask nesting depth (default 3)"),
|
|
312
|
+
expected_output: z.object({
|
|
313
|
+
type: z.string().describe("Expected output format, e.g. 'json', 'text', 'code'"),
|
|
314
|
+
description: z.string().describe("What the output should contain"),
|
|
315
|
+
}).optional().describe("Structured description of expected result"),
|
|
316
|
+
human_contact: z.object({
|
|
317
|
+
allowed: z.boolean().describe("Whether human owner can be contacted for decisions"),
|
|
318
|
+
contact_id: z.string().optional().describe("Human contact identifier"),
|
|
319
|
+
timeout_s: z.number().optional().describe("Seconds to wait for human response before auto-reject"),
|
|
320
|
+
}).optional().describe("Human-in-the-loop contact settings"),
|
|
321
|
+
initiator_id: z.string().describe("Agent ID of the task initiator"),
|
|
322
|
+
}, async (params) => {
|
|
323
|
+
const taskId = `t-${Date.now().toString(36)}`;
|
|
324
|
+
// Local matching (what matcher used to do): check if any local agent covers the domains
|
|
325
|
+
const localAgents = state.listAgents();
|
|
326
|
+
const matchedLocal = params.domains
|
|
327
|
+
? localAgents.filter((a) => a.agent_id !== params.initiator_id &&
|
|
328
|
+
params.domains.some((d) => a.domains.includes(d)))
|
|
329
|
+
: [];
|
|
330
|
+
const task = await net.createTask({
|
|
331
|
+
task_id: taskId,
|
|
332
|
+
initiator_id: params.initiator_id,
|
|
333
|
+
content: {
|
|
334
|
+
description: params.description,
|
|
335
|
+
expected_output: params.expected_output,
|
|
336
|
+
},
|
|
337
|
+
domains: params.domains,
|
|
338
|
+
budget: params.budget,
|
|
339
|
+
deadline: params.deadline,
|
|
340
|
+
max_concurrent_bidders: params.max_concurrent_bidders,
|
|
341
|
+
max_depth: params.max_depth,
|
|
342
|
+
human_contact: params.human_contact,
|
|
343
|
+
});
|
|
344
|
+
// Track locally
|
|
345
|
+
state.updateTask({
|
|
346
|
+
task_id: taskId,
|
|
347
|
+
role: "initiator",
|
|
348
|
+
status: task.status,
|
|
349
|
+
domains: params.domains ?? [],
|
|
350
|
+
description_summary: params.description.slice(0, 100),
|
|
351
|
+
created_at: new Date().toISOString(),
|
|
352
|
+
});
|
|
353
|
+
return ok({
|
|
354
|
+
task_id: taskId,
|
|
355
|
+
status: task.status,
|
|
356
|
+
budget: params.budget,
|
|
357
|
+
local_matches: matchedLocal.map((a) => a.agent_id),
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
// #17 eacn_get_task_results
|
|
361
|
+
server.tool("eacn_get_task_results", "Retrieve task results and adjudications. First call transitions task from awaiting_retrieval to completed.", {
|
|
362
|
+
task_id: z.string(),
|
|
363
|
+
initiator_id: z.string(),
|
|
364
|
+
}, async (params) => {
|
|
365
|
+
const res = await net.getTaskResults(params.task_id, params.initiator_id);
|
|
366
|
+
return ok(res);
|
|
367
|
+
});
|
|
368
|
+
// #18 eacn_select_result
|
|
369
|
+
server.tool("eacn_select_result", "Select the winning result. Triggers economic settlement.", {
|
|
370
|
+
task_id: z.string(),
|
|
371
|
+
agent_id: z.string().describe("ID of the agent whose result to select"),
|
|
372
|
+
initiator_id: z.string(),
|
|
373
|
+
}, async (params) => {
|
|
374
|
+
const res = await net.selectResult(params.task_id, params.initiator_id, params.agent_id);
|
|
375
|
+
return ok(res);
|
|
376
|
+
});
|
|
377
|
+
// #19 eacn_close_task
|
|
378
|
+
server.tool("eacn_close_task", "Manually close a task (stop accepting bids/results).", {
|
|
379
|
+
task_id: z.string(),
|
|
380
|
+
initiator_id: z.string(),
|
|
381
|
+
}, async (params) => {
|
|
382
|
+
const res = await net.closeTask(params.task_id, params.initiator_id);
|
|
383
|
+
return ok(res);
|
|
384
|
+
});
|
|
385
|
+
// #20 eacn_update_deadline
|
|
386
|
+
server.tool("eacn_update_deadline", "Update task deadline.", {
|
|
387
|
+
task_id: z.string(),
|
|
388
|
+
new_deadline: z.string().describe("New ISO 8601 deadline"),
|
|
389
|
+
initiator_id: z.string(),
|
|
390
|
+
}, async (params) => {
|
|
391
|
+
const res = await net.updateDeadline(params.task_id, params.initiator_id, params.new_deadline);
|
|
392
|
+
return ok(res);
|
|
393
|
+
});
|
|
394
|
+
// #21 eacn_update_discussions
|
|
395
|
+
server.tool("eacn_update_discussions", "Add a discussion message to a task. Synced to all bidders.", {
|
|
396
|
+
task_id: z.string(),
|
|
397
|
+
message: z.string(),
|
|
398
|
+
initiator_id: z.string(),
|
|
399
|
+
}, async (params) => {
|
|
400
|
+
const res = await net.updateDiscussions(params.task_id, params.initiator_id, params.message);
|
|
401
|
+
return ok(res);
|
|
402
|
+
});
|
|
403
|
+
// #22 eacn_confirm_budget
|
|
404
|
+
server.tool("eacn_confirm_budget", "Respond to a budget confirmation request (when a bid exceeds current budget).", {
|
|
405
|
+
task_id: z.string(),
|
|
406
|
+
approved: z.boolean(),
|
|
407
|
+
new_budget: z.number().optional(),
|
|
408
|
+
initiator_id: z.string(),
|
|
409
|
+
}, async (params) => {
|
|
410
|
+
const res = await net.confirmBudget(params.task_id, params.initiator_id, params.approved, params.new_budget);
|
|
411
|
+
return ok(res);
|
|
412
|
+
});
|
|
413
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
414
|
+
// Task Operations — Executor (5)
|
|
415
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
416
|
+
// #23 eacn_submit_bid
|
|
417
|
+
server.tool("eacn_submit_bid", "Submit a bid on a task (confidence + price).", {
|
|
418
|
+
task_id: z.string(),
|
|
419
|
+
confidence: z.number().min(0).max(1).describe("0.0-1.0 confidence in ability to complete"),
|
|
420
|
+
price: z.number().describe("Bid price"),
|
|
421
|
+
agent_id: z.string(),
|
|
422
|
+
}, async (params) => {
|
|
423
|
+
const res = await net.submitBid(params.task_id, params.agent_id, params.confidence, params.price);
|
|
424
|
+
// Track locally if not rejected (status could be "executing", "waiting_execution", etc.)
|
|
425
|
+
if (res.status && res.status !== "rejected") {
|
|
426
|
+
state.updateTask({
|
|
427
|
+
task_id: params.task_id,
|
|
428
|
+
role: "executor",
|
|
429
|
+
status: "bidding",
|
|
430
|
+
domains: [],
|
|
431
|
+
description_summary: "",
|
|
432
|
+
created_at: new Date().toISOString(),
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return ok(res);
|
|
436
|
+
});
|
|
437
|
+
// #24 eacn_submit_result
|
|
438
|
+
// Inlines logger: auto-report reputation event
|
|
439
|
+
server.tool("eacn_submit_result", "Submit execution result for a task.", {
|
|
440
|
+
task_id: z.string(),
|
|
441
|
+
content: z.record(z.string(), z.unknown()).describe("Result content object"),
|
|
442
|
+
agent_id: z.string(),
|
|
443
|
+
}, async (params) => {
|
|
444
|
+
const res = await net.submitResult(params.task_id, params.agent_id, params.content);
|
|
445
|
+
// Auto-report reputation event (what logger used to do)
|
|
446
|
+
try {
|
|
447
|
+
await net.reportEvent(params.agent_id, "task_completed");
|
|
448
|
+
}
|
|
449
|
+
catch { /* non-critical */ }
|
|
450
|
+
return ok(res);
|
|
451
|
+
});
|
|
452
|
+
// #25 eacn_reject_task
|
|
453
|
+
// Inlines logger: auto-report reputation event
|
|
454
|
+
server.tool("eacn_reject_task", "Reject/return a task. Frees the execution slot. Note: rejection affects reputation.", {
|
|
455
|
+
task_id: z.string(),
|
|
456
|
+
reason: z.string().optional(),
|
|
457
|
+
agent_id: z.string(),
|
|
458
|
+
}, async (params) => {
|
|
459
|
+
const res = await net.rejectTask(params.task_id, params.agent_id, params.reason);
|
|
460
|
+
// Auto-report reputation event
|
|
461
|
+
try {
|
|
462
|
+
await net.reportEvent(params.agent_id, "task_rejected");
|
|
463
|
+
}
|
|
464
|
+
catch { /* non-critical */ }
|
|
465
|
+
return ok(res);
|
|
466
|
+
});
|
|
467
|
+
// #26 eacn_create_subtask
|
|
468
|
+
server.tool("eacn_create_subtask", "Create a subtask under a parent task. Budget is carved from parent's escrow.", {
|
|
469
|
+
parent_task_id: z.string(),
|
|
470
|
+
description: z.string(),
|
|
471
|
+
domains: z.array(z.string()),
|
|
472
|
+
budget: z.number(),
|
|
473
|
+
deadline: z.string().optional(),
|
|
474
|
+
initiator_id: z.string().describe("Agent ID of the executor creating the subtask"),
|
|
475
|
+
}, async (params) => {
|
|
476
|
+
const task = await net.createSubtask(params.parent_task_id, params.initiator_id, { description: params.description }, params.domains, params.budget, params.deadline);
|
|
477
|
+
return ok({
|
|
478
|
+
subtask_id: task.id,
|
|
479
|
+
parent_task_id: params.parent_task_id,
|
|
480
|
+
status: task.status,
|
|
481
|
+
depth: task.depth,
|
|
482
|
+
});
|
|
483
|
+
});
|
|
484
|
+
// #27 eacn_send_message
|
|
485
|
+
server.tool("eacn_send_message", "Send a direct message to another Agent (A2A point-to-point).", {
|
|
486
|
+
agent_id: z.string().describe("Target agent ID"),
|
|
487
|
+
content: z.string(),
|
|
488
|
+
sender_id: z.string().describe("Your agent ID"),
|
|
489
|
+
}, async (params) => {
|
|
490
|
+
// A2A direct message — for now, use task discussions as transport
|
|
491
|
+
// Future: direct WebSocket routing
|
|
492
|
+
return ok({
|
|
493
|
+
sent: true,
|
|
494
|
+
to: params.agent_id,
|
|
495
|
+
from: params.sender_id,
|
|
496
|
+
note: "Direct A2A messaging will use WebSocket routing in future versions.",
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
500
|
+
// Reputation (2)
|
|
501
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
502
|
+
// #28 eacn_report_event
|
|
503
|
+
server.tool("eacn_report_event", "Report a reputation event. Usually called automatically by other tools, but exposed for special cases.", {
|
|
504
|
+
agent_id: z.string(),
|
|
505
|
+
event_type: z.string().describe("task_completed | task_rejected | task_timeout | bid_declined"),
|
|
506
|
+
}, async (params) => {
|
|
507
|
+
const res = await net.reportEvent(params.agent_id, params.event_type);
|
|
508
|
+
state.updateReputationCache(params.agent_id, res.score);
|
|
509
|
+
return ok(res);
|
|
510
|
+
});
|
|
511
|
+
// #29 eacn_get_reputation
|
|
512
|
+
server.tool("eacn_get_reputation", "Query an Agent's global reputation score.", {
|
|
513
|
+
agent_id: z.string(),
|
|
514
|
+
}, async (params) => {
|
|
515
|
+
const res = await net.getReputation(params.agent_id);
|
|
516
|
+
state.updateReputationCache(params.agent_id, res.score);
|
|
517
|
+
return ok(res);
|
|
518
|
+
});
|
|
519
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
520
|
+
// Economy (2)
|
|
521
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
522
|
+
// #30 eacn_get_balance
|
|
523
|
+
server.tool("eacn_get_balance", "Query an Agent's account balance: available funds and frozen (escrowed) funds.", {
|
|
524
|
+
agent_id: z.string().describe("Agent ID to check balance for"),
|
|
525
|
+
}, async (params) => {
|
|
526
|
+
const res = await net.getBalance(params.agent_id);
|
|
527
|
+
return ok(res);
|
|
528
|
+
});
|
|
529
|
+
// #31 eacn_deposit
|
|
530
|
+
server.tool("eacn_deposit", "Deposit funds into an Agent's account. Increases available balance.", {
|
|
531
|
+
agent_id: z.string().describe("Agent ID to deposit funds for"),
|
|
532
|
+
amount: z.number().positive().describe("Amount to deposit (must be > 0)"),
|
|
533
|
+
}, async (params) => {
|
|
534
|
+
const res = await net.deposit(params.agent_id, params.amount);
|
|
535
|
+
return ok(res);
|
|
536
|
+
});
|
|
537
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
538
|
+
// Events (1)
|
|
539
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
540
|
+
// #32 eacn_get_events
|
|
541
|
+
server.tool("eacn_get_events", "Get pending events. WebSocket connections buffer events in memory; this drains the buffer.", {}, async () => {
|
|
542
|
+
const events = state.drainEvents();
|
|
543
|
+
return ok({
|
|
544
|
+
count: events.length,
|
|
545
|
+
events,
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
// ---------------------------------------------------------------------------
|
|
549
|
+
// Start
|
|
550
|
+
// ---------------------------------------------------------------------------
|
|
551
|
+
// ---------------------------------------------------------------------------
|
|
552
|
+
// WS Event Callbacks — auto-actions when events arrive
|
|
553
|
+
// ---------------------------------------------------------------------------
|
|
554
|
+
function registerEventCallbacks() {
|
|
555
|
+
ws.setEventCallback((agentId, event) => {
|
|
556
|
+
const taskId = event.task_id;
|
|
557
|
+
switch (event.type) {
|
|
558
|
+
case "awaiting_retrieval":
|
|
559
|
+
// Task has results ready — update local status so dashboard/skills see it
|
|
560
|
+
state.updateTaskStatus(taskId, "awaiting_retrieval");
|
|
561
|
+
break;
|
|
562
|
+
case "subtask_completed": {
|
|
563
|
+
// A subtask we created finished — auto-fetch its results
|
|
564
|
+
const subtaskId = event.payload?.subtask_id;
|
|
565
|
+
if (subtaskId) {
|
|
566
|
+
net.getTaskResults(subtaskId, agentId)
|
|
567
|
+
.then((res) => {
|
|
568
|
+
// Buffer a synthetic event with the results for the skill to pick up
|
|
569
|
+
state.pushEvents([{
|
|
570
|
+
type: "subtask_completed",
|
|
571
|
+
task_id: taskId,
|
|
572
|
+
payload: { subtask_id: subtaskId, results: res.results },
|
|
573
|
+
received_at: Date.now(),
|
|
574
|
+
}]);
|
|
575
|
+
})
|
|
576
|
+
.catch(() => { });
|
|
577
|
+
}
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
case "timeout":
|
|
581
|
+
// Task timed out — auto-report reputation event, update local status
|
|
582
|
+
state.updateTaskStatus(taskId, "no_one");
|
|
583
|
+
net.reportEvent(agentId, "task_timeout").catch(() => { });
|
|
584
|
+
break;
|
|
585
|
+
case "budget_confirmation":
|
|
586
|
+
// Bid exceeded budget — mark in local state for initiator to handle
|
|
587
|
+
// The event stays in the buffer for /eacn-bounty to surface
|
|
588
|
+
break;
|
|
589
|
+
case "task_broadcast":
|
|
590
|
+
// New task available — auto-evaluate bid if agent has matching domains
|
|
591
|
+
autoBidEvaluate(agentId, event).catch(() => { });
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
// ---------------------------------------------------------------------------
|
|
597
|
+
// Auto-bid evaluation — communication layer auto-filter per agent.md:172-193
|
|
598
|
+
// ---------------------------------------------------------------------------
|
|
599
|
+
async function autoBidEvaluate(agentId, event) {
|
|
600
|
+
const agent = state.getAgent(agentId);
|
|
601
|
+
if (!agent)
|
|
602
|
+
return;
|
|
603
|
+
const taskId = event.task_id;
|
|
604
|
+
const payload = event.payload;
|
|
605
|
+
const taskDomains = payload?.domains ?? [];
|
|
606
|
+
// Domain overlap check — skip if no overlap
|
|
607
|
+
const overlap = taskDomains.some((d) => agent.domains.includes(d));
|
|
608
|
+
if (!overlap)
|
|
609
|
+
return;
|
|
610
|
+
// Capacity check — skip if at max concurrent tasks
|
|
611
|
+
if (agent.capabilities?.max_concurrent_tasks) {
|
|
612
|
+
const activeTasks = Object.values(state.getState().local_tasks).filter((t) => t.role === "executor" && t.status !== "completed" && t.status !== "no_one");
|
|
613
|
+
if (activeTasks.length >= agent.capabilities.max_concurrent_tasks)
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
// Passed auto-filter — enrich the buffered event with a hint
|
|
617
|
+
// The skill layer (/eacn-bounty) will see this and can fast-track bidding
|
|
618
|
+
state.pushEvents([{
|
|
619
|
+
type: "task_broadcast",
|
|
620
|
+
task_id: taskId,
|
|
621
|
+
payload: { ...payload, auto_match: true, matched_agent: agentId },
|
|
622
|
+
received_at: Date.now(),
|
|
623
|
+
}]);
|
|
624
|
+
}
|
|
625
|
+
// ---------------------------------------------------------------------------
|
|
626
|
+
// Start
|
|
627
|
+
// ---------------------------------------------------------------------------
|
|
628
|
+
async function main() {
|
|
629
|
+
// Load state on startup
|
|
630
|
+
state.load();
|
|
631
|
+
// Register WS event callbacks
|
|
632
|
+
registerEventCallbacks();
|
|
633
|
+
const transport = new StdioServerTransport();
|
|
634
|
+
await server.connect(transport);
|
|
635
|
+
}
|
|
636
|
+
main().catch((e) => {
|
|
637
|
+
console.error("EACN MCP server failed to start:", e);
|
|
638
|
+
process.exit(1);
|
|
639
|
+
});
|
|
640
|
+
//# sourceMappingURL=server.js.map
|