1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MCPのTools/Resources/Promptsを使い分ける — 3機能の実装比較【TypeScript】

1
Posted at

はじめに

当社(合同会社ジョインクラス)は、launchd の17ジョブと Claude Code で社内業務の約98%を自動で回しています。月のAI運用コストは約¥20,000です。その中で毎日動いているのが、note の記事統計やマガジン管理を担う自作の MCP サーバー note-api です。

最初に作ったとき、僕は全部を Tools として実装しました。動きはしました。ただ、読むだけの処理まで「ツール呼び出し」になったので、--allowedTools の管理が面倒になり、プロンプトもスクリプトごとに少しずつ違う形でコピーされていきました。

MCP には Tools / Resources / Prompts の3つの機能があります。この記事では、実際に動いている構成をもとに、次の内容を手順付きで書きます。

  • 3機能をそれぞれどんな場面で使うか(判断基準)
  • TypeScript SDK での最小実装
  • Claude Code からの呼び出しと検証方法
  • 実際にはまった落とし穴

3機能の違い(結論)

機能 誰が使うか決める 副作用 向いている処理
Tools モデル(LLM) あり得る 投稿、追加、更新、外部APIの実行
Resources クライアント/ユーザー なし(読むだけ) 設定値、統計、ドキュメントなどのコンテキスト
Prompts ユーザー(スラッシュコマンド) なし 定型タスクの指示テンプレート

判断に使う問いは1つだけです。「それはモデルに勝手に実行させてよい"動詞"か?」

  • 動詞で、副作用がある → Tool
  • 名詞で、読むだけ → Resource
  • 人間が毎回同じ依頼文を書いている → Prompt

実際の運用:Tools は cron から呼ばれている

当社では、launchd から起動するシェルスクリプトが Claude Code を headless で動かし、MCP の Tools を呼んでいます。以下は実際に動いているスクリプトの抜粋です(.company/scripts/note/add-notes-to-magazine.sh)。

# Claude Code CLI で MCP API を実行
claude --effort low \
  --allowedTools "mcp__note-api__get-my-notes,mcp__note-api__add-magazine-note" \
  -p "
noteアカウント joinclass の全公開記事を取得し、以下の12記事**以外**の記事を全てマガジン「${MAGAZINE_ID}」に追加してください。
...
"

もう1本は毎朝9:00に動くマーケ部門の日次レポートです(auto-dept-marketing.sh)。

RESULT=$(claude -p "
1. note-api MCPを使って最新の記事の統計を確認(ビュー数、スキ数)
...
" --allowedTools "Read,Bash,mcp__note-api*" 2>/dev/null | tail -20)

ここが改善のきっかけでした。後者は読むだけなのに mcp__note-api* とワイルドカードで全ツールを許可しています。つまり、統計を見るだけのジョブが、理屈の上では add-magazine-note も実行できる状態でした。

ここから「書き込みは Tools、読み取りは Resources、定型の依頼は Prompts」という分け方に整理しました。

手順1:プロジェクト作成

mkdir note-mcp && cd note-mcp
npm init -y
npm i @modelcontextprotocol/sdk zod
npm i -D typescript @types/node tsx
npx tsc --init --target es2022 --module nodenext --moduleResolution nodenext --outDir dist

package.json に "type": "module" を追加します。

手順2:3機能を1つのサーバーに実装する

src/server.ts
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { fetchMyNotes, fetchNoteStats, addToMagazine } from "./note-client.js";

const server = new McpServer({ name: "note-api", version: "1.0.0" });

// ---------- Tools:副作用のある"動詞" ----------
server.registerTool(
  "add-magazine-note",
  {
    title: "マガジンに記事を追加",
    description: "指定した記事をマガジンに追加する(書き込み操作)",
    inputSchema: {
      magazineId: z.string().describe("例: m87293889dd94"),
      noteId: z.string().describe("例: n57c7f1dc35d5"),
    },
  },
  async ({ magazineId, noteId }) => {
    await addToMagazine(magazineId, noteId);
    return { content: [{ type: "text", text: `added ${noteId} -> ${magazineId}` }] };
  }
);

// ---------- Resources:読むだけの"名詞" ----------
// 固定URI:自分の記事一覧
server.registerResource(
  "my-notes",
  "note://me/notes",
  { title: "自分の公開記事一覧", mimeType: "application/json" },
  async (uri) => ({
    contents: [{ uri: uri.href, text: JSON.stringify(await fetchMyNotes()) }],
  })
);

// テンプレートURI:記事ごとの統計
server.registerResource(
  "note-stats",
  new ResourceTemplate("note://notes/{noteId}/stats", { list: undefined }),
  { title: "記事統計(ビュー・スキ)", mimeType: "application/json" },
  async (uri, { noteId }) => ({
    contents: [{ uri: uri.href, text: JSON.stringify(await fetchNoteStats(String(noteId))) }],
  })
);

// ---------- Prompts:定型依頼のテンプレート ----------
server.registerPrompt(
  "daily-report",
  {
    title: "マーケ日次レポート",
    description: "記事統計から日次レポートを作る",
    argsSchema: { days: z.string().optional() },
  },
  ({ days }) => ({
    messages: [
      {
        role: "user",
        content: {
          type: "text",
          text: [
            `直近${days ?? "7"}日のnote記事について、note://me/notes を読み、`,
            "ビュー数・スキ数の上位3件と前週比を報告してください。",
            "書き込み系ツールは使わないこと。",
          ].join("\n"),
        },
      },
    ],
  })
);

await server.connect(new StdioServerTransport());

note-client.ts は note の API を叩く薄いラッパーなので省略します。自社APIや社内DBに置き換えても、構造はそのまま使えます。

手順3:Claude Code に登録する

npx tsc
claude mcp add note-api -- node /absolute/path/to/note-mcp/dist/server.js
claude mcp list   # note-api: ✓ Connected になればOK

Claude Code の中から使う方法はそれぞれ違います。

  • Tools:自然文で頼むと、モデルが mcp__note-api__add-magazine-note を選んで呼ぶ
  • Resources:プロンプトに @note-api:note://me/notes と書くと内容が添付される
  • Prompts:/mcp__note-api__daily-report 14 のようにスラッシュコマンドとして呼ぶ

手順4:Inspector で検証する

Claude Code に組み込む前に、MCP Inspector で3機能をそれぞれ単体で確認します。

npx @modelcontextprotocol/inspector node dist/server.js

ブラウザで開いたら、次の3点を確認してください。

  1. Tools タブ:add-magazine-note の inputSchema が表示され、実行できる
  2. Resources タブ:note://me/notes が一覧に出る。テンプレートのほうは Resource Templates 側に出る
  3. Prompts タブ:daily-report に引数 days を渡すと、展開後のメッセージが表示される

headless(cron)運用での使い分け

当社のように claude -p で無人実行する場合、効いてくるのは権限の絞り込みです。読み取りを Resources と Prompts に寄せると、日次レポートのジョブは次のように書けます(当社で移行を進めている形です)。

claude -p "/mcp__note-api__daily-report 7" \
  --allowedTools "Read" 2>/dev/null | tail -20

書き込みツールを許可リストから外せたので、レポート用のジョブが誤ってマガジンを書き換える可能性が、設定の段階でなくなります。書き込みが必要なジョブだけ mcp__note-api__add-magazine-note を名前で明示します。ワイルドカード mcp__note-api* はもう使いません。

はまった落とし穴

1. Resource を「モデルが勝手に読む」と思い込む
Resources は基本的にクライアント側から添付するものです。モデルが自分で取りに行くことを期待すると、何も読まれずに推測で答えが返ってきます。headless で確実に読ませたいなら、Prompt の本文で URI を指定するか、Resource 化せず読み取り専用の Tool として残してください。

2. Tool の description に「読み取り専用」と書けば安全だと思う
description はモデルへのヒントにすぎず、制約にはなりません。安全性は --allowedTools と機能の分け方で担保します。

3. Prompt の引数はすべて文字列
argsSchema に z.number() を書くとクライアントによっては失敗します。数値は文字列で受け取り、本文側で解釈するようにします(上の days がその例です)。

4. stdio サーバーで console.log を使う
stdout は JSON-RPC の通信路なので、ログを出すとプロトコルが壊れて Connected になりません。ログは console.error に出します。

5. 相対パスで claude mcp add する
launchd から起動すると作業ディレクトリが違うため、サーバーが見つからなくなります。パスは必ず絶対パスで書きます。

チェックリスト

  • 副作用のある処理は Tool だけになっているか
  • 読むだけの処理を Tool にしていないか(Resource にできないか)
  • 同じ依頼文を2回以上書いていたら Prompt にしたか
  • cron ジョブの --allowedTools がワイルドカードになっていないか
  • Inspector で3タブすべてを確認したか

おわりに

僕の経営上の方針は「AIは実行に使う。判断は人間。」です。MCP の3機能はこの分担にそのまま当てはまります。Tools は AI に任せる実行、Resources は判断材料、Prompts は人間が決めた依頼の型です。分け方が決まると、権限設計もプロンプトの管理もかなり楽になります。

まずは手元の MCP サーバーで、「読むだけなのに Tool になっているもの」を1つ探して Resource に移すところから始めてみてください。


もっと深く学びたい方へ

MCP サーバーの設計、実装、テスト、Claude Code との連携までを体系的にまとめた書籍を出しています。

📘 『Claude Code × MCP サーバー開発入門』
Tools/Resources/Prompts の設計パターン、認証、エラーハンドリング、運用時の権限設計まで解説しています。

書籍一覧はこちら → https://zenn.dev/joinclass?tab=books

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?