UNPKG

poml-mcp

Version:

MCP server that enhances user prompts using POML-style structure

1,141 lines (1,070 loc) 60.6 kB
#!/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 = [];