注: この記事の構成整理と文章のブラッシュアップには生成AIを使っています。内容の事実確認とコード確認は自分で行っています。
はじめに
最近、会社でも生成AIの活用をかなり推進していて、GitHub CopilotやAIエディタ、MCPみたいな話題に触れる機会が増えた。
ただ、自分はMCPって最近よく聞くけど、正直なところ 何もわかっていなかった。
「AIエディタと外部ツールをつなぐプロトコル」くらいの認識はあったけど、じゃあ具体的に中で何が起きてるの?と聞かれると全く答えられない状態だった。会社として生成AI活用を進めるなら、このあたりをふわっと理解したままにしておくのはよくないなと思った。
ドキュメントを読んでもいまいちピンとこないし、概念だけ追っていても理解が進まない。
じゃあ自分で作ってみるか、ということでMCPサーバーを1から実装してみることにした。
題材はデジタル庁デザインシステム(DADS v2.12.0)。カラートークンやタイポグラフィなどのデザイントークン情報を、AIエディタから直接参照できるMCPサーバーとして実装した。
そもそもMCPって何なのか
会社で生成AI活用の話が増えるほど、MCPは避けて通れない単語になってきたので、まずは最低限の理解を整理した。
MCP(Model Context Protocol) は、AIアシスタントが外部のデータやツールにアクセスするための標準プロトコル。Anthropicが策定している。
ざっくり言うと:
- AIエディタ(Host) → VS Code、Cursor など
- MCPクライアント → エディタ内蔵のMCP接続モジュール
- MCPサーバー → 今回作ったやつ。ツールやリソースを提供する
通信の流れはこう:
ユーザー: 「このカラー、アクセシビリティ大丈夫?」
↓
AIエディタ → MCPクライアント → MCPサーバー → 処理結果を返す
↓
AIが結果を整形して回答
プロトコルの中身は JSON-RPC 2.0。HTTPのREST APIとは全然違って、標準入出力(stdio)でやり取りする。AIエディタがMCPサーバーを子プロセスとして起動して、stdin/stdoutでJSON-RPCメッセージを送り合う仕組み。
最初「stdioで通信ってどういうこと?」と思ったけど、要するにこういうこと:
AIエディタ(親プロセス)
│
│ stdin に JSON-RPC リクエストを書き込む
↓
MCPサーバー(子プロセス)
│
│ stdout に JSON-RPC レスポンスを書き込む
↓
AIエディタが受け取って処理
HTTPサーバーみたいにポートを開けたりしない。プロセス間通信で完結する。
なぜデジタル庁デザインシステムを題材にしたのか
MCPサーバーを作ること自体が目的だったので、題材は何でもよかった。ただ、せっかくなら実用性のあるものにしたかった。
会社で生成AIの活用を進める中では、AIに何を参照させるか、どういうデータなら扱いやすいかも重要になる。その観点でも、デジタル庁デザインシステム(DADS)は題材としてちょうどよかった。
選んだ理由:
- データが構造化されている — カラートークン、タイポグラフィ、スペーシングなど、機械的に扱いやすい
- 検証ロジックが書ける — WCAGコントラスト比の計算など、単なるデータ返却だけじゃない処理がある
- 公開情報である — 自由に参照できる
全体のアーキテクチャ
src/
├── index.ts # エントリポイント。stdio接続、シャットダウン管理
├── server.ts # McpServer生成、7つのTool + 4つのResourceを登録
├── logger.ts # 構造化ログ(stderr出力)
├── types.ts # 型定義
├── services/ # ビジネスロジック
│ ├── color-service.ts # カラートークン取得、WCAG検証
│ ├── guideline-service.ts # ガイドライン検索(スコアリング)
│ ├── component-service.ts # コンポーネント仕様取得
│ └── typography-service.ts # タイポグラフィ仕様取得
└── data/ # 静的データ(DADS v2.12.0ベース)
├── foundations/ # カラー、タイポグラフィ、スペーシング
├── components/ # コンポーネント仕様
└── guidelines/ # ガイドライン文書
使っている主要ライブラリ:
| ライブラリ | 用途 |
|---|---|
@modelcontextprotocol/sdk |
MCP SDK。JSON-RPCの解析やルーティングを全部やってくれる |
zod |
入力バリデーション。スキーマとTypeScriptの型を両立 |
vitest |
テスト。35ケース |
Tool と Resource の違い
MCPには「Tool」と「Resource」の2種類があって、最初この違いがわからなかった。
作ってみてわかったのは:
- Tool = AIが「呼び出す」もの。引数を受け取って処理して結果を返す。関数みたいなもの
- Resource = AIが「読み込む」もの。URIでアクセスする静的データ。ファイルみたいなもの
今回のサーバーでは:
Tool(7つ)
| ツール名 | 何をするか |
|---|---|
search_guidelines |
ガイドラインをキーワード検索 |
get_guideline |
指定セクションの詳細取得 |
get_color_tokens |
カラートークン一覧取得 |
validate_color_usage |
2色のコントラスト比をWCAG検証 |
get_component_spec |
コンポーネント仕様取得 |
get_typography_spec |
タイポグラフィ仕様取得 |
get_spacing_tokens |
スペーシングトークン一覧取得 |
Resource(4つ)
| URI | 内容 |
|---|---|
dads://foundations/color |
カラーシステム全体 |
dads://foundations/typography |
タイポグラフィシステム全体 |
dads://foundations/spacing |
スペーシングシステム全体 |
dads://foundations/layout |
レイアウトシステム全体 |
Toolは「AIが判断して能動的に使う」、Resourceは「AIがコンテキストとして参照する」。この違いが実装してみて初めて腑に落ちた。
生成AIを実務で使うなら、この区別は結構大事だと思った。何を都度呼び出させるのか、何を前提知識として読ませるのかで設計が変わる。
実装で学んだこと
1. SDK がほとんどやってくれる
自分でJSON-RPCのパースやルーティングを書く必要は一切なかった。@modelcontextprotocol/sdkが全部やってくれる。
エントリポイントはこれだけ:
const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
3行でMCPサーバーが起動する。あとはToolとResourceを登録するだけ。
2. Zod でバリデーションとスキーマ定義を統一できる
MCPのTool登録にはスキーマ定義が必要で、Zodを使うとTypeScriptの型とバリデーションが一体化する:
const validateColorSchema = {
foreground: z.string()
.regex(/^#?[0-9a-fA-F]{6}$/, "HEXカラーは #RRGGBB 形式で入力してください")
.describe("前景色(テキスト色)"),
background: z.string()
.regex(/^#?[0-9a-fA-F]{6}$/, "HEXカラーは #RRGGBB 形式で入力してください")
.describe("背景色"),
};
.describe() で書いた説明がそのままAIへのヒントになる。AIがどの引数に何を渡せばいいか理解できるようになる。
3. WCAG コントラスト比の計算は意外とシンプル
アクセシビリティ検証としてWCAG 2.1のコントラスト比計算を実装した。やってることは:
- HEXカラーをRGBに分解
- sRGB → 線形RGBに変換
- 相対輝度を計算(
0.2126R + 0.7152G + 0.0722B) - コントラスト比 =
(明るい方 + 0.05) / (暗い方 + 0.05)
AA基準は通常テキストで4.5:1以上、大テキストで3:1以上。
function relativeLuminance(r: number, g: number, b: number): number {
const [rs, gs, bs] = [r, g, b].map((c) => {
const srgb = c / 255;
return srgb <= 0.04045
? srgb / 12.92
: Math.pow((srgb + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}
数式だけ見ると難しそうだけど、実装してみたら「ああ、こういうことか」となった。
4. エラーハンドリングは2段構え
バリデーションは2段階で行っている:
- Zod — リクエストの型と形式をチェック(SDK層で自動実行)
-
ビジネスロジック — 値の妥当性チェック(
normalizeHexで不正なHEX値を検出)
function normalizeHex(hex: string): string {
const cleaned = hex.replace(/^#/, "");
if (!/^[0-9a-fA-F]{6}$/.test(cleaned)) {
throw new Error("不正なカラー値です。#RRGGBB 形式で入力してください");
}
return `#${cleaned.toUpperCase()}`;
}
Zodを通過しても # の有無で正規化が必要だったりするので、ビジネスロジック側でも検証が必要だった。
5. ログは stderr に出す
MCPサーバーは stdout を JSON-RPC通信に使っている。だから ログを stdout に出すと通信が壊れる。console.log() は使えない。
// NG: stdoutに出力 → JSON-RPCが壊れる
console.log("debug info");
// OK: stderrに出力 → 通信に影響しない
console.error(JSON.stringify({ event: "tool_called", tool: "get_color_tokens" }));
これは実際にハマったポイント。
まとめ
会社で生成AI活用を進める流れの中でMCPをちゃんと理解したかったので、実際にサーバーを1本作ってみた。
その結果、以下が理解できた:
- MCPはJSON-RPC 2.0ベースのプロトコルで、stdioで通信する
- SDKが重い処理を全部やってくれるので、開発者はビジネスロジックに集中できる
- ToolとResourceの使い分けが設計の肝
- Zodとの相性が良く、スキーマ定義と型安全性を両立できる
- stdoutはJSON-RPC専用。ログはstderrに出す
会社でAI活用を進めているけど、MCPはまだよくわからない、という人は小さいMCPサーバーを1つ作ってみるのがおすすめ。概念だけ追うより全体像がかなりクリアになる。
リポジトリはMITライセンスで公開しているので、参考にしてもらえれば。