import type { Resource } from "@modelcontextprotocol/sdk/types.js"; import { loadManifest } from "@mandujs/core"; import { getTransactionStatus } from "@mandujs/core/change"; import type { GeneratedMap } from "@mandujs/core/generator"; import { getWatcher } from "@mandujs/core/watcher"; import { getProjectPaths, readJsonFile } from "../utils/project.js"; import { getGuide, listGuides, getRecipe, listRecipes, listSkills, getSkill, listSkillRules, getSkillRule, } from "./skills/index.js"; import path from "path"; export const resourceDefinitions: Resource[] = [ { uri: "mandu://spec/manifest", name: "Routes Manifest", description: "Current routes.manifest.json content", mimeType: "application/json", }, { uri: "mandu://generated/map", name: "Generated Map", description: "Map of generated files to their source routes", mimeType: "application/json", }, { uri: "mandu://transaction/active", name: "Active Transaction", description: "Current active transaction information", mimeType: "application/json", }, { uri: "mandu://slots/{routeId}", name: "Route Slot", description: "Slot file content for a specific route", mimeType: "text/typescript", }, { uri: "mandu://watch/warnings", name: "Watch Warnings", description: "Recent file watcher warnings (architecture rule violations)", mimeType: "application/json", }, { uri: "mandu://watch/status", name: "Watch Status", description: "File watcher status (active/inactive, uptime, rule count)", mimeType: "application/json", }, // Skills - Guides { uri: "mandu://skills/guides", name: "Skills Guide List", description: "Available guide documents for learning Mandu", mimeType: "application/json", }, { uri: "mandu://skills/guides/slot", name: "Slot Guide", description: "Guide for writing business logic with Mandu.filling() API", mimeType: "text/markdown", }, { uri: "mandu://skills/guides/fs-routes", name: "FS Routes Guide", description: "Guide for file-system based routing patterns", mimeType: "text/markdown", }, { uri: "mandu://skills/guides/hydration", name: "Hydration Guide", description: "Guide for Island hydration and client components", mimeType: "text/markdown", }, { uri: "mandu://skills/guides/guard", name: "Guard Guide", description: "Guide for architecture enforcement and layer dependencies", mimeType: "text/markdown", }, // Skills - Recipes { uri: "mandu://skills/recipes", name: "Skills Recipe List", description: "Step-by-step recipes for common tasks", mimeType: "application/json", }, { uri: "mandu://skills/recipes/add-api-route", name: "Add API Route Recipe", description: "Step-by-step guide to add a new REST API endpoint", mimeType: "text/markdown", }, { uri: "mandu://skills/recipes/add-page", name: "Add Page Recipe", description: "Step-by-step guide to add a new page", mimeType: "text/markdown", }, { uri: "mandu://skills/recipes/add-auth", name: "Add Auth Recipe", description: "Step-by-step guide to add authentication", mimeType: "text/markdown", }, { uri: "mandu://skills/recipes/add-island", name: "Add Island Recipe", description: "Step-by-step guide to add an interactive Island component", mimeType: "text/markdown", }, { uri: "mandu://skills/recipes/add-database", name: "Add Database Recipe", description: "Step-by-step guide to connect a database", mimeType: "text/markdown", }, // Agent Skills (file-based) { uri: "mandu://skills", name: "Available Skills", description: "List of all Agent Skills for learning Mandu patterns", mimeType: "application/json", }, { uri: "mandu://skills/{skillId}", name: "Skill Overview", description: "SKILL.md content with overview and rule categories", mimeType: "text/markdown", }, { uri: "mandu://skills/{skillId}/rules", name: "Skill Rules", description: "List of rules for a specific skill", mimeType: "application/json", }, { uri: "mandu://skills/{skillId}/rules/{ruleId}", name: "Skill Rule", description: "Detailed rule documentation", mimeType: "text/markdown", }, ]; type ResourceHandler = (params: Record) => Promise; export function resourceHandlers( projectRoot: string ): Record { const paths = getProjectPaths(projectRoot); return { "mandu://spec/manifest": async () => { const result = await loadManifest(paths.manifestPath); if (!result.success || !result.data) { return { error: "Failed to load manifest", details: result.errors, }; } return { version: result.data.version, routeCount: result.data.routes.length, routes: result.data.routes, }; }, "mandu://generated/map": async () => { const generatedMap = await readJsonFile(paths.generatedMapPath); if (!generatedMap) { return { exists: false, message: "generated.map.json not found. Run mandu generate first.", }; } return { exists: true, version: generatedMap.version, generatedAt: generatedMap.generatedAt, specSource: generatedMap.specSource, fileCount: Object.keys(generatedMap.files).length, files: generatedMap.files, frameworkPaths: generatedMap.frameworkPaths, }; }, "mandu://transaction/active": async () => { const { state, change } = await getTransactionStatus(projectRoot); if (!state.active) { return { hasActive: false, message: "No active transaction", }; } return { hasActive: true, changeId: state.changeId, snapshotId: state.snapshotId, change: change ? { id: change.id, message: change.message, status: change.status, createdAt: change.createdAt, autoGenerated: change.autoGenerated, } : null, }; }, "mandu://slots/{routeId}": async (params: Record) => { const { routeId } = params; if (!routeId) { return { error: "routeId parameter is required" }; } // Load manifest to find the route const result = await loadManifest(paths.manifestPath); if (!result.success || !result.data) { return { error: result.errors }; } const route = result.data.routes.find((r) => r.id === routeId); if (!route) { return { error: `Route not found: ${routeId}` }; } if (!route.slotModule) { return { error: `Route '${routeId}' does not have a slotModule`, }; } const slotPath = path.join(projectRoot, route.slotModule); const file = Bun.file(slotPath); if (!(await file.exists())) { return { exists: false, slotModule: route.slotModule, message: "Slot file does not exist", }; } const content = await file.text(); return { exists: true, slotModule: route.slotModule, routeId, kind: route.kind, pattern: route.pattern, content, }; }, "mandu://watch/warnings": async () => { const watcher = getWatcher(); if (!watcher) { return { active: false, warnings: [] }; } const warnings = watcher.getRecentWarnings(50); return { active: true, count: warnings.length, warnings: warnings.map((w) => ({ ruleId: w.ruleId, file: w.file, message: w.message, event: w.event, timestamp: w.timestamp.toISOString(), })), }; }, "mandu://watch/status": async () => { const watcher = getWatcher(); if (!watcher) { return { active: false }; } const status = watcher.getStatus(); return { active: status.active, rootDir: status.rootDir, fileCount: status.fileCount, warningCount: status.recentWarnings.length, uptime: status.startedAt ? Math.floor((Date.now() - status.startedAt.getTime()) / 1000) : 0, startedAt: status.startedAt?.toISOString() || null, }; }, // Skills - Guides "mandu://skills/guides": async () => { return { guides: listGuides(), usage: "Read a specific guide with mandu://skills/guides/{id}", }; }, "mandu://skills/guides/slot": async () => { const content = getGuide("slot"); return { id: "slot", content }; }, "mandu://skills/guides/fs-routes": async () => { const content = getGuide("fs-routes"); return { id: "fs-routes", content }; }, "mandu://skills/guides/hydration": async () => { const content = getGuide("hydration"); return { id: "hydration", content }; }, "mandu://skills/guides/guard": async () => { const content = getGuide("guard"); return { id: "guard", content }; }, // Skills - Recipes "mandu://skills/recipes": async () => { return { recipes: listRecipes(), usage: "Read a specific recipe with mandu://skills/recipes/{id}", }; }, "mandu://skills/recipes/add-api-route": async () => { const content = getRecipe("add-api-route"); return { id: "add-api-route", content }; }, "mandu://skills/recipes/add-page": async () => { const content = getRecipe("add-page"); return { id: "add-page", content }; }, "mandu://skills/recipes/add-auth": async () => { const content = getRecipe("add-auth"); return { id: "add-auth", content }; }, "mandu://skills/recipes/add-island": async () => { const content = getRecipe("add-island"); return { id: "add-island", content }; }, "mandu://skills/recipes/add-database": async () => { const content = getRecipe("add-database"); return { id: "add-database", content }; }, // Agent Skills (file-based) "mandu://skills": async () => { return { skills: listSkills(), usage: { overview: "Read skill overview with mandu://skills/{skillId}", rules: "List rules with mandu://skills/{skillId}/rules", rule: "Read specific rule with mandu://skills/{skillId}/rules/{ruleId}", }, availableSkills: [ "mandu-slot", "mandu-fs-routes", "mandu-hydration", "mandu-guard", ], }; }, "mandu://skills/{skillId}": async (params: Record) => { const { skillId } = params; if (!skillId) { return { error: "skillId parameter is required" }; } const skill = await getSkill(skillId); if (!skill) { return { error: `Skill not found: ${skillId}` }; } const rules = await listSkillRules(skillId); return { id: skillId, meta: skill.meta, content: skill.content, rulesCount: rules.length, rulesUri: `mandu://skills/${skillId}/rules`, }; }, "mandu://skills/{skillId}/rules": async (params: Record) => { const { skillId } = params; if (!skillId) { return { error: "skillId parameter is required" }; } const rules = await listSkillRules(skillId); if (rules.length === 0) { return { error: `No rules found for skill: ${skillId}` }; } return { skillId, count: rules.length, rules: rules.map((r) => ({ ...r, uri: `mandu://skills/${skillId}/rules/${r.id}`, })), }; }, "mandu://skills/{skillId}/rules/{ruleId}": async (params: Record) => { const { skillId, ruleId } = params; if (!skillId || !ruleId) { return { error: "skillId and ruleId parameters are required" }; } const rule = await getSkillRule(skillId, ruleId); if (!rule) { return { error: `Rule not found: ${skillId}/${ruleId}` }; } return { skillId, ruleId, meta: rule.meta, content: rule.content, }; }, }; }