manage-mcp について
`@nuxtjs/mcp-toolkit` を使う Nuxt 開発者向けの MCP 管理スキルです。サーバー設定やツール、リソース、プロンプトの作成に加え、複数ハンドラー、認証、ミドルウェア、MCP Apps、トラブルシューティングを扱います。
主な機能
- 01Nuxt MCP サーバーの設定
- 02ツール、リソース、プロンプトの作成
- 03複数ハンドラーエンドポイントの整理
- 04ミドルウェアと条件付き表示の適用
- 05インタラクティブな MCP Apps ウィジェットの構築
- 06テストとトラブルシューティングの案内
活用シーン
- 01Nuxt アプリに MCP サーバーを追加
- 02管理用と公開用のエンドポイントを分離
- 03MCP クライアントにインタラクティブな Vue ウィジェットを公開
- 04ツールの検出や OAuth の問題を調査
MCP 対応
`@nuxtjs/mcp-toolkit` を使って Nuxt に MCP サーバーを構築し、MCP クライアントにツール、リソース、プロンプトを公開する方法を案内します。エンドポイントとミドルウェアの整理にも対応します。
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ nuxt-modules/ mcp-toolkit --skill manage-mcpインストール後、Claude Code で/manage-mcpを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/apps.mdhandlers.mdmiddleware.mdprompts.mdresources.mdsessions.mdtesting.mdtools.mdtroubleshooting.md詳細
MCP を管理する
@nuxtjs/mcp-toolkit を使って、Nuxt で Model Context Protocol(MCP)サーバーを管理するためのスキルです。セットアップ、ツール/リソース/プロンプトの作成、複数ハンドラーのフォルダー規約による整理、インタラクティブな UI ウィジェット(MCP Apps)の構築、セッションとオブザーバビリティの設定、レビュー、トラブルシューティングを行います。
使用する場面
- セットアップ:「Nuxt アプリに MCP サーバーをセットアップする」「Nuxt に MCP を追加する」
- 作成:「BMI を計算するツールを追加する」「README をリソースとして公開する」「コードレビュー用のプロンプトを作成する」
- 整理:「MCP を管理用と公開用のエンドポイントに分割する」「ユーザーごとにツールを絞り込む」
- カスタマイズ:「MCP に認証を追加する」「すべてのツール呼び出しを Axiom に記録する」「進捗をクライアントにストリーミングする」
- アプリの構築:「ChatGPT/Cursor から呼び出せるインタラクティブな Vue ウィジェットを追加する」
- 状態の永続化:「1 つのセッション内のツール呼び出し間でユーザーを記憶する」
- レビュー:「MCP の実装をレビューする」「ベストプラクティスに沿っているか確認する」
- トラブルシューティング:「ツールが検出されない」「自動インポートが機能しない」「OAuth フローが何度も実行される」
- テスト:「これらのプロンプトに対してモデルがどのツールを選ぶか評価する」
MCP サーバーをセットアップする
インストール
::code-group
pnpm add @nuxtjs/mcp-toolkit zod
npm install @nuxtjs/mcp-toolkit zod
yarn add @nuxtjs/mcp-toolkit zod
bun add @nuxtjs/mcp-toolkit zod
::
または、Nuxt CLI でまとめて実行します。
npx nuxt add mcp
nuxt.config.ts に追加します。
export default defineNuxtConfig({
modules: ['@nuxtjs/mcp-toolkit'],
mcp: {
name: 'My MCP Server',
description: 'Read and update todos for the current user.',
},
nitro: {
experimental: { asyncContext: true }, // required for useEvent / useMcpServer / useMcpLogger
},
})
ディレクトリー構成
server/mcp/
├── tools/ # Actions the AI can perform
│ └── admin/ # Subdirectory → group: 'admin'
├── resources/ # Data the AI can read
├── prompts/ # Reusable message templates
└── handlers/ # Optional: named handlers mounted at /mcp/<name>
└── admin/
├── index.ts # defineMcpHandler({ middleware: requireAdmin })
├── tools/ # auto-attached to /mcp/admin
└── resources/
app/mcp/ # Optional: MCP Apps (interactive Vue widgets)
└── palette.vue # → app `palette`, callable from ChatGPT / Cursor
確認
- 開発サーバーを起動します:
pnpm dev - エンドポイントにアクセスします:
curl http://localhost:3000/mcp(MCP JSON-RPC に応答します) - Nuxt DevTools を開き(Shift+Alt+D)、MCP Inspector タブから「Launch Inspector」を選択します(新しいブラウザータブで開きます)。
ツールを作成する
ツールは、AI アシスタントが呼び出せる関数です。server/mcp/tools/ 配下の任意の .ts/.js ファイルから自動検出されます。
基本的なツール
import { z } from 'zod'
export default defineMcpTool({
description: 'Echo a message back to the user',
inputSchema: {
message: z.string().describe('Text to echo'),
},
handler: async ({ message }) => {
return `Echo: ${message}` // string is auto-wrapped into a text content item
},
})
name と title はファイル名から自動的に決まります(echo.ts → name: 'echo'、title: 'Echo')。明示的に設定すると、どちらも上書きできます。
戻り値
ハンドラーは以下のいずれかを返せます。ツールキットが形式を正規化します。
| 戻り値 | ラップ後の形式 |
|---|---|
string / number / boolean | { content: [{ type: 'text', text: String(v) }] } |
| プレーンオブジェクト/配列 | JSON 文字列に変換され、テキストコンテンツ項目に格納されます |
imageResult(base64, mime) | { content: [{ type: 'image', data, mimeType }] } |
audioResult(base64, mime) | { content: [{ type: 'audio', data, mimeType }] } |
完全な CallToolResult | そのまま渡されます(structuredContent、埋め込みリソース、複数コンテンツに使用します) |
| スローされたエラー | キャッチされ、isError: true に変換されます。h3 の createError({ statusCode, message }) を使うと、レスポンスにステータスコードが含まれます。 |
import { z } from 'zod'
export default defineMcpTool({
description: 'Get a user by ID',
inputSchema: { id: z.string().describe('User ID') },
handler: async ({ id }) => {
const user = await getUser(id)
if (!user) throw createError({ statusCode: 404, message: 'User not found' })
return user // plain object — auto-stringified
},
})
型付きの構造化出力を返すには、outputSchema と structuredContent を返す処理を組み合わせます。
import { z } from 'zod'
export default defineMcpTool({
description: 'Calculate Body Mass Index',
inputSchema: {
height: z.number().describe('Height in meters'),
weight: z.number().describe('Weight in kilograms'),
},
outputSchema: {
bmi: z.number(),
category: z.string(),
},
handler: async ({ height, weight }) => {
const bmi = weight / (height * height)
const category = bmi < 18.5 ? 'underweight' : bmi < 25 ? 'normal' : bmi < 30 ? 'overweight' : 'obese'
return { structuredContent: { bmi, category } }
},
})
アノテーションと入力例
クライアントが確認を求めるかどうかを判断するのに役立つ、動作に関するヒントです。
export default defineMcpTool({
description: 'Delete a user account',
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: false,
},
inputSchema: { id: z.string() },
inputExamples: [{ id: 'usr_42' }, { id: 'admin' }],
handler: async ({ id }) => {
await deleteUser(id)
return `Deleted ${id}.`
},
})
よくあるパターン:読み取り専用 → readOnlyHint: true;作成 → idempotentHint: false;更新 → idempotentHint: true;削除 → destructiveHint: true, idempotentHint: true。
グループ、タグ、フォルダーからの推定
group(単一)と tags(自由形式)は、ツールの整理に役立ち、listMcpTools({ group, tags }) のフィルターにも使われます。group は親フォルダーから自動的に推定されます。たとえば、server/mcp/tools/admin/delete-user.ts → group: 'admin' となります。明示的に指定した group が優先されます。
export default defineMcpTool({
group: 'admin',
tags: ['destructive', 'user-management'],
description: 'Delete a user account',
// ...
})
キャッシュ
export default defineMcpTool({
cache: '5m', // also accepts `Number` of ms or full Nitro cache options
description: 'Fetch weather (cached)',
inputSchema: { city: z.string() },
handler: async ({ city }) => $fetch(`/api/weather?city=${city}`),
})
表示条件(enabled)
リクエストごとにツールを非表示にできます。ミドルウェアの実行後に評価されるため、event.context が設定されています。
export default defineMcpTool({
enabled: event => Boolean(event.context.user),
description: 'List the current user’s todos',
handler: async () => listTodos(useEvent().context.user.id),
})
ツールの例 → を参照してください。
リソースを作成する
リソースは、URI で指定できる読み取り専用データを公開します。server/mcp/resources/ から自動検出されます。
ファイルのショートハンド(ハンドラー不要)
export default defineMcpResource({
description: 'Project README file',
file: 'README.md', // URI, MIME type, and handler auto-generated
})
標準リソース(カスタムハンドラー)
export default defineMcpResource({
description: 'Application config',
uri: 'config:///app',
metadata: { mimeType: 'application/json' },
handler: async (uri) => ({
contents: [{
uri: uri.toString(),
mimeType: 'application/json',
text: JSON.stringify({ env: process.env.NODE_ENV }, null, 2),
}],
}),
})
テンプレートリソース(URI 変数)
SDK の ResourceTemplate を uri として渡します。
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js'
export default defineMcpResource({
description: 'Fetch a user by ID',
uri: new ResourceTemplate('user:///{id}', { list: undefined }),
handler: async (uri, { id }) => {
const user = await getUser(String(id))
return {
contents: [{
uri: uri.toString(),
mimeType: 'application/json',
text: JSON.stringify(user, null, 2),
}],
}
},
})
メタデータとアノテーション
トップレベルの metadata フィールドを MIME タイプに、annotations を(対象ユーザー、優先度、最終更新日時に)使用します。
export default defineMcpResource({
description: 'Project README',
file: 'README.md',
metadata: {
mimeType: 'text/markdown',
annotations: {
audience: ['user', 'assistant'],
priority: 0.8,
lastModified: new Date().toISOString(),
},
},
})
リソースの例 →を参照してください。
プロンプトの作成
プロンプトは再利用可能なメッセージテンプレートです。server/mcp/prompts/ から自動検出されます。
シンプルな文字列プロンプト
export default defineMcpPrompt({
description: 'Code-review assistant',
handler: async () => 'You are a senior reviewer. Be concise and actionable.',
})
文字列は単一のユーザーメッセージにラップされます。代わりにアシスタントメッセージにラップするには、role: 'assistant' を設定します。
パラメーター化されたプロンプト
import { z } from 'zod'
export default defineMcpPrompt({
description: 'Generate a focused code review',
inputSchema: {
language: z.string().describe('Programming language'),
focus: z.enum(['performance', 'security', 'maintainability']).describe('Review focus'),
},
handler: async ({ language, focus }) =>
`Review my ${language} code. Focus on ${focus}.`,
})
completable() による引数の自動補完
prompts/complete をサポートするクライアントで動的な候補を表示します。
import { z } from 'zod'
export default defineMcpPrompt({
description: 'Open an issue for a project',
inputSchema: {
project: completable(z.string(), async (value) => {
const projects = await listProjects()
return projects.filter(p => p.startsWith(value)).slice(0, 5)
}).describe('Project name'),
},
handler: async ({ project }) => `Open an issue for ${project}.`,
})
完全な GetPromptResult(複数メッセージ)
export default defineMcpPrompt({
description: 'Structured debugging session',
inputSchema: { issue: z.string(), env: z.enum(['dev', 'staging', 'prod']) },
handler: async ({ issue, env }) => ({
messages: [
{ role: 'user', content: { type: 'text', text: `Debug in ${env}:\n${issue}` } },
{ role: 'assistant', content: { type: 'text', text: 'Let me analyze step by step:' } },
],
}),
})
::callout{icon="i-lucide-info" color="info"}
MCP の仕様で許可されているロールは user と assistant のみです。システム指示はユーザーメッセージのテキスト内に記述してください。
::
プロンプトの例 →を参照してください。
複数ハンドラーの構成
デフォルトでは、server/mcp/{tools,resources,prompts}/ 配下のすべてが /mcp に公開されます。異なるミドルウェアを使用する複数のエンドポイントに分割するには、フォルダー規約を使用します。
server/mcp/
├── tools/ # → /mcp (default handler)
└── handlers/
├── admin/
│ ├── index.ts # defineMcpHandler({ middleware: requireAdmin })
│ ├── tools/ # → /mcp/admin (auto)
│ └── prompts/
└── public/
├── index.ts
└── tools/ # → /mcp/public (auto)
export default defineMcpHandler({
description: 'Admin tools — destructive operations gated by Bearer auth.',
middleware: async (event) => {
const apiKey = getHeader(event, 'authorization')?.replace('Bearer ', '')
const user = apiKey ? await verifyAdmin(apiKey) : null
if (user) event.context.user = user
// No throw — let `enabled` guards on individual tools hide them when there's no user
},
// tools / resources / prompts omitted → folder convention auto-attaches them
})
mcp.defaultHandlerStrategy は、名前付きハンドラーが存在する場合に /mcp で公開する内容を制御します。
'orphans'(デフォルト) — 名前付きハンドラーに紐付けられていない定義のみ。'all'— 検出されたすべての定義(何でも含めるルート)。
横断的なフィルターには関数形式を使用します。
import { defineMcpHandler, getMcpTools } from '@nuxtjs/mcp-toolkit/server'
export default defineMcpHandler({
// Every tool tagged 'searchable', regardless of folder
tools: event => getMcpTools({ event, tags: ['searchable'] }),
})
クライアントは X-MCP-Tools(tools/list と一致する名前をカンマ区切りで指定)を送信することで、このエンドポイントが提供する内容をさらに絞り込めます。不明な名前が指定された場合、HTTP 400 が返されます。enabled() と mcp:config:resolved の適用後に自動で適用されるため、カスタムハンドラーは不要です。
ハンドラーのリファレンス →を参照してください。
ミドルウェア
ミドルウェアは defineMcpHandler に定義します(defineMcpMiddleware は別途存在しません)。MCP リクエストの前後に実行され、event.context に値を設定でき、自動的な next() に対応しています。
// Override the default `/mcp` handler with custom middleware
export default defineMcpHandler({
middleware: async (event) => {
// Soft auth: set context on success, never throw 401 from middleware
// (throwing 401 triggers OAuth discovery in MCP clients)
const apiKey = getHeader(event, 'authorization')?.replace('Bearer ', '')
if (apiKey) {
const user = await verifyApiKey(apiKey).catch(() => null)
if (user) event.context.user = user
}
},
})
::callout{icon="i-lucide-triangle-alert" color="warning"}
ミドルウェアから throw createError({ statusCode: 401 }) を実行しないでください。ほとんどのクライアントは、MCP ルートでの 401 を「このサーバーには OAuth が必要」と解釈し、通常のフローを停止します。明示的に拒否する場合は 403 を返すか、認証を緩やかに保ち、ユーザーがアクセスできない項目をツールごとの enabled ガードで非表示にしてください。
::
ロギングミドルウェア(計測に next() を使用)
import { extractToolNames } from '@nuxtjs/mcp-toolkit/server'
export default defineMcpHandler({
middleware: async (event, next) => {
const start = Date.now()
const response = await next()
const tools = await extractToolNames(event)
console.log(`[mcp] ${tools.join(',') || 'rpc'} took ${Date.now() - start}ms`)
return response
},
})
すべての MCP リクエストで構造化された詳細イベントを記録するには、console.log の代わりに useMcpLogger() を使用してください。mcp.tool、mcp.session_id、user.id などのタグが自動的に付与されます。
ミドルウェアのパターン →を参照してください。
Nitro ランタイムフック
MCP リクエストのライフサイクル中に、リクエストごとに実行される Nitro フックが 2 つあります。server/plugins/*.ts プラグインから登録すると、解決済みの設定を変更したり、任意の場所から SDK の McpServer インスタンスにアクセスしたりできます。defineMcpHandler を所有する必要はありません。例外をスローしたリスナーはログに記録され、リクエストは続行されます。
defineMcpHandler middleware → mcp:config:resolved → X-MCP-Tools allowlist → createMcpServer → mcp:server:created → transport
mcp:config:resolved — リクエストごとにツール/リソース/プロンプトを変更
動的リゾルバーと enabled(event) ガードの実行後、X-MCP-Tools の許可リストが適用される前、およびリクエストごとの McpServer が構築される前に実行されます。ctx.config を直接変更してください。
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('mcp:config:resolved', ({ config, event }) => {
if (!event.context.user) {
config.tools = config.tools.filter(t => !t.tags?.includes('admin'))
}
})
})
mcp:server:created — SDK サーバーへのアクセス
すべてのツール/リソース/プロンプトが登録された後、サーバーがトランスポートに接続される前に実行されます。SDK API を使用して定義を後から登録するか、低レベルのハンドラーには getSdkServer(server) を呼び出してください。
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('mcp:server:created', ({ server, event }) => {
server.registerTool(
'whoami',
{ description: 'Return the current user id' },
async () => ({
content: [{ type: 'text', text: String(event.context.userId ?? 'anonymous') }],
}),
)
})
})
フックのリファレンス →を参照してください。
セッション
ステートフルな MCP では、サーバーが Mcp-Session-Id を割り当て、同じセッション内のツール呼び出し間でデータを保持します。
export default defineNuxtConfig({
mcp: {
sessions: true, // or { maxDuration: 30 * 60_000, maxSessions: 1000 }
},
})
import { z } from 'zod'
export default defineMcpTool({
description: 'Remember a fact for this session',
inputSchema: { key: z.string(), value: z.string() },
handler: async ({ key, value }) => {
const session = useMcpSession<{ facts: Record<string, string> }>()
const facts = (await session.get('facts')) ?? {}
facts[key] = value
await session.set('facts', facts)
return `Remembered ${key}.`
},
})
セッションを使用すると、SSE ストリーミング、サーバーからクライアントへの通知、エリシテーションが可能になります。セッションのリファレンス →を参照してください。
Elicitation
リクエストの途中で、接続中のクライアントに構造化された入力を求めるか、ユーザーを URL に誘導します(MCP spec 2025-11-25)。
import { z } from 'zod'
export default defineMcpTool({
description: 'Create a release after asking for the channel',
inputSchema: { name: z.string() },
handler: async ({ name }) => {
const elicit = useMcpElicitation()
if (!elicit.supports('form')) {
return `Pass --channel via your client; "${name}" needs a release channel.`
}
const result = await elicit.form({
message: `Pick a channel for "${name}"`,
schema: {
channel: z.enum(['stable', 'beta']).describe('Release channel'),
notify: z.boolean().default(true),
},
})
if (result.action !== 'accept') return `Cancelled (${result.action}).`
return `Released ${name} on ${result.content.channel}.`
},
})
- フォームモード — Zod の raw shape を使用し、検証と型付けを行います。スキーマは、プリミティブ型、列挙型、または文字列列挙型の配列からなるフラットなオブジェクトである必要があります。
- URL モード —
elicit.url({ message, url })。仕様に従ってオプトインする必要があります。elicit.supports('url')で利用可否を確認してください。 - 確認 —
await elicit.confirm('Continue?')はbooleanを返します。 - エラー —
McpElicitationError(code: 'unsupported' | 'invalid-schema' | 'invalid-response')を捕捉し、適切にフォールバックします。
nitro.experimental.asyncContext: true と、elicitation 機能を宣言したクライアントが必要です。
elicitation のドキュメント →をご覧ください。
オブザーバビリティ — useMcpLogger()
チャネル分離型ロガーです。notify は接続中のクライアントに送信され、set / event / evlog は、オブザーバビリティが有効な場合にリクエストのevlog ワイドイベントに記録されます。
import { z } from 'zod'
export default defineMcpTool({
description: 'Charge a payment method',
inputSchema: { userId: z.string(), amount: z.number().int().positive() },
annotations: { destructiveHint: true, idempotentHint: false },
handler: async ({ userId, amount }) => {
const log = useMcpLogger('billing')
log.set({ billing: { amount } })
await log.notify.info({ msg: 'starting charge', amount })
try {
const receipt = await chargeCard(userId, amount)
log.event('charge_completed', { receiptId: receipt.id })
return `Charged ${amount}. Receipt: ${receipt.id}`
}
catch (err) {
log.evlog.error('charge failed', err as Error)
throw err
}
},
})
- クライアントチャネル(
log.notify):notify(level, data, logger?)と.debug/.info/.warning/.error。常に解決され、例外はスローされません。 - サーバーチャネル:
set(fields)/event(name, fields?)/setUser({ id, email, name })/setSession({ id })/evlog。無効な場合はMcpObservabilityNotEnabledErrorをスローします。 - ワイドイベントには次のタグが自動的に付与されます:
mcp.method、mcp.tool、mcp.session_id、mcp.request_id、service: '<env.service>/mcp'。ミドルウェアがevent.context.user/event.context.sessionを設定すると、user.*/session.*も自動的に引き継がれます。
セットアップ
::code-group
pnpm add evlog
npm install evlog
::
export default defineNuxtConfig({
modules: ['evlog/nuxt', '@nuxtjs/mcp-toolkit'],
evlog: { env: { service: 'my-app' } },
})
mcp.logging:省略(自動検出)、true(evlog/nuxt が登録されていることを前提とします)、false(オプトアウト)。
ドレイン
1 つの Nitro プラグインで、すべての MCP ワイドイベントを Axiom、Sentry、OTLP、HyperDX、Datadog、Better Stack、または PostHog に送信できます。
import { createAxiomDrain } from 'evlog/adapters/axiom'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
})
ログのドキュメント →とevlog.dev →をご覧ください。
MCP Apps(インタラクティブ UI ウィジェット)
app/mcp/ に Vue Single-File Components を作成すると、MCP ツールハンドラーをバックエンドとするインタラクティブなウィジェットとして、MCP-Apps 対応ホスト(ChatGPT、Cursor)に配信されます。
<script setup lang="ts">
import { z } from 'zod'
defineMcpApp({
description: 'Pick a colour and preview a palette.',
inputSchema: { base: z.string().describe('Hex colour, e.g. #2563eb') },
handler: async ({ base }) => ({
structuredContent: await $fetch('/api/palette', { query: { base } }),
}),
})
const { data, sendPrompt } = useMcpApp<{ swatches: { name: string, hex: string }[] }>()
</script>
<template>
<div class="grid grid-cols-3 gap-2">
<button
v-for="s in data?.swatches"
:key="s.hex"
class="rounded-md p-3 text-white"
:style="{ background: s.hex }"
@click="sendPrompt(`Use ${s.name}`)"
>
{{ s.name }}
</button>
</div>
</template>
各 SFC は、ツール、ui://mcp-app/<name> の UI リソース、および単一ファイルの HTML バンドルになります。ハンドラーはサーバーサイドで実行され、structuredContent は HTML にインライン化されるため、iframe は初回描画時から完全なデータを利用できます。
useMcpApp<T>() は data、loading、error、hostContext、callTool(name, params)、sendPrompt(prompt)、openLink(url) を公開します。
CSP はデフォルトで厳格です。追加のオリジンを許可するには、明示的に設定してください。
defineMcpApp({
csp: {
resourceDomains: ['https://images.example.com'], // <img>, <style>, fonts
connectDomains: ['https://api.example.com'], // fetch / XHR / WebSocket
},
// ...
})
MCP Apps リファレンス →をご覧ください。
サーバーメタデータ
クライアント UI でサーバーを識別し、運用上の指示で LLM を誘導します。
export default defineNuxtConfig({
mcp: {
name: 'Todos MCP',
description: 'Read and update todos for the current user.', // shown in client UIs
instructions: 'Always call list-todos before create-todo. Group results by status.', // injected into the LLM system prompt
icons: [
{ src: 'https://example.com/icon.png', mimeType: 'image/png', sizes: ['64x64'] },
],
},
})
エンドポイントごとに別の ID が必要な場合(たとえば、独自の説明とアイコンを持つ /mcp/admin)は、ハンドラー単位で上書きできます。
エージェントの検出(llms.txt)
nuxt-llms がこのモジュールとともに登録されている場合、ツールキットは /llms.txt に ## MCP Server セクションを追加し、エンドポイントを示します。これにより、llms.txt を通じてサイトを検出したエージェントは、URL を手動で設定しなくても接続できます。設定は不要です。
export default defineNuxtConfig({
modules: ['@nuxtjs/mcp-toolkit', 'nuxt-llms'],
llms: { domain: 'https://example.com', title: 'Example' },
mcp: { name: 'Example MCP', description: 'Query Example data from your agent.' },
})
このセクションでは、URL に llms.domain + mcp.route、ラベルに mcp.name、セクションの説明に mcp.description を使用します。また、mcp.browserRedirect が設定されている場合は、ドキュメントへのリンクとして追加します。オプトアウトするには mcp.llms: false を設定してください。自分でタイトルを MCP Server にしたセクションがある場合は、常にそちらが優先されます。nuxt-llms がインストールされていない場合は何も行いません。
これは検出可能性のための慣例であり、MCP 仕様の一部ではありません。仕様に定められた検出方法(AI Catalog / Server Cards)は、引き続きドラフトです。
一覧定義(カタログを確認する)
listMcp* を使って概要を公開し(カタログのエンドポイント)、getMcp* を使ってハンドラーに渡す生の定義を取得します。
import { listMcpDefinitions } from '@nuxtjs/mcp-toolkit/server'
export default defineEventHandler(async (event) => {
const { tools, resources, prompts } = await listMcpDefinitions({ event })
return {
name: 'My MCP Server',
tools: tools.map(t => ({ name: t.name, description: t.description })),
resources: resources.map(r => ({ name: r.name, uri: r.uri })),
prompts: prompts.map(p => ({ name: p.name, description: p.description })),
}
})
フィルター:event(enabled ガードを適用)、group、tags、handler、orphansOnly。リクエストに表示される内容と正確に一致させるには、event を渡します。
コードモード(実験的)
ハンドラーが公開するすべてのツールを単一の code ツールでラップします。LLM がツールを呼び出す JavaScript を codemode.* 経由で記述し、secure-exec を介したセキュアな V8 アイソレート内で実行します。
export default defineMcpHandler({
experimental_codeMode: {
progressive: true,
memoryLimit: 128, // MB
cpuTimeLimitMs: 5000,
maxToolCalls: 20,
},
})
npm install secure-exec
LLM が 1 回のラウンドトリップで多数のツール呼び出しをオーケストレーションするのに便利です。Cloudflare Workers ではサポートされていません(明確なランタイムエラーが返されます)。
コードモードのドキュメント → を参照してください。
レビューとベストプラクティス
server/mcp/** をレビューまたはモダナイズする際は、次のチェックリストを確認してください。
ツール
✅ 完全な CallToolResult ではなく、直接返す(string、オブジェクト、配列)
✅ 失敗時は throw createError({ statusCode, message }) を使う(捕捉され、isError として返されます)
✅ すべての Zod フィールドに .describe() を設定する
✅ annotations(readOnlyHint、destructiveHint、idempotentHint、openWorldHint)を正直に設定する
✅ 複雑なスキーマには inputExamples を設定する
✅ リクエストごとにツールを非表示にするには enabled: event => Boolean(event.context.user) を設定する
✅ コストの高い冪等な操作には cache: '5m'(または Nitro の全オプション)を設定する
✅ useEvent()/useMcpServer()/useMcpSession()/useMcpLogger()/useMcpElicitation() の呼び出し時は nitro.experimental.asyncContext: true を設定する
❌ textResult/jsonResult/errorResult(非推奨です。直接返すか、例外をスローしてください)
❌ defineMcpMiddleware(存在しません。ミドルウェアは defineMcpHandler のフィールドです)
❌ ミドルウェアから 401 をスローする(クライアントで OAuth ディスカバリーが起動します。403 を返すか、ソフト認証と enabled ガードを使用してください)
❌ 一般的な説明、await の欠落、未検証の入力、公開されたシークレット
リソース
✅ mimeType はトップレベルではなく metadata に設定する
✅ 静的ファイルには file: 'README.md' の省略記法を使う(URI、MIME、ハンドラーが自動生成されます)
✅ パラメーター付き URI には ResourceTemplate を使う
✅ audience/priority/lastModified には metadata.annotations を使う
❌ 巨大なペイロードを返す。テンプレートでページネーションしてください ❌ MIME タイプを省略する。クライアントは正しく表示するためにこれを使用します
プロンプト
✅ ユーザーメッセージが 1 つで十分な場合は、プレーンな string を返す(role フィールドが使用され、デフォルトは 'user' です)
✅ 引数のオートコンプリートには completable(z.string(), async value => suggestions) を使う
✅ 各プロンプトの目的は 1 つに絞る
❌ user メッセージの外にシステム指示を混在させる(仕様では system ロールが拒否されます)
ハンドラー
✅ 手動の tools: [...] 配列より、フォルダー規約(server/mcp/handlers/<name>/)を使う
✅ ハンドラーごとに役割を 1 つにする(admin/public/apps)。適切な defaultHandlerStrategy を選ぶ
✅ エンドポイントに独自の識別情報がある場合は、ハンドラーごとに description/instructions/icons を設定する
トラブルシューティング
ツール/リソース/プロンプトが検出されない
- ファイルは
server/mcp/{tools,resources,prompts}/(またはhandlers/<name>/サブフォルダー内)にありますか? - 名前付きエクスポートではなく、
export defaultを使っていますか? - 開発サーバーを再起動して、
pnpm nuxt prepareを実行してください。
自動インポートが機能しない
nuxt.config.tsのmodulesに'@nuxtjs/mcp-toolkit'が指定されていますか?- 型スタブを再生成するため、
pnpm dev:prepare(またはpnpm nuxt prepare)を実行してください。 - IDE で TypeScript サーバーを再起動してください。
mcp.autoImports: falseを設定している場合は、@nuxtjs/mcp-toolkit/serverから明示的にインポートしてください。
エンドポイントにアクセスできない
curl -X POST http://localhost:3000/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
JSON-RPC レスポンスが返されるはずです。404 の場合は、mcp.enabled !== false と mcp.route を確認してください。
useEvent() / useMcpServer() / useMcpSession() が「no async context」をスローする
nuxt.config.ts に nitro.experimental.asyncContext: true を追加してください。これらのコンポーザブルは、Nitro の非同期ローカルストレージから現在のイベントを読み取ります。これは asyncContext が有効な場合にのみ機能します(Nuxt 3.8 以降ではデフォルトで有効です)。
IDE または vue-tsc がこれらのいずれかについて Promise<…> と推論する場合(既知の自動インポートの不具合です)、念のため呼び出しに await を付けてください。どちらでも実行時には動作します:const mcp = await useMcpServer()。
クライアントがリクエストのたびに OAuth フローを開始する
ミドルウェアから 401 をスローしています。ソフト認証に切り替え、認証に成功した場合は event.context.user を設定し、ハード拒否の場合は 403 を返し、enabled: event => Boolean(event.context.user) を使ってツールごとに制御してください。
ブラウザクライアントで Origin チェックに拒否される
mcp.security.allowedOrigins を設定してください:
mcp: {
security: {
allowedOrigins: ['https://my-app.vercel.app'],
// allowedOrigins: '*' // explicit opt-out — use with care
},
}
トラブルシューティングのリファレンス →を参照してください。
Eval によるテスト
Evalite を使って、プロンプトに適したツールの選択を評価します:
pnpm add -D evalite vitest @ai-sdk/mcp ai
{
"scripts": {
"eval": "evalite",
"eval:ui": "evalite watch"
}
}
import { experimental_createMCPClient as createMCPClient } from '@ai-sdk/mcp'
import { generateText } from 'ai'
import { evalite } from 'evalite'
import { toolCallAccuracy } from 'evalite/scorers'
evalite('MCP Tool Selection', {
data: async () => [
{
input: 'Calculate BMI for 70kg 1.75m',
expected: [{ toolName: 'bmi-calculator', input: { weight: 70, height: 1.75 } }],
},
],
task: async (input) => {
const mcp = await createMCPClient({
transport: { type: 'http', url: 'http://localhost:3000/mcp' },
})
try {
const result = await generateText({
model: 'openai/gpt-4o',
prompt: input,
tools: await mcp.tools(),
})
return result.toolCalls ?? []
}
finally {
await mcp.close()
}
},
scorers: [
({ output, expected }) => toolCallAccuracy({ actualCalls: output, expectedCalls: expected }),
],
})
実行します:
pnpm dev # in one terminal
pnpm eval # in another, or `pnpm eval:ui` for the watcher UI
テストのリファレンス →を参照してください。
クイックリファレンス
設定
export default defineNuxtConfig({
modules: ['@nuxtjs/mcp-toolkit'],
mcp: {
name: 'My Server',
description: 'What this server does',
instructions: 'How the LLM should use it',
route: '/mcp',
sessions: true,
defaultHandlerStrategy: 'orphans', // or 'all'
security: { allowedOrigins: ['https://my-app.vercel.app'] },
logging: true, // requires evlog/nuxt
llms: true, // advertise in /llms.txt when nuxt-llms is registered
},
nitro: { experimental: { asyncContext: true } },
})
サーバーサイド API(自動インポート)
| ヘルパー | 目的 |
|---|---|
defineMcpTool / defineMcpResource / defineMcpPrompt | 定義を宣言します(自動検出されます)。 |
defineMcpHandler | ミドルウェアや動的ツールを使うカスタムハンドラーです。 |
defineMcpApp(app/mcp/*.vue 内) | インタラクティブな Vue ウィジェットです。 |
imageResult / audioResult | バイナリコンテンツをツールレスポンスにラップします。 |
completable | プロンプトの引数を自動補完します。 |
extractToolNames | ミドルウェアで JSON-RPC ボディからツール名を解析します。 |
useMcpServer() | セッション中にツールを登録/登録解除します。 |
useMcpSession<T>() | セッションごとのストレージです(セッションを有効にする必要があります)。 |
useMcpLogger(name?) | クライアント通知とサーバーサイドの広範なイベントを扱います。 |
useMcpElicitation() | クライアントにフォーム/URL/確認のプロンプトを表示します。 |
useMcpApp<T>()(MCP App SFC 内) | リアクティブな data と callTool / sendPrompt ブリッジです。 |
listMcpTools / listMcpResources / listMcpPrompts / listMcpDefinitions | JSON フレンドリーな概要です(カタログエンドポイント用)。 |
getMcpTools / getMcpResources / getMcpPrompts | 生の定義です(ハンドラーに渡し直すために使います)。 |
getSdkServer | McpServer から低レベル SDK の Server にアクセスします(上級者向け)。 |
Nitro フック
| フック | 実行タイミング |
|---|---|
mcp:config:resolved | リクエストごとに、動的リゾルバーの後で実行されます。config.tools / resources / prompts / instructions / icons / name を変更します。X-MCP-Tools はこのフックの後に適用されます。 |
mcp:server:created | リクエストごとに、すべての定義の登録後に実行されます。server.registerTool(...)、getSdkServer(server).setRequestHandler(...) などを呼び出します。 |
デバッグ
- DevTools:Shift+Alt+D → MCP Inspector タブ → Launch Inspector(新しいブラウザータブで開きます)。
- CLI Inspector:
npx @modelcontextprotocol/inspector http://localhost:3000/mcp - curl スモークテスト:
curl -X POST … -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
認証が必要なエンドポイントの場合は、DevTools のランチャーにヘッダーを渡してください:
const token = process.env.MCP_TOKEN
export default defineNuxtConfig({
mcp: {
inspector: {
headers: { Authorization: token ? `Bearer ${token}` : undefined },
},
},
})