@adamancyzhang/claude-orchestrator 0.2.8 → 0.3.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/cli/commands.d.ts +8 -4
- package/dist/cli/commands.js +111 -162
- package/dist/cli/commands.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.js +11 -2
- package/dist/config.js.map +1 -1
- package/dist/index.js +71 -28
- package/dist/index.js.map +1 -1
- package/dist/leader/event-bus.d.ts +11 -0
- package/dist/leader/event-bus.js +21 -0
- package/dist/leader/event-bus.js.map +1 -0
- package/dist/leader/index.d.ts +7 -0
- package/dist/leader/index.js +86 -0
- package/dist/leader/index.js.map +1 -0
- package/dist/leader/monitor.d.ts +14 -0
- package/dist/leader/monitor.js +55 -0
- package/dist/leader/monitor.js.map +1 -0
- package/dist/leader/orchestrator.d.ts +14 -0
- package/dist/leader/orchestrator.js +83 -0
- package/dist/leader/orchestrator.js.map +1 -0
- package/dist/leader/recovery.d.ts +9 -0
- package/dist/leader/recovery.js +46 -0
- package/dist/leader/recovery.js.map +1 -0
- package/dist/leader/state.d.ts +23 -0
- package/dist/leader/state.js +85 -0
- package/dist/leader/state.js.map +1 -0
- package/dist/leader/tui.d.ts +5 -0
- package/dist/leader/tui.js +133 -0
- package/dist/leader/tui.js.map +1 -0
- package/dist/leader/watcher.d.ts +16 -0
- package/dist/leader/watcher.js +77 -0
- package/dist/leader/watcher.js.map +1 -0
- package/dist/models/schemas.d.ts +64 -17
- package/dist/models/schemas.js +29 -3
- package/dist/models/schemas.js.map +1 -1
- package/dist/modules/message-router.d.ts +2 -0
- package/dist/modules/message-router.js +17 -0
- package/dist/modules/message-router.js.map +1 -1
- package/dist/modules/task-queue.d.ts +4 -1
- package/dist/modules/task-queue.js +87 -9
- package/dist/modules/task-queue.js.map +1 -1
- package/dist/templates/leader.md +10 -0
- package/dist/templates/worker.md +8 -0
- package/dist/utils/exec.d.ts +3 -0
- package/dist/utils/exec.js +20 -0
- package/dist/utils/exec.js.map +1 -0
- package/dist/worker/watcher.d.ts +16 -0
- package/dist/worker/watcher.js +82 -0
- package/dist/worker/watcher.js.map +1 -0
- package/dist/zk/client.d.ts +5 -0
- package/dist/zk/client.js +18 -1
- package/dist/zk/client.js.map +1 -1
- package/dist/zk/paths.d.ts +2 -0
- package/dist/zk/paths.js +4 -0
- package/dist/zk/paths.js.map +1 -1
- package/package.json +3 -6
- package/dist/modules/message-watcher.d.ts +0 -12
- package/dist/modules/message-watcher.js +0 -133
- package/dist/modules/message-watcher.js.map +0 -1
- package/dist/server.d.ts +0 -2
- package/dist/server.js +0 -490
- package/dist/server.js.map +0 -1
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { spawn } from "child_process";
|
|
2
|
-
import * as paths from "../zk/paths.js";
|
|
3
|
-
import { MessageSchema } from "../models/schemas.js";
|
|
4
|
-
export class MessageWatcher {
|
|
5
|
-
zk;
|
|
6
|
-
instances = new Map();
|
|
7
|
-
constructor(zk) {
|
|
8
|
-
this.zk = zk;
|
|
9
|
-
}
|
|
10
|
-
async startWatching(instanceId, workDir) {
|
|
11
|
-
this.stopWatching(instanceId);
|
|
12
|
-
await this.zk.mkdirp(paths.messageDirPath(instanceId));
|
|
13
|
-
const state = {
|
|
14
|
-
workDir,
|
|
15
|
-
queue: [],
|
|
16
|
-
processing: false,
|
|
17
|
-
stopped: false,
|
|
18
|
-
child: null,
|
|
19
|
-
inFlight: new Set(),
|
|
20
|
-
};
|
|
21
|
-
this.instances.set(instanceId, state);
|
|
22
|
-
console.log(`[MessageWatcher] Started watching ${instanceId.slice(0, 8)} at ${workDir}`);
|
|
23
|
-
await this._watchLoop(instanceId);
|
|
24
|
-
}
|
|
25
|
-
stopWatching(instanceId) {
|
|
26
|
-
const state = this.instances.get(instanceId);
|
|
27
|
-
if (!state)
|
|
28
|
-
return;
|
|
29
|
-
state.stopped = true;
|
|
30
|
-
if (state.child) {
|
|
31
|
-
state.child.kill("SIGTERM");
|
|
32
|
-
state.child = null;
|
|
33
|
-
}
|
|
34
|
-
this.instances.delete(instanceId);
|
|
35
|
-
console.log(`[MessageWatcher] Stopped watching ${instanceId.slice(0, 8)}`);
|
|
36
|
-
}
|
|
37
|
-
stopAll() {
|
|
38
|
-
for (const id of this.instances.keys()) {
|
|
39
|
-
this.stopWatching(id);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
async _watchLoop(instanceId) {
|
|
43
|
-
const state = this.instances.get(instanceId);
|
|
44
|
-
if (!state || state.stopped)
|
|
45
|
-
return;
|
|
46
|
-
try {
|
|
47
|
-
const children = await this.zk.watchMessageDir(instanceId, async (newChildren) => {
|
|
48
|
-
await this._onChildrenChanged(instanceId, newChildren);
|
|
49
|
-
this._watchLoop(instanceId);
|
|
50
|
-
});
|
|
51
|
-
await this._onChildrenChanged(instanceId, children);
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
console.error(`[MessageWatcher] Watch failed for ${instanceId.slice(0, 8)}:`, err);
|
|
55
|
-
this.stopWatching(instanceId);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
async _onChildrenChanged(instanceId, children) {
|
|
59
|
-
const state = this.instances.get(instanceId);
|
|
60
|
-
if (!state || state.stopped)
|
|
61
|
-
return;
|
|
62
|
-
for (const msgId of children) {
|
|
63
|
-
if (state.inFlight.has(msgId))
|
|
64
|
-
continue;
|
|
65
|
-
const data = await this.zk.getMessage(instanceId, msgId);
|
|
66
|
-
if (!data)
|
|
67
|
-
continue;
|
|
68
|
-
const msg = MessageSchema.parse({ ...data, id: msgId });
|
|
69
|
-
if (!msg.read) {
|
|
70
|
-
state.inFlight.add(msgId);
|
|
71
|
-
state.queue.push(msg);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
if (!state.processing && state.queue.length > 0) {
|
|
75
|
-
this._processQueue(instanceId);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
async _processQueue(instanceId) {
|
|
79
|
-
const state = this.instances.get(instanceId);
|
|
80
|
-
if (!state || state.stopped)
|
|
81
|
-
return;
|
|
82
|
-
state.processing = true;
|
|
83
|
-
while (state.queue.length > 0 && !state.stopped) {
|
|
84
|
-
const msg = state.queue.shift();
|
|
85
|
-
const fromLabel = msg.from_name || msg.from_instance?.slice(0, 8) || "unknown";
|
|
86
|
-
const prompt = `[${msg.type} from ${fromLabel}] ${msg.content}`;
|
|
87
|
-
console.log(`[MessageWatcher] Processing message ${msg.id} for ${instanceId.slice(0, 8)}`);
|
|
88
|
-
try {
|
|
89
|
-
const child = spawn("claude", ["--session-id", instanceId, "-p", prompt], {
|
|
90
|
-
cwd: state.workDir,
|
|
91
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
92
|
-
env: { ...process.env },
|
|
93
|
-
});
|
|
94
|
-
state.child = child;
|
|
95
|
-
let stdout = "";
|
|
96
|
-
let stderr = "";
|
|
97
|
-
child.stdout?.on("data", (d) => (stdout += d.toString()));
|
|
98
|
-
child.stderr?.on("data", (d) => (stderr += d.toString()));
|
|
99
|
-
const { code, error } = await new Promise((resolve) => {
|
|
100
|
-
child.on("exit", (code) => resolve({ code: code ?? -1, error: null }));
|
|
101
|
-
child.on("error", (err) => resolve({ code: -1, error: err }));
|
|
102
|
-
});
|
|
103
|
-
state.child = null;
|
|
104
|
-
if (error) {
|
|
105
|
-
console.error(`[MessageWatcher] claude failed for ${instanceId.slice(0, 8)}: ${error.message}`);
|
|
106
|
-
}
|
|
107
|
-
else if (code !== 0) {
|
|
108
|
-
console.error(`[MessageWatcher] claude exited ${code} for ${instanceId.slice(0, 8)}: ${stderr.slice(0, 200)}`);
|
|
109
|
-
}
|
|
110
|
-
else {
|
|
111
|
-
console.log(`[MessageWatcher] Message ${msg.id} processed for ${instanceId.slice(0, 8)}`);
|
|
112
|
-
}
|
|
113
|
-
if (stdout) {
|
|
114
|
-
console.log(`[MessageWatcher] Output: ${stdout.slice(0, 500)}`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
catch (err) {
|
|
118
|
-
console.error(`[MessageWatcher] Unexpected error processing ${msg.id}:`, err);
|
|
119
|
-
}
|
|
120
|
-
// Always mark as read
|
|
121
|
-
try {
|
|
122
|
-
msg.read = true;
|
|
123
|
-
await this.zk.updateMessage(instanceId, msg.id, msg);
|
|
124
|
-
}
|
|
125
|
-
catch (err) {
|
|
126
|
-
console.error(`[MessageWatcher] Failed to mark ${msg.id} as read:`, err);
|
|
127
|
-
}
|
|
128
|
-
state.inFlight.delete(msg.id);
|
|
129
|
-
}
|
|
130
|
-
state.processing = false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
//# sourceMappingURL=message-watcher.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"message-watcher.js","sourceRoot":"","sources":["../../src/modules/message-watcher.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
DELETED
package/dist/server.js
DELETED
|
@@ -1,490 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import express from "express";
|
|
3
|
-
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
-
import { ZkClient } from "./zk/client.js";
|
|
6
|
-
import { InstanceRegistry } from "./modules/registry.js";
|
|
7
|
-
import { TaskQueue } from "./modules/task-queue.js";
|
|
8
|
-
import { MessageRouter } from "./modules/message-router.js";
|
|
9
|
-
import { MessageWatcher } from "./modules/message-watcher.js";
|
|
10
|
-
import { ContextStore } from "./modules/context-store.js";
|
|
11
|
-
import { TaskPriorityName, } from "./models/schemas.js";
|
|
12
|
-
// ── Start server ──
|
|
13
|
-
export async function startServer(config) {
|
|
14
|
-
const zk = new ZkClient(config.zkHosts);
|
|
15
|
-
await zk.connect();
|
|
16
|
-
const registry = new InstanceRegistry(zk);
|
|
17
|
-
const taskQueue = new TaskQueue(zk);
|
|
18
|
-
const messageRouter = new MessageRouter(zk);
|
|
19
|
-
const messageWatcher = new MessageWatcher(zk);
|
|
20
|
-
const contextStore = new ContextStore(zk);
|
|
21
|
-
// ── Setup MCP Server ──
|
|
22
|
-
const mcp = new McpServer({
|
|
23
|
-
name: "ClaudeMCP",
|
|
24
|
-
version: "0.2.1",
|
|
25
|
-
}, {
|
|
26
|
-
capabilities: {
|
|
27
|
-
resources: { subscribe: true },
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
// ── Register Tools (18) ──
|
|
31
|
-
// 1. server_status
|
|
32
|
-
mcp.tool("server_status", async () => {
|
|
33
|
-
const zkOk = zk.connected;
|
|
34
|
-
return {
|
|
35
|
-
content: [
|
|
36
|
-
{
|
|
37
|
-
type: "text",
|
|
38
|
-
text: `Server: running\nZooKeeper: ${zkOk ? "connected" : "DISCONNECTED"}\nPort: ${config.port}`,
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
};
|
|
42
|
-
});
|
|
43
|
-
// 2. register_instance
|
|
44
|
-
mcp.tool("register_instance", {
|
|
45
|
-
name: z.string().min(1),
|
|
46
|
-
role: z.string().default("general"),
|
|
47
|
-
instance_id: z.string().optional(),
|
|
48
|
-
work_dir: z.string().optional(),
|
|
49
|
-
}, async ({ name, role, instance_id, work_dir }) => {
|
|
50
|
-
if (!zk.connected) {
|
|
51
|
-
return { content: [{ type: "text", text: "Error: ZooKeeper is not connected." }] };
|
|
52
|
-
}
|
|
53
|
-
const instance = await registry.register(name, role, instance_id);
|
|
54
|
-
const action = instance_id && instance.id === instance_id ? "re-registered" : "registered";
|
|
55
|
-
let watcherNote = "";
|
|
56
|
-
if (work_dir) {
|
|
57
|
-
await messageWatcher.startWatching(instance.id, work_dir);
|
|
58
|
-
watcherNote = `\nMessage watcher started for: ${work_dir}`;
|
|
59
|
-
}
|
|
60
|
-
return {
|
|
61
|
-
content: [
|
|
62
|
-
{
|
|
63
|
-
type: "text",
|
|
64
|
-
text: `Instance ${action}:\n${JSON.stringify(instance, null, 2)}\n\n` +
|
|
65
|
-
`To receive real-time messages, subscribe to: orchestrator://messages/${instance.id}${watcherNote}`,
|
|
66
|
-
},
|
|
67
|
-
],
|
|
68
|
-
};
|
|
69
|
-
});
|
|
70
|
-
// 3. heartbeat
|
|
71
|
-
mcp.tool("heartbeat", {
|
|
72
|
-
instance_id: z.string(),
|
|
73
|
-
current_task: z.string().optional(),
|
|
74
|
-
}, async ({ instance_id, current_task }) => {
|
|
75
|
-
await registry.heartbeat(instance_id, current_task);
|
|
76
|
-
return { content: [{ type: "text", text: "ok" }] };
|
|
77
|
-
});
|
|
78
|
-
// 4. list_instances
|
|
79
|
-
mcp.tool("list_instances", async () => {
|
|
80
|
-
const instances = await registry.listAll();
|
|
81
|
-
const n = instances.length;
|
|
82
|
-
const header = `${n} active instance${n !== 1 ? "s" : ""}:\n`;
|
|
83
|
-
const lines = instances.map((i) => ` [${i.role}] ${i.name} (${i.id.slice(0, 8)}...) status=${i.status}`);
|
|
84
|
-
return { content: [{ type: "text", text: header + lines.join("\n") }] };
|
|
85
|
-
});
|
|
86
|
-
// 5. push_task
|
|
87
|
-
mcp.tool("push_task", {
|
|
88
|
-
title: z.string().min(1),
|
|
89
|
-
description: z.string().default(""),
|
|
90
|
-
priority: z.number().int().min(0).max(2).default(1),
|
|
91
|
-
instance_id: z.string().default(""),
|
|
92
|
-
assignee: z.string().optional(),
|
|
93
|
-
}, async ({ title, description, priority, instance_id, assignee }) => {
|
|
94
|
-
const task = await taskQueue.push(title, description, priority, instance_id, assignee);
|
|
95
|
-
const prioName = TaskPriorityName[task.priority] ?? "MEDIUM";
|
|
96
|
-
return {
|
|
97
|
-
content: [
|
|
98
|
-
{
|
|
99
|
-
type: "text",
|
|
100
|
-
text: `Task ${task.id} created:\n title: ${task.title}\n priority: ${prioName}`,
|
|
101
|
-
},
|
|
102
|
-
],
|
|
103
|
-
};
|
|
104
|
-
});
|
|
105
|
-
// 6. claim_task
|
|
106
|
-
mcp.tool("claim_task", { instance_id: z.string() }, async ({ instance_id }) => {
|
|
107
|
-
const task = await taskQueue.claim(instance_id);
|
|
108
|
-
if (!task) {
|
|
109
|
-
return { content: [{ type: "text", text: "No pending tasks available." }] };
|
|
110
|
-
}
|
|
111
|
-
return {
|
|
112
|
-
content: [
|
|
113
|
-
{
|
|
114
|
-
type: "text",
|
|
115
|
-
text: `Claimed task ${task.id}\n title: ${task.title}\n description: ${task.description}`,
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
};
|
|
119
|
-
});
|
|
120
|
-
// 7. complete_task
|
|
121
|
-
mcp.tool("complete_task", {
|
|
122
|
-
instance_id: z.string(),
|
|
123
|
-
task_id: z.string(),
|
|
124
|
-
result: z.string(),
|
|
125
|
-
}, async ({ instance_id, task_id, result: resultText }) => {
|
|
126
|
-
const task = await taskQueue.complete(instance_id, task_id, resultText);
|
|
127
|
-
return { content: [{ type: "text", text: `Task ${task.id} completed.` }] };
|
|
128
|
-
});
|
|
129
|
-
// 8. list_tasks
|
|
130
|
-
mcp.tool("list_tasks", { status: z.string().optional() }, async ({ status }) => {
|
|
131
|
-
const tasks = await taskQueue.listTasks(status);
|
|
132
|
-
if (tasks.length === 0) {
|
|
133
|
-
return { content: [{ type: "text", text: "No tasks found." }] };
|
|
134
|
-
}
|
|
135
|
-
const lines = [`${tasks.length} task(s):`];
|
|
136
|
-
for (const t of tasks) {
|
|
137
|
-
lines.push(` [${t.status}] ${t.id}: ${t.title || "(no title)"}`);
|
|
138
|
-
}
|
|
139
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
140
|
-
});
|
|
141
|
-
// 9. send_message
|
|
142
|
-
mcp.tool("send_message", {
|
|
143
|
-
instance_id: z.string(),
|
|
144
|
-
content: z.string().min(1),
|
|
145
|
-
to_instance: z.string().optional(),
|
|
146
|
-
to_name: z.string().optional(),
|
|
147
|
-
broadcast: z.boolean().default(false),
|
|
148
|
-
}, async ({ instance_id, content, to_instance, to_name, broadcast }) => {
|
|
149
|
-
const inst = await registry.get(instance_id);
|
|
150
|
-
const fromName = inst?.name ?? instance_id.slice(0, 8);
|
|
151
|
-
const messages = await messageRouter.send(instance_id, fromName, content, to_instance, broadcast, to_name);
|
|
152
|
-
// Push notifications to targets so subscribed clients get real-time delivery
|
|
153
|
-
const targets = messages.map((m) => m.to_instance);
|
|
154
|
-
await Promise.allSettled(targets.map((tid) => mcp.server.sendResourceUpdated({ uri: `orchestrator://messages/${tid}` })));
|
|
155
|
-
return {
|
|
156
|
-
content: [{ type: "text", text: `Message sent to: ${JSON.stringify(targets)}` }],
|
|
157
|
-
};
|
|
158
|
-
});
|
|
159
|
-
// 10. poll_messages
|
|
160
|
-
mcp.tool("poll_messages", { instance_id: z.string() }, async ({ instance_id }) => {
|
|
161
|
-
const messages = await messageRouter.poll(instance_id);
|
|
162
|
-
if (messages.length === 0) {
|
|
163
|
-
return { content: [{ type: "text", text: "No messages." }] };
|
|
164
|
-
}
|
|
165
|
-
const lines = [`${messages.length} message(s):`];
|
|
166
|
-
for (const m of messages) {
|
|
167
|
-
const readMark = m.read ? " [read]" : "";
|
|
168
|
-
lines.push(` [${m.type}] from ${m.from_name}: ${m.content.slice(0, 100)}${readMark}`);
|
|
169
|
-
}
|
|
170
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
171
|
-
});
|
|
172
|
-
// 11. wait_for_message
|
|
173
|
-
mcp.tool("wait_for_message", {
|
|
174
|
-
instance_id: z.string(),
|
|
175
|
-
timeout_seconds: z.number().int().min(1).default(30),
|
|
176
|
-
}, async ({ instance_id, timeout_seconds }) => {
|
|
177
|
-
const messages = await messageRouter.waitForMessage(instance_id, timeout_seconds);
|
|
178
|
-
if (messages.length === 0) {
|
|
179
|
-
return { content: [{ type: "text", text: "No messages received within timeout." }] };
|
|
180
|
-
}
|
|
181
|
-
const lines = [`${messages.length} message(s):`];
|
|
182
|
-
for (const m of messages) {
|
|
183
|
-
lines.push(` [${m.type}] from ${m.from_name}: ${m.content.slice(0, 200)}`);
|
|
184
|
-
}
|
|
185
|
-
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
186
|
-
});
|
|
187
|
-
// 12. mark_read
|
|
188
|
-
mcp.tool("mark_read", {
|
|
189
|
-
instance_id: z.string(),
|
|
190
|
-
message_id: z.string(),
|
|
191
|
-
}, async ({ instance_id, message_id }) => {
|
|
192
|
-
await messageRouter.markRead(instance_id, message_id);
|
|
193
|
-
return { content: [{ type: "text", text: `Message ${message_id} marked as read.` }] };
|
|
194
|
-
});
|
|
195
|
-
// 13. dismiss_message
|
|
196
|
-
mcp.tool("dismiss_message", {
|
|
197
|
-
instance_id: z.string(),
|
|
198
|
-
message_id: z.string(),
|
|
199
|
-
}, async ({ instance_id, message_id }) => {
|
|
200
|
-
await messageRouter.dismissMessage(instance_id, message_id);
|
|
201
|
-
return { content: [{ type: "text", text: `Message ${message_id} dismissed.` }] };
|
|
202
|
-
});
|
|
203
|
-
// 14. request_help
|
|
204
|
-
mcp.tool("request_help", {
|
|
205
|
-
instance_id: z.string(),
|
|
206
|
-
question: z.string().min(1),
|
|
207
|
-
context: z.string().optional(),
|
|
208
|
-
}, async ({ instance_id, question, context }) => {
|
|
209
|
-
const inst = await registry.get(instance_id);
|
|
210
|
-
const fromName = inst?.name ?? instance_id.slice(0, 8);
|
|
211
|
-
const messages = await messageRouter.requestHelp(instance_id, fromName, question, context);
|
|
212
|
-
const targets = messages.map((m) => m.to_instance);
|
|
213
|
-
await Promise.allSettled(targets.map((tid) => mcp.server.sendResourceUpdated({ uri: `orchestrator://messages/${tid}` })));
|
|
214
|
-
return {
|
|
215
|
-
content: [
|
|
216
|
-
{
|
|
217
|
-
type: "text",
|
|
218
|
-
text: `Help request broadcast to ${targets.length} instance(s): ${JSON.stringify(targets)}`,
|
|
219
|
-
},
|
|
220
|
-
],
|
|
221
|
-
};
|
|
222
|
-
});
|
|
223
|
-
// 15. set_context
|
|
224
|
-
mcp.tool("set_context", {
|
|
225
|
-
key: z.string().min(1),
|
|
226
|
-
value: z.string(),
|
|
227
|
-
instance_id: z.string().default(""),
|
|
228
|
-
}, async ({ key, value, instance_id }) => {
|
|
229
|
-
await contextStore.set(key, value, instance_id);
|
|
230
|
-
return { content: [{ type: "text", text: `Context set: ${key} = ${value}` }] };
|
|
231
|
-
});
|
|
232
|
-
// 16. get_context
|
|
233
|
-
mcp.tool("get_context", { key: z.string() }, async ({ key }) => {
|
|
234
|
-
const value = await contextStore.get(key);
|
|
235
|
-
if (value === null) {
|
|
236
|
-
return { content: [{ type: "text", text: `No context found for key: ${key}` }] };
|
|
237
|
-
}
|
|
238
|
-
return { content: [{ type: "text", text: `${key} = ${value}` }] };
|
|
239
|
-
});
|
|
240
|
-
// 17. delete_context
|
|
241
|
-
mcp.tool("delete_context", { key: z.string() }, async ({ key }) => {
|
|
242
|
-
await contextStore.delete(key);
|
|
243
|
-
return { content: [{ type: "text", text: `Context deleted: ${key}` }] };
|
|
244
|
-
});
|
|
245
|
-
// 18. list_context_keys
|
|
246
|
-
mcp.tool("list_context_keys", async () => {
|
|
247
|
-
const keys = await contextStore.listKeys();
|
|
248
|
-
if (keys.length === 0) {
|
|
249
|
-
return { content: [{ type: "text", text: "No context keys found." }] };
|
|
250
|
-
}
|
|
251
|
-
return { content: [{ type: "text", text: `${keys.length} key(s):\n${keys.join("\n")}` }] };
|
|
252
|
-
});
|
|
253
|
-
// ── Register Resources (5) ──
|
|
254
|
-
// 1. orchestrator://instances
|
|
255
|
-
mcp.resource("instances", "orchestrator://instances", { description: "All active instances" }, async () => {
|
|
256
|
-
const instances = await registry.listAll();
|
|
257
|
-
return {
|
|
258
|
-
contents: [
|
|
259
|
-
{
|
|
260
|
-
uri: "orchestrator://instances",
|
|
261
|
-
mimeType: "application/json",
|
|
262
|
-
text: JSON.stringify(instances, null, 2),
|
|
263
|
-
},
|
|
264
|
-
],
|
|
265
|
-
};
|
|
266
|
-
});
|
|
267
|
-
// 2. orchestrator://tasks/pending
|
|
268
|
-
mcp.resource("tasks-pending", "orchestrator://tasks/pending", { description: "Pending tasks" }, async () => {
|
|
269
|
-
const tasks = await taskQueue.listTasks("pending");
|
|
270
|
-
return {
|
|
271
|
-
contents: [
|
|
272
|
-
{
|
|
273
|
-
uri: "orchestrator://tasks/pending",
|
|
274
|
-
mimeType: "application/json",
|
|
275
|
-
text: JSON.stringify(tasks, null, 2),
|
|
276
|
-
},
|
|
277
|
-
],
|
|
278
|
-
};
|
|
279
|
-
});
|
|
280
|
-
// 3. orchestrator://tasks/in-progress
|
|
281
|
-
mcp.resource("tasks-in-progress", "orchestrator://tasks/in-progress", { description: "Claimed tasks in progress" }, async () => {
|
|
282
|
-
const tasks = await taskQueue.listTasks("claimed");
|
|
283
|
-
return {
|
|
284
|
-
contents: [
|
|
285
|
-
{
|
|
286
|
-
uri: "orchestrator://tasks/in-progress",
|
|
287
|
-
mimeType: "application/json",
|
|
288
|
-
text: JSON.stringify(tasks, null, 2),
|
|
289
|
-
},
|
|
290
|
-
],
|
|
291
|
-
};
|
|
292
|
-
});
|
|
293
|
-
// 4. orchestrator://messages/{instance_id} (supports subscription)
|
|
294
|
-
mcp.resource("messages", new ResourceTemplate("orchestrator://messages/{instance_id}", {
|
|
295
|
-
list: undefined,
|
|
296
|
-
}), { description: "Messages for a specific instance" }, async (uri, variables) => {
|
|
297
|
-
const instanceId = variables.instance_id ?? "";
|
|
298
|
-
const messages = await messageRouter.poll(instanceId);
|
|
299
|
-
return {
|
|
300
|
-
contents: [
|
|
301
|
-
{
|
|
302
|
-
uri: uri.href,
|
|
303
|
-
mimeType: "application/json",
|
|
304
|
-
text: JSON.stringify(messages, null, 2),
|
|
305
|
-
},
|
|
306
|
-
],
|
|
307
|
-
};
|
|
308
|
-
});
|
|
309
|
-
// 5. orchestrator://context/{key} (supports subscription)
|
|
310
|
-
mcp.resource("context", new ResourceTemplate("orchestrator://context/{key}", {
|
|
311
|
-
list: undefined,
|
|
312
|
-
}), { description: "Shared context value by key" }, async (uri, variables) => {
|
|
313
|
-
const key = variables.key ?? "";
|
|
314
|
-
const value = await contextStore.get(key);
|
|
315
|
-
return {
|
|
316
|
-
contents: [
|
|
317
|
-
{
|
|
318
|
-
uri: uri.href,
|
|
319
|
-
mimeType: "application/json",
|
|
320
|
-
text: JSON.stringify(value !== null ? { key, value } : { key, value: null }, null, 2),
|
|
321
|
-
},
|
|
322
|
-
],
|
|
323
|
-
};
|
|
324
|
-
});
|
|
325
|
-
// ── Register Prompts (2) ──
|
|
326
|
-
// 1. orchestrate-task
|
|
327
|
-
mcp.prompt("orchestrate-task", "Help break down a goal into assignable tasks", {
|
|
328
|
-
goal: z.string().describe("The goal or objective to break down"),
|
|
329
|
-
context: z.string().optional().describe("Additional context about team state"),
|
|
330
|
-
}, async ({ goal, context: ctx }) => {
|
|
331
|
-
const instances = await registry.listAll();
|
|
332
|
-
const pendingTasks = await taskQueue.listTasks("pending");
|
|
333
|
-
const members = instances
|
|
334
|
-
.map((i) => `- ${i.name} (${i.id.slice(0, 8)}...) [${i.role}] status=${i.status}`)
|
|
335
|
-
.join("\n");
|
|
336
|
-
return {
|
|
337
|
-
messages: [
|
|
338
|
-
{
|
|
339
|
-
role: "user",
|
|
340
|
-
content: {
|
|
341
|
-
type: "text",
|
|
342
|
-
text: `Goal: ${goal}\n\n` +
|
|
343
|
-
`Team members:\n${members}\n\n` +
|
|
344
|
-
`Pending tasks: ${pendingTasks.length}\n` +
|
|
345
|
-
(ctx ? `Additional context: ${ctx}\n\n` : "") +
|
|
346
|
-
`Please break this goal down into specific, assignable tasks. For each task, suggest:\n` +
|
|
347
|
-
`1. A clear title\n` +
|
|
348
|
-
`2. A brief description\n` +
|
|
349
|
-
`3. Priority (HIGH=0, MEDIUM=1, LOW=2)\n` +
|
|
350
|
-
`4. Which team member should handle it\n\n` +
|
|
351
|
-
`Output the tasks in a format ready to assign.`,
|
|
352
|
-
},
|
|
353
|
-
},
|
|
354
|
-
],
|
|
355
|
-
};
|
|
356
|
-
});
|
|
357
|
-
// 2. team-status
|
|
358
|
-
mcp.prompt("team-status", "Summarize current team state", async () => {
|
|
359
|
-
const instances = await registry.listAll();
|
|
360
|
-
const pending = await taskQueue.listTasks("pending");
|
|
361
|
-
const claimed = await taskQueue.listTasks("claimed");
|
|
362
|
-
const members = instances
|
|
363
|
-
.map((i) => `- ${i.name} (${i.id.slice(0, 8)}...) [${i.role}] status=${i.status}`)
|
|
364
|
-
.join("\n");
|
|
365
|
-
const pendingList = pending
|
|
366
|
-
.map((t) => `- [${TaskPriorityName[t.priority]}] ${t.title} (${t.id})`)
|
|
367
|
-
.join("\n");
|
|
368
|
-
const claimedList = claimed
|
|
369
|
-
.map((t) => `- ${t.title} by ${t.claimed_by?.slice(0, 8)}... (${t.id})`)
|
|
370
|
-
.join("\n");
|
|
371
|
-
return {
|
|
372
|
-
messages: [
|
|
373
|
-
{
|
|
374
|
-
role: "user",
|
|
375
|
-
content: {
|
|
376
|
-
type: "text",
|
|
377
|
-
text: `# Team Status\n\n` +
|
|
378
|
-
`## Members (${instances.length})\n${members}\n\n` +
|
|
379
|
-
`## Pending Tasks (${pending.length})\n${pendingList || "(none)"}\n\n` +
|
|
380
|
-
`## In Progress (${claimed.length})\n${claimedList || "(none)"}`,
|
|
381
|
-
},
|
|
382
|
-
},
|
|
383
|
-
],
|
|
384
|
-
};
|
|
385
|
-
});
|
|
386
|
-
// ── Start HTTP server with MCP transport ──
|
|
387
|
-
const app = express();
|
|
388
|
-
app.use(express.json());
|
|
389
|
-
const transport = new StreamableHTTPServerTransport({
|
|
390
|
-
sessionIdGenerator: undefined, // stateless mode
|
|
391
|
-
});
|
|
392
|
-
// Connect MCP server to transport
|
|
393
|
-
await mcp.connect(transport);
|
|
394
|
-
// MCP endpoint
|
|
395
|
-
app.post("/mcp", async (req, res) => {
|
|
396
|
-
try {
|
|
397
|
-
await transport.handleRequest(req, res, req.body);
|
|
398
|
-
}
|
|
399
|
-
catch (err) {
|
|
400
|
-
if (!res.headersSent) {
|
|
401
|
-
res.status(500).json({ error: "MCP request handling failed" });
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
app.get("/mcp", async (req, res) => {
|
|
406
|
-
try {
|
|
407
|
-
await transport.handleRequest(req, res);
|
|
408
|
-
}
|
|
409
|
-
catch (err) {
|
|
410
|
-
if (!res.headersSent) {
|
|
411
|
-
res.status(500).json({ error: "MCP request handling failed" });
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
app.delete("/mcp", async (req, res) => {
|
|
416
|
-
try {
|
|
417
|
-
await transport.handleRequest(req, res);
|
|
418
|
-
}
|
|
419
|
-
catch (err) {
|
|
420
|
-
if (!res.headersSent) {
|
|
421
|
-
res.status(500).json({ error: "MCP request handling failed" });
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
// REST endpoint for CLI-based registration (uses server's persistent ZK connection)
|
|
426
|
-
app.post("/register", async (req, res) => {
|
|
427
|
-
try {
|
|
428
|
-
const { name, role, instance_id, work_dir } = req.body;
|
|
429
|
-
if (!name || typeof name !== "string") {
|
|
430
|
-
res.status(400).json({ error: "name is required" });
|
|
431
|
-
return;
|
|
432
|
-
}
|
|
433
|
-
if (!zk.connected) {
|
|
434
|
-
res.status(503).json({ error: "ZooKeeper is not connected" });
|
|
435
|
-
return;
|
|
436
|
-
}
|
|
437
|
-
const instance = await registry.register(name, typeof role === "string" ? role : "general", typeof instance_id === "string" ? instance_id : undefined);
|
|
438
|
-
if (typeof work_dir === "string" && work_dir.length > 0) {
|
|
439
|
-
await messageWatcher.startWatching(instance.id, work_dir);
|
|
440
|
-
}
|
|
441
|
-
res.json(instance);
|
|
442
|
-
}
|
|
443
|
-
catch (err) {
|
|
444
|
-
res.status(500).json({ error: String(err) });
|
|
445
|
-
}
|
|
446
|
-
});
|
|
447
|
-
// REST endpoint for CLI-based unregistration
|
|
448
|
-
app.post("/unregister", async (req, res) => {
|
|
449
|
-
try {
|
|
450
|
-
const { instance_id } = req.body;
|
|
451
|
-
if (!instance_id || typeof instance_id !== "string") {
|
|
452
|
-
res.status(400).json({ error: "instance_id is required" });
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
messageWatcher.stopWatching(instance_id);
|
|
456
|
-
if (!zk.connected) {
|
|
457
|
-
res.json({ status: "unregistered", instance_id });
|
|
458
|
-
return;
|
|
459
|
-
}
|
|
460
|
-
await registry.unregister(instance_id);
|
|
461
|
-
res.json({ status: "unregistered", instance_id });
|
|
462
|
-
}
|
|
463
|
-
catch (err) {
|
|
464
|
-
res.status(500).json({ error: String(err) });
|
|
465
|
-
}
|
|
466
|
-
});
|
|
467
|
-
// Health check
|
|
468
|
-
app.get("/health", (_req, res) => {
|
|
469
|
-
res.json({ status: "ok", zookeeper: zk.connected ? "connected" : "disconnected" });
|
|
470
|
-
});
|
|
471
|
-
// ── Shutdown handlers ──
|
|
472
|
-
const shutdown = async (signal) => {
|
|
473
|
-
console.log(`\n[server] Received ${signal}, shutting down...`);
|
|
474
|
-
messageWatcher.stopAll();
|
|
475
|
-
await zk.disconnect();
|
|
476
|
-
process.exit(0);
|
|
477
|
-
};
|
|
478
|
-
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
479
|
-
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
480
|
-
return new Promise((resolve) => {
|
|
481
|
-
app.listen(config.port, config.host, () => {
|
|
482
|
-
console.log(`MCP server listening on ${config.host}:${config.port}`);
|
|
483
|
-
console.log(`MCP endpoint: http://${config.host}:${config.port}/mcp`);
|
|
484
|
-
console.log(`Register CLI: http://${config.host}:${config.port}/register`);
|
|
485
|
-
console.log(`Health check: http://${config.host}:${config.port}/health`);
|
|
486
|
-
resolve();
|
|
487
|
-
});
|
|
488
|
-
});
|
|
489
|
-
}
|
|
490
|
-
//# sourceMappingURL=server.js.map
|