お知らせ・ブログ

オノコムからの最新情報や、生成AI、AWSクラウド、ノーコードアプリケーションに関する有益な情報をお届けします。

MCPの仕組みをよくわからないまま使っていたので自作してみた

2026年03月26日
あきひが
ブログ

【はじめに】

私は普段の開発でClaude Codeを使っています。 Claude Codeには「MCP(Model Context Protocol)」という仕組みでさまざまな外部ツールを接続できるのですが、正直なところ、MCPの仕組みをよくわかっていないまま使っていました。 「設定ファイルに書けば使える」くらいの理解で、裏側でどういった動きをしているのかは意識していませんでした。

それに加えて、もうひとつ困っていたことがあります。 Claude Codeは頻繁にアップデートされるのですが、CHANGELOGの確認が面倒でした。 GitHubのリポジトリを毎回見に行くのも手間ですし、「最新の変更点を教えて」と聞けたら楽だなと思っていました。

そこで、MCPの仕組みを理解するために自分で作ってみることにしました。 題材は「MCPを使用したClaude CodeのCHANGELOGの取得」です。

さらに、以前だとMCPサーバーを公開するにはEC2やLambdaで自前構築が必要でしたが、Amazon Bedrock AgentCoreを使えばインフラやセキュリティも任せてデプロイできると知り、やってみることにしました。

【環境や使用したもの】

  • PC:MacBook Air M4(Apple Silicon)
  • Node.js:v20以上
  • TypeScript:6.x
  • MCP SDK:@modelcontextprotocol/sdk 1.27.1
  • Claude Code:2.1.81
  • AWS CLI:設定済み(SSOログイン)
  • Python:3.12(AgentCoreデプロイツール用)
  • bedrock-agentcore-starter-toolkit:0.3.3(AgentCoreデプロイ用CLIツール)

【最終的なプロジェクト構成図】

プロジェクト名を「akihiga-mcp-changelog」で作成していますので、適時読み替え、ご自身のお好きなプロジェクト名に置き換えてください。

akihiga-mcp-changelog/
  ├── .bedrock_agentcore/
  │   └── akihiga_mcp_changelog/
  │       └── Dockerfile              (agentcore configureで自動生成)
  ├── .bedrock_agentcore.yaml         (agentcore configureで自動生成)
  ├── build/
  │   └── index.js                    (ビルド後に生成)
  ├── node_modules/                   (npm installで自動生成)
  ├── package-lock.json               (npm installで自動生成)
  ├── package.json
  ├── src/
  │   └── index.ts                    (MCPサーバーのソースコード)
  └── tsconfig.json

【MCPとは】

MCPは「Model Context Protocol」の略で、AIアシスタントと外部ツールをつなぐための規格です。

たとえばClaude Codeには、ファイルを読む、検索する、コマンドを実行するといった機能が最初から備わっています。 MCPを使うと、ここに「AWSのドキュメントを検索する」、「GitHubのIssueを操作する」など、好きな機能を追加できます。

【MCPの仕組み】

MCPは、クライアント側とサーバー側に分かれていて、JSON-RPCという形式でやり取りをします。

流れは以下の通りです。

  • ユーザーが自然言語で指示を出す(例:「Claude Codeの最新のCHANGELOGを教えて」)
  • AIが指示を解釈し、どのMCPツールを呼ぶか判断する
  • Claude CodeがMCPサーバーにJSON-RPCリクエストを送る
  • MCPサーバーが結果を返す
  • AIが結果をユーザーにわかりやすく伝える

ポイントは、ユーザーの自然言語はMCPサーバーには送られないということです。 AIが構造化されたリクエストに変換します。

【Claude CodeのSkillsとの違い】

Claude Codeには「Skills」という仕組みもあります。

MCPとSkillsは代替ではなく、補完関係にあります。

  • MCP:外部のAPIやサービスと連携する。サーバーとして独立して動く
  • Skills:Claude Codeの内部で動くプロンプトのテンプレート。ファイル操作やコード生成のワークフローを定義する

「外部と連携するならMCP、内部のワークフローを定義するならSkills」という使い分けになります。

【MCPサーバーを作る】

Claude CodeのCHANGELOGをGitHubから取得するMCPサーバーを作ります。

ツールは3つです。

  • get_latest_changelog:最新バージョンの変更点を取得
  • get_recent_changelogs:直近N件の変更点を取得
  • get_full_changelog:全CHANGELOGを取得

【プロジェクトのセットアップ】

プロジェクトフォルダを作成し、必要なものをインストール、初期設定します。

mkdir akihiga-mcp-changelog
cd akihiga-mcp-changelog
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

tsconfig.jsonを作成します。

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "types": ["node"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "build"]
}

package.jsonに以下を追加します。

{
  "type": "module",
  "scripts": {
    "build": "tsc"
  }
}

MCPサーバーのコード

src/index.tsを作成します。 まずはローカルで動かすためのstdio版です。

#!/usr/bin/env node

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

const CHANGELOG_URL =
  "https://raw.githubusercontent.com/anthropics/claude-code/main/CHANGELOG.md";

async function fetchChangelog(): Promise<string> {
  const response = await fetch(CHANGELOG_URL);
  if (!response.ok) {
    throw new Error(
      `Failed to fetch CHANGELOG: ${response.status} ${response.statusText}`
    );
  }
  return response.text();
}

function extractLatestVersion(changelog: string): string {
  const lines = changelog.split("\n");
  const versionPattern = /^## /;
  let startIndex = -1;
  let endIndex = -1;

  for (let i = 0; i < lines.length; i++) {
    if (versionPattern.test(lines[i])) {
      if (startIndex === -1) {
        startIndex = i;
      } else {
        endIndex = i;
        break;
      }
    }
  }

  if (startIndex === -1) return "No version information found.";
  if (endIndex === -1) return lines.slice(startIndex).join("\n");
  return lines.slice(startIndex, endIndex).join("\n").trim();
}

function extractVersionCount(changelog: string, count: number): string {
  const lines = changelog.split("\n");
  const versionPattern = /^## /;
  const versionIndices: number[] = [];

  for (let i = 0; i < lines.length; i++) {
    if (versionPattern.test(lines[i])) {
      versionIndices.push(i);
    }
  }

  if (versionIndices.length === 0) return "No version information found.";

  const endVersionIndex = Math.min(count, versionIndices.length);
  const endLineIndex = versionIndices[endVersionIndex] ?? lines.length;
  return lines.slice(versionIndices[0], endLineIndex).join("\n").trim();
}

const server = new McpServer({
  name: "akihiga-mcp-changelog",
  version: "1.0.0",
});

server.registerTool(
  "get_latest_changelog",
  {
    description: "Get the latest version entry from Claude Code CHANGELOG",
  },
  async () => {
    const changelog = await fetchChangelog();
    const latest = extractLatestVersion(changelog);
    return {
      content: [{ type: "text", text: latest }],
    };
  }
);

server.registerTool(
  "get_recent_changelogs",
  {
    description: "Get recent version entries from Claude Code CHANGELOG",
    inputSchema: {
      count: z
        .number()
        .min(1)
        .max(20)
        .default(3)
        .describe("Number of recent versions to retrieve (1-20, default: 3)"),
    },
  },
  async ({ count }) => {
    const changelog = await fetchChangelog();
    const recent = extractVersionCount(changelog, count);
    return {
      content: [{ type: "text", text: recent }],
    };
  }
);

server.registerTool(
  "get_full_changelog",
  {
    description: "Get the full Claude Code CHANGELOG",
  },
  async () => {
    const changelog = await fetchChangelog();
    return {
      content: [{ type: "text", text: changelog }],
    };
  }
);

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch(console.error);

get_recent_changelogsでは、inputSchemaにzodでパラメータを定義しています。 こうすることで、AIに「直近5件のCHANGELOGを見せて」と伝えると「count: 5」を渡してくれます。

ビルドとClaude Codeへの登録

ビルドコマンドでビルドします。

npm run build

プロジェクトの.mcp.jsonにMCPの設定を追加します。

{
  "mcpServers": {
    "akihiga-mcp-changelog": {
      "command": "node",
      "args": ["/path/to/akihiga-mcp-changelog/build/index.js"]
    }
  }
}

MCPを追加した後は、Claude Codeを再起動すれば、登録したMCPが使えるようになります。

動作確認

Claude Codeに「最新のCHANGELOGを教えて」と指示をすると、作成したMCPが呼び出されてCHANGELOGが返ってきます。

mcp-bedrock-agentcore-20260326_1.png

裏側で流れているJSON-RPC

MCPの裏側で実際にどのようなデータが流れているのかを見てみます。 Claude Codeが自作MCPに送っているリクエストは以下の通りです。

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "get_latest_changelog",
    "arguments": {}
  }
}

自作MCPサーバーからのレスポンスは以下の通りです。

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "## 2.1.81\n\n- Added `--bare` flag for scripted `-p` calls ...\n- Added `--channels` permission relay ...\n- Fixed multiple concurrent Claude Code sessions ...\n- Improved MCP read/search tool calls to collapse ...\n(以下省略)"
      }
    ]
  }
}

私は日本語で「自作MCPで最新のClaude CodeのCHANGELOGを取得してください。」と指示をしましたが、MCPサーバーに送られるリクエストにはその日本語は含まれていません。

Claude Codeがユーザーの自然言語を解釈して、MCPが理解できる構造化されたリクエストに変換しています。

MCPサーバー側は「get_latest_changelogというツールが呼ばれた」ということだけがわかればよく、ユーザーが何語で話しかけたかは知る必要がないようです。

【AgentCoreへのデプロイ】

ローカルで動くことは確認できたので、次はこのMCPサーバーをAWS上にデプロイしてみます。 Amazon Bedrock AgentCoreは、AIエージェントやMCPサーバーをサーバーレスでホスティングできるサービスです。 コンテナ化やインフラ管理を自動でやってくれるので、開発者はコードに集中できます。

【トランスポートの変更】

ローカルではstdioで通信していましたが、AgentCoreではHTTPで通信する必要があります。 そこで、環境変数MCP_TRANSPORTで切り替えられるように変更しました。

変更点は以下の3つです。

  1. importの追加

    import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
    import express from "express";
    
  2. ツール登録を関数にまとめる

      function createServer(): McpServer {
        const server = new McpServer({
          name: "akihiga-mcp-changelog",
          version: "1.0.0",
        });
    
        server.registerTool(
          "get_latest_changelog",
          // ... 省略(内容は同じ)
        );
    
        server.registerTool(
          "get_recent_changelogs",
          // ... 省略(内容は同じ)
        );
    
        server.registerTool(
          "get_full_changelog",
          // ... 省略(内容は同じ)
        );
    
        return server;
      }
    
  3. main関数にHTTPモードの分岐を追加

    async function main() {
      if (process.env.MCP_TRANSPORT === "http") {
        // AgentCore用: HTTPサーバーとして起動
        const app = express();
        app.use(express.json());
        app.post("/mcp", async (req, res) => {
          const server = createServer();
          const transport = new StreamableHTTPServerTransport({
            sessionIdGenerator: undefined, // ステートレスモード
          });
          await server.connect(transport);
          await transport.handleRequest(req, res, req.body);
        });
        app.listen(8000, "0.0.0.0", () => {
          console.log("MCP server listening on http://0.0.0.0:8000/mcp");
        });
      } else {
        // ローカル用: stdio(今まで通り)
        const server = createServer();
        const transport = new StdioServerTransport();
        await server.connect(transport);
      }
    }
    

環境変数にMCP_TRANSPORTの値が設定されていない場合は今まで通りstdioで動くので、ローカルでもそのまま使用できます。

デプロイ手順

AgentCoreへのデプロイには「starter toolkit」というCLIツールを使います。

pip install bedrock-agentcore-starter-toolkit

※ デプロイ時にRequestsDependencyWarningという警告が表示されることがあります。これはデプロイツールの依存パッケージ間のバージョン不一致によるもので、動作に影響はありませんのでそのまま進めます。

設定ファイルの生成

agentcore configure \
  --entrypoint src/index.ts \
  --protocol MCP \
  --region ap-northeast-1 \
  --name my_mcp_changelog \
  --language typescript

対話形式でいくつか質問されます。

  • 実行用IAMロール:Enterで自動作成
  • ECRリポジトリ:Enterで自動作成
  • OAuth認証:noを選択(IAM認証を使用)

TypeScriptプロジェクトの場合、コンテナデプロイが自動で選択されます。 ただし、ローカルにDockerは不要です。 AWS CodeBuildがクラウド上でビルドしてくれます。

※ Pythonの場合はコードを直接デプロイできますが、TypeScriptの場合はCodeBuildによるコンテナビルドが必要です。しかし、ユーザー側でDockerfileを書いたりする必要はなく、ツールが自動的に処理してくれますが次の修正が必要になります。

Dockerfileの修正

agentcore configureで Dockerfile が自動生成されますが、今回のプロジェクトでは修正が必要な箇所がありました。

自動生成された起動コマンド

CMD ["node", "--require", "@opentelemetry/auto-instrumentations-node/register", "dist/src/index.js"]

このままでは以下の問題があります。

  • @opentelemetry/auto-instrumentations-nodeが依存関係に含まれていないため、起動時にエラーになる
  • エントリポイントがdist/src/index.jsになっているが、実際のビルド出力先はbuild/index.js

Dockerfile(.bedrock_agentcore/akihiga_mcp_changelog/Dockerfile)を開き、以下のように修正します。

ENV行にMCP_TRANSPORT=httpを追加

ENV DOCKER_CONTAINER=1 \
    AWS_REGION=ap-northeast-1 \
    AWS_DEFAULT_REGION=ap-northeast-1 \
    MCP_TRANSPORT=http

CMD行を修正

CMD ["node", "build/index.js"]

デプロイ実行

agentcore deploy --env MCP_TRANSPORT=http

このコマンドで以下が自動的に行われます。

  • CodeBuildでコンテナをビルド
  • ECRにプッシュ
  • AgentCore Runtimeにデプロイ
  • IAMロール、CloudWatchログの設定

動作確認

デプロイ後、CLIから呼び出して確認します。

agentcore invoke '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_latest_changelog","arguments":{}}}'

AgentCore上でも、ローカルの時と同じようにCHANGELOGが正しく返ってくることを確認できます。

現在の状態でどこから利用できるか

デフォルト設定(IAM認証)の場合、SigV4署名ができるクライアントから利用できます。 具体的には以下のようなケースです。

  • agentcore invoke コマンド:AWS CLIの認証情報を使って呼び出し
  • 同じAWSアカウント内のサービス:他のAgentCoreエージェントやLambdaなどからの呼び出し
  • AWS SDKを使ったプログラム:SigV4署名を処理するコードからの呼び出し

外部クライアントから接続するには

今回の記事では外部クライアントからの接続は実施していませんが、接続を行いたい場合は以下の設定が必要になります。

たとえば、Claude CodeのMCPクライアントからAgentCore上のMCPサーバーに直接接続したい場合、現時点ではIAM認証(SigV4署名)での接続には対応していません。 Claude CodeからリモートMCPサーバーに接続するには、Bearer token方式の認証が必要です。 そのためには、Amazon Cognitoでユーザープールを作成し、OAuth認証を設定する必要があります。

手順の概要は以下の通りです。

  • Amazon Cognitoでユーザープールを作成する
  • agentcore configureでOAuth設定を追加する(Discovery URLとClient IDを指定)
  • 再デプロイする
  • Cognitoから取得したBearer tokenを使ってMCPクライアントから接続する

Cognito設定後、Claude Codeの.mcp.jsonに以下のようにURLとBearer tokenを設定することで、リモートMCPサーバーに接続できるようになります。

{
  "mcpServers": {
    "remote-mcp": {
      "url": "https://bedrock-agentcore.ap-northeast-1.amazonaws.com/runtimes/{エンコード済みARN}/invocations?qualifier=DEFAULT",
      "headers": {
        "Authorization": "Bearer {Cognitoから取得したトークン}"
      }
    }
  }
}

【まとめ】

MCPを自作しAgentCoreにデプロイするまでを行いました。

  • JSON-RPCベースのシンプルなプロトコル。AIが自然言語を構造化リクエストに変換し、MCPサーバーへ送信する。
  • MCP SDKを使えば、ツールの登録は簡単にできる。CHANGELOGの取得くらいであれば100行ほどで完成する。
  • AgentCoreへのデプロイは、agentcore configure=>agentcore deployの2コマンド。TypeScriptでもCodeBuildが自動でコンテナ化してくれるので、Dockerの知識は不要。
  • 認証のデフォルトはIAM認証。外部クライアントから接続したい場合はOAuth(Cognito等)の追加設定が必要。

ちなみに、2026年3月にAgentCore RuntimeにStateful MCPの機能(elicitation、sampling、progress通知)が追加されました。 これにより、MCPサーバーがユーザーに質問を返したり、AI生成コンテンツを要求したりできるようになっています。

最初は、これらの内容も含めたものにしようと思っていましたが、現時点ではelicitationはAIからの質問と見分けがつきにくく、実用的なメリットは限定的という印象だったので見送りました。

もう少しわかりやすい見せ方などを見つけたら、ブログ化を検討してみたいと思います。

個人的には、「仕組みを知らないまま使っている」状態から「裏側でどのようなやりとりが行われているのかを少しだけわかった」状態になれたのが一番の収穫でした。

MCPに興味がある方は、ぜひ自分でも作ってみてください。

【参考リンク】

【商標について】

  • Amazon Web Services、AWS、Amazon Bedrock、Amazon Bedrock AgentCoreは、米国および、またはその他の諸国における、Amazon.com, Inc.またはその関連会社の商標です
  • Claude、Claude Codeは、Anthropic, PBCの商標です
  • GitHubは、GitHub, Inc.の商標です
  • Node.js は、OpenJS Foundationの商標です
  • TypeScriptは、Microsoft Corporationの商標です
  • Dockerは、Docker, Inc.の商標です
  • その他記載されている会社名、製品名は、各社の商標または登録商標です