@arvoretech/agent-teams-teammate-mcp 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/README.md +38 -0
- package/dist/filelock.js +77 -0
- package/dist/filelock.js.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/server.js +268 -0
- package/dist/server.js.map +1 -0
- package/dist/store.js +195 -0
- package/dist/store.js.map +1 -0
- package/package.json +35 -0
- package/src/filelock.ts +83 -0
- package/src/index.ts +19 -0
- package/src/server.ts +314 -0
- package/src/store.ts +299 -0
- package/tsconfig.json +23 -0
- package/vitest.config.ts +14 -0
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arvoretech/agent-teams-teammate-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for teammate agents — claim tasks, communicate, publish artifacts",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"agent-teams-teammate-mcp": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"dev": "tsx src/index.ts",
|
|
13
|
+
"start": "node dist/index.js",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:cov": "vitest run --coverage",
|
|
16
|
+
"lint": "eslint src/**/*.ts",
|
|
17
|
+
"lint:fix": "eslint src/**/*.ts --fix"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
21
|
+
"zod": "^3.22.4"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^20.10.0",
|
|
25
|
+
"@vitest/coverage-v8": "^1.0.0",
|
|
26
|
+
"tsx": "^4.6.0",
|
|
27
|
+
"typescript": "^5.3.0",
|
|
28
|
+
"vitest": "^1.0.0"
|
|
29
|
+
},
|
|
30
|
+
"author": "Arvore",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20.0.0"
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/filelock.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { mkdir, rmdir, stat } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
const STALE_LOCK_MS = 10_000;
|
|
4
|
+
const RETRY_INTERVAL_MS = 50;
|
|
5
|
+
const MAX_WAIT_MS = 5_000;
|
|
6
|
+
|
|
7
|
+
async function isLockStale(lockPath: string): Promise<boolean> {
|
|
8
|
+
try {
|
|
9
|
+
const info = await stat(lockPath);
|
|
10
|
+
return Date.now() - info.mtimeMs > STALE_LOCK_MS;
|
|
11
|
+
} catch {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function acquireLock(lockPath: string): Promise<void> {
|
|
17
|
+
const deadline = Date.now() + MAX_WAIT_MS;
|
|
18
|
+
|
|
19
|
+
while (Date.now() < deadline) {
|
|
20
|
+
try {
|
|
21
|
+
await mkdir(lockPath);
|
|
22
|
+
return;
|
|
23
|
+
} catch (err: unknown) {
|
|
24
|
+
const code = (err as { code?: string }).code;
|
|
25
|
+
if (code === "EEXIST") {
|
|
26
|
+
if (await isLockStale(lockPath)) {
|
|
27
|
+
try {
|
|
28
|
+
await rmdir(lockPath);
|
|
29
|
+
} catch {
|
|
30
|
+
// noop
|
|
31
|
+
}
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
await new Promise((r) =>
|
|
35
|
+
setTimeout(r, RETRY_INTERVAL_MS + Math.random() * 30)
|
|
36
|
+
);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!(await isLockStale(lockPath))) {
|
|
44
|
+
throw new Error(`Failed to acquire lock: ${lockPath} (timeout)`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await rmdir(lockPath);
|
|
49
|
+
} catch {
|
|
50
|
+
// noop
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await mkdir(lockPath);
|
|
55
|
+
} catch (err: unknown) {
|
|
56
|
+
const code = (err as { code?: string }).code;
|
|
57
|
+
if (code === "EEXIST") {
|
|
58
|
+
throw new Error(`Failed to acquire lock: ${lockPath} (contention)`);
|
|
59
|
+
}
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function releaseLock(lockPath: string): Promise<void> {
|
|
65
|
+
try {
|
|
66
|
+
await rmdir(lockPath);
|
|
67
|
+
} catch {
|
|
68
|
+
// noop
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function withFileLock<T>(
|
|
73
|
+
filePath: string,
|
|
74
|
+
fn: () => Promise<T>
|
|
75
|
+
): Promise<T> {
|
|
76
|
+
const lockPath = `${filePath}.lock`;
|
|
77
|
+
await acquireLock(lockPath);
|
|
78
|
+
try {
|
|
79
|
+
return await fn();
|
|
80
|
+
} finally {
|
|
81
|
+
await releaseLock(lockPath);
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { TeammateMCPServer } from "./server.js";
|
|
4
|
+
|
|
5
|
+
const teammateId = process.env.TEAMMATE_ID;
|
|
6
|
+
const teammateName = process.env.TEAMMATE_NAME;
|
|
7
|
+
const workspacePath = process.env.WORKSPACE_PATH || process.cwd();
|
|
8
|
+
|
|
9
|
+
if (!teammateId || !teammateName) {
|
|
10
|
+
console.error("Missing required env vars: TEAMMATE_ID, TEAMMATE_NAME");
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const server = new TeammateMCPServer(workspacePath, teammateId, teammateName);
|
|
15
|
+
server.setupGracefulShutdown();
|
|
16
|
+
server.start().catch((error) => {
|
|
17
|
+
console.error("Failed to start teammate MCP server:", error);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
});
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { TeammateStore } from "./store.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
export class TeammateMCPServer {
|
|
7
|
+
private server: McpServer;
|
|
8
|
+
private store: TeammateStore;
|
|
9
|
+
private teammateId: string;
|
|
10
|
+
private teammateName: string;
|
|
11
|
+
|
|
12
|
+
constructor(workspacePath: string, teammateId: string, teammateName: string) {
|
|
13
|
+
this.server = new McpServer({
|
|
14
|
+
name: "agent-teams-teammate",
|
|
15
|
+
version: "0.1.0",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
this.store = new TeammateStore(workspacePath);
|
|
19
|
+
this.teammateId = teammateId;
|
|
20
|
+
this.teammateName = teammateName;
|
|
21
|
+
|
|
22
|
+
this.setupTools();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
private ok(data: Record<string, unknown>) {
|
|
26
|
+
return {
|
|
27
|
+
content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private err(error: unknown) {
|
|
32
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
33
|
+
return {
|
|
34
|
+
content: [{ type: "text" as const, text: JSON.stringify({ error: message }, null, 2) }],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private setupTools(): void {
|
|
39
|
+
this.server.registerTool(
|
|
40
|
+
"whoami",
|
|
41
|
+
{
|
|
42
|
+
title: "Who Am I",
|
|
43
|
+
description: "Returns your identity: name, role, team objective.",
|
|
44
|
+
inputSchema: z.object({}).shape,
|
|
45
|
+
},
|
|
46
|
+
async () => {
|
|
47
|
+
try {
|
|
48
|
+
await this.store.load();
|
|
49
|
+
const team = this.store.getTeam();
|
|
50
|
+
const teammate = this.store.getTeammate(this.teammateId);
|
|
51
|
+
return this.ok({
|
|
52
|
+
id: this.teammateId,
|
|
53
|
+
name: this.teammateName,
|
|
54
|
+
role: teammate?.role || "unknown",
|
|
55
|
+
team_objective: team?.objective || "unknown",
|
|
56
|
+
teammates: team?.teammates
|
|
57
|
+
.filter((t) => t.id !== this.teammateId && t.status === "active")
|
|
58
|
+
.map((t) => ({ id: t.id, name: t.name, role: t.role })) || [],
|
|
59
|
+
});
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return this.err(error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
this.server.registerTool(
|
|
67
|
+
"list_tasks",
|
|
68
|
+
{
|
|
69
|
+
title: "List Tasks",
|
|
70
|
+
description: "List available tasks. Filter by status (pending, in_progress, completed, blocked).",
|
|
71
|
+
inputSchema: z.object({
|
|
72
|
+
status: z.string().optional(),
|
|
73
|
+
}).shape,
|
|
74
|
+
},
|
|
75
|
+
async (params) => {
|
|
76
|
+
try {
|
|
77
|
+
await this.store.load();
|
|
78
|
+
const tasks = this.store.listTasks(params.status ? { status: params.status } : undefined);
|
|
79
|
+
return this.ok({
|
|
80
|
+
count: tasks.length,
|
|
81
|
+
tasks: tasks.map((t) => ({
|
|
82
|
+
id: t.id,
|
|
83
|
+
title: t.title,
|
|
84
|
+
description: t.description,
|
|
85
|
+
status: t.status,
|
|
86
|
+
assigned_to: t.assigned_to,
|
|
87
|
+
depends_on: t.depends_on,
|
|
88
|
+
acceptance_criteria: t.acceptance_criteria,
|
|
89
|
+
})),
|
|
90
|
+
});
|
|
91
|
+
} catch (error) {
|
|
92
|
+
return this.err(error);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
this.server.registerTool(
|
|
98
|
+
"claim_task",
|
|
99
|
+
{
|
|
100
|
+
title: "Claim Task",
|
|
101
|
+
description: "Claim a pending task to work on it. The task must be pending and have no unresolved dependencies.",
|
|
102
|
+
inputSchema: z.object({
|
|
103
|
+
task_id: z.string().min(1),
|
|
104
|
+
}).shape,
|
|
105
|
+
},
|
|
106
|
+
async (params) => {
|
|
107
|
+
try {
|
|
108
|
+
const task = await this.store.claimTask(params.task_id, this.teammateId);
|
|
109
|
+
return this.ok({ claimed: true, task_id: task.id, title: task.title });
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return this.err(error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
this.server.registerTool(
|
|
117
|
+
"update_task",
|
|
118
|
+
{
|
|
119
|
+
title: "Update Task",
|
|
120
|
+
description: "Update a task you own. Change status or add a note.",
|
|
121
|
+
inputSchema: z.object({
|
|
122
|
+
task_id: z.string().min(1),
|
|
123
|
+
status: z.enum(["in_progress", "blocked"]).optional(),
|
|
124
|
+
note: z.string().optional(),
|
|
125
|
+
}).shape,
|
|
126
|
+
},
|
|
127
|
+
async (params) => {
|
|
128
|
+
try {
|
|
129
|
+
const task = await this.store.updateTask(params.task_id, this.teammateId, {
|
|
130
|
+
status: params.status,
|
|
131
|
+
note: params.note,
|
|
132
|
+
});
|
|
133
|
+
return this.ok({ updated: true, task_id: task.id, status: task.status });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return this.err(error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
this.server.registerTool(
|
|
141
|
+
"complete_task",
|
|
142
|
+
{
|
|
143
|
+
title: "Complete Task",
|
|
144
|
+
description: "Mark a task as completed with a summary of what was done.",
|
|
145
|
+
inputSchema: z.object({
|
|
146
|
+
task_id: z.string().min(1),
|
|
147
|
+
summary: z.string().min(1),
|
|
148
|
+
touched_paths: z.array(z.string()).optional().default([]),
|
|
149
|
+
}).shape,
|
|
150
|
+
},
|
|
151
|
+
async (params) => {
|
|
152
|
+
try {
|
|
153
|
+
const task = await this.store.completeTask(params.task_id, this.teammateId, {
|
|
154
|
+
summary: params.summary,
|
|
155
|
+
touched_paths: params.touched_paths,
|
|
156
|
+
});
|
|
157
|
+
return this.ok({ completed: true, task_id: task.id, title: task.title });
|
|
158
|
+
} catch (error) {
|
|
159
|
+
return this.err(error);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
this.server.registerTool(
|
|
165
|
+
"send_message",
|
|
166
|
+
{
|
|
167
|
+
title: "Send Message",
|
|
168
|
+
description: "Send a message to another teammate or to the lead.",
|
|
169
|
+
inputSchema: z.object({
|
|
170
|
+
to: z.string().optional(),
|
|
171
|
+
to_lead: z.boolean().optional().default(false),
|
|
172
|
+
subject: z.string().min(1),
|
|
173
|
+
body: z.string().min(1),
|
|
174
|
+
kind: z.enum(["info", "question", "answer", "blocker", "decision"]).optional().default("info"),
|
|
175
|
+
}).shape,
|
|
176
|
+
},
|
|
177
|
+
async (params) => {
|
|
178
|
+
try {
|
|
179
|
+
const message = await this.store.sendMessage({
|
|
180
|
+
from: this.teammateId,
|
|
181
|
+
from_name: this.teammateName,
|
|
182
|
+
to: params.to,
|
|
183
|
+
to_lead: params.to_lead,
|
|
184
|
+
subject: params.subject,
|
|
185
|
+
body: params.body,
|
|
186
|
+
kind: params.kind,
|
|
187
|
+
});
|
|
188
|
+
return this.ok({ message_id: message.id, sent: true });
|
|
189
|
+
} catch (error) {
|
|
190
|
+
return this.err(error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
this.server.registerTool(
|
|
196
|
+
"fetch_messages",
|
|
197
|
+
{
|
|
198
|
+
title: "Fetch Messages",
|
|
199
|
+
description: "Fetch messages sent to you. Optionally filter by unread only.",
|
|
200
|
+
inputSchema: z.object({
|
|
201
|
+
unread_only: z.boolean().optional().default(true),
|
|
202
|
+
thread: z.string().optional(),
|
|
203
|
+
}).shape,
|
|
204
|
+
},
|
|
205
|
+
async (params) => {
|
|
206
|
+
try {
|
|
207
|
+
await this.store.load();
|
|
208
|
+
const messages = this.store.fetchMessages(this.teammateId, {
|
|
209
|
+
unread_only: params.unread_only,
|
|
210
|
+
thread: params.thread,
|
|
211
|
+
});
|
|
212
|
+
return this.ok({
|
|
213
|
+
count: messages.length,
|
|
214
|
+
messages: messages.map((m) => ({
|
|
215
|
+
id: m.id,
|
|
216
|
+
from_name: m.from_name,
|
|
217
|
+
kind: m.kind,
|
|
218
|
+
subject: m.subject,
|
|
219
|
+
body: m.body,
|
|
220
|
+
created_at: m.created_at,
|
|
221
|
+
})),
|
|
222
|
+
});
|
|
223
|
+
} catch (error) {
|
|
224
|
+
return this.err(error);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
this.server.registerTool(
|
|
230
|
+
"ack_messages",
|
|
231
|
+
{
|
|
232
|
+
title: "Acknowledge Messages",
|
|
233
|
+
description: "Mark messages as read.",
|
|
234
|
+
inputSchema: z.object({
|
|
235
|
+
message_ids: z.array(z.string().min(1)),
|
|
236
|
+
}).shape,
|
|
237
|
+
},
|
|
238
|
+
async (params) => {
|
|
239
|
+
try {
|
|
240
|
+
const count = await this.store.ackMessages(this.teammateId, params.message_ids);
|
|
241
|
+
return this.ok({ acknowledged: count });
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return this.err(error);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
this.server.registerTool(
|
|
249
|
+
"write_artifact",
|
|
250
|
+
{
|
|
251
|
+
title: "Write Artifact",
|
|
252
|
+
description: "Publish an artifact (output of your work) linked to a task.",
|
|
253
|
+
inputSchema: z.object({
|
|
254
|
+
task_id: z.string().min(1),
|
|
255
|
+
name: z.string().min(1),
|
|
256
|
+
content: z.string().min(1),
|
|
257
|
+
type: z.enum(["markdown", "json", "code"]).optional().default("markdown"),
|
|
258
|
+
}).shape,
|
|
259
|
+
},
|
|
260
|
+
async (params) => {
|
|
261
|
+
try {
|
|
262
|
+
const artifact = await this.store.writeArtifact({
|
|
263
|
+
task_id: params.task_id,
|
|
264
|
+
agent_id: this.teammateId,
|
|
265
|
+
agent_name: this.teammateName,
|
|
266
|
+
name: params.name,
|
|
267
|
+
content: params.content,
|
|
268
|
+
type: params.type,
|
|
269
|
+
});
|
|
270
|
+
return this.ok({ artifact_id: artifact.id, name: artifact.name });
|
|
271
|
+
} catch (error) {
|
|
272
|
+
return this.err(error);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
this.server.registerTool(
|
|
278
|
+
"read_artifact",
|
|
279
|
+
{
|
|
280
|
+
title: "Read Artifact",
|
|
281
|
+
description: "Read an artifact by ID.",
|
|
282
|
+
inputSchema: z.object({
|
|
283
|
+
artifact_id: z.string().min(1),
|
|
284
|
+
}).shape,
|
|
285
|
+
},
|
|
286
|
+
async (params) => {
|
|
287
|
+
try {
|
|
288
|
+
await this.store.load();
|
|
289
|
+
const artifact = this.store.getArtifact(params.artifact_id);
|
|
290
|
+
if (!artifact) return this.ok({ error: `Artifact ${params.artifact_id} not found` });
|
|
291
|
+
return this.ok(artifact as unknown as Record<string, unknown>);
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return this.err(error);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async start(): Promise<void> {
|
|
300
|
+
await this.store.load();
|
|
301
|
+
const transport = new StdioServerTransport();
|
|
302
|
+
await this.server.connect(transport);
|
|
303
|
+
console.error(`Teammate MCP Server started for ${this.teammateName} (${this.teammateId})`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
setupGracefulShutdown(): void {
|
|
307
|
+
const shutdown = async (signal: string): Promise<void> => {
|
|
308
|
+
console.error(`[${this.teammateName}] Received ${signal}, shutting down...`);
|
|
309
|
+
process.exit(0);
|
|
310
|
+
};
|
|
311
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
312
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
313
|
+
}
|
|
314
|
+
}
|