| 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
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:
myProcedure: protectedProcedure
.meta({ mcp: { enabled: true, description: "Get all items with their status and metadata" } })
.input(z.object({ limit: z.number().default(10) }))
.query(async ({ ctx, input }) => { ... })
myAction: protectedProcedure
.meta({ mcp: { enabled: true, description: "Delete an item by ID. Requires interact permission on the integration" } })
.input(z.object({ id: z.string() }))
.mutation(async ({ ctx, input }) => { ... })
getAll: protectedProcedure
.meta({ mcp: { enabled: true, description: "List all resources" } })
.query(async ({ ctx }) => { ... })
The .meta() call must come before .input() and .query()/.mutation() in the chain.
Writing good descriptions
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:
"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:
"Get calendar events"
For tools that return permission fields, explain what they mean:
"List all integrations. Returns permissions.hasUseAccess (read) and permissions.hasInteractAccess (actions) — false means the API key lacks that permission, not an error"
Registering in the MCP router
After adding .meta() to procedures, you must register the router in packages/api/src/mcp.ts. This file uses eager imports (no lazy()) because MCP tool extraction requires synchronous access to all procedure definitions.
import { myNewRouter } from "./router/my-new-feature";
export const mcpRouter = createTRPCRouter({
myFeature: myNewRouter,
});
If your router is nested (e.g., widget sub-routers), import and register the sub-router directly — do not import a parent that uses lazy().
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
- Procedures that return non-serializable data (streams, blobs)
Input schema rules
- Use
z.object({...}) for inputs — this maps cleanly to MCP tool parameters
z.void() (no .input()) works — the MCP handler patches these to accept empty objects
- Avoid
z.union() or z.discriminatedUnion() at the top level — AI clients struggle with these
- Use
.default() on optional fields so the AI doesn't need to guess values
Checklist for new features
- Add
.meta({ mcp: { enabled: true, description: "..." } }) to relevant procedures
- Import and register the router in
packages/api/src/mcp.ts (eager, no lazy)
- Write descriptions that explain what the tool does, what it needs, and what it returns
- If the tool requires an ID from another tool, say which one in the description
- Test with
curl against /api/mcp/mcp to verify the tool appears in tools/list