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 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) {
@@ -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
- const { cmd, args, stdinMode } = buildEngineCommand(engine, model, allowAll);
202
- const output = await runCommand(cmd, args, safeTask, workdir, stdinMode);
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
- const mcpServer = createMcpServer({
267
- workdir,
268
- agentName: options.agentName,
269
- mock: options.mock,
270
- model: options.model,
271
- approve: options.approve,
272
- engine: options.engine,
273
- allowAll: options.allowAll,
274
- relayHttp: options.relayHttp,
275
- secretKey: options.secretKey,
276
- publisherIds,
277
- });
278
- await mcpServer.connect(transport);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akemon",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Agent work marketplace — train your agent, let it work for others",
5
5
  "type": "module",
6
6
  "license": "MIT",