#!/usr/bin/env node /** * all-agents MCP server — shared agent infrastructure over the Model Context Protocol. * * Prompts and tools are auto-discovered from sibling directories: * ../agents/*.md → slash-command prompts (requires description: frontmatter) * ../skills/*.md → model-controlled tools (requires description: frontmatter) * * Agent/skill bodies are read from disk at invocation time — editing any .md * file takes effect immediately without restarting the server. * * Frontmatter fields: * description (required) — routing description for the prompt/tool * toolName (skills only, optional) — override the derived tool name * default: load_ (e.g. research-methodology.md → load_research-methodology) * * Not handled here (stays bespoke): * hooks/ — MCP has no lifecycle intercept primitive * AGENTS.md — always-on bootstrap; model needs it before tools/list * * Run: node --experimental-strip-types .agents/mcp/index.ts * Config: ~/.vscode-server/data/User/mcp.json (Copilot), * ~/.config/opencode/opencode.json (OpenCode global) */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { readFileSync, readdirSync } from "node:fs"; import { basename, resolve } from "node:path"; import { z } from "zod"; const agentsDir = resolve(import.meta.dirname, "../agents"); const skillsDir = resolve(import.meta.dirname, "../skills"); interface ParsedFile { description: string; toolName?: string | undefined; body: string; } /** Parse YAML frontmatter and return description, optional toolName, and body. */ function parseFrontmatter(content: string): ParsedFile { const lines = content.split("\n"); if (lines[0] !== "---") return { description: "", body: content.trim() }; const end = lines.indexOf("---", 1); if (end === -1) return { description: "", body: content.trim() }; const frontmatter = lines.slice(1, end).join("\n"); const body = lines .slice(end + 1) .join("\n") .trim(); // Simple single-line or quoted-string extraction for description and toolName const descMatch = frontmatter.match( /^description:\s*['"]?([\s\S]*?)['"]?\s*$/m, ); const toolMatch = frontmatter.match(/^toolName:\s*['"]?([^'"]+)['"]?\s*$/m); // Handle multi-line description values (block scalar or wrapped string) let description = ""; if (descMatch) { // If the match includes a leading quote, strip matching quotes const raw = frontmatter.match(/^description:\s*(['"])([\s\S]*?)\1\s*$/m); description = raw ? raw[2]?.trim() ?? '' : descMatch[1]?.trim() ?? ''; } return { description, toolName: toolMatch?.[1]?.trim(), body, }; } function stripLocalBlocks(body: string): string { return body.replace(/[\s\S]*?\n?/g, ""); } function stripCloudBlocks(body: string): string { return body.replace(/[\s\S]*?\n?/g, ""); } /** * Returns 'local' when the MCP client identifies as `opencode` (local-model * harness), 'cloud' for any other client (Copilot / VS Code etc.). */ function getClientProfile(): "local" | "cloud" { const info = server.server.getClientVersion(); return info?.name === "opencode" ? "local" : "cloud"; } function applyClientProfile(body: string): string { return getClientProfile() === "local" ? stripCloudBlocks(body) : stripLocalBlocks(body); } const server = new McpServer({ name: "all-agents", version: "1.0.0" }); // ── Prompts (auto-discovered from ../agents/*.md) ───────────────────────────── const agentFiles = readdirSync(agentsDir).filter( (f) => f.endsWith(".md") && f !== "AGENTS.md", ); for (const file of agentFiles) { const name = basename(file, ".md"); const { description, body } = parseFrontmatter( readFileSync(resolve(agentsDir, file), "utf8"), ); if (!description) { process.stderr.write( `[all-agents] WARNING: ${file} has no description — skipping\n`, ); continue; } const argKey = name === "orchestrator" ? "goal" : name === "brainstorm" ? "topic" : "task"; const argDesc = name === "orchestrator" ? "The high-level goal to decompose" : name === "brainstorm" ? "The problem or decision to brainstorm" : "The specific task or question"; server.registerPrompt( name, { description, argsSchema: { [argKey]: z.string().optional().describe(argDesc) }, }, (args: Record) => { const input = args[argKey]; const agentBody = applyClientProfile( parseFrontmatter(readFileSync(resolve(agentsDir, file), "utf8")).body, ); return { messages: [ { role: "user" as const, content: { type: "text" as const, text: input ? `${agentBody}\n\n${input}` : agentBody, }, }, ], }; }, ); } // ── Tools (auto-discovered from ../skills/*.md) ─────────────────────────────── const skillFiles = readdirSync(skillsDir).filter((f) => f.endsWith(".md")); for (const file of skillFiles) { const name = basename(file, ".md"); const { description, toolName } = parseFrontmatter( readFileSync(resolve(skillsDir, file), "utf8"), ); if (!description) { process.stderr.write( `[all-agents] WARNING: ${file} has no description — skipping\n`, ); continue; } server.registerTool(toolName ?? `load_${name}`, { description }, () => ({ content: [ { type: "text" as const, text: parseFrontmatter(readFileSync(resolve(skillsDir, file), "utf8")) .body, }, ], })); } // ── Resources (no-op to satisfy resources/list) ────────────────────────────── server.registerResource( "noop", "noop://noop", { description: "No-op resource (satisfies resources/list)" }, () => ({ contents: [{ uri: "noop://noop", mimeType: "text/plain", text: "" }], }), ); // ── Connect ─────────────────────────────────────────────────────────────────── const transport = new StdioServerTransport(); try { await server.connect(transport); } catch (err) { process.stderr.write( `MCP connect failed: ${err instanceof Error ? err.message : String(err)}\n`, ); process.exit(1); }