akemon 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2 -0
- package/dist/relay-client.js +127 -0
- package/dist/server.js +142 -16
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -31,6 +31,7 @@ program
|
|
|
31
31
|
.option("--approve", "Review every task before execution")
|
|
32
32
|
.option("--mock", "Use mock responses (for demo/testing)")
|
|
33
33
|
.option("--allow-all", "Skip all permission prompts (for self-use)")
|
|
34
|
+
.option("--mcp-server <command>", "Wrap a community MCP server (stdio) and expose its tools via relay")
|
|
34
35
|
.option("--relay <url>", "Relay WebSocket URL", RELAY_WS)
|
|
35
36
|
.action(async (opts) => {
|
|
36
37
|
const port = parseInt(opts.port);
|
|
@@ -51,6 +52,7 @@ program
|
|
|
51
52
|
engine,
|
|
52
53
|
relayHttp,
|
|
53
54
|
secretKey: credentials.secretKey,
|
|
55
|
+
mcpServer: opts.mcpServer,
|
|
54
56
|
});
|
|
55
57
|
console.log(``);
|
|
56
58
|
if (!opts.public) {
|
package/dist/relay-client.js
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
2
|
import http from "http";
|
|
3
3
|
const DEFAULT_RELAY_URL = "wss://relay.akemon.dev";
|
|
4
|
+
// Pending agent_call results (callId → resolve function)
|
|
5
|
+
const pendingAgentCalls = new Map();
|
|
6
|
+
let relayWsRef = null;
|
|
7
|
+
/** Call another agent through the relay. Available to any engine. */
|
|
8
|
+
export function callAgent(target, task) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
if (!relayWsRef || relayWsRef.readyState !== WebSocket.OPEN) {
|
|
11
|
+
reject(new Error("Not connected to relay"));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const callId = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
|
15
|
+
pendingAgentCalls.set(callId, resolve);
|
|
16
|
+
relayWsRef.send(JSON.stringify({
|
|
17
|
+
type: "agent_call",
|
|
18
|
+
call_id: callId,
|
|
19
|
+
target,
|
|
20
|
+
task,
|
|
21
|
+
}));
|
|
22
|
+
// Timeout after 5 minutes
|
|
23
|
+
setTimeout(() => {
|
|
24
|
+
if (pendingAgentCalls.has(callId)) {
|
|
25
|
+
pendingAgentCalls.delete(callId);
|
|
26
|
+
reject(new Error(`agent_call to ${target} timed out`));
|
|
27
|
+
}
|
|
28
|
+
}, 300_000);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
4
31
|
export function connectRelay(options) {
|
|
5
32
|
const relayUrl = options.relayUrl || DEFAULT_RELAY_URL;
|
|
6
33
|
let wsUrl = relayUrl.replace(/^http/, "ws");
|
|
@@ -52,6 +79,7 @@ export function connectRelay(options) {
|
|
|
52
79
|
ws.on("open", () => {
|
|
53
80
|
console.log(`[relay-ws] Connected. Registering agent "${options.agentName}"...`);
|
|
54
81
|
reconnectDelay = 1000; // reset backoff
|
|
82
|
+
relayWsRef = ws;
|
|
55
83
|
// Send registration message
|
|
56
84
|
const reg = {
|
|
57
85
|
type: "register",
|
|
@@ -93,6 +121,12 @@ export function connectRelay(options) {
|
|
|
93
121
|
case "control":
|
|
94
122
|
handleControl(ws, msg);
|
|
95
123
|
break;
|
|
124
|
+
case "agent_call":
|
|
125
|
+
handleIncomingAgentCall(ws, msg, options.localPort);
|
|
126
|
+
break;
|
|
127
|
+
case "agent_call_result":
|
|
128
|
+
handleAgentCallResult(msg);
|
|
129
|
+
break;
|
|
96
130
|
default:
|
|
97
131
|
console.log(`[relay-ws] Unknown message type: ${msg.type}`);
|
|
98
132
|
}
|
|
@@ -116,6 +150,99 @@ export function connectRelay(options) {
|
|
|
116
150
|
}
|
|
117
151
|
connect();
|
|
118
152
|
}
|
|
153
|
+
function handleIncomingAgentCall(ws, msg, localPort) {
|
|
154
|
+
const callId = msg.call_id || "";
|
|
155
|
+
const caller = msg.caller || "unknown";
|
|
156
|
+
const task = msg.task || "";
|
|
157
|
+
console.log(`[agent_call] Incoming from ${caller}: ${task.slice(0, 80)}`);
|
|
158
|
+
// Forward to local MCP as a submit_task call
|
|
159
|
+
const initBody = JSON.stringify({
|
|
160
|
+
jsonrpc: "2.0", id: 1,
|
|
161
|
+
method: "initialize",
|
|
162
|
+
params: { protocolVersion: "2025-03-26", capabilities: {}, clientInfo: { name: "agent-call", version: "1.0" } },
|
|
163
|
+
});
|
|
164
|
+
const doRequest = (body, sessionId) => {
|
|
165
|
+
return new Promise((resolve, reject) => {
|
|
166
|
+
const headers = {
|
|
167
|
+
"Content-Type": "application/json",
|
|
168
|
+
"Accept": "application/json, text/event-stream",
|
|
169
|
+
};
|
|
170
|
+
if (sessionId)
|
|
171
|
+
headers["mcp-session-id"] = sessionId;
|
|
172
|
+
const req = http.request({ hostname: "127.0.0.1", port: localPort, path: "/mcp", method: "POST", headers: { ...headers, "Content-Length": Buffer.byteLength(body) } }, (res) => {
|
|
173
|
+
const chunks = [];
|
|
174
|
+
res.on("data", (c) => chunks.push(c));
|
|
175
|
+
res.on("end", () => {
|
|
176
|
+
const sid = res.headers["mcp-session-id"];
|
|
177
|
+
resolve({ data: Buffer.concat(chunks).toString(), sessionId: sid || sessionId });
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
req.on("error", reject);
|
|
181
|
+
req.write(body);
|
|
182
|
+
req.end();
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
// Initialize → call tool → return result
|
|
186
|
+
doRequest(initBody)
|
|
187
|
+
.then(({ sessionId: sid }) => {
|
|
188
|
+
const callBody = JSON.stringify({
|
|
189
|
+
jsonrpc: "2.0", id: 2,
|
|
190
|
+
method: "tools/call",
|
|
191
|
+
params: { name: "submit_task", arguments: { task } },
|
|
192
|
+
});
|
|
193
|
+
return doRequest(callBody, sid);
|
|
194
|
+
})
|
|
195
|
+
.then(({ data }) => {
|
|
196
|
+
// Extract text from SSE or JSON response
|
|
197
|
+
let result = data;
|
|
198
|
+
try {
|
|
199
|
+
// Try SSE extraction
|
|
200
|
+
const lines = data.split("\n");
|
|
201
|
+
let lastData = "";
|
|
202
|
+
for (const line of lines) {
|
|
203
|
+
if (line.startsWith("data: "))
|
|
204
|
+
lastData = line.slice(6);
|
|
205
|
+
}
|
|
206
|
+
if (lastData) {
|
|
207
|
+
const parsed = JSON.parse(lastData);
|
|
208
|
+
const content = parsed?.result?.content;
|
|
209
|
+
if (content)
|
|
210
|
+
result = content.map((c) => c.text || "").join("\n");
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const parsed = JSON.parse(data);
|
|
214
|
+
const content = parsed?.result?.content;
|
|
215
|
+
if (content)
|
|
216
|
+
result = content.map((c) => c.text || "").join("\n");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch { /* use raw */ }
|
|
220
|
+
ws.send(JSON.stringify({
|
|
221
|
+
type: "agent_call_result",
|
|
222
|
+
call_id: callId,
|
|
223
|
+
caller,
|
|
224
|
+
result,
|
|
225
|
+
}));
|
|
226
|
+
console.log(`[agent_call] Replied to ${caller} (${result.length} bytes)`);
|
|
227
|
+
})
|
|
228
|
+
.catch((err) => {
|
|
229
|
+
ws.send(JSON.stringify({
|
|
230
|
+
type: "agent_call_result",
|
|
231
|
+
call_id: callId,
|
|
232
|
+
caller,
|
|
233
|
+
result: `[error] ${err.message}`,
|
|
234
|
+
}));
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function handleAgentCallResult(msg) {
|
|
238
|
+
const callId = msg.call_id || "";
|
|
239
|
+
const resolve = pendingAgentCalls.get(callId);
|
|
240
|
+
if (resolve) {
|
|
241
|
+
pendingAgentCalls.delete(callId);
|
|
242
|
+
resolve(msg.result || "");
|
|
243
|
+
console.log(`[agent_call] Got result for call_id=${callId.slice(0, 8)} from ${msg.caller}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
119
246
|
function handleControl(ws, msg) {
|
|
120
247
|
const action = msg.action || "";
|
|
121
248
|
console.log(`[control] Received: ${action}`);
|
package/dist/server.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
4
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
7
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
4
8
|
import { z } from "zod";
|
|
5
|
-
import { spawn } from "child_process";
|
|
9
|
+
import { spawn, exec } from "child_process";
|
|
6
10
|
import { createServer } from "http";
|
|
7
11
|
import { createInterface } from "readline";
|
|
12
|
+
import { callAgent } from "./relay-client.js";
|
|
8
13
|
function runCommand(cmd, args, task, cwd, stdinMode = true) {
|
|
9
14
|
return new Promise((resolve, reject) => {
|
|
10
15
|
const { CLAUDECODE, ...cleanEnv } = process.env;
|
|
@@ -45,6 +50,19 @@ function runCommand(cmd, args, task, cwd, stdinMode = true) {
|
|
|
45
50
|
child.on("error", reject);
|
|
46
51
|
});
|
|
47
52
|
}
|
|
53
|
+
function runTerminal(command, cwd) {
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
exec(command, { cwd, timeout: 300_000, maxBuffer: 1024 * 1024 }, (err, stdout, stderr) => {
|
|
56
|
+
const output = (stdout || "") + (stderr ? "\n[stderr]\n" + stderr : "");
|
|
57
|
+
if (err && !output.trim()) {
|
|
58
|
+
resolve(`[error] ${err.message}`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
resolve(output.trim() || "[no output]");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
48
66
|
// stdinMode: true = send task via stdin, false = send task as argument
|
|
49
67
|
function buildEngineCommand(engine, model, allowAll) {
|
|
50
68
|
switch (engine) {
|
|
@@ -198,8 +216,15 @@ function createMcpServer(opts) {
|
|
|
198
216
|
console.log(`[approve] Owner approved. Executing with ${engine}...`);
|
|
199
217
|
}
|
|
200
218
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
219
|
+
let output;
|
|
220
|
+
if (engine === "terminal") {
|
|
221
|
+
console.log(`[terminal] Executing: ${task}`);
|
|
222
|
+
output = await runTerminal(task, workdir);
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
const { cmd, args, stdinMode } = buildEngineCommand(engine, model, allowAll);
|
|
226
|
+
output = await runCommand(cmd, args, safeTask, workdir, stdinMode);
|
|
227
|
+
}
|
|
203
228
|
// Store updated context
|
|
204
229
|
if (contextEnabled && publisherId) {
|
|
205
230
|
const newContext = buildContextPayload(prevContext, task, output);
|
|
@@ -217,10 +242,105 @@ function createMcpServer(opts) {
|
|
|
217
242
|
};
|
|
218
243
|
}
|
|
219
244
|
});
|
|
245
|
+
// Agent-to-agent calling tool
|
|
246
|
+
server.tool("call_agent", "Call another akemon agent by name. The target agent will execute the task and return the result. Use this to delegate subtasks to specialized agents.", {
|
|
247
|
+
agent: z.string().describe("Name of the target agent to call"),
|
|
248
|
+
task: z.string().describe("Task to send to the target agent"),
|
|
249
|
+
}, async ({ agent: target, task }) => {
|
|
250
|
+
console.log(`[call_agent] ${agentName} → ${target}: ${task.slice(0, 80)}`);
|
|
251
|
+
try {
|
|
252
|
+
const result = await callAgent(target, task);
|
|
253
|
+
return {
|
|
254
|
+
content: [{ type: "text", text: result }],
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
return {
|
|
259
|
+
content: [{ type: "text", text: `[error] Failed to call agent "${target}": ${err.message}` }],
|
|
260
|
+
isError: true,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
return server;
|
|
265
|
+
}
|
|
266
|
+
async function initMcpProxy(mcpServerCmd, workdir) {
|
|
267
|
+
const parts = mcpServerCmd.match(/(?:[^\s"]+|"[^"]*")+/g) || [mcpServerCmd];
|
|
268
|
+
const [command, ...args] = parts.map(p => p.replace(/^"|"$/g, ""));
|
|
269
|
+
console.log(`[mcp-proxy] Starting child MCP server: ${command} ${args.join(" ")}`);
|
|
270
|
+
const transport = new StdioClientTransport({ command, args, cwd: workdir, stderr: "pipe" });
|
|
271
|
+
const client = new Client({ name: "akemon-proxy", version: "0.1.0" });
|
|
272
|
+
await client.connect(transport);
|
|
273
|
+
const { tools } = await client.listTools();
|
|
274
|
+
console.log(`[mcp-proxy] Connected. ${tools.length} tools: ${tools.map((t) => t.name).join(", ")}`);
|
|
275
|
+
return { client, tools };
|
|
276
|
+
}
|
|
277
|
+
function createMcpProxyServer(proxy, agentName) {
|
|
278
|
+
const server = new Server({ name: agentName, version: "0.1.0" }, { capabilities: { tools: {} } });
|
|
279
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
280
|
+
return {
|
|
281
|
+
tools: [
|
|
282
|
+
...proxy.tools,
|
|
283
|
+
{
|
|
284
|
+
name: "call_agent",
|
|
285
|
+
description: "Call another akemon agent by name. The target agent will execute the task and return the result.",
|
|
286
|
+
inputSchema: {
|
|
287
|
+
type: "object",
|
|
288
|
+
properties: {
|
|
289
|
+
agent: { type: "string", description: "Target agent name" },
|
|
290
|
+
task: { type: "string", description: "Task to send" },
|
|
291
|
+
},
|
|
292
|
+
required: ["agent", "task"],
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
],
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
299
|
+
const { name, arguments: toolArgs } = request.params;
|
|
300
|
+
if (name === "call_agent") {
|
|
301
|
+
console.log(`[call_agent] ${agentName} → ${toolArgs?.agent}: ${String(toolArgs?.task).slice(0, 80)}`);
|
|
302
|
+
try {
|
|
303
|
+
const result = await callAgent(toolArgs?.agent, toolArgs?.task);
|
|
304
|
+
return { content: [{ type: "text", text: result }] };
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
return { content: [{ type: "text", text: `[error] ${err.message}` }], isError: true };
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// Forward to child MCP server
|
|
311
|
+
console.log(`[mcp-proxy] → ${name}(${JSON.stringify(toolArgs).slice(0, 100)})`);
|
|
312
|
+
try {
|
|
313
|
+
const result = await proxy.client.callTool({ name, arguments: toolArgs });
|
|
314
|
+
// Normalize response format
|
|
315
|
+
if ("toolResult" in result) {
|
|
316
|
+
return { content: [{ type: "text", text: JSON.stringify(result.toolResult) }] };
|
|
317
|
+
}
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
catch (err) {
|
|
321
|
+
console.error(`[mcp-proxy] Tool ${name} error: ${err.message}`);
|
|
322
|
+
return { content: [{ type: "text", text: `[error] ${err.message}` }], isError: true };
|
|
323
|
+
}
|
|
324
|
+
});
|
|
220
325
|
return server;
|
|
221
326
|
}
|
|
222
327
|
export async function serve(options) {
|
|
223
328
|
const workdir = options.workdir || process.cwd();
|
|
329
|
+
// Expose port to engine subprocesses so they can callback to local MCP server
|
|
330
|
+
process.env.AKEMON_PORT = String(options.port);
|
|
331
|
+
if (options.key)
|
|
332
|
+
process.env.AKEMON_KEY = options.key;
|
|
333
|
+
// Initialize MCP proxy if --mcp-server specified
|
|
334
|
+
let mcpProxy = null;
|
|
335
|
+
if (options.mcpServer) {
|
|
336
|
+
try {
|
|
337
|
+
mcpProxy = await initMcpProxy(options.mcpServer, workdir);
|
|
338
|
+
}
|
|
339
|
+
catch (err) {
|
|
340
|
+
console.error(`[mcp-proxy] Failed to start child MCP server: ${err.message}`);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
224
344
|
const sessions = new Map();
|
|
225
345
|
const publisherIds = new Map();
|
|
226
346
|
const httpServer = createServer(async (req, res) => {
|
|
@@ -263,19 +383,25 @@ export async function serve(options) {
|
|
|
263
383
|
publisherIds.delete(sid);
|
|
264
384
|
}
|
|
265
385
|
};
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
386
|
+
if (mcpProxy) {
|
|
387
|
+
const proxyServer = createMcpProxyServer(mcpProxy, options.agentName);
|
|
388
|
+
await proxyServer.connect(transport);
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
const mcpServer = createMcpServer({
|
|
392
|
+
workdir,
|
|
393
|
+
agentName: options.agentName,
|
|
394
|
+
mock: options.mock,
|
|
395
|
+
model: options.model,
|
|
396
|
+
approve: options.approve,
|
|
397
|
+
engine: options.engine,
|
|
398
|
+
allowAll: options.allowAll,
|
|
399
|
+
relayHttp: options.relayHttp,
|
|
400
|
+
secretKey: options.secretKey,
|
|
401
|
+
publisherIds,
|
|
402
|
+
});
|
|
403
|
+
await mcpServer.connect(transport);
|
|
404
|
+
}
|
|
279
405
|
await transport.handleRequest(req, res);
|
|
280
406
|
if (transport.sessionId) {
|
|
281
407
|
sessions.set(transport.sessionId, transport);
|