---
title: "Authoring & defineMcpApp"
description: "SFC location, quick start, the defineMcpApp macro, server handler, and shared types."
canonical_url: "https://mcp-toolkit.nuxt.dev/apps/authoring"
---
# Authoring & defineMcpApp

> SFC location, quick start, the defineMcpApp macro, server handler, and shared types.

## Quick Start

A complete app — schema, server handler, UI — in one file:

```vue [app/mcp/color-picker.vue]
<script setup lang="ts">
import { z } from 'zod'

interface PalettePayload {
  base: string
  swatches: { name: string, hex: string }[]
}

defineMcpApp({
  description: 'Pick a colour and preview a 5-tone palette.',
  inputSchema: {
    base: z.string().describe('Hex colour to anchor the palette, e.g. #2563eb'),
  },
  handler: async ({ base }): Promise<{ structuredContent: PalettePayload }> => {
    const swatches = await $fetch<{ name: string, hex: string }[]>('/api/palette', {
      query: { base },
    })
    return { structuredContent: { base, swatches } }
  },
})

const { data, loading, sendPrompt } = useMcpApp<PalettePayload>()
</script>

<template>
  <main class="picker">
    <p v-if="loading">
      Mixing colours…
    </p>
    <ul v-else-if="data" class="swatches">
      <li v-for="s in data.swatches" :key="s.hex">
        <button
          type="button"
          :style="{ background: s.hex }"
          @click="sendPrompt(`Use ${s.name} (${s.hex}) as the primary colour.`)"
        >
          {{ s.name }}
        </button>
      </li>
    </ul>
  </main>
</template>

<style scoped>
.picker { padding: 16px; font-family: system-ui, sans-serif; }
.swatches { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; padding: 0; list-style: none; }
.swatches button { width: 100%; aspect-ratio: 1; border-radius: 8px; border: 0; cursor: pointer; }
</style>
```

That's it. The toolkit:

1. Detects `defineMcpApp` and **registers an MCP tool** named `color-picker` (from the filename).
2. Generates a **UI resource** at `ui://mcp-app/color-picker` exposing `text/html;profile=mcp-app`.
3. Bundles the SFC + assets into a single HTML file with [`vite-plugin-singlefile`](https://github.com/richardtallent/vite-plugin-singlefile).
4. Wires the `handler`'s `structuredContent` into the iframe as `data` once the host pushes the tool result.

## File Convention

MCP Apps live in **app/mcp/** by default (not `server/mcp/`). Change the app-side directory with `mcp.appsDir` in `nuxt.config.ts`. They sit on the client side of Nuxt because they author Vue components — but the `handler` you declare runs server-side, just like a tool.

```bash
app/
└── mcp/
    ├── color-picker.vue    # → tool: color-picker, resource: ui://mcp-app/color-picker
    └── admin/
        └── audit-log.vue   # → tool: audit-log
```

<callout color="info" icon="i-lucide-info">

Co-locate helpers next to the SFC (e.g. `format.ts`) — the bundler inlines them. Keep data generation in `server/api/` and call it via `$fetch` from the handler.

</callout>

### Auto-Generated Name & Title

Like tools and resources, `name` and `title` are inferred from the filename:

<table>
<thead>
  <tr>
    <th>
      File
    </th>
    
    <th>
      Name
    </th>
    
    <th>
      Title
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        color-picker.vue
      </code>
    </td>
    
    <td>
      <code>
        color-picker
      </code>
    </td>
    
    <td>
      <code>
        Color Picker
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        weather-card.vue
      </code>
    </td>
    
    <td>
      <code>
        weather-card
      </code>
    </td>
    
    <td>
      <code>
        Weather Card
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        admin/audit-log.vue
      </code>
    </td>
    
    <td>
      <code>
        audit-log
      </code>
    </td>
    
    <td>
      <code>
        Audit Log
      </code>
    </td>
  </tr>
</tbody>
</table>

Override either by passing `name` / `title` to `defineMcpApp`.

### Routing Apps to a Specific Handler

By default, every app is attached to the implicit `apps` handler and only surfaces on `/mcp/apps`. Two ways to route an app to a different named handler:

**1. Sub-folder convention** — the first sub-directory under `app/mcp/` becomes the handler attribution:

```bash
app/
└── mcp/
    ├── color-picker.vue          # → /mcp/apps (default)
    ├── finder/
    │   └── stay-finder.vue       # → /mcp/finder
    └── checkout/
        └── stay-checkout.vue     # → /mcp/checkout
```

Pair each handler folder with `server/mcp/handlers/<name>/index.ts`:

```ts [server/mcp/handlers/finder/index.ts]
import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpHandler({})
```

**2. Explicit attachTo override** — overrides the sub-folder default if both are present:

```vue [app/mcp/stay-finder.vue]
<script setup lang="ts">
defineMcpApp({
  attachTo: 'finder',
  group: 'stays',
  tags: ['searchable'],
  // ...
})
</script>
```

The generated tool and resource carry `_meta.handler = 'finder'`, top-level `group = 'stays'`, and `tags = ['searchable']`. Filter on them with `getMcpTools({ handler: 'finder' })`, `getMcpTools({ tags: ['searchable'] })`, etc.

## `defineMcpApp`

A macro — like `definePageMeta` — extracted at build time and **stripped from the browser bundle**. The fields it accepts:

```ts
defineMcpApp({
  name?: string                          // Override auto-derived name
  title?: string                         // Override auto-derived title
  description?: string                   // Shown to the LLM to help it pick this app
  inputSchema?: ZodRawShape              // Validates tool input on the server
  handler?: (args, extra) => Result      // Runs server-side; defaults to (args) => ({ structuredContent: args })
  csp?: McpAppCsp | false                // Tighten or disable iframe CSP
  attachTo?: string                      // Named MCP handler this app routes to (default: 'apps' or sub-folder)
  group?: string                         // Top-level group label (default: same as attachTo)
  tags?: string[]                        // Top-level tags forwarded to the generated tool
  _meta?: Record<string, unknown>        // Extra _meta fields surfaced to the host
})
```

<callout color="info" icon="i-lucide-info">

`attachTo`, `group`, and `tags` must be **literals** (`'finder'`, `['a', 'b']`) — the toolkit reads them statically at build time to route the generated tool and resource. A dynamic expression (`attachTo: someVar`) fails the build with a clear error.

</callout>

### Server Handler

The `handler` runs in your Nitro server, not in the iframe. It receives validated input and returns the `structuredContent` the UI renders. **Treat it like a tool handler** — call APIs, query a database, hit `$fetch`:

```ts
defineMcpApp({
  description: 'Pick a colour and preview a 5-tone palette.',
  inputSchema: {
    base: z.string().describe('Hex colour to anchor the palette, e.g. #2563eb'),
  },
  handler: async ({ base }) => {
    const swatches = await $fetch('/api/palette', { query: { base } })
    return { structuredContent: { base, swatches } }
  },
})
```

<callout color="primary" icon="i-lucide-zap">

The host pushes the handler's `structuredContent` into the iframe once the view is ready, and `useMcpApp()` exposes it as `data`. Until then `data` is `null` — read it reactively and guard the first render with `loading`.

</callout>

If you omit `handler`, the toolkit defaults to `(args) => ({ structuredContent: args })`. Useful for stateless apps that only need the input echoed back.

### Sharing Types Between Server & UI

Place shared types in Nuxt's `shared/types/` directory — they're **auto-imported globally** in both the SFC and your API endpoints, no `import` statement required:

```ts [shared/types/palette.ts]
export interface Swatch { name: string, hex: string }
export interface PalettePayload { base: string, swatches: Swatch[] }
```

```ts [server/api/palette.get.ts]
export default defineEventHandler(async (event): Promise<PalettePayload> => {
  const { base } = getQuery(event)
  return { base: String(base), swatches: buildPalette(String(base)) }
})
```

```vue [app/mcp/color-picker.vue]
<script setup lang="ts">
defineMcpApp({
  inputSchema: { base: z.string() },
  handler: async ({ base }): Promise<{ structuredContent: PalettePayload }> => ({
    structuredContent: await $fetch('/api/palette', { query: { base } }),
  }),
})

const { data } = useMcpApp<PalettePayload>()
</script>
```

Type-only references are stripped from the browser bundle by esbuild — nothing has to resolve inside the iframe at runtime.


## Sitemap

See the full [sitemap](https://mcp-toolkit.nuxt.dev/sitemap.md) for all pages.
