poml-mcp
Version:
MCP server that enhances user prompts using POML-style structure
1,141 lines (1,070 loc) • 60.6 kB
JavaScript
#!/usr/bin/env node
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import fg from "fast-glob";
import { buildPoml, DEFAULT_OUTPUT_FORMAT, DEFAULT_STYLE_BASICS, DEFAULT_CONSTRAINTS_BASICS } from "./src/conventions.mjs";
import YAML from "yaml";
const server = new McpServer({ name: "POML", version: "0.1.0" });
const ENABLE_EXAMPLES = process.env.POML_ENABLE_EXAMPLES === "1" || process.env.POML_ENABLE_EXAMPLES === "true";
// Optional debug logging
function dbg(...args) {
if (process.env.DEBUG) {
try { console.log("[MCP]", ...args); } catch {}
}
}
// Heuristic: find project root by walking up for .git, .windsurf, or package.json
async function findProjectRoot(startDir) {
let dir = startDir;
for (let i = 0; i < 12; i++) {
try { await fs.access(path.join(dir, ".git")); return dir; } catch {}
try { await fs.access(path.join(dir, ".windsurf")); return dir; } catch {}
try { await fs.access(path.join(dir, "package.json")); return dir; } catch {}
const parent = path.dirname(dir);
if (parent === dir) break;
dir = parent;
}
return startDir;
}
// Optional: load user config (YAML) from project root
let _loadedConfig = undefined;
async function loadConfig(rootDir) {
if (_loadedConfig !== undefined) return _loadedConfig;
const candidates = [
process.env.POML_MCP_CONFIG,
path.join(rootDir, "poml-mcp.config.yml"),
path.join(rootDir, "poml-mcp.config.yaml"),
].filter(Boolean);
for (const p of candidates) {
try {
const text = await fs.readFile(p, "utf8");
_loadedConfig = YAML.parse(text) || null;
return _loadedConfig;
} catch {}
}
_loadedConfig = null;
return null;
}
function deepMerge(base, override) {
if (Array.isArray(base) && Array.isArray(override)) return [...base, ...override];
if (base && typeof base === "object" && override && typeof override === "object") {
const out = { ...base };
for (const k of Object.keys(override)) out[k] = deepMerge(base[k], override[k]);
return out;
}
return override === undefined ? base : override;
}
// Compute effective enhance args from config (profiles + triggers)
function applyEnhanceConfig(userArgs = {}, cfg) {
if (!cfg) return userArgs;
let eff = { ...userArgs };
const profiles = cfg.profiles || {};
const def = profiles.default?.enhance || {};
// defaults first
eff = deepMerge(def, eff);
// triggers
const tgs = cfg.triggers || [];
for (const t of tgs) {
try {
const ur = (userArgs.user_request || "").toString().toLowerCase();
let matches = false;
const cond = t.match || {};
if (cond.includesAny && Array.isArray(cond.includesAny)) {
if (cond.includesAny.some((s) => ur.includes((s || "").toLowerCase()))) matches = true;
}
if (!matches && cond.regex) {
try { if (new RegExp(cond.regex, "i").test(ur)) matches = true; } catch {}
}
if (!matches) continue;
// apply profile then override
if (t.profile && profiles[t.profile]?.enhance) eff = deepMerge(profiles[t.profile].enhance, eff);
if (t.enhance) eff = deepMerge(t.enhance, eff);
} catch {}
}
return eff;
}
// Optional POML integration: try to dynamically import a built JS entry.
let _pomlReadCached;
async function getPomlRead() {
if (_pomlReadCached !== undefined) return _pomlReadCached;
const candidates = [
"../packages/poml/index.js",
"../packages/poml/dist/index.js",
// As a last resort, attempt TS if a loader is present
"../packages/poml/index.ts",
];
for (const rel of candidates) {
try {
const url = new URL(rel, import.meta.url);
const mod = await import(url.href);
if (mod && typeof mod.read === "function") {
_pomlReadCached = mod.read;
return _pomlReadCached;
}
} catch {
// continue
}
}
_pomlReadCached = null;
return null;
}
// buildPoml moved to src/conventions.mjs
// Heuristic enhancer: structure the user's request into a clearer, actionable brief and POML template
function enhance(args = {}) {
const {
user_request,
audience,
style,
target_language,
domain,
output_format,
include = [],
constraints = [],
examples = [],
} = args;
// Task line: concise imperative goal
const task = `Create a high-quality response for the following request: ${user_request ?? "Test request"}`;
// Output format defaults if not provided
const outputFormatItems = output_format?.length ? output_format.split("\n").filter(Boolean) : DEFAULT_OUTPUT_FORMAT;
// Style items
const styleItems = [];
if (style) styleItems.push(`Tone: ${style}`);
if (audience) styleItems.push(`Audience: ${audience}`);
if (domain) styleItems.push(`Domain: ${domain}`);
styleItems.push(...DEFAULT_STYLE_BASICS);
// Tool: auto_configure – detect project type and generate config/workflows
server.registerTool(
"auto_configure",
{
title: "Auto-configure MCP for this repo",
description: "Detect project type (Node/Python/Go/Java/…); plan or apply initial YAML config and Windsurf workflows.",
inputSchema: {
mode: z.enum(["detect", "plan", "apply"]).optional(),
root: z.string().optional(),
preferLanguage: z.string().optional(),
projectKind: z.string().optional(),
},
outputSchema: {
detection: z.any().optional(),
poml: z.string().optional(),
written: z.array(z.string()).optional(),
},
},
async (args) => {
const mode = args.mode || "detect";
const cwdRoot = process.cwd();
const root = args.root ? path.resolve(args.root) : await findProjectRoot(cwdRoot);
async function exists(p) { try { await fs.access(p); return true; } catch { return false; } }
async function detectKind() {
const signals = {};
signals.node = await exists(path.join(root, "package.json"));
signals.python = (await exists(path.join(root, "pyproject.toml"))) || (await exists(path.join(root, "requirements.txt")));
signals.go = await exists(path.join(root, "go.mod"));
signals.rust = await exists(path.join(root, "Cargo.toml"));
signals.java = (await exists(path.join(root, "pom.xml"))) || (await exists(path.join(root, "build.gradle")));
signals.dotnet = (await fg(["**/*.csproj", "**/*.sln"], { cwd: root, ignore: ["**/node_modules/**", "**/.git/**"], dot: true })).length > 0;
let kind = "generic";
if (signals.node) kind = "node";
else if (signals.python) kind = "python";
else if (signals.go) kind = "go";
else if (signals.rust) kind = "rust";
else if (signals.java) kind = "java";
else if (signals.dotnet) kind = "dotnet";
return { kind, signals };
}
function generateYaml(kind, preferLanguage) {
const lang = preferLanguage || (kind === "node" ? "ES/EN" : "ES");
const base = {
profiles: {
default: {
enhance: {
audience: kind === "node" ? "Equipo de Ingeniería" : "Equipo Técnico",
style: "Profesional y conciso",
constraints: [
"No incluyas datos sensibles",
"Cita evidencia cuando uses números",
],
},
},
error: {
enhance: {
audience: "On-call / SRE",
include: [
"Contexto del incidente",
"Pasos para reproducir",
"Logs relevantes y stack traces",
],
constraints: [
"Evita suposiciones; cita evidencia verificable",
],
output_format: [
"Resumen",
"Evidencia",
"Impacto",
"Siguientes pasos",
"Dueño",
].join("\n"),
},
},
},
triggers: [
{ match: { includesAny: ["error", "exception", "stack trace", "traceback", "parábola error", "palabra error"] }, profile: "error", enhance: { style: "Técnico y accionable" } },
{ match: { regex: "(?i)incidente|falla|bug|regression|fallo" }, profile: "error" },
],
};
return YAML.stringify(base);
}
function generateOnboarding(kind) {
return `---\ndescription: Onboarding y auto-configuración MCP\n---\n\n# Onboarding MCP para ${kind}\n\n- Title: Setup MCP automático\n- Goal: Detectar el tipo de proyecto, generar configuración YAML y flujos base.\n\n## Pasos\n1. Detectar tipo de proyecto\n - Tool: auto_configure\n - Args: { "mode": "detect" }\n2. Plan de configuración\n - Tool: auto_configure\n - Args: { "mode": "plan" }\n - Revisa el POML de setup y ajusta si es necesario.\n3. Aplicar configuración\n - Tool: auto_configure\n - Args: { "mode": "apply" }\n - Se crearán: poml-mcp.config.yml y este workflow (si no existe).\n4. Descubrimiento de incidencias\n - Tool: incident_discovery (discover/plan)\n5. Descubrir y traducir docs/config a POML\n - Tool: poml_translate (discover/plan/apply)\n`;
}
const detection = args.projectKind ? { kind: args.projectKind, signals: { override: true } } : await detectKind();
if (mode === "detect") {
return { content: [{ type: "text", text: `Detected kind: ${detection.kind}` }], structuredContent: { detection } };
}
if (mode === "plan") {
const task = `Configurar automáticamente el MCP para un proyecto de tipo ${detection.kind}.`;
const outputFormatItems = DEFAULT_OUTPUT_FORMAT;
const styleItems = [ ...DEFAULT_STYLE_BASICS, "Orientado a onboarding" ];
const includeItems = [
"Crear poml-mcp.config.yml con perfiles y triggers",
"Crear workflow .windsurf/workflows/onboarding-setup.md",
"Sugerir ejecutar incident_discovery y poml_translate",
];
const constraintItems = [ ...DEFAULT_CONSTRAINTS_BASICS ];
const poml = buildPoml({ task, outputFormatItems, styleItems, includeItems, constraintItems });
return { content: [{ type: "text", text: poml }], structuredContent: { poml, detection } };
}
// apply
const written = [];
const yamlText = generateYaml(detection.kind, args.preferLanguage);
const yamlPath = path.join(root, "poml-mcp.config.yml");
try { await fs.writeFile(yamlPath, yamlText, { flag: "wx" }); written.push(yamlPath); } catch {
// if exists, overwrite only if empty
try {
const cur = await fs.readFile(yamlPath, "utf8");
if (!cur || cur.trim().length === 0) { await fs.writeFile(yamlPath, yamlText, { flag: "w" }); written.push(yamlPath); }
} catch {}
}
const wfDir = path.join(root, ".windsurf", "workflows");
try { await fs.mkdir(wfDir, { recursive: true }); } catch {}
const wfPath = path.join(wfDir, "onboarding-setup.md");
const wfText = generateOnboarding(detection.kind);
try { await fs.writeFile(wfPath, wfText, { flag: "wx" }); written.push(wfPath); } catch {}
return { content: [{ type: "text", text: `Wrote ${written.length} files` }], structuredContent: { detection, written } };
}
);
// Include/constraints from args
const includeItems = [...include];
const constraintItems = [...DEFAULT_CONSTRAINTS_BASICS, ...constraints];
// Build POML
const poml = buildPoml({
task,
outputFormatItems,
styleItems,
includeItems,
constraintItems,
});
// Enhanced prompt (plain text) to guide an LLM
const headerLang = target_language ? ` (${target_language})` : "";
const enhanced = [
`PROMPT ENHANCED${headerLang}`,
"",
"Goal:",
task,
"",
audience ? `Audience: ${audience}` : undefined,
domain ? `Domain: ${domain}` : undefined,
style ? `Tone/Style: ${style}` : undefined,
"",
"Output Format:",
...outputFormatItems.map((i, idx) => `${idx + 1}. ${i}`),
"",
includeItems.length ? "Must Include:" : undefined,
...(includeItems.length ? includeItems.map((i) => `- ${i}`) : []),
"",
"Constraints:",
...constraintItems.map((i) => `- ${i}`),
"",
examples.length ? "Examples (hints):" : undefined,
...(examples.length ? examples.map((e, idx) => `Example ${idx + 1}: ${e}`) : []),
"",
"Quality Checklist:",
"- Is the response aligned with the goal and audience?",
"- Are steps concrete and verifiable?",
"- Are assumptions and limitations stated?",
"- Is formatting scannable (headings/lists/tables)?",
].filter(Boolean).join("\n");
return { enhanced, poml };
}
// Tool: enhance_prompt -> returns improved plain-text prompt and a POML template
server.registerTool(
"enhance_prompt",
{
title: "Enhance Prompt",
description: "Analyze a user request and produce an enhanced prompt and a POML template.",
inputSchema: {
user_request: z.string().optional(),
audience: z.string().optional(),
style: z.string().optional(),
target_language: z.string().optional(),
domain: z.string().optional(),
output_format: z.string().optional(), // newline-separated items
include: z.array(z.string()).optional(),
constraints: z.array(z.string()).optional(),
examples: z.array(z.string()).optional(),
},
outputSchema: {
enhanced: z.string(),
poml: z.string(),
rendered: z.string().optional(),
messages: z.array(z.any()).optional(),
},
},
async (args) => {
// Apply YAML configuration (profiles + triggers)
const cwdRoot = process.cwd();
const root = args.root ? path.resolve(args.root) : await findProjectRoot(cwdRoot);
const cfg = await loadConfig(root);
const effective = applyEnhanceConfig(args, cfg);
const { enhanced, poml } = enhance(effective);
let rendered;
const pomlRead = await getPomlRead();
if (pomlRead) {
try {
rendered = await pomlRead(poml);
} catch {
rendered = undefined;
}
}
return {
content: [
{ type: "text", text: enhanced },
{ type: "text", text: "\n---\nPOML Template:\n" },
{ type: "text", text: poml },
],
structuredContent: { enhanced, poml, rendered },
};
}
);
// Tool: incident_discovery – discover or plan workflows to find incidents/errors quickly
server.registerTool(
"incident_discovery",
{
title: "Discover and plan incident triage",
description: "Search repository for error signals and generate a POML plan to triage incidents. Modes: discover | plan | apply (alias of plan).",
inputSchema: {
mode: z.enum(["discover", "plan", "apply"]).optional(),
root: z.string().optional(),
query: z.string().optional(),
includeGlobs: z.array(z.string()).optional(),
excludeGlobs: z.array(z.string()).optional(),
errorTerms: z.array(z.string()).optional(),
maxFiles: z.number().int().positive().optional(),
},
outputSchema: {
findings: z.array(z.any()).optional(),
poml: z.string().optional(),
},
},
async (args) => {
const mode = args.mode || "discover";
const cwdRoot = process.cwd();
const root = args.root ? path.resolve(args.root) : await findProjectRoot(cwdRoot);
const includeGlobs = args.includeGlobs ?? [
"**/*.{js,ts,tsx,jsx,py,go,java,cs,rb,rs,md,log,json,yml,yaml}",
];
const excludeGlobs = args.excludeGlobs ?? [
"**/node_modules/**",
"**/.git/**",
"**/dist/**",
"**/build/**",
"**/.next/**",
];
const terms = (args.errorTerms && args.errorTerms.length ? args.errorTerms : [
"error", "exception", "fail", "failed", "panic", "stack trace", "traceback",
"TODO", "FIXME", "BUG", "broken", "incident",
]).map((s) => s.toLowerCase());
const q = (args.query || "").toLowerCase();
if (mode === "plan" || mode === "apply") {
const outputFormatItems = DEFAULT_OUTPUT_FORMAT;
const styleItems = [
...DEFAULT_STYLE_BASICS,
"Be precise and actionable",
];
const includeItems = [
"List prioritized incidents with paths and signals",
"Propose next steps and owners",
"Link to related commits/issues if present",
];
const constraintItems = [
...DEFAULT_CONSTRAINTS_BASICS,
"Avoid false positives; cite evidence",
];
const task = q
? `Discover and triage incidents related to: ${q}`
: "Discover and triage likely incidents across the repository";
const poml = buildPoml({ task, outputFormatItems, styleItems, includeItems, constraintItems });
return {
content: [
{ type: "text", text: "Generated POML plan for incident discovery" },
{ type: "text", text: "\n---\nPOML Template:\n" },
{ type: "text", text: poml },
],
structuredContent: { poml },
};
}
// discover
const files = await fg(includeGlobs, { cwd: root, ignore: excludeGlobs, dot: true, absolute: true });
const maxFiles = Math.max(1, Math.min(args.maxFiles ?? 200, 2000));
const findings = [];
for (const file of files.slice(0, maxFiles)) {
try {
const stat = await fs.stat(file);
if (stat.size > 1024 * 512) continue; // skip >512KB for speed
const text = (await fs.readFile(file, "utf8")).toString();
const lower = text.toLowerCase();
let hitCount = 0;
for (const t of terms) {
if (lower.includes(t)) hitCount++;
}
if (q && !lower.includes(q)) continue;
if (hitCount > 0) {
findings.push({ file, hits: hitCount });
}
} catch {}
}
findings.sort((a, b) => b.hits - a.hits);
return {
content: [{ type: "text", text: `Found ${findings.length} files with incident signals` }],
structuredContent: { findings },
};
}
);
// Tool: adk_scaffold_from_poml – generate a minimal ADK agent project skeleton from a POML brief or inline content
server.registerTool(
"adk_scaffold_from_poml",
{
title: "Scaffold ADK agent from POML",
description: "Create a minimal Google ADK (Gemini) agent project skeleton. Inputs: a POML brief path or inline content. Writes files under outputDir.",
inputSchema: {
pomlPath: z.string().optional(),
pomlContent: z.string().optional(),
outputDir: z.string().optional(), // default: adk-agent
agentName: z.string().optional(), // default: agent
language: z.enum(["python"]).optional(), // currently only python supported
toolset: z.enum(["none", "github"]).optional(), // include example github tools
force: z.boolean().optional(), // overwrite existing files
},
outputSchema: {
wrote: z.array(z.string()).optional(),
warnings: z.array(z.string()).optional(),
notes: z.array(z.string()).optional(),
},
},
async (args) => {
const cwdRoot = process.cwd();
const root = await findProjectRoot(cwdRoot);
const language = args.language || "python";
const agentName = (args.agentName || "agent").replace(/[^a-zA-Z0-9_\-]+/g, "_");
const outDir = path.resolve(root, args.outputDir || "adk-agent");
const force = !!args.force;
const warnings = [];
const notes = [];
let brief = args.pomlContent || null;
if (!brief && args.pomlPath) {
try { brief = await fs.readFile(path.resolve(root, args.pomlPath), "utf8"); } catch { warnings.push(`No se pudo leer POML: ${args.pomlPath}`); }
}
if (!brief) warnings.push("No POML provided; se generará un README con placeholders.");
await fs.mkdir(outDir, { recursive: true });
async function safeWrite(p, content) {
try {
if (!force) {
await fs.writeFile(p, content, { flag: "wx" });
} else {
await fs.writeFile(p, content, { flag: "w" });
}
return true;
} catch {
return false;
}
}
const wrote = [];
if (language === "python") {
const req = ["google-adk"]; // core
if ((args.toolset || "none") === "github") req.push("PyGithub");
const requirementsTxt = req.join("\n") + "\n";
const readme = `# ${agentName} (ADK + Gemini)\n\n` +
`This is a minimal ADK agent scaffold generated by POML (poml-mcp).\n\n` +
`## Recommended steps\n` +
`1. Download llms-full.txt from the ADK repo and place it at the project root.\n` +
`2. Use Gemini CLI with @llms-full.txt to ideate and convert the plan to code.\n` +
`3. Update the \`instruction\` in main.py with your enhanced prompt or distilled POML.\n\n` +
`## POML (brief)\n` +
`\n\n` + (brief ? "```poml\n" + brief + "\n```\n" : "(add your .poml here)") + "\n";
const mainPy = `import argparse\nimport asyncio\nfrom google.adk.agents import Agent\nfrom google.adk.runners import Runner\nfrom google.adk.sessions import InMemorySessionService\n\ntry:\n from tools import TOOL_FUNCS\nexcept Exception:\n TOOL_FUNCS = []\n\nlabeling_agent = Agent(\n name="${agentName}",\n model="gemini-1.5-flash",\n instruction="""\nYou are an AI agent following a clear operations brief.\nReplace this instruction with your enhanced prompt or distilled POML.\nProvide explicit steps, constraints, and output expectations.\n""",\n tools=TOOL_FUNCS,\n)\n\nasync def main():\n parser = argparse.ArgumentParser()\n parser.add_argument("--arg", help="example arg", default="")\n args = parser.parse_args()\n\n session_service = InMemorySessionService()\n runner = Runner(session_service=session_service)\n result = await runner.run(labeling_agent, input=args.arg)\n print(result)\n\nif __name__ == "__main__":\n asyncio.run(main())\n`;
const toolsPyNone = `# Optional tools placeholder for ADK agent\nTOOL_FUNCS = []\n`;
const toolsPyGithub = `from github import Github\nimport os\n\nGITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "")\n_g = Github(GITHUB_TOKEN) if GITHUB_TOKEN else None\n\n# Tools for GitHub issue labeling example\n\ndef get_issue(repo_name: str, issue_number: int) -> str:\n if not _g: return "GITHUB_TOKEN not set"\n repo = _g.get_repo(repo_name)\n issue = repo.get_issue(number=issue_number)\n return f"Title: {issue.title}\\nBody: {issue.body}"\n\n\ndef get_available_labels(repo_name: str) -> list[str]:\n if not _g: return []\n repo = _g.get_repo(repo_name)\n return [label.name for label in repo.get_labels()]\n\n\ndef apply_label(repo_name: str, issue_number: int, label: str) -> str:\n if not _g: return "GITHUB_TOKEN not set"\n repo = _g.get_repo(repo_name)\n issue = repo.get_issue(number=issue_number)\n issue.add_to_labels(label)\n return f"Successfully applied label '{label}' to issue #{issue_number}."\n\nTOOL_FUNCS = [get_issue, get_available_labels, apply_label]\n`;
if (await safeWrite(path.join(outDir, "requirements.txt"), requirementsTxt)) wrote.push(path.join(outDir, "requirements.txt"));
if (await safeWrite(path.join(outDir, "README.md"), readme)) wrote.push(path.join(outDir, "README.md"));
if (await safeWrite(path.join(outDir, "main.py"), mainPy)) wrote.push(path.join(outDir, "main.py"));
const toolsBody = (args.toolset || "none") === "github" ? toolsPyGithub : toolsPyNone;
if (await safeWrite(path.join(outDir, "tools.py"), toolsBody)) wrote.push(path.join(outDir, "tools.py"));
// simple .gitignore
const gi = "__pycache__/\n.venv/\n.env\n";
if (await safeWrite(path.join(outDir, ".gitignore"), gi)) wrote.push(path.join(outDir, ".gitignore"));
}
dbg("adk_scaffold_from_poml wrote", wrote);
return { content: [{ type: "text", text: `Wrote ${wrote.length} files under ${outDir}` }], structuredContent: { wrote, warnings, notes } };
}
);
// Tool: windsurf_to_poml -> convenience alias focused on Windsurf workflows
server.registerTool(
"windsurf_to_poml",
{
title: "Translate Windsurf workflows to POML",
description: "Discover and convert .windsurf/workflows/*.md into agent-executable POML. Alias of poml_translate with interpretWindsurf=true.",
inputSchema: {
mode: z.enum(["discover", "plan", "apply"]).optional(),
root: z.string().optional(),
selections: z.array(z.string()).optional(),
outputDir: z.string().optional(),
stepwise: z.boolean().optional(),
aggregateIndex: z.boolean().optional(),
indexName: z.string().optional(),
},
outputSchema: {
discovered: z.any().optional(),
proposals: z.any().optional(),
wrote: z.array(z.string()).optional(),
notes: z.array(z.string()).optional(),
warnings: z.array(z.string()).optional(),
},
},
async (args) => {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const cwdRoot = process.cwd();
const root = args.root ? path.resolve(args.root) : await findProjectRoot(cwdRoot);
// default globs restricted to Windsurf workflows
const includeGlobs = [".windsurf/workflows/**/*.md"];
const excludeGlobs = [
"**/node_modules/**",
"**/.git/**",
"**/dist/**",
"**/build/**",
"**/.next/**",
"**/.cache/**",
];
const mode = args.mode ?? "discover";
const selections = (args.selections && args.selections.length
? args.selections
: (await fg(includeGlobs.map((g)=>g), { cwd: root, dot: true, onlyFiles: true }))
);
const forwarded = {
mode,
root,
includeGlobs,
excludeGlobs,
selections,
outputDir: args.outputDir ?? "poml-output",
stepwise: !!args.stepwise,
interpretWindsurf: true,
aggregateIndex: !!args.aggregateIndex,
indexName: args.indexName,
};
dbg("windsurf_to_poml forward -> core", forwarded);
// Reuse the shared core implementation to avoid protocol-level recursion
return await pomlTranslateCore(forwarded);
}
);
// Tool: poml_translate -> discover .md/config files, plan conversions to .poml, and optionally write outputs
// Core implementation shared by poml_translate and windsurf_to_poml
async function pomlTranslateCore(args) {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const cwdRoot = process.cwd();
const root = args.root ? path.resolve(args.root) : await findProjectRoot(cwdRoot);
const includeGlobs = args.includeGlobs ?? [
"**/*.md",
"**/*.markdown",
"**/*.mdx",
"**/*.yml",
"**/*.yaml",
"**/*.toml",
"**/*.json",
];
const excludeGlobs = args.excludeGlobs ?? [
"**/node_modules/**",
"**/.git/**",
"**/dist/**",
"**/build/**",
"**/.next/**",
"**/.cache/**",
];
const isMarkdown = (p) => /\.(md|markdown|mdx)$/i.test(p);
const mode = args.mode ?? "discover";
dbg("poml_translate", { root, mode, includeGlobs, excludeGlobs });
const vendorOf = (p) => {
const s = p.toLowerCase();
if (s.includes("windsurf")) return "windsurf";
if (s.includes("cursor")) return "cursor";
if (s.includes("claude") || s.includes("anthropic")) return "claude";
if (s.includes("openai")) return "openai";
if (s.includes("github/workflows") || s.includes(".github/workflows")) return "github";
if (s.includes("modelcontextprotocol") || s.includes("mcp")) return "mcp";
return "generic";
};
const rel = (p) => path.relative(root, p).split(path.sep).join("/");
// Discover
const patterns = includeGlobs.map((g) => g.startsWith("!") ? g : g);
excludeGlobs.forEach((g) => patterns.push("!" + g.replace(/^!/, "")));
const entries = await fg(patterns, { cwd: root, dot: true, onlyFiles: true });
const markdown = entries.filter(isMarkdown);
const configs = entries.filter((p) => !isMarkdown(p));
dbg("discover counts", { markdown: markdown.length, configs: configs.length });
if (mode === "discover") {
return {
content: [{ type: "text", text: `Discovered ${markdown.length} Markdown and ${configs.length} config files under ${root}` }],
structuredContent: {
discovered: {
root,
markdown: markdown.map(rel),
configs: configs.map(rel),
},
},
};
}
// Build proposals for selections
const selections = (args.selections && args.selections.length
? args.selections
: markdown.map(rel)
).map((p) => path.resolve(root, p));
const outDir = args.outputDir ? path.resolve(root, args.outputDir) : path.resolve(root, "poml-output");
dbg("apply selections", selections.map(rel));
function mdToPoml(relPath) {
const title = path.basename(relPath);
const isWindsurf = relPath.includes(".windsurf/workflows/");
if (isWindsurf && (args.interpretWindsurf ?? true)) {
return (
`<poml>
<task className="instruction">Convert the following Windsurf Workflow into an agent-executable POML plan. Extract description, explicit ordered steps, automation flags (// turbo, // turbo-all), commands to run, and expected artifacts. Resolve implicit context into concrete instructions.</task>
<output-format className="instruction">
<list listStyle="decimal">
<item>Title and Short Summary</item>
<item>Inputs and Preconditions</item>
<item>Ordered Steps (explicit, verifiable)</item>
<item>Automation Flags (turbo/turbo-all) and Effects</item>
<item>Commands To Run (where applicable)</item>
<item>Artifacts/Deliverables</item>
<item>Constraints and Safety Notes</item>
<item>Completion Criteria</item>
<item>Next Actions / Follow-ups</item>
</list>
</output-format>
<cp className="instruction" caption="Style">
<list>
<item>Tone: Clear, operational, unambiguous</item>
<item>Use short sentences and bullet lists</item>
<item>Make hidden assumptions explicit</item>
<item>Prefer reproducible commands</item>
</list>
</cp>
<stepwise-instructions>
<list listStyle="decimal">
<item>Read frontmatter (YAML) and extract description</item>
<item>Parse steps and annotate turbo/turbo-all markers</item>
<item>Identify shell commands and parameters</item>
<item>Define explicit outputs and completion criteria</item>
<item>Render the final POML following the output format</item>
</list>
</stepwise-instructions>
<cp caption="WORKFLOW (${title})">
<code inline="false"><document src="${relPath}" parser="txt" /></code>
</cp>
</poml>`
);
}
return (
`<poml>
<task className="instruction">Transform the following Markdown document into a structured prompt that agents can execute reliably. Extract objectives, constraints, and expected outputs. Keep instructions concise and explicit.</task>
<output-format className="instruction">
<list listStyle="decimal">
<item>Objectives (bullet list)</item>
<item>Deliverables (what to produce)</item>
<item>Constraints (rules, limits, compliance)</item>
<item>Final Output (format and sections)</item>
</list>
</output-format>
<cp className="instruction" caption="Style">
<list>
<item>Tone: Clear and operational</item>
<item>Format: Use lists and short sentences</item>
<item>Be explicit about required outputs</item>
<item>Avoid ambiguity and unnecessary fluff</item>
</list>
</cp>
${args.stepwise ? `<stepwise-instructions>
<list listStyle="decimal">
<item>Read the source and extract key objectives</item>
<item>List constraints and requirements</item>
<item>Define expected outputs with structure</item>
<item>Draft the final prompt using the above</item>
</list>
</stepwise-instructions>
` : ``}
<cp caption="SOURCE (${title})">
<code inline="false"><document src="${relPath}" parser="txt" /></code>
</cp>
</poml>`
);
}
function configToPoml(relPath) {
const title = path.basename(relPath);
return (
`<poml>
<task className="instruction">Read the following configuration and generate a structured POML instruction set capturing its intent as agent-executable rules and parameters.</task>
<cp className="instruction" caption="Include">
<list>
<item>Purpose and scope</item>
<item>Key parameters and defaults</item>
<item>Operational constraints and safety notes</item>
<item>Expected behaviors and outputs</item>
</list>
</cp>
<cp caption="CONFIG (${title})">
<code inline="false"><document src="${relPath}" parser="txt" /></code>
</cp>
</poml>`
);
}
const proposals = [];
for (const absPath of selections) {
try {
const relPath = rel(absPath);
const isMd = isMarkdown(absPath);
const pomlRel = relPath.replace(/\.(md|markdown|mdx|ya?ml|json|toml)$/i, ".poml");
const pomlAbs = path.join(outDir, pomlRel);
const vendor = vendorOf(relPath);
const pomlContent = isMd ? mdToPoml(relPath) : configToPoml(relPath);
proposals.push({ source: relPath, pomlPath: rel(pomlAbs), preview: pomlContent.slice(0, 400), vendor });
} catch {
// skip unreadable
}
}
if (mode === "plan") {
return {
content: [{ type: "text", text: `Proposed ${proposals.length} .poml conversions (dry-run). Output dir: ${outDir}` }],
structuredContent: { proposals },
};
}
// apply: write files (non-destructive, mirrors into outDir)
const wrote = [];
await Promise.all(proposals.map(async (p) => {
const pomlAbs = path.resolve(root, p.pomlPath);
await fs.mkdir(path.dirname(pomlAbs), { recursive: true });
// Recreate content to ensure latest template usage
const content = isMarkdown(p.source) ? mdToPoml(p.source) : configToPoml(p.source);
await fs.writeFile(pomlAbs, content, "utf8");
wrote.push(rel(pomlAbs));
}));
dbg("wrote", wrote);
// Optionally create aggregator index with <let src="..." name="..."/>
if (args.aggregateIndex && wrote.length > 0) {
const indexName = (args.indexName && args.indexName.trim()) || "index.poml";
const outAbs = outDir; // already absolute
const toPosix = (p) => p.split(path.sep).join("/");
const relsFromOut = wrote.map((w) => {
const absW = path.resolve(root, w);
const relFromOut = toPosix(path.relative(outAbs, absW));
return relFromOut.startsWith(".") ? relFromOut : relFromOut;
});
const used = new Set();
const toName = (p) => {
const base = path.basename(p, path.extname(p)).replace(/[^a-zA-Z0-9_]+/g, "_");
let name = base || "doc";
let i = 1;
while (used.has(name)) { name = `${base}_${i++}`; }
used.add(name);
return name;
};
const lets = relsFromOut.map((r) => ` <let src="${toPosix(r)}" name="${toName(r)}" />`).join("\n");
const indexContent = `<poml>\n${lets}\n</poml>\n`;
const indexAbs = path.join(outAbs, indexName);
await fs.writeFile(indexAbs, indexContent, "utf8");
wrote.push(rel(indexAbs));
}
return {
content: [{ type: "text", text: `Wrote ${wrote.length} .poml files to ${outDir}` }],
structuredContent: { wrote },
};
}
server.registerTool(
"poml_translate",
{
title: "Discover and translate docs/configs to POML",
description: "Discover Markdown and config files, plan POML (.poml) conversions, and optionally write outputs (non-destructive).",
inputSchema: {
mode: z.enum(["discover", "plan", "apply"]).optional(),
root: z.string().optional(),
includeGlobs: z.array(z.string()).optional(),
excludeGlobs: z.array(z.string()).optional(),
selections: z.array(z.string()).optional(),
outputDir: z.string().optional(),
stepwise: z.boolean().optional(),
interpretWindsurf: z.boolean().optional(),
aggregateIndex: z.boolean().optional(),
indexName: z.string().optional(),
},
outputSchema: {
discovered: z.any().optional(),
proposals: z.any().optional(),
wrote: z.array(z.string()).optional(),
notes: z.array(z.string()).optional(),
warnings: z.array(z.string()).optional(),
},
},
async (args) => {
return await pomlTranslateCore(args);
}
);
// Tool: generate_docs -> scaffold PLANNING.md, TASK.md, and AI IDE Global Rules
server.registerTool(
"generate_docs",
{
title: "Generate project docs (planning, tasks, rules)",
description: "Scaffold PLANNING.md, TASK.md, and AI IDE Global Rules based on repository scan. Non-destructive by default.",
inputSchema: {
mode: z.enum(["plan", "apply"]).optional(),
root: z.string().optional(),
outputDir: z.string().optional(),
planning: z.boolean().optional(),
task: z.boolean().optional(),
rules: z.boolean().optional(),
overwrite: z.boolean().optional(),
ideRules: z.array(z.enum(["windsurf", "gemini", "cursor", "cline", "roo"]))
.optional(),
projectName: z.string().optional(),
format: z.enum(["markdown", "poml", "both"]).optional(),
includePrpSkeleton: z.boolean().optional(),
},
outputSchema: {
proposals: z.any().optional(),
wrote: z.array(z.string()).optional(),
warnings: z.array(z.string()).optional(),
notes: z.array(z.string()).optional(),
},
},
async (args) => {
const cwdRoot = process.cwd();
const root = args.root ? path.resolve(args.root) : await findProjectRoot(cwdRoot);
const mode = args.mode ?? "plan";
const outDir = path.resolve(root, args.outputDir || "docs");
const wantPlanning = args.planning ?? true;
const wantTask = args.task ?? true;
const wantRules = args.rules ?? true;
const overwrite = !!args.overwrite;
const ideRules = Array.isArray(args.ideRules) && args.ideRules.length
? args.ideRules
: ["windsurf", "gemini", "cursor", "cline", "roo"];
const projName = args.projectName || path.basename(root);
const format = args.format ?? "markdown";
const includePrpSkeleton = args.includePrpSkeleton ?? false;
const rel = (p) => path.relative(root, p).split(path.sep).join("/");
const notes = [];
const warnings = [];
// quick scan for context
const entries = await fg([
"**/*.{md,markdown,mdx,yml,yaml,toml,json}",
"!**/node_modules/**",
"!**/.git/**",
], { cwd: root, dot: true, onlyFiles: true });
const mdCount = entries.filter((e) => /\.(md|markdown|mdx)$/i.test(e)).length;
const cfgCount = entries.length - mdCount;
notes.push(`Discovered ${mdCount} Markdown and ${cfgCount} config files`);
await fs.mkdir(outDir, { recursive: true });
const planningAbs = path.join(outDir, "PLANNING.md");
const taskAbs = path.join(outDir, "TASK.md");
const rulesAbs = path.join(outDir, "AI_RULES.md");
const proposals = [];
if (wantPlanning) proposals.push({ path: rel(planningAbs), kind: "planning" });
if (wantTask) proposals.push({ path: rel(taskAbs), kind: "task" });
if (wantRules) proposals.push({ path: rel(rulesAbs), kind: "rules", ideRules });
if (includePrpSkeleton) proposals.push({ path: rel(path.join(root, "PRPs/templates/prp_base.md")), kind: "template" });
if (mode === "plan") {
return {
content: [{ type: "text", text: `Would generate ${proposals.length} files under ${rel(outDir)}` }],
structuredContent: { proposals, notes },
};
}
const wrote = [];
async function writeFileIfNeeded(abs, content) {
if (!overwrite) {
try { await fs.access(abs); warnings.push(`Exists, skipped: ${rel(abs)}`); return; } catch {}
}
await fs.mkdir(path.dirname(abs), { recursive: true });
await fs.writeFile(abs, content, "utf8");
wrote.push(rel(abs));
}
if (wantPlanning) {
const planningMd = `# PLANNING (${projName})\n\n- Purpose: High-level vision, architecture, constraints, tech stack, tools.\n- Golden rule: “Use the structure and decisions outlined in PLANNING.md.”\n\n## Vision\n\n## Architecture\n\n## Constraints\n\n## Tech Stack\n\n## Tools & Integrations\n\n## Naming & Structure\n\n## Non-Goals\n\n`;
await writeFileIfNeeded(planningAbs, planningMd);
}
if (wantTask) {
const today = new Date().toISOString().slice(0, 10);
const taskMd = `# TASKS (${projName})\n_Last updated: ${today}_\n\n## Executive Summary & Current Status\n- Overall Status: []% – short summary (1–2 sentences)\n\n## Phase (e.g., “Payments Integration”)\n- Objective: ...\n\n| ID | Task | Priority | Status | Owner |\n|----|------|----------|--------|-------|\n| F1-01 | ... | HIGH | ⬜ Pending | Cascade |\n\nLegend: ⬜ Pending · ⚙️ In Progress · ✅ Done · ❌ Blocked\n\n## Completed Milestones\n- ...\n\n## Technical Debt\n| ID | Task | Priority | Status | Owner |\n|----|------|----------|--------|-------|\n| TD-01 | ... | MED | ⬜ Pending | Cascade |\n\n## Discovered During Development\n| ID | Task | Priority | Status | Owner |\n|----|------|----------|--------|-------|\n| AD-01 | ... | HIGH | ⚙️ In Progress | Cascade |\n`;
await writeFileIfNeeded(taskAbs, taskMd);
}
if (wantRules) {
const links = [
"- Gemini Rules: https://github.com/google-gemini/gemini-cli/blob/main/GEMINI.md",
"- Cursor Rules: https://docs.cursor.com/context/rules-for-ai",
"- Windsurf Rules: https://docs.codeium.com/windsurf/memories#windsurfrules",
"- Cline Rules: https://docs.cline.bot/improving-your-prompting-skills/prompting",
"- Roo Code Rules: Works like Cline",
].join("\n");
const rulesMd = `# Global Rules (AI IDEs)\n\n## Project Awareness & Context\n- Always read PLANNING.md at the start of a new conversation.\n- Check TASK.md before starting; update as you work.\n\n## Code Structure & Modularity\n- Avoid files > 500 LOC; refactor as needed.\n- Use clear, consistent imports and module boundaries.\n\n## Testing & Reliability\n- Add/update tests alongside code.\n- Keep tests under /tests mirroring app structure.\n\n## Task Completion\n- Mark completed tasks in TASK.md and add discovered TODOs.\n\n## Style & Conventions\n- Prefer typed, formatted, documented code appropriate to the stack.\n\n## Documentation & Explainability\n- Update README when features/deps/setup change.\n- Comment non-obvious logic; add a '# Reason:' note for complex decisions.\n\n## AI Behavior Rules\n- Never assume missing context; ask.\n- Do not hallucinate APIs; verify packages and paths.\n- Confirm file paths/modules exist before referencing.\n- Prefer POML-structured prompts via the POML MCP. When an agent cannot ingest POML, use the MCP's rendered output.\n- Use the \'enhance_prompt\' tool to transform free-text instructions into POML before execution.\n- Convert IDE workflows (e.g., .windsurf/workflows/*.md) to POML using \'windsurf_to_poml\' or \'poml_translate\' when automation is desired.\n\n${links}\n`;
await writeFileIfNeeded(rulesAbs, rulesMd);
}
if (includePrpSkeleton) {
const prpTemplateAbs = path.join(root, "PRPs", "templates", "prp_base.md");
const prpTemplateMd = `# PRP: <feature name>\n\n## Context\n- Linked docs: PLANNING.md, TASK.md, APIs\n- Goals & Non-Goals\n\n## Implementation Plan\n- Steps with validation gates\n\n## Validation Gates\n- Tests to pass (unit/e2e)\n- Lint/build checks\n\n## Risks & Mitigations\n- ...\n\n## Rollout\n- ...\n`;
await writeFileIfNeeded(prpTemplateAbs, prpTemplateMd);
}
return {
content: [{ type: "text", text: `Wrote ${wrote.length} docs to ${rel(outDir)}` }],
structuredContent: { wrote, warnings, notes },
};
}
);
// Tool: generate_prp -> build a Product Requirements Prompt from INITIAL.md and context
server.registerTool(
"generate_prp",
{
title: "Generate a Product Requirements Prompt (PRP)",
description: "Create a PRP from an INITIAL brief and repository context. Optionally emit .poml.",
inputSchema: {
initialPath: z.string().optional(),
outputDir: z.string().optional(), // defaults to PRPs
format: z.enum(["markdown", "poml", "both"]).optional(),
docsLinks: z.array(z.string()).optional(),
examples: z.array(z.string()).optional(),
projectName: z.string().optional(),
overwrite: z.boolean().optional(),
},
outputSchema: {
wrote: z.array(z.string()).optional(),
notes: z.array(z.string()).optional(),
warnings: z.array(z.string()).optional(),
proposals: z.any().optional(),
},
},
async (args) => {
const cwdRoot = process.cwd();
const root = await findProjectRoot(cwdRoot);
const outDir = path.resolve(root, args.outputDir || path.join("PRPs"));
const format = args.format ?? "markdown";
const projName = args.projectName || path.basename(root);
const initialPath = args.initialPath ? path.resolve(root, args.initialPath) : path.join(root, "docs", "INITIAL.md");
const overwrite = !!args.overwrite;
const notes = [];
const warnings = [];
const rel = (p) => path.relative(root, p).split(path.sep).join("/");
let initialText = "";
try { initialText = await fs.readFile(initialPath, "utf8"); }
catch { warnings.push(`INITIAL not found at ${rel(initialPath)}, generating PRP skeleton.`); }
const featureSlug = (initialText.match(/##\s*FEATURE:\s*([^\n]+)/i)?.[1] || `feature-${Date.now()}`)
.trim().toLowerCase().replace(/[^a-z0-9-]+/g, "-");
await fs.mkdir(outDir, { recursive: true });
const mdOut = path.join(outDir, `${featureSlug}.md`);
const pomlOut = path.join(outDir, `${featureSlug}.poml`);
const linksBlock = (args.docsLinks?.map((u) => `- ${u}`).join("\n") || "- PLANNING.md\n- TASK.md");
const examplesBlock = (args.examples?.map((e) => `- ${e}`).join("\n") || "- examples/ (patterns)");
const md = `# PRP: ${featureSlug} (${projName})\n\n## Context\n${linksBlock}\n\n## Initial Brief\n${initialText || "<Add details>"}\n\n## Implementation Plan\n- Step 1: ...\n- Step 2: ...\n\n## Validation Gates\n- Tests: npm test\n- Lint/Build checks\n\n## Examples to Follow\n${examplesBlock}\n\n## Risks & Mitigations\n- ...\n`;
const poml = `<poml>\n <cp caption="Context">\n <list>\n${(args.docsLinks||["PLANNING.md","TASK.md"]).map((u)=>` <item>${u}</item>`).join("\n")}\n </list>\n </cp>\n <cp caption="Implementation Plan">\n <list>\n <item>Step 1: ...</item>\n <item>Step 2: ...</item>\n </list>\n </cp>\n <cp caption="Validation Gates">\n <list>\n <item>Tests: npm test</item>\n <item>Lint/Build checks</item>\n </list>\n </cp>\n</poml>\n`;
const wrote = [];
async function writeIfNeeded(abs, content) {
if (!overwrite) { try { await fs.access(abs); warnings.push(`Exists, skipped: ${rel(abs)}`); return; } catch {} }
await fs.mkdir(path.dirname(abs), { recursive: true });
await fs.writeFile(abs, content, "utf8");
wrote.push(rel(abs));
}
if (format === "markdown" || format === "both") await writeIfNeeded(mdOut, md);
if (format === "poml" || format === "both") await writeIfNeeded(pomlOut, poml);
return {
content: [{ type: "text", text: `Generated PRP (${format}) under ${rel(outDir)}` }],
structuredContent: { wrote, notes, warnings },
};
}
);
// Tool: execute_prp -> parse a PRP and output an execution plan (non-destructive)
server.registerTool(
"execute_prp",
{
title: "Execute a PRP (plan)",
description: "Parses a PRP (.md or .poml) and outputs a stepwise execution plan. Non-destructive.",
inputSchema: {
prpPath: z.string(),
mode: z.enum(["plan", "apply"]).optional(),
root: z.string().optional(),
},
outputSchema: {
steps: z.array(z.string()).optional(),
warnings: z.array(z.string()).optional(),
notes: z.array(z.string()).optional(),
},
},
async (args) => {
const root = args.root ? path.resolve(args.root) : await findProjectRoot(process.cwd());
const abs = path.resolve(root, args.prpPath);
const warnings = [];
const notes = [];
let text = "";
try { text = await fs.readFile(abs, "utf8"); } catch (e) { return { content: [{ type: "text", text: `PRP not found: ${args.prpPath}` }], structuredContent: { warnings: [String(e)] } }; }
// naive extraction of steps from markdown bullets under "Implementation Plan"
const steps = [];
const planSection = text.split(/##\s*Implementation Plan/i)[1] || text;
for (const line of planSection.split("\n")) {
const m = line.match(/^\s*[-*]\s+(.*)$/);
if (m) steps.push(m[1].trim());
if (/^##\s+/.test(line)) break;
}
if (!steps.length) warnings.push("No steps detected; please structure PRP with 'Implementation Plan' bullets.");
if ((args.mode ?? "plan") === "apply") notes.push("Apply mode: guidance-only; implement steps manually or via IDE workflow.");
return {
content: [{ type: "text", text: `Parsed ${steps.length} steps from PRP` }],
structuredContent: { steps, warnings, notes },
};
}
);
// Tool: pm_sync -> sync tasks with external PM systems (initial: GitHub Issues)
server.registerTool(
"pm_sync",
{
title: "Project Management sync (GitHub)",
description: "Plan/apply sync between TASK.md and GitHub Issues.",
inputSchema: {
provider: z.enum(["github"]).optional(),
mode: z.enum(["plan", "apply"]).optional(),
root: z.string().optional(),
docsPath: z.string().optional(), // default docs/TASK.md
repo: z.string().optional(), // owner/repo
token: z.string().optional(), // if not set, use process.env.GITHUB_TOKEN
labels: z.array(z.string()).optional(),
dryRun: z.boolean().optional(),
},
outputSchema: {
proposals: z.any().optional(),
wrote: z.array(z.string()).optional(),
notes: z.array(z.string()).optional(),
warnings: z.array(z.string()).optional(),
},
},
async (args) => {
const provider = args.provider ?? "github";
const mode = args.mode ?? "plan";
const root = args.root ? path.resolve(args.root) : await findProjectRoot(process.cwd());
const docsPath = path.resolve(root, args.docsPath || path.join("docs", "TASK.md"));
const warnings = [];