🧠

今流行りのMCPサーバーとは? 理解~自作MCPサーバー・クライアントのコード例まで

に公開
2025/12/07

はじめに

本稿では、MCP(Model Context Protcol)サーバーについて全く知らない人でも、MCPサーバーを用いてできることやメリットを学ぶことを目標としています。「まだ、MCPについて学べていない...」「結局MCPサーバーってよくわからない」という方のお力になれたら幸いです。

また、後半では自作のMCPサーバーを用いて作成した成果物やその構築方法を紹介していきます。自作MCPクライアントの構築も実施してみたので、是非見て頂ければ嬉しいです!

私たちの研究室(Nislab)

https://nisk.doshisha.ac.jp/

アドベントカレンダー 3 日目~

https://nislab-advendcallender-2025.vercel.app/
https://zenn.dev/nislab/articles/2025-advendcaldenr

MCPサーバーとは?

初学者の方が読むことも想定して「MCPサーバーとは何なのか?」について丁寧に説明していきます。

MCPサーバーは、AI(LLM)が外部にあるツールやデータソースとやり取りするための標準プロトコルです。簡潔に言うと、AIが「外部ツール」を使うための手段で、その書き方・ルールを定めたのがMCP(Model Context Protocol)ということになります。AI(LLM)単体では実現できない「データベースへのアクセス」や「API利用」など様々な機能を、MCPサーバーとして実装するだけで、AI(LLM)が実行の選択肢として選べるようになるということです。

ユースケース

具体的には以下のようなことができるようになります。
※以下は理論上可能であるもので、実際に実装できるかを確認していません🙇

  • Slackの○○channelで発生している議論を要約する
  • GitHubで新しいissueを作成する(LLMとの議論内容を反映する)
  • DBから売上データを取得 → BIツールでグラフ生成 → Teamsにレポートを送信
  • Jiraから未完了タスクを取得 → 優先度を再計算してガントチャート生成 → Slackに進捗レポートを送信
  • Google Calendarから会議予定を取得 → Zoom APIで自動的に会議URLを生成 → 参加者へメール送信

このように様々なツールとAIの橋渡しを行うのがMCPサーバーの責務となります。また、複数のツールとの連携をサポートすることもできるというのが大きな強みです。MCPサーバーの登場により実装の自由度や難易度が大きく改善し、拡張性の高い仕組みを短時間で構築できます。

MCPの三層構造

MCPを理解し、構築するためにはMCP Host/Client/Server について理解する必要があります。

  1. MCP Host
    AI(LLM)が動作する環境に相当します。ユーザーとの対話やMCPサーバーで用意したtoolの利用判断などを行います
  2. MCP Client
    Host側に位置し、MCPサーバーとの実際の通信を担います。1つのMCPサーバーに対して専用のClientが接続し、リクエストとレスポンスのやり取りを仲介します。実際にはJSON-RPCという形式を基盤にした形式で通信を行います。
  3. MCP Server
    外部サービスやデータソースを実際に操作する役割を担います。SlackやGitHub、データベースなどをMCPサーバーとして実装することで、AIは標準化された方法でそれらの機能を利用できます。

ユーザーがAI(LLM)に指示を与える → MCP Hostがユーザーの意図を理解し利用するtoolを選択 → 利用するtoolに対応するMCP Clientで対応するMCP Serverを呼び出す

この一連の流れによって、AIは外部サービスを統一的かつ安全に利用できるようになります。

MCPサーバーについての勘違い

ここからは、私が初見で勘違いしたものを紹介していきます

  1. MCP「サーバー」ってサーバー?
    MCPサーバーというくらいだから、物理サーバーや仮想サーバーのようなものを想像してしまう人も多いと思います。しかし、MCPサーバーはそういう意味ではありません。ここでいう「サーバー」は、外部サービスやデータソースをAIが扱えるようにするための“窓口”や“ラッパー”のことです。APIやデータベースなどをMCPのルールに従って包み込んだものが「MCPサーバー」であり、物理的なサーバーを新しく立てるというものではありません!
  2. AIってもともと外部ツールを使えるのでは?
    「ChatGPTは検索エンジンを勝手に使っている」「GitHub Copilotはコードに直接干渉してくる」これらは外部ツールを使っているのでは?と思われた方もいると思います。これは大方間違いではありません。確かにこれらの生成AIでは外部ツールを用いていますが、OpenAIやMicrosoftなどのサービス提供者が内部で独自に組み込んだ専用ツールであるということを意識する必要があります。私たちのような生成AIの利用者が自分の手で追加・変更する余地は非常に限定されていて、自由度は高くありません。MCP の最大の特徴は 「ユーザー自身が自由にツールを追加し、AIに使わせられる」 という点にあります。

実装したもの

ここからは実装パートです。今回の記事はハンズオンではないので、成果物やその手順を簡易的に説明させていただきます!
私はMCPを用いて、書評AIを作成しました。開発理由・実装動機としては以下の通りです。

  • 書評のベースをAIに書かせたい!
  • ChatGPTを用いて書評を書くと、当たり障りのないことを書いてしまう!(レビューや要約を参考に書いて言って欲しい)
  • ChatGPTを用いて書評を書くと、書き方が画一的になってしまう!(プロンプトを工夫しないと文章に個性が出ない)

このような動機は社内ブログを書くような会社など様々なケースで起こりうると思います。もちろんアウトプットの重要性については語るまでもないと思うので、時間が無い時などに使う想定です笑(この記事についても純度100%の人間が書いています…)。
以下が今回のシステムの概要図になります。

gRPCやWebSocketなどを用いていますが、MCPについて書きたいので本稿では触れません。ご容赦ください...

MCP ClientとServerの実装

MCPについての記事なので、その部分の実装についてのみ書かせていただこうと思います。この開発ではModelContextProtocolというライブラリを使っています

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

まずは、MCPサーバーです。以下のように外部ツールをtoolとして登録していきます。toolの登録には"ツール名","スキーマ","AIへのツール説明","実際の関数"という形式で並びます。サーバーとクライアント間の通信は先ほども触れた通りJSON-RPCという形式が基盤となっています。これについては深く触れませんが、実装の際には是非調べてみてください!

const updatePromptParams = {
  userId: z.string(),
  content: z.string(),
};

export async function startMCPServer() {
    const server = new McpServer({
        name: 'book-review-mcp',
        version: '1.0.0',
        description: 'MCP server for Book Review AI',
    });

    server.tool(
        'create_prompt',
        updatePromptParams,
        { title: "ユーザーが書いた書評の書き方をまとめた文章特徴プロンプトをユーザーと紐づけて新規保存します" },
        async ({ userId, content }) => {
            try {
                const res = await new Promise<any>((resolve, reject) => {
                    grpcClient.UpdatePrompt({ userId, content }, (err: any, res: any) => {
                        if(err) {
                            console.error("❌ gRPC UpdatePrompt error:", err);
                            return reject(err);
                        }
                        resolve({ success: res.success });
                    })
                })
                const responseContent = Array.isArray(res.success)
                    ? res.success.map((c: string) => ({ type: "text", text: c }))
                    : [{ type: "text", text: String(res.success) }];
                return { content: responseContent };                
            } catch (err) {
                return {
                    content: [
                        {
                            type: "text",
                            text: `Error: ${ String(err) }`,
                        },
                    ],
                    isError: true,
                };  
            }
        }
    );
    const transport = new StdioServerTransport();
    await server.connect(transport);
}

続いてMCPクライアント側です。まずは、MCPクライアントクラスを定義します

export class MCPClient {
  private mcp: Client;
  private anthropic: Anthropic;
  private transport: StdioClientTransport | null = null;
  private tools: Tool[] = [];

  private rl: readline.Interface | null = null;

  constructor() {
    this.anthropic = anthropic;
    this.mcp = new Client({ name: "mcp-client-cli", version: "1.0.0" });
  }
...

このクライアントを先ほど作ったサーバーと繋げられるようにクラス内に接続を定義していきます。

  async connectToServer(serverScriptPath: string) {
    if (!serverScriptPath.endsWith(".js")) throw new Error("Server script must be a .js file");

    const command = process.execPath;
    this.transport = new StdioClientTransport({ command, args: [serverScriptPath] });
    await this.mcp.connect(this.transport);

    const toolsResult = await this.mcp.listTools();
    this.tools = toolsResult.tools.map((tool) => ({
      name: tool.name,
      description: tool.description ?? "",
      input_schema: { ...tool.inputSchema, required: tool.inputSchema.required ?? null },
    }));

    console.log("✅ Connected to MCP server with tools:", this.tools.map((t) => t.name));
  }

残りはMCPクライアントやMCPホストの動きを定義していきます。テキストの受け取りやMCPホストへのクエリの投げ方など要件に合わせて作ってください。以下の例は汎用的なクエリ対応です。

  async processQuery(query: string) {
    console.log("[debug] Query:", query);
    console.log("[debug] Tools:", this.tools.map((t) => t.name));

    const messages: MessageParam[] = [
      { role: "assistant", content: system_prompt },
      { role: "user", content: query },
    ];

    let response;
    try {
      response = await this.anthropic.messages.create({
        model: "claude-3-haiku-20240307",
        max_tokens: 4000,
        messages,
        tools: this.tools,
      });
    } catch (err) {
      console.error("[debug] Error calling Claude API:", err);
      return "⚠️ Claude API did not return a response";
    }

    const finalText: string[] = [];
    const MAX_ROUNDS = 10;
    let rounds = 0;

    while (response.content.some((c) => c.type === "tool_use")) {
      if (++rounds > MAX_ROUNDS) break;

      for (const content of response.content) {
        if (content.type === "text") {
          finalText.push(content.text);
        } else if (content.type === "tool_use") {
          const toolName = content.name;
          const toolArgs = content.input;

          messages.push({ role: "assistant", content: [content] });

          try {
            const result = await this.mcp.callTool({
              name: toolName,
              arguments: { ...(toolArgs as Record<string, unknown> || {}) },
            });

            const toolResultContent =
              typeof result.content === "string"
                ? result.content
                : JSON.stringify(result.content);

            messages.push({
              role: "user",
              content: [{ type: "tool_result", tool_use_id: content.id, content: toolResultContent }],
            });

            response = await this.anthropic.messages.create({
              model: "claude-3-haiku-20240307",
              max_tokens: 4000,
              messages,
              tools: this.tools,
            });

            for (const block of response.content ?? []) {
              if (block.type === "text" && block.text) finalText.push(block.text);
            }
          } catch (err) {
            finalText.push(`❌ Tool "${toolName}" failed: ${err}`);
          }
        }
      }
    }

    return finalText.join("\n");
  }

これで基本的な構成は完了です。後は煮るなり焼くなりして、目的に合うように拡張していってください。私が今回作成した書評AIではLangChainという技術を用いて出力の精度を上げています。こういった工夫をするのもシステム開発の醍醐味ですよね!!

完成品



実際に生成された書評
# 世界一流の仕事術に学ぶ、生産性革命への道筋

牛尾剛氏の『世界一流エンジニアの思考法』は、日本のエンジニア文化に一石を投じる極めて実践的な指南書である。米マイクロソフトという世界最高峰の環境で培われた思考法を体系化した本書は、単なる技術書を超えて、現代の知識労働者すべてに通用する生産性向上の哲学を提示している。

## 各章が描く一流への道程

第一章では、世界一流エンジニアの根本的な違いとして「試行錯誤の排除」という概念が提示される。障害調査においても、闇雲に手を動かすのではなく、仮説を立てて証明していく科学的アプローチの重要性が説かれている。これは日本の現場でよく見られる「とりあえず動かしてみる」文化との明確な対比を示している。

第二章で紹介される「Be Lazy(怠惰であれ)」というマインドセットは、一見矛盾するようだが、実は深い洞察に基づいている。真の怠惰とは、無駄な作業を徹底的に排除し、本質的な価値創造にのみ集中することを意味する。この思考法は、日本的な「頑張る」文化とは対極にありながら、より高い成果を生み出す可能性を秘めている。

第三章では脳の負荷軽減という観点から、情報整理と記憶術が論じられる。著者は同僚たちの「ガチで才能のある」エンジニアたちの極意として、認知負荷を最小化する技術を紹介している。これは単なるライフハックではなく、持続可能な高パフォーマンスを実現するための基盤となる考え方である。

第四章のコミュニケーション論では「情報量は少なく」という原則が示される。冗長な説明や過剰な詳細を排除し、本質のみを伝える技術は、グローバル環境での競争力に直結する能力である。日本の「察する」文化とは異なる、明確で効率的なコミュニケーションの在り方が具体的に示されている。

第五章で展開されるチームビルディング論は、従来の日本的な組織運営への挑戦状とも言える。リーダーがビジョンとKPIを明示し、具体的な実行方法はチームが主体的に決定するという「サーバントリーダーシップ」と「自己組織型チーム」の概念は、マイクロマネジメントが横行する日本企業にとって革新的な視点を提供している。

第六章では生活習慣術として「定時退社の強制実行」が提唱される。生産性向上のために無理やりでもきっかり仕事を終える「タイムボックス制」は、長時間労働を美徳とする日本の労働文化に対する根本的な問題提起である。

第七章では、AI時代の生存戦略として日米のエンジニア文化の違いが分析される。変化への適応力と「批判文化」からの脱却が、将来的な競争力の源泉として位置づけられている。

## 著者の核心的主張

著者が一貫して主張するのは、「効率性と本質への集中こそが真の生産性を生む」ということである。日本的な「努力と根性」の文化から脱却し、科学的で合理的なアプローチを採用することで、個人も組織も飛躍的な成長を遂げることができるという信念が全編を貫いている。

## 実践的価値と普遍性

本書の最大の価値は、抽象的な理論ではなく、実際の現場で検証された具体的な手法を提示していることにある。「理解・記憶・反復」という黄金則や、メンタルモデルの構築法、ファクトベースの判断基準など、どれも即座に実践可能な技術である。

特に注目すべきは、これらの手法がエンジニアリング分野に留まらず、あらゆる知識労働に応用可能な汎用性を持っていることである。営業、企画、マネジメント、研究開発など、どの分野においても「本質を理解し、無駄を排除し、価値を最大化する」という原則は有効である。

また、著者自身が「のび太君」だったと自称する謙虚さも、読者との距離を縮める効果的な要素となっている。完璧な天才の話ではなく、努力と工夫によって一流に近づいた人物の体験談として、より説得力を持って受け入れられる。

## 文化的考察と変革の必要性

本書が提起する最も重要な問題は、日本の労働文化そのものへの疑問符である。「長時間働くことが美徳」「準備に時間をかけることが丁寧」「みんなで頑張ることが大切」といった従来の価値観に対し、データと実績に基づいた反証を提示している。

この文化的な差異は、単に仕事の進め方の違いを超えて、国際競争力の根幹に関わる問題である。優秀な人材が海外に流出する現象も、こうした思考法の違いに起因する部分が大きいと考えられる。

## 変化への適応戦略

AI時代を迎える現在、本書が示す思考法はより一層重要性を増している。ルーティンワークがAIに代替される中で、人間にしかできない創造的で価値の高い仕事に集中するためには、まさに本書が提唱する「本質への集中」と「無駄の排除」が不可欠である。

また、リモートワークやグローバル化が進む中で、明確で効率的なコミュニケーション能力、自律的な判断力、継続的な学習能力といった要素は、生存に直結するスキルとなっている。

## 実装への道筋

本書の価値を最大化するためには、単に読んで終わりではなく、実際の業務や生活に組み込んでいくことが重要である。特に「タイムボックス制」や「理解に時間をかける」習慣は、即座に始められる実践的な手法である。

組織レベルでの変革を目指す場合は、まず個人が実践し、その効果を示すことから始めるのが現実的であろう。トップダウンでの文化変革は困難だが、ボトムアップでの改善は十分に可能である。

## 結論と読者への示唆

『世界一流エンジニアの思考法』は、日本の知識労働者にとって必読の書である。技術的なスキルアップ以上に、働き方や考え方の根本的な見直しを促す力を持っている。本書が示す思考法を実践することで、個人の生産性向上はもちろん、より充実した職業人生を送ることが可能になるだろう。

現代の激変する環境において、従来の成功パターンに固執することは危険である。本書は、変化に適応し、持続的に成長し続けるための羅針盤として機能する。エンジニアに限らず、すべての知識労働者が、本書から学べることは多い。真の意味での「一流」を目指すすべての人に、強く推薦したい一冊である。

書評AIの実コード

https://github.com/NaokiTani1548/book_review_ai
※動作確認のために作った個人製作成果物なので、セキュリティやアーキテクチャ等は全く意識していません。基本的に参考にはならないと思うので暖かい目で見てくださると助かりますm(_ _)m

最後に

本稿では「結局MCPサーバーとは何なのか?」というところから実際のコード例まで包括的に取り扱いました。いかがだったでしょうか?
私自身、記事を投稿するのは初めての試みだったので、説明不足な点や不可解な点などあると思います。書きたいことが多くて内容も勇み足になってしまいましたが、少しでも誰かのお力になれたら幸いです。
読んでくださりありがとうございました!!

NISLab 小板・小比賀 研究室

Discussion