取り組みの背景 — MCPがもたらす開発体験の変革
**MCP(Model Context Protocol)**は、AIエージェントと外部ツールをつなぐ標準プロトコルです。AntigravityのAIエージェントは、MCPサーバーを通じてデータベースクエリの実行、外部APIの呼び出し、ファイル操作など、IDE内から直接さまざまな外部リソースにアクセスできます。
公式が提供するMCPサーバー(GitHub、Slack、Notion等)は便利ですが、企業固有の社内ツールや独自APIとの連携にはカスタムMCPサーバーの構築が不可欠です。ここで扱うのはTypeScriptによるMCPサーバーの設計・実装から、認証、エラーハンドリング、テスト、デプロイまでを、プロダクション品質で解説します。
MCPサーバーの基本概念を理解している前提で進めますので、初めての方はそちらを先にお読みください。
MCPサーバーのアーキテクチャ設計
プロダクション向けMCPサーバーを設計する際に押さえるべきアーキテクチャの原則を解説します。
レイヤード・アーキテクチャ
堅牢なMCPサーバーは、以下の3層で構成するのが効果的です。
- プロトコル層: MCP仕様に準拠したリクエスト/レスポンスの処理。ツール定義、バリデーション、シリアライゼーションを担当
- ビジネスロジック層: 実際のツール機能を実装。外部APIの呼び出し、データ加工、キャッシュ管理を担当
- インフラ層: 認証、レート制限、ロギング、ヘルスチェックなどの横断的関心事を担当
// プロジェクト構成
// src/
// ├── server.ts — MCPサーバーのエントリポイント
// ├── tools/ — ツール定義と実装
// │ ├── index.ts
// │ ├── database.ts — DB操作ツール
// │ └── api-client.ts — 外部API呼び出しツール
// ├── middleware/ — 認証・レート制限・ロギング
// │ ├── auth.ts
// │ ├── rate-limit.ts
// │ └── logger.ts
// └── config/ — 設定管理
// └── index.ts
ツール定義のベストプラクティス
MCPツールの定義では、AIエージェントが適切にツールを選択できるよう、明確なdescriptionと厳密なinputSchemaを記述することが極めて重要です。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({
name: "company-internal-tools",
version: "1.0.0",
});
// ツール定義: 社内ナレッジベース検索
server.tool(
"search_knowledge_base",
// descriptionはエージェントがツール選択の判断に使う重要な要素
"Search the company's internal knowledge base for technical documentation, " +
"runbooks, and architecture decision records. Returns relevant documents " +
"with titles, snippets, and relevance scores. Use this when the user asks " +
"about internal processes, system architecture, or troubleshooting steps.",
{
// inputSchemaはzodで厳密に定義
query: z.string().describe("Search query in natural language"),
category: z.enum(["runbook", "adr", "api-doc", "onboarding"])
.optional()
.describe("Filter by document category"),
limit: z.number().min(1).max(20).default(5)
.describe("Maximum number of results to return"),
},
async ({ query, category, limit }) => {
// ビジネスロジック層に委譲
const results = await knowledgeBase.search(query, { category, limit });
return {
content: [{
type: "text",
text: JSON.stringify(results, null, 2),
}],
};
}
);
認証とセキュリティの実装
プロダクション環境では、MCPサーバーへのアクセス制御が不可欠です。トークンベース認証、権限管理、入力バリデーションの実装パターンを紹介します。
トークンベース認証ミドルウェア
// src/middleware/auth.ts
interface AuthConfig {
tokenHeader: string;
validTokens: Set<string>;
allowedTools?: Map<string, string[]>; // token -> allowed tool names
}
function createAuthMiddleware(config: AuthConfig) {
return async (request: McpRequest, next: () => Promise<McpResponse>) => {
const token = request.headers?.[config.tokenHeader];
if (!token || !config.validTokens.has(token)) {
return {
error: {
code: -32001,
message: "Authentication required. Provide a valid API token.",
},
};
}
// ツールレベルの権限チェック
if (config.allowedTools && request.method === "tools/call") {
const toolName = request.params?.name;
const allowed = config.allowedTools.get(token) || [];
if (!allowed.includes(toolName)) {
return {
error: {
code: -32002,
message: `Token does not have permission to use tool: ${toolName}`,
},
};
}
}
return next();
};
}
入力サニタイゼーション
AIエージェントが生成する入力は、意図しない形式や悪意のあるペイロードを含む可能性があります。SQLインジェクションやコマンドインジェクションの防止策を必ず実装してください。
// src/tools/database.ts
import { z } from "zod";
// SQLインジェクション防止: パラメータ化クエリのみ許可
server.tool(
"query_metrics",
"Query application metrics from the analytics database. " +
"Supports filtering by date range, metric name, and environment.",
{
metric_name: z.enum([
"page_views", "api_latency", "error_rate",
"active_users", "conversion_rate"
]).describe("The metric to query"),
start_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/)
.describe("Start date in YYYY-MM-DD format"),
end_date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/)
.describe("End date in YYYY-MM-DD format"),
environment: z.enum(["production", "staging"]).default("production"),
},
async ({ metric_name, start_date, end_date, environment }) => {
// パラメータ化クエリでSQLインジェクションを防止
const rows = await db.query(
`SELECT date, value FROM metrics
WHERE metric_name = $1
AND date BETWEEN $2 AND $3
AND environment = $4
ORDER BY date`,
[metric_name, start_date, end_date, environment]
);
return {
content: [{
type: "text",
text: JSON.stringify({
metric: metric_name,
period: `${start_date} to ${end_date}`,
data_points: rows.length,
results: rows,
}, null, 2),
}],
};
}
);
レート制限とリソース管理
MCPサーバーが外部APIやデータベースに過負荷をかけないよう、レート制限を実装します。
// src/middleware/rate-limit.ts
class SlidingWindowRateLimiter {
private windows: Map<string, number[]> = new Map();
constructor(
private maxRequests: number,
private windowMs: number,
) {}
check(key: string): { allowed: boolean; retryAfterMs?: number } {
const now = Date.now();
const timestamps = this.windows.get(key) || [];
// ウィンドウ外のタイムスタンプを除去
const valid = timestamps.filter(t => now - t < this.windowMs);
this.windows.set(key, valid);
if (valid.length >= this.maxRequests) {
const oldestInWindow = valid[0];
const retryAfterMs = this.windowMs - (now - oldestInWindow);
return { allowed: false, retryAfterMs };
}
valid.push(now);
return { allowed: true };
}
}
// ツールごとに異なるレート制限を設定
const rateLimiters = {
search_knowledge_base: new SlidingWindowRateLimiter(30, 60_000), // 30回/分
query_metrics: new SlidingWindowRateLimiter(10, 60_000), // 10回/分
deploy_service: new SlidingWindowRateLimiter(2, 300_000), // 2回/5分
};
エラーハンドリングとリトライ戦略
外部APIの呼び出しは失敗する前提で設計します。エージェントが適切に対処できるよう、エラーメッセージを構造化して返します。
// src/tools/api-client.ts
interface ToolError {
recoverable: boolean;
message: string;
suggestion?: string;
retryAfterMs?: number;
}
async function withRetry<T>(
fn: () => Promise<T>,
options: {
maxRetries: number;
baseDelayMs: number;
maxDelayMs: number;
} = { maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 10000 }
): Promise<T> {
let lastError: Error | null = null;
for (let attempt = 0; attempt <= options.maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error as Error;
if (attempt === options.maxRetries) break;
// 指数バックオフ + ジッター
const delay = Math.min(
options.baseDelayMs * Math.pow(2, attempt) + Math.random() * 1000,
options.maxDelayMs
);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
// エージェント向けの構造化エラーレスポンス
function formatToolError(error: ToolError) {
return {
content: [{
type: "text",
text: JSON.stringify({
status: "error",
recoverable: error.recoverable,
message: error.message,
suggestion: error.suggestion,
retry_after_ms: error.retryAfterMs,
}),
}],
isError: true,
};
}
実践例 — Slack通知 + GitHub Issues統合
具体的なユースケースとして、Slack通知とGitHub Issuesの作成を統合したMCPサーバーの実装例を紹介します。
// src/tools/incident-response.ts
server.tool(
"create_incident",
"Create an incident report that simultaneously files a GitHub Issue " +
"and sends a notification to the team's Slack channel. Use this when " +
"a production issue is detected and needs immediate team awareness.",
{
title: z.string().min(5).max(200),
severity: z.enum(["critical", "high", "medium", "low"]),
description: z.string().min(20),
affected_services: z.array(z.string()).min(1),
},
async ({ title, severity, description, affected_services }) => {
// 並列実行でレイテンシを削減
const [githubResult, slackResult] = await Promise.allSettled([
// GitHub Issue作成
createGitHubIssue({
title: `[${severity.toUpperCase()}] ${title}`,
body: `## Incident Report\n\n${description}\n\n**Affected Services:** ${affected_services.join(", ")}`,
labels: ["incident", severity],
}),
// Slack通知
sendSlackNotification({
channel: severity === "critical" ? "#incidents-critical" : "#incidents",
text: `🚨 *${severity.toUpperCase()} Incident*: ${title}\n${description}`,
}),
]);
return {
content: [{
type: "text",
text: JSON.stringify({
github_issue: githubResult.status === "fulfilled"
? { url: githubResult.value.html_url, number: githubResult.value.number }
: { error: "Failed to create GitHub Issue" },
slack_notification: slackResult.status === "fulfilled"
? { sent: true, channel: slackResult.value.channel }
: { error: "Failed to send Slack notification" },
}, null, 2),
}],
};
}
);
この連携パターンは、マルチエージェントアーキテクチャの設計ガイドで解説しているManager Surfaceとも組み合わせて使えます。
テストとデバッグ
MCPサーバーのテストでは、MCP Inspector やユニットテストを組み合わせてツールの動作を検証します。
// tests/tools/knowledge-base.test.ts
import { describe, it, expect, vi } from "vitest";
describe("search_knowledge_base tool", () => {
it("正常なクエリで結果を返す", async () => {
// モックDBのセットアップ
const mockSearch = vi.fn().mockResolvedValue([
{ title: "Deploy Guide", snippet: "Steps to deploy...", score: 0.95 },
]);
const result = await searchKnowledgeBase({
query: "how to deploy",
limit: 5,
}, { search: mockSearch });
expect(result.content[0].type).toBe("text");
const parsed = JSON.parse(result.content[0].text);
expect(parsed).toHaveLength(1);
expect(parsed[0].title).toBe("Deploy Guide");
});
it("空の結果でも正常にレスポンスを返す", async () => {
const mockSearch = vi.fn().mockResolvedValue([]);
const result = await searchKnowledgeBase({
query: "nonexistent topic",
limit: 5,
}, { search: mockSearch });
const parsed = JSON.parse(result.content[0].text);
expect(parsed).toHaveLength(0);
});
it("DB接続エラー時に構造化エラーを返す", async () => {
const mockSearch = vi.fn().mockRejectedValue(new Error("Connection timeout"));
const result = await searchKnowledgeBase({
query: "test",
limit: 5,
}, { search: mockSearch });
expect(result.isError).toBe(true);
const parsed = JSON.parse(result.content[0].text);
expect(parsed.recoverable).toBe(true);
});
});
デプロイとモニタリング
本番環境へのデプロイでは、コンテナ化とヘルスチェックの実装が推奨されます。
# Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --production=false
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./
# ヘルスチェックエンドポイント
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3100/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"
EXPOSE 3100
CMD ["node", "dist/server.js"]
Antigravityのデバッグツールと組み合わせることで、MCPサーバーの動作確認を効率的に行えます。
個人開発者の視点から(実体験メモ)
ここまでの要点
AntigravityのMCPプロトコルを活用したカスタムサーバー構築は、AIエージェントの能力を飛躍的に拡張する手段です。本記事で解説したレイヤードアーキテクチャ、認証・レート制限、構造化エラーハンドリング、テスト戦略を組み合わせることで、プロダクション品質のMCPサーバーを安全に運用できます。
まずは社内の1つのAPIをMCPサーバーでラップするところから始め、段階的にツールを追加していくアプローチがおすすめです。