Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions packages/agent-teams-lead/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@arvoretech/agent-teams-lead-mcp",
"version": "0.1.0",
"description": "MCP server for the team lead agent — spawn teammates, create tasks, coordinate work",
"main": "dist/index.js",
"type": "module",
"publishConfig": {
"access": "public"
},
"bin": {
"agent-teams-lead-mcp": "./dist/index.js"
},
"scripts": {
"build": "tsc",
"dev": "tsx src/index.ts",
"start": "node dist/index.js",
"test": "vitest run",
"test:cov": "vitest run --coverage",
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20.10.0",
"@vitest/coverage-v8": "^1.0.0",
"tsx": "^4.6.0",
"typescript": "^5.3.0",
"vitest": "^1.0.0"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
"keywords": [
"mcp",
"model-context-protocol",
"agent-teams",
"lead",
"orchestration",
"arvore"
],
"author": "Arvore",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
}
69 changes: 69 additions & 0 deletions packages/agent-teams-lead/src/filelock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { mkdir, rmdir, stat } from "node:fs/promises";
import { existsSync } from "node:fs";

const STALE_LOCK_MS = 10_000;
const RETRY_INTERVAL_MS = 50;
const MAX_WAIT_MS = 5_000;

async function isLockStale(lockPath: string): Promise<boolean> {
try {
const info = await stat(lockPath);
return Date.now() - info.mtimeMs > STALE_LOCK_MS;
} catch {
return true;
}
}

async function acquireLock(lockPath: string): Promise<void> {
const deadline = Date.now() + MAX_WAIT_MS;

while (Date.now() < deadline) {
try {
await mkdir(lockPath);
return;
} catch (err: unknown) {
const code = (err as NodeJS.ErrnoException).code;
if (code === "EEXIST") {
if (await isLockStale(lockPath)) {
try {
await rmdir(lockPath);
} catch {
// noop
}
continue;
}
await new Promise((r) => setTimeout(r, RETRY_INTERVAL_MS + Math.random() * 30));
continue;
}
throw err;
}
}

try {
await rmdir(lockPath);
} catch {
// noop
}
await mkdir(lockPath);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

async function releaseLock(lockPath: string): Promise<void> {
try {
await rmdir(lockPath);
} catch {
// noop
}
}

export async function withFileLock<T>(
filePath: string,
fn: () => Promise<T>
): Promise<T> {
const lockPath = `${filePath}.lock`;
await acquireLock(lockPath);
try {
return await fn();
} finally {
await releaseLock(lockPath);
}
}
21 changes: 21 additions & 0 deletions packages/agent-teams-lead/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env node

import { resolve } from "node:path";
import { LeadMCPServer } from "./server.js";

const workspacePath = resolve(process.env.WORKSPACE_PATH || process.cwd());

try {
const server = new LeadMCPServer(workspacePath);
server.setupGracefulShutdown();
await server.start();
} catch (error) {
console.error("Failed to start Agent Teams Lead MCP Server:", error);
process.exit(1);
}

export { LeadMCPServer } from "./server.js";
export { TeamStore } from "./store.js";
export { LeadTools } from "./tools.js";
export * from "./types.js";
export * from "./schemas.js";
27 changes: 27 additions & 0 deletions packages/agent-teams-lead/src/names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const FIRST_NAMES = [
"Sofia", "Lucas", "Marina", "Pedro", "Clara",
"Rafael", "Beatriz", "Gabriel", "Laura", "Mateus",
"Helena", "Thiago", "Camila", "André", "Isabela",
"Diego", "Valentina", "Bruno", "Alice", "Caio",
"Luana", "Felipe", "Manuela", "Gustavo", "Lívia",
"Renato", "Júlia", "Vinícius", "Letícia", "Henrique",
];

const usedNames = new Set<string>();

export function generateTeammateName(): string {
const available = FIRST_NAMES.filter((n) => !usedNames.has(n));

if (available.length === 0) {
usedNames.clear();
return generateTeammateName();
}

const name = available[Math.floor(Math.random() * available.length)];
usedNames.add(name);
return name;
}

export function resetNames(): void {
usedNames.clear();
}
75 changes: 75 additions & 0 deletions packages/agent-teams-lead/src/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { z } from "zod";
import { MESSAGE_KINDS } from "./types.js";

export const SpawnTeamTeammateSchema = z.object({
agent: z.string().min(1, "Agent file path is required (e.g. refinement.md)"),
mcp_servers: z.array(z.string()).optional(),
});

export const SpawnTeamSchema = z.object({
objective: z.string().min(1, "Team objective is required"),
teammates: z
.array(SpawnTeamTeammateSchema)
.min(1, "At least one teammate is required"),
});

export const AddTeammateSchema = z.object({
agent: z.string().min(1, "Agent file path is required"),
mcp_servers: z.array(z.string()).optional(),
});

export const RemoveTeammateSchema = z.object({
teammate_id: z.string().min(1, "Teammate ID is required"),
});

export const CreateTaskSchema = z.object({
title: z.string().min(1, "Task title is required"),
description: z.string().min(1, "Task description is required"),
depends_on: z.array(z.string()).optional().default([]),
exclusive_paths: z.array(z.string()).optional().default([]),
acceptance_criteria: z.array(z.string()).optional().default([]),
});

export const TeamStatusSchema = z.object({});

export const SendMessageSchema = z.object({
to: z.string().optional(),
broadcast: z.boolean().optional().default(false),
subject: z.string().min(1, "Subject is required"),
body: z.string().min(1, "Body is required"),
kind: z.enum(MESSAGE_KINDS).optional().default("info"),
});

export const WaitForTeamSchema = z.object({
timeout_seconds: z.number().positive().optional().default(300),
});

export const ReadArtifactSchema = z.object({
artifact_id: z.string().min(1, "Artifact ID is required"),
});

export type SpawnTeamParams = z.infer<typeof SpawnTeamSchema>;
export type AddTeammateParams = z.infer<typeof AddTeammateSchema>;
export type RemoveTeammateParams = z.infer<typeof RemoveTeammateSchema>;
export type CreateTaskParams = z.infer<typeof CreateTaskSchema>;
export type SendMessageParams = z.infer<typeof SendMessageSchema>;
export type WaitForTeamParams = z.infer<typeof WaitForTeamSchema>;
export type ReadArtifactParams = z.infer<typeof ReadArtifactSchema>;

export interface McpToolResult {
[key: string]: unknown;
content: Array<{
type: "text";
text: string;
}>;
}

export class AgentTeamsError extends Error {
constructor(
message: string,
public code: string
) {
super(message);
this.name = "AgentTeamsError";
}
}
Loading
Loading