runner/agent/src/mcp.ts
Markov 8b6ea6c462 Initial runner: agent container + host runner + IPC
- 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
2026-02-16 22:31:30 +01:00

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);