結論:社内APIをClaude Codeから直接呼べるMCPサーバーは60分で作れる
公式MCPサーバーだけでは足りない。社内の独自APIをClaude Codeからシームレスに呼び出せるMCPサーバーを、ゼロから60分で作ります。
この記事を読むと、以下のことができるようになります。
- 社内のREST APIをClaude Codeの自然言語から呼び出せるMCPサーバーをTypeScriptで自作できる
- ツール定義・リソース定義・エラーハンドリングの実装パターンを理解できる
- 本番運用に向けた認証・レート制限・ログ設計の勘所がわかる
環境・前提条件
| 項目 | バージョン/要件 |
|---|---|
| Node.js | v20 以上 |
| TypeScript | 5.x |
| @modelcontextprotocol/sdk | 1.x(2025年6月時点の最新) |
| Claude Code | 最新版(CLI) |
| 社内API(想定) | REST API(JSON)、Bearer Token認証 |
社内APIとして、以下のようなユーザー管理APIが存在する想定で進めます。
GET https://internal-api.example.com/users/:id
POST https://internal-api.example.com/users/search
1. MCPサーバーのアーキテクチャ概要
MCP(Model Context Protocol)とは
MCPは、AIアシスタント(Claude Codeなど)が外部のツールやデータソースにアクセスするための標準プロトコルです。MCPサーバーを自作することで、Claude Codeから社内APIを自然言語で叩けるようになります。
stdio vs SSE ― トランスポートの選び方
MCPには2つのトランスポート方式があります。
| 項目 | stdio | SSE(HTTP) |
|---|---|---|
| 通信方式 | 標準入出力 | HTTP Server-Sent Events |
| 起動方式 | Claude Codeがプロセスを起動 | 常駐サーバーとして稼働 |
| 適したケース | ローカル開発・個人利用 | チーム共有・リモートサーバー |
| セットアップ難度 | 低い | やや高い |
| Claude Codeとの相性 | ◎ 公式推奨 | ○ 対応済み |
結論:Claude Codeで使うなら、まずstdioトランスポートから始めましょう。 本記事でもstdioを採用します。
2. 環境構築とMCP SDKのセットアップ
プロジェクト初期化
mkdir mcp-internal-api && cd mcp-internal-api
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init
tsconfig.json の設定
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"declaration": true
},
"include": ["src/**/*"]
}
package.json に追記
{
"type": "module",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
},
"bin": {
"mcp-internal-api": "./dist/index.js"
}
}
ディレクトリ構成
mcp-internal-api/
├── src/
│ ├── index.ts # エントリポイント
│ ├── tools/
│ │ ├── getUser.ts # ツール定義: ユーザー取得
│ │ └── searchUsers.ts # ツール定義: ユーザー検索
│ ├── resources/
│ │ └── userProfile.ts # リソース定義
│ └── lib/
│ └── apiClient.ts # 社内APIクライアント
├── tsconfig.json
└── package.json
3. ステップバイステップ実装
3-1. 社内APIクライアント(src/lib/apiClient.ts)
まず、社内APIとの通信を担当するクライアントを作ります。
// src/lib/apiClient.ts
export interface User {
id: number;
name: string;
email: string;
department: string;
role: string;
}
export interface SearchResult {
users: User[];
total: number;
}
export class InternalApiClient {
private baseUrl: string;
private token: string;
constructor() {
this.baseUrl = process.env.INTERNAL_API_URL ?? "https://internal-api.example.com";
this.token = process.env.INTERNAL_API_TOKEN ?? "";
if (!this.token) {
console.error("WARNING: INTERNAL_API_TOKEN is not set");
}
}
private async request<T>(path: string, options?: RequestInit): Promise<T> {
const url = `${this.baseUrl}${path}`;
const response = await fetch(url, {
...options,
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${this.token}`,
...options?.headers,
},
});
if (!response.ok) {
throw new Error(
`API Error: ${response.status} ${response.statusText} - ${await response.text()}`
);
}
return response.json() as Promise<T>;
}
async getUser(userId: number): Promise<User> {
return this.request<User>(`/users/${userId}`);
}
async searchUsers(query: string, department?: string): Promise<SearchResult> {
const params = new URLSearchParams({ q: query });
if (department) params.set("department", department);
return this.request<SearchResult>(`/users/search?${params}`);
}
}
3-2. ツール定義(src/tools/getUser.ts)
MCPの「ツール」は、Claude Codeが能動的に呼び出す関数です。
// src/tools/getUser.ts
import { z } from "zod";
import { InternalApiClient } from "../lib/apiClient.js";
// 入力スキーマをZodで定義
export const GetUserSchema = z.object({
userId: z.number().describe("取得したいユーザーのID"),
});
export type GetUserInput = z.infer<typeof GetUserSchema>;
export async function getUserHandler(
input: GetUserInput,
client: InternalApiClient
) {
try {
const user = await client.getUser(input.userId);
return {
content: [
{
type: "text" as const,
text: JSON.stringify(user, null, 2),
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return {
content: [
{
type: "text" as const,
text: `ユーザー取得に失敗しました: ${message}`,
},
],
isError: true,
};
}
}
3-3. ツール定義(src/tools/searchUsers.ts)
// src/tools/searchUsers.ts
import { z } from "zod";
import { InternalApiClient } from "../lib/apiClient.js";
export const SearchUsersSchema = z.object({
query: z.string().describe("検索キーワード(名前・メールアドレスなど)"),
department: z
.string()
.optional()
.describe("部署名でフィルタ(例: 'エンジニアリング')"),
});
export type SearchUsersInput = z.infer<typeof SearchUsersSchema>;
export async function searchUsersHandler(
input: SearchUsersInput,
client: InternalApiClient
) {
try {
const result = await client.searchUsers(input.query, input.department);
if (result.users.length === 0) {
return {
content: [
{
type: "text" as const,
text: `「${input.query}」に該当するユーザーは見つかりませんでした。`,
},
],
};
}
return {
content: [
{
type: "text" as const,
text: `${result.total}件のユーザーが見つかりました:\n${JSON.stringify(result.users, null, 2)}`,
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error";
return {
content: [
{
type: "text" as const,
text: `ユーザー検索に失敗しました: ${message}`,
},
],
isError: true,
};
}
}
3-4. リソース定義(src/resources/userProfile.ts)
MCPの「リソース」は、Claude Codeがコンテキストとして読み込むデータです。ツールとの違いを意識しましょう。
// src/resources/userProfile.ts
import { InternalApiClient } from "../lib/apiClient.js";
export async function readUserProfile(
userId: number,
client: InternalApiClient
) {
const user = await client.getUser(userId);
return {
contents: [
{
uri: `internal://users/${userId}/profile`,
mimeType: "application/json",
text: JSON.stringify(user, null, 2),
},
],
};
}
3-5. エントリポイント(src/index.ts)
すべてを統合するメインファイルです。
#!/usr/bin/env node
// src/index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { InternalApiClient } from "./lib/apiClient.js";
import {
GetUserSchema,
getUserHandler,
} from "./tools/getUser.js";
import {
SearchUsersSchema,
searchUsersHandler,
} from "./tools/searchUsers.js";
import { readUserProfile } from "./resources/userProfile.js";
// APIクライアントの初期化
const apiClient = new InternalApiClient();
// MCPサーバーの作成
const server = new McpServer({
name: "internal-api",
version: "1.0.0",
});
// ── ツール登録 ──────────────────────────────
server.tool(
"get_user",
"社内ユーザーをIDで取得します。ユーザーの名前・メール・部署・役職が返ります。",
GetUserSchema.shape,
async (input) => getUserHandler(input, apiClient)
);
server.tool(
"search_users",
"社内ユーザーをキーワードで検索します。名前やメールアドレスで部分一致検索できます。",
SearchUsersSchema.shape,
async (input) => searchUsersHandler(input, apiClient)
);
// ── リソース登録 ─────────────────────────────
server.resource(
"user-profile",
"internal://users/{userId}/profile",
{ description: "社内ユーザーのプロフィール情報" },
async (uri) => {
const match = uri.pathname.match(/\/users\/(\d+)\/profile/);
if (!match) throw new Error("Invalid URI format");
const userId = parseInt(match[1], 10);
return readUserProfile(userId, apiClient);
}
);
// ── サーバー起動 ─────────────────────────────
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Internal API MCP Server is running on stdio");
}
main().catch((error) => {
console.error("Server failed to start:", error);
process.exit(1);
});
ポイント:
console.errorを使うのは、stdioトランスポートではstdoutがMCPプロトコルの通信路として使われるためです。ログ出力は必ずstderrに流しましょう。
4. 実装パターンの整理
ツール定義のベストプラクティス
守るべき4つの原則を挙げます。
- descriptionを丁寧に書く ― Claude CodeがどのツールをいつAIが選択するかは、このdescriptionが判断材料になります
-
Zodスキーマで
.describe()を全フィールドに付ける ― AIが引数の意味を正しく理解できます -
エラー時は
isError: trueを返す ― Claude Codeがエラーを認識してリトライや説明ができます -
レスポンスはJSON文字列をテキストで返す ― 構造化データも
type: "text"で返すのがMCPの標準パターンです
ツール vs リソース ― 使い分け
| 観点 | ツール(Tool) | リソース(Resource) |
|---|---|---|
| 呼び出し主体 | AIが判断して呼ぶ | ユーザーまたはAIがコンテキストとして読む |
| 副作用 | あってよい(POST/PUT/DELETE) | 読み取り専用 |
| 典型的な用途 | API呼び出し、データ操作 | 設定ファイル、プロフィール情報の参照 |
5. Claude Codeへの登録・動作確認・デバッグ
ビルドと登録
# ビルド
npm run build
# Claude Codeにプロジェクトスコープで登録
claude mcp add internal-api -- node /absolute/path/to/mcp-internal-api/dist/index.js
# 環境変数付きで登録する場合
claude mcp add internal-api \
-e INTERNAL_API_URL=https://internal-api.example.com \
-e INTERNAL_API_TOKEN=your-secret-token \
-- node /absolute/path/to/mcp-internal-api/dist/index.js
登録確認
# 登録済みMCPサーバー一覧
claude mcp list
# 出力例:
# internal-api: node /path/to/dist/index.js (project)
動作確認
Claude Codeを起動して、自然言語で試してみましょう。
> ユーザーID 42の情報を取得して
> エンジニアリング部の田中さんを検索して
> ID 100番のユーザープロフィールを見せて
デバッグのコツ
① MCP Inspectorを使う
npx @modelcontextprotocol/inspector node dist/index.js
ブラウザでMCPサーバーのツール一覧確認やテスト呼び出しが可能です。開発中はこれを常に開いておくと効率が上がります。
② stderrログを確認する
# Claude Codeのデバッグモードでstderrを表示
claude --mcp-debug
③ よくあるトラブルと対処
| 症状 | 原因 | 対処 |
|---|---|---|
| ツールが表示されない | パスが間違っている / ビルドしていない |
claude mcp listでパスを確認、npm run buildを実行 |
spawn ENOENT |
nodeのパスが見つからない | 絶対パスでnodeを指定する |
| 接続後すぐ切れる | サーバー起動時にstdoutに出力している |
console.log → console.error に変更 |
| 引数が正しく渡されない | Zodスキーマの.describe()が不足 |
各フィールドに説明を追加 |
6. 本番運用に向けた設計
認証の強化
// 環境変数からトークンを取得(必須チェック付き)
function getRequiredEnv(key: string): string {
const value = process.env[key];
if (!value) {
console.error(`ERROR: Environment variable ${key} is required`);
process.exit(1);
}
return value;
}
const token = getRequiredEnv("INTERNAL_API_TOKEN");
Claude Code側の登録時に-eフラグで環境変数を渡すか、.claude/ディレクトリ内の設定ファイルで管理できます。トークンをソースコードにハードコードしないことが鉄則です。
レート制限
社内APIに負荷をかけすぎないための簡易リミッターの例です。
class RateLimiter {
private timestamps: number[] = [];
constructor(
private maxRequests: number = 30,
private windowMs: number = 60_000
) {}
async check(): Promise<void> {
const now = Date.now();
this.timestamps = this.timestamps.filter((t) => now - t < this.windowMs);
if (this.timestamps.length >= this.maxRequests) {
throw new Error(
`Rate limit exceeded: max ${this.maxRequests} requests per ${this.windowMs / 1000}s`
);
}
this.timestamps.push(now);
}
}
// APIクライアントで使用
const limiter = new RateLimiter(30, 60_000);
async function requestWithLimit<T>(fn: () => Promise<T>): Promise<T> {
await limiter.check();
return fn();
}
ログ設計
stdioトランスポートではstdoutが使えないため、ログは全てstderrに出力します。
function log(level: "INFO" | "WARN" | "ERROR", message: string, meta?: object) {
const entry = {
timestamp: new Date().toISOString(),
level,
message,
...meta,
};
console.error(JSON.stringify(entry));
}
// 使用例
log("INFO", "Tool called", { tool: "get_user", input: { userId: 42 } });
本格的に運用する場合は、stderrのログをファイルやログ管理サービスにリダイレクトする構成を検討してください。
まとめ
-
MCPサーバーは
@modelcontextprotocol/sdkとzodだけで自作できる。 stdioトランスポートを使えば、Claude Codeとの連携設定はコマンド1つで完了します -
ツール定義の
descriptionと.describe()がAIの判断精度を左右する。 日本語で丁寧に書くことで、Claude Codeが適切なタイミングで正しい引数を渡してくれます - 本番運用では認証・レート制限・ログの3点を初期設計に含める。 stdoutに何も出さない(stderrに出す)というstdioトランスポート特有の制約も忘れずに