104 lines
4.0 KiB
Plaintext
104 lines
4.0 KiB
Plaintext
---
|
|
description: How to expose tRPC procedures as MCP tools for AI clients
|
|
globs: packages/api/src/router/**/*.ts
|
|
alwaysApply: false
|
|
---
|
|
|
|
# MCP Integration Guide
|
|
|
|
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.
|
|
|
|
## Adding MCP to a procedure
|
|
|
|
Add `.meta({ mcp: { enabled: true, description: "..." } })` to the procedure chain:
|
|
|
|
```typescript
|
|
// 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) }))
|
|
.query(async ({ ctx, input }) => { ... })
|
|
|
|
// Write tool (mutation)
|
|
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 }) => { ... })
|
|
|
|
// No-input procedure (z.void) — works fine, no .input() needed
|
|
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.
|
|
|
|
```typescript
|
|
// packages/api/src/mcp.ts
|
|
import { myNewRouter } from "./router/my-new-feature";
|
|
|
|
export const mcpRouter = createTRPCRouter({
|
|
// ... existing routers
|
|
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
|
|
|
|
1. Add `.meta({ mcp: { enabled: true, description: "..." } })` to relevant procedures
|
|
2. Import and register the router in `packages/api/src/mcp.ts` (eager, no lazy)
|
|
3. Write descriptions that explain what the tool does, what it needs, and what it returns
|
|
4. If the tool requires an ID from another tool, say which one in the description
|
|
5. Test with `curl` against `/api/mcp/mcp` to verify the tool appears in `tools/list`
|