- agent/: Claude Agent SDK inside Docker container - Persistent sessions (resume/checkpoint) - MCP tools for Tracker (chat, tasks) - File-based IPC protocol - runner.py: Host-side container manager - Docker lifecycle management - IPC file processing → Tracker REST API - Interactive CLI for testing - Dockerfile: node:22-slim + Claude Agent SDK - Based on NanoClaw architecture, stripped to essentials
147 lines
4.2 KiB
TypeScript
147 lines
4.2 KiB
TypeScript
/**
|
|
* MCP Server for Team Board Agent
|
|
* Provides tools for agent to interact with Tracker:
|
|
* - send_message: send chat message via IPC
|
|
* - update_task: update task status
|
|
* - create_comment: comment on a task
|
|
*
|
|
* Runs as a child process of the agent, communicates via stdio.
|
|
* Uses file-based IPC to communicate with the host runner.
|
|
*/
|
|
|
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
import { z } from 'zod';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
const IPC_DIR = '/workspace/ipc';
|
|
const MESSAGES_DIR = path.join(IPC_DIR, 'messages');
|
|
const TASKS_DIR = path.join(IPC_DIR, 'tasks');
|
|
|
|
const agentSlug = process.env.AGENT_SLUG || 'unknown';
|
|
const agentName = process.env.AGENT_NAME || 'Agent';
|
|
|
|
function writeIpcFile(dir: string, data: object): string {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`;
|
|
const filepath = path.join(dir, filename);
|
|
const tmpPath = `${filepath}.tmp`;
|
|
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
|
|
fs.renameSync(tmpPath, filepath);
|
|
return filename;
|
|
}
|
|
|
|
const server = new McpServer({
|
|
name: 'tracker',
|
|
version: '1.0.0',
|
|
});
|
|
|
|
// --- Chat ---
|
|
|
|
server.tool(
|
|
'send_message',
|
|
'Send a message to a chat room (lobby, project, or task chat). Use for progress updates or responses.',
|
|
{
|
|
chat_id: z.string().describe('Chat ID to send the message to'),
|
|
text: z.string().describe('Message text'),
|
|
},
|
|
async (args) => {
|
|
writeIpcFile(MESSAGES_DIR, {
|
|
type: 'chat_message',
|
|
chat_id: args.chat_id,
|
|
content: args.text,
|
|
sender_type: 'agent',
|
|
sender_name: agentName,
|
|
sender_slug: agentSlug,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
return { content: [{ type: 'text' as const, text: 'Message sent.' }] };
|
|
},
|
|
);
|
|
|
|
// --- Tasks ---
|
|
|
|
server.tool(
|
|
'update_task_status',
|
|
'Move a task to a different status (backlog, todo, in_progress, in_review, done)',
|
|
{
|
|
task_id: z.string().describe('Task ID'),
|
|
status: z.enum(['backlog', 'todo', 'in_progress', 'in_review', 'done']).describe('New status'),
|
|
comment: z.string().optional().describe('Optional comment about the status change'),
|
|
},
|
|
async (args) => {
|
|
writeIpcFile(TASKS_DIR, {
|
|
type: 'task_status',
|
|
task_id: args.task_id,
|
|
status: args.status,
|
|
comment: args.comment,
|
|
agent_slug: agentSlug,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
return { content: [{ type: 'text' as const, text: `Task ${args.task_id} → ${args.status}` }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
'add_task_comment',
|
|
'Add a comment to a task (for progress updates, questions, or results)',
|
|
{
|
|
task_id: z.string().describe('Task ID'),
|
|
comment: z.string().describe('Comment text'),
|
|
},
|
|
async (args) => {
|
|
writeIpcFile(TASKS_DIR, {
|
|
type: 'task_comment',
|
|
task_id: args.task_id,
|
|
content: args.comment,
|
|
sender_type: 'agent',
|
|
sender_name: agentName,
|
|
agent_slug: agentSlug,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
return { content: [{ type: 'text' as const, text: 'Comment added.' }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
'take_task',
|
|
'Take a task and start working on it. Changes status to in_progress.',
|
|
{
|
|
task_id: z.string().describe('Task ID to take'),
|
|
},
|
|
async (args) => {
|
|
writeIpcFile(TASKS_DIR, {
|
|
type: 'task_take',
|
|
task_id: args.task_id,
|
|
agent_slug: agentSlug,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
return { content: [{ type: 'text' as const, text: `Task ${args.task_id} taken.` }] };
|
|
},
|
|
);
|
|
|
|
server.tool(
|
|
'complete_task',
|
|
'Mark a task as done.',
|
|
{
|
|
task_id: z.string().describe('Task ID'),
|
|
summary: z.string().optional().describe('Completion summary'),
|
|
},
|
|
async (args) => {
|
|
writeIpcFile(TASKS_DIR, {
|
|
type: 'task_complete',
|
|
task_id: args.task_id,
|
|
summary: args.summary,
|
|
agent_slug: agentSlug,
|
|
timestamp: new Date().toISOString(),
|
|
});
|
|
return { content: [{ type: 'text' as const, text: `Task ${args.task_id} completed.` }] };
|
|
},
|
|
);
|
|
|
|
// --- Start ---
|
|
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|