From 05ba57c2fa12a25984a1d93df7aef62c535f4fad Mon Sep 17 00:00:00 2001 From: Thomas Date: Sun, 16 Aug 2026 16:27:51 +0200 Subject: [PATCH] chore: migrate cursor rules to portable agent skills + mantine MCP guidance * chore: migrate cursor rules to portable agent skills and add mantine MCP guidance Replace Cursor-only .cursor/rules/*.mdc with Agent Skills open-standard SKILL.md files in .agents/skills/ (canonical), mirrored to .cursor/skills/ and .claude/skills/ so they work across Claude Code, Cursor, Codex, and OpenCode. Add mantine MCP server guidance to AGENTS.md and un-ignore .agents/skills/ in .gitignore. * refactor: single-source agent skills at .agents/skills Cursor, Codex, and OpenCode all read .agents/skills/ natively, so the .cursor/skills/ mirror was redundant. Claude Code reads .claude/skills/, which is now a symlink back to .agents/skills/ so the content lives once. Deletes .cursor/skills/ and converts .claude/skills/ to a symlink. * chore: ship mantine MCP server via project-scoped .mcp.json Declares the keyless @mantine/mcp-server in .mcp.json (Claude Code project-scope standard, committable) and documents MCP server setup and the mantine entry in AGENTS.md. * chore: address coderabbit security-review and markdown lint findings Add explicit security-review guidance before exposing MCP tools and label plain-text code fences to satisfy markdownlint MD040. --- .../skills/codebase-context/SKILL.md | 35 +-- .../skills/datatable-migration/SKILL.md | 215 ++++++++++++------ .agents/skills/documentation-sync/SKILL.md | 44 ++++ .../skills/mcp-integration/SKILL.md | 23 +- .claude/skills | 1 + .cursor/rules/documentation-sync.mdc | 50 ---- .gitignore | 3 +- .mcp.json | 8 + AGENTS.md | 10 + 9 files changed, 234 insertions(+), 155 deletions(-) rename .cursor/rules/codebase-context.mdc => .agents/skills/codebase-context/SKILL.md (89%) rename .cursor/rules/mantine-datatable-migration.mdc => .agents/skills/datatable-migration/SKILL.md (80%) create mode 100644 .agents/skills/documentation-sync/SKILL.md rename .cursor/rules/mcp-integration.mdc => .agents/skills/mcp-integration/SKILL.md (81%) create mode 120000 .claude/skills delete mode 100644 .cursor/rules/documentation-sync.mdc create mode 100644 .mcp.json diff --git a/.cursor/rules/codebase-context.mdc b/.agents/skills/codebase-context/SKILL.md similarity index 89% rename from .cursor/rules/codebase-context.mdc rename to .agents/skills/codebase-context/SKILL.md index b04b1db1d..d10509bff 100644 --- a/.cursor/rules/codebase-context.mdc +++ b/.agents/skills/codebase-context/SKILL.md @@ -1,6 +1,6 @@ --- -description: Comprehensive Homarr codebase architecture and conventions reference -alwaysApply: true +name: codebase-context +description: Comprehensive Homarr codebase architecture and conventions reference. Use when orienting in the monorepo, understanding package dependency layers, the three runtime services and ports, tRPC router structure, multi-database support, widget/integration/cron system architecture, routing, auth providers, env vars, or key patterns before writing code. --- # Homarr Codebase Context @@ -27,7 +27,7 @@ Homarr is an open-source, self-hosted dashboard for managing homelab services. I ## Repository Structure -``` +```text homarr/ ├── apps/ │ ├── nextjs/ # Main Next.js application (port 3000) @@ -66,8 +66,6 @@ homarr/ │ ├── analytics/ # Server-side analytics (Umami) │ ├── server-settings/ # Server setting keys/types │ ├── settings/ # User-facing settings UI context -│ ├── old-import/ # Legacy Homarr import -│ ├── old-schema/ # Legacy Homarr zod schemas │ └── cli/ # Node CLI for ops (brocli) ├── tooling/ │ ├── typescript/ # Base tsconfig @@ -79,11 +77,11 @@ homarr/ ## Three Runtime Services -| Service | Port | Role | -|---------|------|------| -| Next.js | 3000 | Main web app (SSR + API routes) | -| WebSocket | 3001 | tRPC subscriptions via `ws` | -| Tasks | 3002 | Cron job runner + management API via Fastify | +| Service | Port | Role | +| --------- | ---- | -------------------------------------------- | +| Next.js | 3000 | Main web app (SSR + API routes) | +| WebSocket | 3001 | tRPC subscriptions via `ws` | +| Tasks | 3002 | Cron job runner + management API via Fastify | In Docker, nginx listens on **7575** and proxies to all three. The tasks service authenticates via `CRON_JOB_API_KEY` header. @@ -112,6 +110,7 @@ In Docker, nginx listens on **7575** and proxies to all three. The tasks service ### Multi-Database Support Drizzle schemas exist in three parallel implementations: + - `packages/db/schema/sqlite.ts` - `packages/db/schema/mysql.ts` - `packages/db/schema/postgresql.ts` @@ -121,6 +120,7 @@ Selected at runtime via `DB_DRIVER` env (`better-sqlite3` | `mysql2` | `node-pos ### Widget System Widgets are registered in `packages/widgets/src/index.tsx` as `widgetImports` (must satisfy `Record`). Each widget: + - Has a `definition` created via `createWidgetDefinition(kind, { icon, createOptions, supportedIntegrations? })` - Uses `optionsBuilder.from(factory => ({ ... }))` for typed options - Loads dynamically via `next/dynamic` @@ -133,6 +133,7 @@ Integrations are defined in `packages/definitions/src/integration.ts` (`integrat ### Cron Job System Jobs defined in `packages/cron-jobs/src/jobs/`. Each job: + - Uses `createCronJob` with a cron expression - Publishes results to Redis channels - Widget subscriptions consume those channels via tRPC subscriptions @@ -160,6 +161,7 @@ Configured via `AUTH_PROVIDERS` env. Supports: `credentials` (local), `ldap`, `o ## Environment Variables Key env vars (from `.env.example`): + - `DB_DRIVER`: `better-sqlite3` | `mysql2` | `node-postgres` - `DB_URL`: Database connection string - `AUTH_SECRET`: NextAuth secret @@ -191,16 +193,3 @@ Key env vars (from `.env.example`): - Drag-and-drop via `@dnd-kit/*` - `typescript.ignoreBuildErrors: true` in next.config (types checked separately via `typecheck`) - `serverExternalPackages`: `dockerode`, `isomorphic-dompurify`, `jsdom` - -## Documentation (`apps/docs`) - -Docusaurus 3 site at `apps/docs/`, package name `@homarr/docs`. - -- Run with `pnpm dev:docs` from root -- Build with `pnpm turbo build --filter=@homarr/docs` -- Uses Tailwind CSS 3 for custom pages (not Mantine) -- Imports `@homarr/definitions` for type-safe integration/widget metadata -- When adding or modifying widgets, integrations, cron jobs, API routes, env vars, or user-facing features, you MUST also update corresponding docs in `apps/docs/docs/` -- Integration docs: `apps/docs/docs/integrations//index.mdx` + `index.ts` -- Widget docs: `apps/docs/docs/widgets//index.mdx` + `index.ts` -- Strict link checking: broken links/anchors cause build failure diff --git a/.cursor/rules/mantine-datatable-migration.mdc b/.agents/skills/datatable-migration/SKILL.md similarity index 80% rename from .cursor/rules/mantine-datatable-migration.mdc rename to .agents/skills/datatable-migration/SKILL.md index 08d2b8202..62bbc94c3 100644 --- a/.cursor/rules/mantine-datatable-migration.mdc +++ b/.agents/skills/datatable-migration/SKILL.md @@ -1,3 +1,8 @@ +--- +name: datatable-migration +description: Playbook for migrating tables from mantine-react-table (MRT) to mantine-datatable. Use when converting any table component from mantine-react-table to mantine-datatable, working on DataTable column/sort/expansion/context-menu code, or implementing responsive column visibility, column persistence, or transparent tables in widgets. +--- + # Migrating Tables from mantine-react-table to mantine-datatable This document captures every pattern, fix, and decision from the downloads widget rework (PR #6430). Use it as a playbook when porting other tables. @@ -7,10 +12,12 @@ This document captures every pattern, fix, and decision from the downloads widge Current `mantine-react-table` consumers that could be migrated: **Widgets (in `packages/widgets/`):** + - `docker/component.tsx` — Docker container table - `media-server/component.tsx` — Media server sessions **Admin pages (in `apps/nextjs/src/app/[locale]/manage/`):** + - `tools/docker/docker-table.tsx` - `tools/tasks/_components/tasks-table.tsx` - `tools/api/components/api-keys.tsx` @@ -19,12 +26,12 @@ Current `mantine-react-table` consumers that could be migrated: - `tools/kubernetes/` — 8 Kubernetes resource tables **Modals:** + - `packages/modals-collection/src/search-engines/request-media-modal.tsx` **Shared hook to remove once all migrated:** -- `packages/ui/src/hooks/use-translated-mantine-react-table.ts` ---- +- `packages/ui/src/hooks/use-translated-mantine-react-table.ts` ## 1. Core Migration: mantine-react-table → mantine-datatable @@ -67,6 +74,7 @@ const columns: DataTableColumn[] = [ ``` Key differences: + - `accessorKey` → `accessor` - `header` → `title` - `size` → `width` @@ -118,8 +126,6 @@ const sortedData = useMemo(() => { ``` ---- - ## 2. Making Tables Transparent (Widget Context) Widgets sit inside board cards that have user-controlled opacity. The table must be fully transparent so the card background shows through. mantine-datatable defaults to `var(--mantine-color-body)` which is opaque. @@ -144,13 +150,21 @@ Create a CSS file alongside the component: /* Hover: translucent instead of opaque */ .my-table .mantine-datatable-table tbody tr:hover { - background-color: color-mix(in srgb, var(--mantine-color-default-hover) 40%, transparent) !important; + background-color: color-mix( + in srgb, + var(--mantine-color-default-hover) 40%, + transparent + ) !important; } /* Headers: frosted glass effect */ .my-table th { white-space: nowrap; - background-color: color-mix(in srgb, var(--mantine-color-body) 60%, transparent) !important; + background-color: color-mix( + in srgb, + var(--mantine-color-body) 60%, + transparent + ) !important; backdrop-filter: blur(8px); } @@ -172,14 +186,16 @@ Create a CSS file alongside the component: /* Row expansion area */ .my-table .mantine-datatable-row-expansion-cell { - background-color: color-mix(in srgb, var(--mantine-color-body) 40%, transparent) !important; + background-color: color-mix( + in srgb, + var(--mantine-color-body) 40%, + transparent + ) !important; } ``` Import it: `import "./styles.css";` and apply via `className="my-table"`. ---- - ## 3. Responsive Column Visibility Use a lookup table of width breakpoints instead of media queries. The widget receives `width` as a prop. @@ -195,11 +211,38 @@ interface SizeConfig { } const SIZE_BREAKPOINTS: { maxWidth: number; config: SizeConfig }[] = [ - { maxWidth: 300, config: { fontSize: "xs", iconSize: 12, cellPadding: 2, showSpeedColumns: false, showTimeColumn: false, showStateColumn: false } }, - { maxWidth: 500, config: { fontSize: "xs", iconSize: 14, cellPadding: 4, showSpeedColumns: false, showTimeColumn: true, showStateColumn: false } }, + { + maxWidth: 300, + config: { + fontSize: "xs", + iconSize: 12, + cellPadding: 2, + showSpeedColumns: false, + showTimeColumn: false, + showStateColumn: false, + }, + }, + { + maxWidth: 500, + config: { + fontSize: "xs", + iconSize: 14, + cellPadding: 4, + showSpeedColumns: false, + showTimeColumn: true, + showStateColumn: false, + }, + }, ]; -const DEFAULT_SIZE_CONFIG: SizeConfig = { fontSize: "sm", iconSize: 14, cellPadding: 4, showSpeedColumns: true, showTimeColumn: true, showStateColumn: true }; +const DEFAULT_SIZE_CONFIG: SizeConfig = { + fontSize: "sm", + iconSize: 14, + cellPadding: 4, + showSpeedColumns: true, + showTimeColumn: true, + showStateColumn: true, +}; function getSizeConfig(width: number): SizeConfig { for (const { maxWidth, config } of SIZE_BREAKPOINTS) { @@ -215,7 +258,9 @@ const size = useMemo(() => getSizeConfig(width), [width]); Then use a visibility check lookup table for columns: ```typescript -const columnVisibilityChecks: Partial boolean>> = { +const columnVisibilityChecks: Partial< + Record boolean> +> = { upSpeed: (ctx) => ctx.hasTorrents && ctx.size.showSpeedColumns, downSpeed: (ctx) => ctx.size.showSpeedColumns, time: (ctx) => ctx.size.showTimeColumn, @@ -230,8 +275,6 @@ function shouldIncludeColumn(accessor: string, ctx: ColumnContext): boolean { } ``` ---- - ## 4. Column Persistence (Drag/Resize → Server) `useDataTableColumns` stores column order and widths in localStorage by default. For widgets, this must persist server-side so it works across devices. @@ -263,15 +306,6 @@ Even hidden options need translation keys or the translation test fails: "widget.mywidget.option.columnWidths.label": "Column widths" ``` -### Old-import mappings - -Add to `packages/old-import/src/widgets/options.ts`: - -```typescript -columnOrder: () => undefined, -columnWidths: () => undefined, -``` - ### Dynamic storeKey Use a key that includes `itemId` and sorted column names. This prevents cross-widget localStorage bleeding: @@ -285,8 +319,13 @@ const storeKey = `mytable-${itemId ?? "preview"}-${[...options.columns].toSorted On mount, seed `useDataTableColumns` with server-persisted values. Use a `hydrated` ref to prevent the persist effect from immediately writing defaults back: ```typescript -const { effectiveColumns, columnsOrder, columnsWidth, setColumnsOrder, setMultipleColumnWidths } = - useDataTableColumns({ key: storeKey, columns }); +const { + effectiveColumns, + columnsOrder, + columnsWidth, + setColumnsOrder, + setMultipleColumnWidths, +} = useDataTableColumns({ key: storeKey, columns }); const lastStoreKey = useRef(storeKey); const hydrated = useRef(false); @@ -300,11 +339,22 @@ useEffect(() => { if (savedOrder.length > 0) setColumnsOrder(savedOrder); if (Object.keys(savedWidths).length > 0) { setMultipleColumnWidths( - Object.entries(savedWidths).map(([accessor, w]) => ({ accessor, width: w })), + Object.entries(savedWidths).map(([accessor, w]) => ({ + accessor, + width: w, + })), ); } - requestAnimationFrame(() => { hydrated.current = true; }); -}, [storeKey, savedOrder, savedWidths, setColumnsOrder, setMultipleColumnWidths]); + requestAnimationFrame(() => { + hydrated.current = true; + }); +}, [ + storeKey, + savedOrder, + savedWidths, + setColumnsOrder, + setMultipleColumnWidths, +]); ``` ### Persist localStorage → server @@ -317,22 +367,26 @@ const prevWidths = useRef(columnsWidth); useEffect(() => { if (!hydrated.current) return; - const orderChanged = JSON.stringify(columnsOrder) !== JSON.stringify(prevOrder.current); - const widthsChanged = JSON.stringify(columnsWidth) !== JSON.stringify(prevWidths.current); + const orderChanged = + JSON.stringify(columnsOrder) !== JSON.stringify(prevOrder.current); + const widthsChanged = + JSON.stringify(columnsWidth) !== JSON.stringify(prevWidths.current); prevOrder.current = columnsOrder; prevWidths.current = columnsWidth; if (!orderChanged && !widthsChanged) return; - if (orderChanged) persistOption({ columnOrder: JSON.stringify(columnsOrder) }); + if (orderChanged) + persistOption({ columnOrder: JSON.stringify(columnsOrder) }); if (widthsChanged) { const widthMap: Record = {}; for (const entry of columnsWidth) { const key = Object.keys(entry)[0]; if (!key) continue; - const w = entry[key as keyof typeof entry]; - if (typeof w === "number") widthMap[key] = w; - else if (typeof w === "string" && w.endsWith("px")) widthMap[key] = parseInt(w, 10); + const v = entry[key as keyof typeof entry]; + if (typeof v === "number") widthMap[key] = v; + else if (typeof v === "string" && v.endsWith("px")) + widthMap[key] = parseInt(v, 10); } persistOption({ columnWidths: JSON.stringify(widthMap) }); } @@ -341,8 +395,6 @@ useEffect(() => { The `persistOption` helper calls both `setOptions` (local state) and `saveItemOptions` (server mutation). ---- - ## 5. Context Menu (Right-Click) Do NOT install `mantine-contextmenu`. Use Mantine's `Menu` + `Portal` directly: @@ -374,15 +426,41 @@ const handleContextMenu = useCallback( The menu component uses `Portal` for correct z-index: ```tsx -function RowContextMenu({ state, onClose }: { state: ContextMenuState; onClose: () => void }) { +function RowContextMenu({ + state, + onClose, +}: { + state: ContextMenuState; + onClose: () => void; +}) { return ( - + - + - { /* action */ onClose(); }}>Action + { + /* action */ onClose(); + }} + > + Action + @@ -392,8 +470,6 @@ function RowContextMenu({ state, onClose }: { state: ContextMenuState; onClose: **Key fix:** Use `closeOnItemClick={false}` if any menu item needs a two-step confirmation (like delete → confirm). Close the menu on scroll: `onScroll={() => { if (contextMenu) closeContextMenu(); }}`. ---- - ## 6. Row Expansion ```typescript @@ -413,8 +489,6 @@ function RowContextMenu({ state, onClose }: { state: ContextMenuState; onClose: **Critical:** Do NOT set `onRowClick` when using `rowExpansion` with `trigger: "click"`. They conflict and the expansion won't work. ---- - ## 7. Common Pitfalls & Fixes ### Status indicator dots @@ -475,8 +549,11 @@ Always wrap in try/catch: ```typescript function parseJsonOption(value: string | undefined, fallback: T): T { if (!value) return fallback; - try { return JSON.parse(value) as T; } - catch { return fallback; } + try { + return JSON.parse(value) as T; + } catch { + return fallback; + } } ``` @@ -494,20 +571,19 @@ If the widget has configurable default sort, sync it: ```typescript useEffect(() => { - setSortStatus({ columnAccessor: options.defaultSort, direction: defaultSortDirection }); + setSortStatus({ + columnAccessor: options.defaultSort, + direction: defaultSortDirection, + }); }, [options.defaultSort, defaultSortDirection]); ``` ---- - ## 8. Accessibility - All `ActionIcon` components need `aria-label` - Filter toggles (client badges, status badges) need `aria-pressed={isActive}` - `textSelectionDisabled` on DataTable prevents accidental text selection during drag/resize ---- - ## 9. Error Handling - Every mutation needs an `onError` callback with `showErrorNotification` @@ -518,12 +594,14 @@ useEffect(() => { ```typescript const mutationOptions = { onSettled: () => void utils.myQuery.invalidate(), - onError: () => showErrorNotification({ title: t("errors.actionFailed"), message: t("errors.actionFailedMessage") }), + onError: () => + showErrorNotification({ + title: t("errors.actionFailed"), + message: t("errors.actionFailedMessage"), + }), }; ``` ---- - ## 10. i18n - No hardcoded strings. Every user-visible string goes through `useScopedI18n` @@ -534,8 +612,6 @@ const mutationOptions = { - Errors: `t("errors.{errorKey}")` - Hidden options still need `.label` translation keys ---- - ## 11. Edit Mode Disable pointer events on the table during edit mode to prevent conflicts with the board's drag-and-drop: @@ -550,8 +626,6 @@ if (isEditMode) rowContextMenuHandler = undefined; ``` ---- - ## 12. DataTable Props Reference (Commonly Used) ```typescript @@ -590,8 +664,6 @@ if (isEditMode) rowContextMenuHandler = undefined; /> ``` ---- - ## 13. Layout Stability - Don't use `table-layout: fixed` — it fights mantine-datatable's resize logic @@ -600,8 +672,6 @@ if (isEditMode) rowContextMenuHandler = undefined; - Names should truncate at the cell boundary, not a character count, so the layout is stable regardless of content length - `flexShrink: 0` on badges/icons prevents them from being squeezed by long names ---- - ## 14. Checklist for Each Migration 1. Replace imports (`mantine-react-table` → `mantine-datatable`) @@ -610,13 +680,12 @@ if (isEditMode) rowContextMenuHandler = undefined; 4. Add CSS file for transparent backgrounds (if widget context) 5. Add responsive column visibility (if widget context) 6. Add hidden options + hydration logic (if column persistence needed) -7. Add old-import mappings for new options -8. Add translation keys (including hidden option labels) -9. Add context menu if row actions exist -10. Add row expansion if detail view exists -11. Add error handling on all mutations -12. Add accessibility attributes -13. Test at multiple widget widths -14. Update docs in `apps/docs/docs/widgets/` if applicable -15. Run `pnpm turbo typecheck` — catches old-import mismatches -16. Run translation spec — catches missing keys +7. Add translation keys (including hidden option labels) +8. Add context menu if row actions exist +9. Add row expansion if detail view exists +10. Add error handling on all mutations +11. Add accessibility attributes +12. Test at multiple widget widths +13. Update docs in `apps/docs/docs/widgets/` if applicable +14. Run `pnpm turbo typecheck` +15. Run translation spec — catches missing keys diff --git a/.agents/skills/documentation-sync/SKILL.md b/.agents/skills/documentation-sync/SKILL.md new file mode 100644 index 000000000..48aed0e7e --- /dev/null +++ b/.agents/skills/documentation-sync/SKILL.md @@ -0,0 +1,44 @@ +--- +name: documentation-sync +description: Keep Homarr documentation in sync with code changes. Use when modifying code that affects user-facing behavior, or when adding/modifying widgets, integrations, cron jobs, API routes, env vars, permissions, auth providers, or UI in apps/docs. Covers the docs/ mapping table, integration doc pattern, and widget doc pattern. +--- + +# Documentation Sync + +When modifying code that affects user-facing behavior, you MUST also update the documentation in `apps/docs/`. + +## What triggers a doc update + +| Code change | Doc location | +| ---------------------- | ----------------------------------------------------------- | +| New integration | `apps/docs/docs/integrations//index.mdx` + `index.ts` | +| New widget | `apps/docs/docs/widgets//index.mdx` + `index.ts` | +| Changed API surface | `apps/docs/docs/management/api.mdx` | +| New/changed env vars | `apps/docs/docs/advanced/` | +| New CLI commands | `apps/docs/docs/advanced/command-line/` | +| Changed auth providers | `apps/docs/docs/advanced/` SSO pages | +| UI/UX changes | Relevant getting-started or management pages | +| New cron job | `apps/docs/docs/management/tasks.mdx` | +| Changed permissions | `apps/docs/docs/management/users.mdx` | + +## Integration doc pattern + +Each integration has a folder under `apps/docs/docs/integrations//`: + +- `index.ts` — exports a typed `IntegrationDefinition` with name, description, iconUrl, path +- `index.mdx` — uses shared components: `IntegrationHeader`, `IntegrationCapabilites`, `IntegrationSecrets` +- Import metadata from `@homarr/definitions` where possible + +## Widget doc pattern + +Each widget has a folder under `apps/docs/docs/widgets//`: + +- `index.ts` — exports a typed `WidgetDefinition` with icon, name, description, path, configuration +- `index.mdx` — uses shared components: `WidgetHeader`, `WidgetConfig`, `WidgetAdding` + +## Development + +- Run docs locally: `pnpm dev:docs` from root +- Build docs: `pnpm turbo build --filter=@homarr/docs` +- The docs app can import from `@homarr/definitions` for type-safe integration/widget metadata +- Docusaurus strict mode throws on broken links — fix any broken references before committing diff --git a/.cursor/rules/mcp-integration.mdc b/.agents/skills/mcp-integration/SKILL.md similarity index 81% rename from .cursor/rules/mcp-integration.mdc rename to .agents/skills/mcp-integration/SKILL.md index 989b0c4f6..38658da5a 100644 --- a/.cursor/rules/mcp-integration.mdc +++ b/.agents/skills/mcp-integration/SKILL.md @@ -1,13 +1,14 @@ --- -description: How to expose tRPC procedures as MCP tools for AI clients -globs: packages/api/src/router/**/*.ts -alwaysApply: false +name: mcp-integration +description: Expose tRPC procedures as MCP tools for AI clients. Use when modifying procedures in packages/api/src/router, adding a new tRPC procedure or router, or deciding what to expose via Homarr's MCP interface. Covers .meta({ mcp }), router registration in packages/api/src/mcp.ts, input schema rules, and what to expose vs skip. --- -# MCP Integration Guide +# MCP Integration Every new tRPC procedure that provides useful functionality should be exposed as an MCP tool so AI assistants (Claude, Cursor, etc.) can use it. This is a first-class feature of Homarr. +Exposing a procedure makes it callable by any AI client with an API key. Before adding `.meta({ mcp })`, review what the tool exposes: authorization requirements, sensitive data, tenant isolation, auditability, and whether the action is destructive. Prefer exposing read-only queries over mutations unless the mutation is safe and permission-scoped. When in doubt, ask for a security review before shipping a new MCP tool. + ## Adding MCP to a procedure Add `.meta({ mcp: { enabled: true, description: "..." } })` to the procedure chain: @@ -16,7 +17,7 @@ Add `.meta({ mcp: { enabled: true, description: "..." } })` to the procedure cha // Read-only tool (query) myProcedure: protectedProcedure .meta({ mcp: { enabled: true, description: "Get all items with their status and metadata" } }) - .input(z.object({ limit: z.number().default(50) })) + .input(z.object({ limit: z.number().default(10) })) .query(async ({ ctx, input }) => { ... }) // Write tool (mutation) @@ -38,23 +39,27 @@ The `.meta()` call must come **before** `.input()` and `.query()`/`.mutation()` The description is the **only thing** an AI sees to decide when and how to use the tool. Write it like you're explaining to a colleague who has never used Homarr. **Must include:** + - What the tool returns or does - Which integrations/services it works with (if applicable) - How to get required IDs (e.g., "Use integration_all to get the integrationId") - Permission requirements (if non-obvious) **Good:** -``` + +```text "Get calendar events for upcoming and recent media releases. Fetches from all connected Sonarr (TV), Radarr (movies), Lidarr (music), and Readarr (books) integrations. Requires integrationIds from integration_all" ``` **Bad:** -``` + +```text "Get calendar events" ``` **For tools that return permission fields**, explain what they mean: -``` + +```text "List all integrations. Returns permissions.hasUseAccess (read) and permissions.hasInteractAccess (actions) — false means the API key lacks that permission, not an error" ``` @@ -77,11 +82,13 @@ export const mcpRouter = createTRPCRouter({ ## What to expose vs. what to skip **Expose:** + - Data queries (list, search, get by ID) - Actions users would ask an AI to do (toggle, create, delete, approve) - Status checks (health, stats, summaries) **Skip:** + - Subscriptions (WebSocket-only, not supported by MCP) - Internal procedures (session management, onboarding steps) - File upload/download procedures diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 000000000..2b7a412b8 --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.cursor/rules/documentation-sync.mdc b/.cursor/rules/documentation-sync.mdc deleted file mode 100644 index a05ca5d84..000000000 --- a/.cursor/rules/documentation-sync.mdc +++ /dev/null @@ -1,50 +0,0 @@ ---- -description: Keep documentation in sync with code changes -globs: - - packages/definitions/src/** - - packages/widgets/src/** - - packages/integrations/src/** - - packages/api/src/** - - apps/nextjs/src/** - - apps/docs/** ---- - -# Documentation Sync Rule - -When modifying code that affects user-facing behavior, you MUST also update the documentation in `apps/docs/`. - -## What triggers a doc update - -| Code change | Doc location | -|---|---| -| New integration | `apps/docs/docs/integrations//index.mdx` + `index.ts` | -| New widget | `apps/docs/docs/widgets//index.mdx` + `index.ts` | -| Changed API surface | `apps/docs/docs/management/api.mdx` | -| New/changed env vars | `apps/docs/docs/advanced/` | -| New CLI commands | `apps/docs/docs/advanced/command-line/` | -| Changed auth providers | `apps/docs/docs/advanced/` SSO pages | -| UI/UX changes | Relevant getting-started or management pages | -| New cron job | `apps/docs/docs/management/tasks.mdx` | -| Changed permissions | `apps/docs/docs/management/users.mdx` | - -## Integration doc pattern - -Each integration has a folder under `apps/docs/docs/integrations//`: - -- `index.ts` — exports a typed `IntegrationDefinition` with name, description, iconUrl, path -- `index.mdx` — uses shared components: `IntegrationHeader`, `IntegrationCapabilites`, `IntegrationSecrets` -- Import metadata from `@homarr/definitions` where possible - -## Widget doc pattern - -Each widget has a folder under `apps/docs/docs/widgets//`: - -- `index.ts` — exports a typed `WidgetDefinition` with icon, name, description, path, configuration -- `index.mdx` — uses shared components: `WidgetHeader`, `WidgetConfig`, `WidgetAdding` - -## Development - -- Run docs locally: `pnpm dev:docs` from root -- Build docs: `pnpm turbo build --filter=@homarr/docs` -- The docs app can import from `@homarr/definitions` for type-safe integration/widget metadata -- Docusaurus strict mode throws on broken links — fix any broken references before committing diff --git a/.gitignore b/.gitignore index 71a27829a..932d6c9c8 100644 --- a/.gitignore +++ b/.gitignore @@ -71,7 +71,8 @@ apps/nextjs/public/images/background.png # personal agent config .opencode -.agents/ +.agents/* +!.agents/skills/ skills-lock.json # docusaurus diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 000000000..515d300c4 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "mantine": { + "command": "npx", + "args": ["-y", "@mantine/mcp-server"] + } + } +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 6c3d36da4..42e69efdd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -84,6 +84,16 @@ When modifying user-facing code, you MUST also update the corresponding document - Lint: oxlint (not ESLint) - Format: oxfmt (not Prettier) - UI: Mantine (not Tailwind) — Tailwind is only used in docs app +- Mantine: use the `mantine` MCP server (configured in `.mcp.json`; tools `get_item_doc` / `get_item_props` / `search_docs`) for current v9 APIs before writing component code. Prefer built-in primitives (`Combobox`/`useCombobox` for selects, the polymorphic `component` prop, `@mantine/hooks`) over hand-rolled equivalents. Check `packages/ui/` for existing conventions first. + +## MCP servers + +`.mcp.json` at the repo root declares project-scoped MCP servers (the standard Claude Code layout; other tools can import the same `mcpServers` block). See the `mantine` entry there for the Mantine v9 server, and the upstream docs at https://mantine.dev/guides/llms/ for per-tool setup (OpenCode: `.opencode/opencode.json`, Codex: `.codex/config.toml`). Only keyless server definitions belong in `.mcp.json`; anything needing secrets stays in local dotfiles. + +## Agent Skills + +Portable agent skills live in `.agents/skills/` (the Agent Skills open standard). Cursor, Codex, and OpenCode read `.agents/skills/` natively. Claude Code does not scan it, so `.claude/skills/` is a symlink back to `.agents/skills/`. Install skills at `.agents/skills//SKILL.md` — that single copy is used everywhere. See `.agents/skills/` for available skills (documentation-sync, mcp-integration, codebase-context, datatable-migration). + - Icons: @tabler/icons-react - Docs app can import from `@homarr/definitions` for shared types - Run `pnpm dev:cli -- dev` to browse local `homarr:*` images and remote PR images.