取り組みの背景 — MCPサーバーをプロダクトとして捉える
Model Context Protocol(MCP)は、ClaudeをはじめとするAIエージェントが外部ツールやデータソースとスムーズに連携するための標準プロトコルです。2025年後半からMCPエコシステムは急速に拡大し、2026年には「MCPサーバーを自社サービスの一部として提供する」企業や個人開発者が増えてきましました。
しかし多くのガイドは「MCPサーバーを作る方法」で止まっています。実際にユーザーに使ってもらい、安全に運用し、そして収益を得るための本番運用知識は、散在した情報をつなぎ合わせる必要があります。
本番アーキテクチャ設計 (Cloudflare Workers / Docker / VPS)
認証・認可 (OAuth 2.0 / APIキー管理)
レート制限とクォータ管理
セキュリティ強化 (プロンプトインジェクション対策・入力バリデーション)
監視・ロギング
Stripe連携による収益化
CI/CDパイプラインとゼロダウンタイム更新
対象読者は、MCPサーバーの基礎実装(MCPサーバー自作ガイド 、カスタムMCPサーバー完全実装ガイド )をすでに理解しており、次のステップとして「実際にユーザーに提供できる状態にしたい」方です。
本番アーキテクチャの設計パターン
MCPサーバーの本番デプロイには主に3つのアーキテクチャが選択肢となります。それぞれのトレードオフを理解した上で選択する点が肝心です。
パターン1: Cloudflare Workers(エッジデプロイ)
最も推奨される構成です。グローバルなエッジネットワークでリクエストを処理するため、レイテンシが低く、スケーラビリティも高い。無料枠も充実しており、個人開発者にも現実的です。
// src/index.ts — Cloudflare Workers MCP サーバー
import { Server } from "@modelcontextprotocol/sdk/server/index.js" ;
import { MCPWorker } from "./mcp-worker" ;
import { AuthMiddleware } from "./auth" ;
import { RateLimiter } from "./rate-limiter" ;
export interface Env {
KV : KVNamespace ; // セッション・APIキー保管
DB : D1Database ; // ユーザー・利用状況
STRIPE_SECRET_KEY : string ;
JWT_SECRET : string ;
RATE_LIMIT_REQUESTS : string ;
}
export default {
async fetch ( request : Request , env : Env ) : Promise < Response > {
// 1. 認証チェック
const authResult = await AuthMiddleware. verify (request, env);
if ( ! authResult.ok) {
return new Response ( JSON . stringify ({ error: "Unauthorized" }), {
status: 401 ,
headers: { "Content-Type" : "application/json" },
});
}
// 2. レート制限チェック
const rateOk = await RateLimiter. check (authResult.userId, env);
if ( ! rateOk) {
return new Response ( JSON . stringify ({ error: "Rate limit exceeded" }), {
status: 429 ,
headers: {
"Content-Type" : "application/json" ,
"Retry-After" : "60" ,
},
});
}
// 3. MCPリクエスト処理
const worker = new MCPWorker (env, authResult.userId);
return worker. handle (request);
} ,
} ;
パターン2: Docker + VPS(フルコントロール)
データのプライバシー要件が厳しい企業向けや、カスタム依存関係が必要な場合に適しています。
# Dockerfile — 本番用 MCP サーバー
FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:22-alpine AS runner
WORKDIR /app
# 非rootユーザーで実行(セキュリティ強化)
RUN addgroup -S mcpgroup && adduser -S mcpuser -G mcpgroup
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER mcpuser
# ヘルスチェック設定
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', r => process.exit(r.statusCode === 200 ? 0 : 1))"
EXPOSE 3000
CMD [ "node" , "dist/server.js" ]
パターン3: サーバーレス関数(AWS Lambda / Vercel Functions)
イベントドリブンな処理や、既存のクラウドインフラに統合したい場合に適しています。コールドスタートが問題になる場合があるため、プロビジョニングドコンカレンシーの設定が必要です。
OAuth 2.0認証の完全実装
MCPサーバーの本番運用において、認証は最も重要な要素のひとつです。2026年のMCP仕様ではOAuth 2.0が正式にサポートされており、これを正しく実装することで安全なサービス提供が可能になります。
APIキー + JWTのハイブリッド認証
シンプルさとセキュリティのバランスを取るため、APIキーによる認証とJWTセッションを組み合わせるパターンが実用的です。
// src/auth/middleware.ts
import { verify, sign } from "jsonwebtoken" ;
import { hash, compare } from "bcryptjs" ;
export interface AuthResult {
ok : boolean ;
userId ?: string ;
planType ?: "free" | "pro" | "enterprise" ;
error ?: string ;
}
export class AuthMiddleware {
static async verify ( request : Request , env : Env ) : Promise < AuthResult > {
const authHeader = request.headers. get ( "Authorization" );
if ( ! authHeader) {
return { ok: false , error: "Missing Authorization header" };
}
// Bearer Token (JWT) か APIキーかを判定
if (authHeader. startsWith ( "Bearer " )) {
return this . verifyJWT (authHeader. slice ( 7 ), env);
} else if (authHeader. startsWith ( "ApiKey " )) {
return this . verifyApiKey (authHeader. slice ( 7 ), env);
}
return { ok: false , error: "Invalid auth scheme" };
}
private static async verifyJWT ( token : string , env : Env ) : Promise < AuthResult > {
try {
const payload = verify (token, env. JWT_SECRET ) as {
sub : string ;
planType : "free" | "pro" | "enterprise" ;
exp : number ;
};
// トークン有効期限チェック(5分前から更新を促す)
const expiresIn = payload.exp - Math. floor (Date. now () / 1000 );
if (expiresIn < 300 ) {
// X-Refresh-Tokenヘッダーで更新を通知(クライアント側で処理)
return {
ok: true ,
userId: payload.sub,
planType: payload.planType,
};
}
return { ok: true , userId: payload.sub, planType: payload.planType };
} catch {
return { ok: false , error: "Invalid or expired JWT" };
}
}
private static async verifyApiKey ( apiKey : string , env : Env ) : Promise < AuthResult > {
// APIキーは "mcp_live_xxxxx" or "mcp_test_xxxxx" 形式
if ( ! apiKey. startsWith ( "mcp_" )) {
return { ok: false , error: "Invalid API key format" };
}
// KVからAPIキー情報を取得(ハッシュ化されて保存)
const keyHash = await this . hashApiKey (apiKey);
const keyData = await env. KV . get ( `apikey:${ keyHash }` , "json" ) as {
userId : string ;
planType : "free" | "pro" | "enterprise" ;
active : boolean ;
} | null ;
if ( ! keyData || ! keyData.active) {
return { ok: false , error: "API key not found or inactive" };
}
return { ok: true , userId: keyData.userId, planType: keyData.planType };
}
private static async hashApiKey ( key : string ) : Promise < string > {
const encoder = new TextEncoder ();
const data = encoder. encode (key);
const hashBuffer = await crypto.subtle. digest ( "SHA-256" , data);
const hashArray = Array. from ( new Uint8Array (hashBuffer));
return hashArray. map ( b => b. toString ( 16 ). padStart ( 2 , "0" )). join ( "" );
}
// APIキー生成(ユーザー登録時に呼び出す)
static generateApiKey ( type : "live" | "test" = "live" ) : string {
const randomBytes = crypto. getRandomValues ( new Uint8Array ( 32 ));
const randomHex = Array. from (randomBytes)
. map ( b => b. toString ( 16 ). padStart ( 2 , "0" ))
. join ( "" );
return `mcp_${ type }_${ randomHex }` ;
}
}
レート制限とクォータ管理
プランごとに異なるレート制限を設けることで、フェアユースを確保しながら高付加価値プランの差別化ができます。
// src/rate-limiter/index.ts
// スライディングウィンドウアルゴリズムによるレート制限
interface RateLimitConfig {
requestsPerMinute : number ;
requestsPerDay : number ;
tokensPerDay : number ; // ツール呼び出しトークン換算
}
const PLAN_LIMITS : Record < string , RateLimitConfig > = {
free: {
requestsPerMinute: 10 ,
requestsPerDay: 100 ,
tokensPerDay: 50_000 ,
},
pro: {
requestsPerMinute: 60 ,
requestsPerDay: 5_000 ,
tokensPerDay: 2_000_000 ,
},
enterprise: {
requestsPerMinute: 600 ,
requestsPerDay: 100_000 ,
tokensPerDay: 50_000_000 ,
},
};
export class RateLimiter {
static async check (
userId : string ,
env : Env ,
planType : string = "free"
) : Promise <{ allowed : boolean ; remaining : number ; resetAt : number }> {
const config = PLAN_LIMITS [planType] ?? PLAN_LIMITS .free;
const now = Date. now ();
const windowKey = `ratelimit:${ userId }:${ Math . floor ( now / 60_000 ) }` ;
// KVを使ったスライディングウィンドウカウンター
const current = await env. KV . get (windowKey);
const count = current ? parseInt (current) : 0 ;
if (count >= config.requestsPerMinute) {
return {
allowed: false ,
remaining: 0 ,
resetAt: Math. ceil (now / 60_000 ) * 60_000 ,
};
}
// カウンターをインクリメント(60秒後に自動削除)
await env. KV . put (windowKey, String (count + 1 ), { expirationTtl: 60 });
return {
allowed: true ,
remaining: config.requestsPerMinute - count - 1 ,
resetAt: Math. ceil (now / 60_000 ) * 60_000 ,
};
}
// 日次クォータチェック
static async checkDailyQuota ( userId : string , env : Env , planType : string ) : Promise < boolean > {
const config = PLAN_LIMITS [planType] ?? PLAN_LIMITS .free;
const today = new Date (). toISOString (). slice ( 0 , 10 ); // YYYY-MM-DD
const quotaKey = `quota:${ userId }:${ today }` ;
const used = await env. KV . get (quotaKey);
const usedCount = used ? parseInt (used) : 0 ;
return usedCount < config.requestsPerDay;
}
}
セキュリティ強化 — プロンプトインジェクション対策
MCPサーバーはユーザー入力をそのままClaudeに渡すリスクがあります。プロンプトインジェクション攻撃への対策は必須です。
// src/security/input-validator.ts
// 危険なパターンのリスト(定期的に更新が必要)
const INJECTION_PATTERNS = [
/ignore previous instructions/ gi ,
/disregard all prior/ gi ,
/system prompt:/ gi ,
/ \[ SYSTEM \] / gi ,
/< \| im_start \| >/ gi ,
/you are now/ gi ,
/act as/ gi ,
/jailbreak/ gi ,
];
const MAX_INPUT_LENGTH = 10_000 ; // 文字数上限
export class InputValidator {
static sanitize ( input : string ) : { safe : boolean ; sanitized : string ; reason ?: string } {
// 1. 長さチェック
if (input. length > MAX_INPUT_LENGTH ) {
return {
safe: false ,
sanitized: "" ,
reason: `Input exceeds maximum length of ${ MAX_INPUT_LENGTH } characters` ,
};
}
// 2. プロンプトインジェクション検出
for ( const pattern of INJECTION_PATTERNS ) {
if (pattern. test (input)) {
return {
safe: false ,
sanitized: "" ,
reason: "Potential prompt injection detected" ,
};
}
}
// 3. HTMLエスケープ(XSS対策)
const sanitized = input
. replace ( /&/ g , "&" )
. replace ( /</ g , "<" )
. replace ( />/ g , ">" )
. replace ( /"/ g , """ )
. replace ( /'/ g , "'" );
return { safe: true , sanitized };
}
// ツールパラメーターのバリデーション
static validateToolParams < T >( params : unknown , schema : Record < string , unknown >) : T | null {
// JSON Schema による検証(実装は ajv などのライブラリを使用)
try {
// 型チェックと必須フィールド検証
if ( typeof params !== "object" || params === null ) return null ;
return params as T ;
} catch {
return null ;
}
}
}
詳細なセキュリティパターンについては、Claude API プロダクションセキュリティ完全ガイドも参照してください。
監視・ロギング・エラートラッキング
本番MCPサーバーは、ツール呼び出しのすべてを記録・監視する必要があります。可観測性(Observability)なくして本番運用は成立しません。
// src/observability/logger.ts
export interface ToolCallLog {
requestId : string ;
userId : string ;
toolName : string ;
inputSize : number ;
outputSize : number ;
durationMs : number ;
status : "success" | "error" | "rate_limited" ;
errorMessage ?: string ;
timestamp : string ;
}
export class MCPLogger {
private env : Env ;
constructor ( env : Env ) {
this .env = env;
}
async logToolCall ( log : ToolCallLog ) : Promise < void > {
// 1. 構造化ログ出力(Cloudflare Workers は console.log が Logpush に転送される)
console. log ( JSON . stringify ({
level: log.status === "error" ? "error" : "info" ,
... log,
}));
// 2. D1 データベースへの永続化(分析・請求用)
await this .env. DB . prepare ( `
INSERT INTO tool_call_logs
(request_id, user_id, tool_name, input_size, output_size, duration_ms, status, error_message, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
` ). bind (
log.requestId,
log.userId,
log.toolName,
log.inputSize,
log.outputSize,
log.durationMs,
log.status,
log.errorMessage ?? null ,
log.timestamp,
). run ();
}
// ユーザーごとの使用統計を取得
async getUserStats ( userId : string , days : number = 30 ) : Promise <{
totalCalls : number ;
successRate : number ;
avgDurationMs : number ;
topTools : { name : string ; count : number }[];
}> {
const since = new Date (Date. now () - days * 86400_000 ). toISOString ();
const result = await this .env. DB . prepare ( `
SELECT
COUNT(*) as total_calls,
AVG(CASE WHEN status = 'success' THEN 1.0 ELSE 0.0 END) as success_rate,
AVG(duration_ms) as avg_duration_ms
FROM tool_call_logs
WHERE user_id = ? AND created_at >= ?
` ). bind (userId, since). first <{
total_calls : number ;
success_rate : number ;
avg_duration_ms : number ;
}>();
return {
totalCalls: result?.total_calls ?? 0 ,
successRate: result?.success_rate ?? 0 ,
avgDurationMs: result?.avg_duration_ms ?? 0 ,
topTools: [],
};
}
}
Stripe連携によるMCPサーバーのSaaS収益化
MCPサーバーをSaaSとして展開する核心は、課金フローの設計です。ここでは実績あるパターンを紹介します。
課金モデルの選択
MCPサーバーには主に3つの課金モデルが適しています。
サブスクリプション型 (月額固定)はユーザーにとって予測可能なコストが魅力で、ヘビーユーザーにとってお得感があります。従量課金型 (呼び出し回数・トークン数)は軽量ユーザーへの門戸を広げますが、請求の予測がしづらいデメリットがあります。ハイブリッド型 (基本料金 + 従量)が現実的なバランスで、月額を低く抑えつつ過剰使用には追加課金する設計です。
// src/billing/stripe.ts
import Stripe from "stripe" ;
export class BillingService {
private stripe : Stripe ;
constructor ( secretKey : string ) {
this .stripe = new Stripe (secretKey, { apiVersion: "2024-12-18.acacia" });
}
// サブスクリプション作成
async createSubscription ( params : {
customerId : string ;
planId : "free" | "pro" | "enterprise" ;
successUrl : string ;
cancelUrl : string ;
}) : Promise < string > {
const PRICE_IDS : Record < string , string > = {
pro: process.env. STRIPE_PRICE_PRO ! ,
enterprise: process.env. STRIPE_PRICE_ENTERPRISE ! ,
};
const session = await this .stripe.checkout.sessions. create ({
customer: params.customerId,
mode: "subscription" ,
line_items: [
{
price: PRICE_IDS [params.planId],
quantity: 1 ,
},
],
success_url: params.successUrl,
cancel_url: params.cancelUrl,
// メタデータでプラン種別を記録
metadata: {
plan_type: params.planId,
},
});
return session.url ! ;
}
// Webhookでサブスクリプション状態を同期
async handleWebhook ( payload : string , signature : string , env : Env ) : Promise < void > {
const event = this .stripe.webhooks. constructEvent (
payload,
signature,
env. STRIPE_WEBHOOK_SECRET ,
);
switch (event.type) {
case "customer.subscription.created" :
case "customer.subscription.updated" : {
const subscription = event.data.object as Stripe . Subscription ;
const customerId = subscription.customer as string ;
const planType = subscription.metadata.plan_type ?? "free" ;
const status = subscription.status;
// KVにサブスクリプション情報を保存
await env. KV . put (
`subscription:${ customerId }` ,
JSON . stringify ({ planType, status, updatedAt: Date. now () }),
);
break ;
}
case "customer.subscription.deleted" : {
const subscription = event.data.object as Stripe . Subscription ;
const customerId = subscription.customer as string ;
// フリープランにダウングレード
await env. KV . put (
`subscription:${ customerId }` ,
JSON . stringify ({ planType: "free" , status: "active" , updatedAt: Date. now () }),
);
break ;
}
}
}
// 使用量ベースの追加課金(従量課金)
async reportUsage ( subscriptionItemId : string , quantity : number ) : Promise < void > {
await this .stripe.subscriptionItems. createUsageRecord (subscriptionItemId, {
quantity,
timestamp: Math. floor (Date. now () / 1000 ),
action: "increment" ,
});
}
}
CI/CDパイプラインとゼロダウンタイム更新
MCPサーバーを継続的にデプロイするためのGitHub Actionsパイプラインです。
# .github/workflows/deploy.yml
name : Deploy MCP Server
on :
push :
branches : [ main ]
pull_request :
branches : [ main ]
jobs :
test :
runs-on : ubuntu-latest
steps :
- uses : actions/checkout@v4
- uses : actions/setup-node@v4
with :
node-version : "22"
cache : "npm"
- run : npm ci
- run : npm run test
- run : npm run typecheck
deploy-staging :
needs : test
runs-on : ubuntu-latest
if : github.event_name == 'pull_request'
steps :
- uses : actions/checkout@v4
- uses : actions/setup-node@v4
with :
node-version : "22"
cache : "npm"
- run : npm ci
- name : Deploy to Cloudflare Workers (staging)
run : npx wrangler deploy --env staging
env :
CLOUDFLARE_API_TOKEN : ${{ secrets.CF_API_TOKEN }}
deploy-production :
needs : test
runs-on : ubuntu-latest
if : github.ref == 'refs/heads/main'
environment : production # GitHub環境保護ルールを活用
steps :
- uses : actions/checkout@v4
- uses : actions/setup-node@v4
with :
node-version : "22"
cache : "npm"
- run : npm ci
- name : Deploy to Cloudflare Workers (production)
run : npx wrangler deploy --env production
env :
CLOUDFLARE_API_TOKEN : ${{ secrets.CF_API_TOKEN }}
# デプロイ後のスモークテスト
- name : Run smoke tests
run : npm run test:smoke
env :
MCP_SERVER_URL : ${{ secrets.PRODUCTION_URL }}
MCP_TEST_API_KEY : ${{ secrets.TEST_API_KEY }}
ゼロダウンタイム更新のポイント
Cloudflare Workersはデプロイが原子的(atomic)なため、本質的にゼロダウンタイムです。しかしステートフルなデータ(KV・D1)のスキーマ変更がある場合は注意が必要です。
後方互換性のある変更 (新フィールド追加など)は通常デプロイで問題なし
破壊的変更 (フィールド削除・型変更)は、古いコードと新しいコードが共存できる「移行期間」を設けてから切り替える
D1のマイグレーションは wrangler d1 migrations apply でバージョン管理する
パフォーマンス最適化と水平スケーリング
Cloudflare WorkersのDurable Objectsによる状態管理
セッション管理や複雑な状態を必要とする場合、Durable Objectsを使えば、エッジで一貫した状態管理が可能になります。
// src/session/durable-object.ts
export class MCPSession implements DurableObject {
private state : DurableObjectState ;
private env : Env ;
constructor ( state : DurableObjectState , env : Env ) {
this .state = state;
this .env = env;
}
async fetch ( request : Request ) : Promise < Response > {
const url = new URL (request.url);
switch (url.pathname) {
case "/session/get" : {
const sessionData = await this .state.storage. get ( "session" );
return Response. json (sessionData ?? {});
}
case "/session/set" : {
const data = await request. json ();
await this .state.storage. put ( "session" , data);
// 24時間後に自動削除
await this .state.storage. setAlarm (Date. now () + 86400_000 );
return Response. json ({ ok: true });
}
default :
return new Response ( "Not found" , { status: 404 });
}
}
// アラーム(セッションの自動クリーンアップ)
async alarm () : Promise < void > {
await this .state.storage. deleteAll ();
}
}
まとめ — MCPサーバーをビジネスに育てる
ここで扱うのはMCPサーバーを単なる開発成果物ではなく、ユーザーに価値を提供し収益を生む「プロダクト」として運用するための知識を体系的に紹介しました。
重要なポイントをまとめます。
認証は妥協しない : OAuth 2.0 / APIキー + JWTのハイブリッドで、セキュアかつ使いやすい認証を実現する
レート制限はプラン差別化の核心 : フリーから有料への誘導をレート制限で自然に設計する
監視なくして本番運用なし : D1への利用ログ記録と構造化ログで問題の早期発見が可能になる
Stripeとの統合はシンプルに : Checkout + Webhookの組み合わせで、安全な課金フローを最小工数で構築できる
CI/CDで品質と速度を両立 : GitHub Actions + Cloudflare Workersで、安全なゼロダウンタイムデプロイを実現する
次のステップとしては、実際にClaudeから接続してエンドツーエンドのテストを行い、カスタムMCPサーバー完全実装ガイド で基礎を確認しながら、本記事のセキュリティ・課金部分を段階的に追加していくことをおすすめします。