# MCP Integration

> 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.

- Skill: `homarr-labs/mcp-integration` (Agent Skill)
- Install (CLI): `npx skillmds@latest add homarr-labs/mcp-integration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/homarr-labs/mcp-integration/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: homarr-labs (https://skillmd.com/u/homarr-labs)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/homarr-labs/mcp-integration

---


# 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:

```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(10) }))
  .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:**

```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"
```

## 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`

