- AGENTS.md: design principles, enforcement hierarchy, deferred loading - agents/: brainstorm, build, orchestrator, research (auto-discovered by MCP server) - skills/: research methodology (auto-discovered by MCP server) - hooks/: pre-tool-use, post-tool-use (BFF block removed), session-start, stop, pre-compact, user-prompt-submit - frameworks/: opencode/plugin.ts (resolves hooks via import.meta.url — works as project-local or global plugin), github/hooks.json - mcp/index.ts: auto-discovers agents/*.md and skills/*.md from frontmatter (replaces hand-maintained registry); server renamed all-agents - docs/: agent-infrastructure.md (generalized), research docs (7 files), ai_architectures.md, llama-server-cuda-wsl2.md - install.sh: idempotent setup — Copilot global hooks, OpenCode global plugin + AGENTS.md + MCP entry, VS Code global MCP config
190 lines
6.2 KiB
JavaScript
190 lines
6.2 KiB
JavaScript
#!/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_<basename> (e.g. research.md → load_research)
|
|
*
|
|
* 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;
|
|
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 ? toolMatch[1].trim() : undefined,
|
|
body,
|
|
};
|
|
}
|
|
|
|
function stripLocalBlocks(body: string): string {
|
|
return body.replace(/<!-- @local -->[\s\S]*?<!-- @endlocal -->\n?/g, "");
|
|
}
|
|
|
|
function stripCloudBlocks(body: string): string {
|
|
return body.replace(/<!-- @cloud -->[\s\S]*?<!-- @endcloud -->\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<string, string | undefined>) => {
|
|
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,
|
|
},
|
|
],
|
|
}));
|
|
}
|
|
|
|
// ── 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);
|
|
}
|