Model Context Protocol (MCP)を使ったSaaS向けMCPサーバーの実装ガイド

2025年のAIツール分野に注目していれば、Model Context Protocol (MCP)が「Anthropicの興味深い仕様」から「すべての本気のSaaS製品が対応する必要がある技術」へと進化したことに気づいているはずです。そして理由は十分あります。MCPはAIエージェント(Claude、GPT系アシスタント、カスタムエージェント)に対して、あなたのAPIを発見して呼び出すための標準化された方法を提供します。これはagentic時代のOpenAPIのようなものですが、リアルタイム双方向通信が組み込まれています。

過去数ヶ月間、複数のSaaS製品向けMCPサーバーを構築してきた経験から、実際に動くこと、ドキュメントに書かれていないこと、そして重要なアーキテクチャの決定について共有したいと思います。これは仕様の繰り返しではありません。私が最初に始めたときに欲しかったガイドです。

目次

Model Context Protocol (MCP)とは?

MCPはAnthropicが開発したオープンプロトコルで、AIモデルが外部ツールとデータソースと通信する方法を定義しています。2024年後半にリリースされ、2025年初頭にv1.0の安定版に到達しており、Claude Desktop、Cursor、Windsurf、OpenAI Agents SDK、その他数十のAIクライアントでサポートされています。

核心的な考え方:あらゆるAI統合がプロプライエタリ形式を持つカスタムプラグインである代わりに、MCPは任意の準拠クライアントが使用できる単一プロトコルを提供し、あなたのサービスが何を提供しているかを発見し、それと相互作用できます。

MCPが定義するもの:

  • ツール -- AIが呼び出せる関数(create_ticketsearch_usersgenerate_reportなど)
  • リソース -- AIが読み取れるデータ(ドキュメント、データベースレコード、設定ファイルなど)
  • プロンプト -- サーバーが公開できる再利用可能なプロンプトテンプレート
  • サンプリング -- サーバーがクライアントに対してLLM補完をリクエストできる機能

トランスポート層はstdio(ローカルサーバー用)またはServer-Sent Events (SSE)を使用したHTTP(リモートサーバー用)で、JSON-RPC 2.0を使用します。2025-03仕様改訂で導入されたより新しいStreamable HTTPトランスポートは、プロダクションSaaS展開に適しています。

SaaS製品がMCPサーバーを必要とする理由

ぶっちゃけ言うと、APIを持つSaaS製品なら、今すぐMCPサーバーを構築すべきです。理由はこうです。

AIエージェントがプライマリAPIコンシューマーになりつつあります。 2025年Q1、Anthropicはアムステルダムで開催されたカンファレンスで、Claude Desktopユーザーが1日200万回以上MCPツールを呼び出していると報告しました。その数は急速に増加しています。顧客は既にあなたの製品とやり取りするためにAIエージェントを使おうとしています。問題はそれを簡単にするか、それとも面倒にするかです。

これは流通チャネルです。 誰かがClaude Desktopをインストールして「プロジェクト管理を手伝って」と入力すると、AIはユーザーが設定したMCPサーバーを発見して使用できます。あなたの製品は自然言語を通じてアクセス可能になります。これは単なる見せかけではなく、真の新しい製品の露出領域です。

競合他社がこれをやっています。 Stripe、Linear、Notion、GitHub、Sentry、Supabaseはすべて2025年上半期にMCPサーバーを提供しました。B2BのSaaS企業で、もしあなたが持っていなければ、後れを取っています。

要因 REST API のみ REST API + MCPサーバー
AI エージェントのアクセシビリティ エージェントごとのカスタム統合が必要 任意の MCP クライアントが自動的に動作
発見 開発者がドキュメントを読む AI が実行時に機能を発見
最初の統合までの時間 数時間から数日 数分
自然言語交互作用 ラッパーなしでは不可能 組み込み
保守負荷 1つのコードベース 2つのコードベース(ただし MCP が REST をラップする)

MCPアーキテクチャ:実際の動作方法

コードを書く前に、アーキテクチャをはっきりさせましょう。MCPデプロイメントには3つの部分があります:

  1. MCPクライアント -- AIアプリケーション(Claude Desktop、Cursor、カスタムエージェント)。サーバーの機能を発見して呼び出します。
  2. MCPサーバー -- あなたのコード。MCPプロトコル経由でツール、リソース、プロンプトを公開します。これが私たちが構築しているものです。
  3. あなたのSaaS API -- 実際のバックエンド。MCPサーバーが物事を成し遂げるために呼び出します。

フローはこのようになります:

ユーザー → AI クライアント → MCP クライアント → MCP サーバー → あなたの SaaS API
                                      ↓
                              レスポンスが戻ってくる

あなたのMCPサーバーは本質的にプロトコルアダプターです。MCPプロトコル(AIクライアントが話す)と既存のREST/GraphQL APIの間を翻訳します。つまり、既存のAPIを全く変更する必要がありません。MCPサーバーはそのそばに存在します。

トランスポートオプション

SaaS製品には2つの現実的なトランスポートオプションがあります:

  • Streamable HTTP(推奨):標準HTTPリクエストをSSEのオプション付きで使用します。ロードバランサー、CDN、標準インフラの背後で動作します。これはリモート/ホストされたMCPサーバーに対して必要です。
  • SSE(レガシー):元のリモートトランスポート。まだ動作しますが、仕様は新規実装にStreamable HTTPを推奨しています。

Stdioトランスポートはローカルツールに適していますが、SaaS製品のMCPサーバーには適用できません。

MCPサーバープロジェクトのセットアップ

TypeScriptでこれを構築しましょう。公式の@modelcontextprotocol/sdkパッケージはよくメンテナンスされており、プロダクション使用に適しています。Pythonにはmcp(公式Python SDK)がありますが、一緒に働くほとんどのSaaSバックエンドがNode.jsを使用するため、TypeScriptに焦点を当てます。

mkdir my-saas-mcp-server
cd my-saas-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod express
npm install -D typescript @types/node @types/express tsx

tsconfig.jsonをセットアップします:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

基本的なサーバー構造を作成しましょう:

// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";

const app = express();
app.use(express.json());

const server = new McpServer({
  name: "my-saas-mcp",
  version: "1.0.0",
  description: "MCP server for My SaaS Product",
});

// ツール、リソース、プロンプトをここに追加します

app.post("/mcp", async (req, res) => {
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: () => crypto.randomUUID(),
  });
  
  await server.connect(transport);
  await transport.handleRequest(req, res);
});

app.get("/mcp", async (req, res) => {
  // ストリーミングレスポンス用の SSE エンドポイント
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: () => crypto.randomUUID(),
  });
  
  await server.connect(transport);
  await transport.handleRequest(req, res);
});

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
  console.log(`MCP server running on port ${PORT}`);
});

これはスケルトンです。埋めていきましょう。

ツール、リソース、プロンプトの定義

ツール

ツールは最も重要なプリミティブです。各ツールはAIが構造化パラメータで呼び出せる関数です。定義方法は以下のとおりです:

import { z } from "zod";

server.tool(
  "create_project",
  "Create a new project in the workspace",
  {
    name: z.string().describe("The project name"),
    description: z.string().optional().describe("Project description"),
    team_id: z.string().describe("ID of the team that owns this project"),
  },
  async ({ name, description, team_id }) => {
    // SaaS APIをここで呼び出します
    const project = await apiClient.createProject({ name, description, team_id });
    
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(project, null, 2),
        },
      ],
    };
  }
);

ツール設計について学んだいくつかのこと:

説明は具体的に。 AIはツール説明とパラメータ説明を使用して、いつどのように呼び出すかを決定します。曖昧な説明は間違ったツール選択につながります。「新しいプロジェクトを作成する」はまあ大丈夫です。「ユーザーのワークスペースに新しいプロジェクトを作成します。team_idが必要です。これはlist_teamsツールから取得できます」はずっと良いです。

ツール数は管理可能に保つ。 50以上のツールを公開して、AIが混乱するのはなぜか不思議に思う人を見てきました。10~15個のコア操作から始めます。いつでも追加できます。

構造化データを返す。 テキストコンテンツ内には常にJSONを返します。AIはテキストより良く解析します。

リソース

リソースは読み取り可能なデータを公開します。AIの消費向けのGETエンドポイントのように考えてください:

server.resource(
  "project-list",
  "projects://list",
  "List of all projects in the workspace",
  async () => {
    const projects = await apiClient.listProjects();
    return {
      contents: [
        {
          uri: "projects://list",
          mimeType: "application/json",
          text: JSON.stringify(projects, null, 2),
        },
      ],
    };
  }
);

プロンプト

プロンプトは再利用可能なテンプレートです。過小評価されていますが強力です:

server.prompt(
  "weekly-report",
  "Generate a weekly status report for a project",
  {
    project_id: z.string().describe("The project ID to report on"),
  },
  async ({ project_id }) => {
    const stats = await apiClient.getProjectStats(project_id);
    return {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `Generate a weekly status report based on this data: ${JSON.stringify(stats)}. Include completed tasks, blockers, and upcoming deadlines.`,
          },
        },
      ],
    };
  }
);

SaaS APIへの接続

MCPサーバーは既存のAPIと会話する必要があります。型付きAPIクライアントクラスを作成することをお勧めします:

// src/api-client.ts
class SaaSApiClient {
  private baseUrl: string;
  private apiKey: string;

  constructor(baseUrl: string, apiKey: string) {
    this.baseUrl = baseUrl;
    this.apiKey = apiKey;
  }

  private async request<T>(path: string, options?: RequestInit): Promise<T> {
    const response = await fetch(`${this.baseUrl}${path}`, {
      ...options,
      headers: {
        "Authorization": `Bearer ${this.apiKey}`,
        "Content-Type": "application/json",
        ...options?.headers,
      },
    });

    if (!response.ok) {
      const error = await response.text();
      throw new Error(`API error ${response.status}: ${error}`);
    }

    return response.json() as T;
  }

  async createProject(data: CreateProjectInput): Promise<Project> {
    return this.request<Project>("/api/v1/projects", {
      method: "POST",
      body: JSON.stringify(data),
    });
  }

  async listProjects(): Promise<Project[]> {
    return this.request<Project[]>("/api/v1/projects");
  }

  // ... 他のメソッド
}

このクライアントは薄く保ちます。パススルーであり、ビジネスロジック層ではありません。

認証とマルチテナンシー

ここからが面白くなります。そしてほとんどのチュートリアルが軽く扱う難しい部分です。

MCPサーバーは2つの方法でリクエストを認証する必要があります:

  1. MCPクライアントがあなたのMCPサーバーを認証する(「これは有効なユーザーか?」)
  2. あなたのMCPサーバーがあなたのSaaS APIを認証する(「このユーザーに代わって動作する」)

OAuth 2.0(プロダクション向け推奨)

MCP仕様にはOAuth 2.1に基づく認可フレームワークが含まれています。SaaS製品の場合、これが正しいアプローチです:

// OAuth トークンを抽出して検証するミドルウェア
app.use("/mcp", async (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Missing authorization" });
  }

  const token = authHeader.slice(7);
  
  try {
    const user = await validateOAuthToken(token);
    req.user = user;
    next();
  } catch {
    return res.status(401).json({ error: "Invalid token" });
  }
});

次に、ユーザーコンテキストをツールハンドラーに渡します。これはマルチテナンシーにとって重要です。すべてのAPI呼び出しは認証されたユーザーのワークスペースにスコープされるべきです。

APIキーアプローチ(シンプル、内部向け/初期段階向け)

初期段階または内部用のみの場合、APIキーは問題なく機能します:

const apiKey = req.headers["x-api-key"] as string;
const client = new SaaSApiClient(process.env.API_BASE_URL!, apiKey);

ユーザーはMCPサーバーをクライアントで設定するときにAPIキーを提供し、それがあなたのAPIにパススルーされます。

エラーハンドリングと検証

AIエージェントには明確なエラーメッセージが必要です。ツールが失敗したら、AIはなぜ失敗したのかを理解する必要があり、入力を修正するか、ユーザーに問題を説明できます。

server.tool(
  "get_project",
  "Get project details by ID",
  { project_id: z.string().uuid().describe("The project's UUID") },
  async ({ project_id }) => {
    try {
      const project = await apiClient.getProject(project_id);
      return {
        content: [{ type: "text", text: JSON.stringify(project, null, 2) }],
      };
    } catch (error) {
      const message = error instanceof Error ? error.message : "Unknown error";
      return {
        isError: true,
        content: [
          {
            type: "text",
            text: `Failed to get project: ${message}. Make sure the project_id is a valid UUID and the project exists in your workspace.`,
          },
        ],
      };
    }
  }
);

isError: trueフラグに注目してください。これはツール呼び出しが失敗したことをAIクライアントに伝えるため、適切に処理できます。常にエラーメッセージに実行可能なガイダンスを含めます。

MCPサーバーのテスト

MCPサーバーのテストは2025年にはかなり簡単になりました。選択肢は次のとおりです:

MCP インスペクター

公式MCP Inspectorは開発中の最良の友です:

npx @modelcontextprotocol/inspector

これはウェブUIを提供します。ここでサーバーに接続し、ツール/リソースを閲覧し、対話的に呼び出すことができます。絶えず使用します。

自動化テスト

CI/CDの場合、ツールを通常の非同期関数としてテストします:

import { describe, it, expect } from "vitest";

describe("create_project tool", () => {
  it("should create a project with valid input", async () => {
    const result = await createProjectHandler({
      name: "Test Project",
      team_id: "team-123",
    });

    expect(result.isError).toBeUndefined();
    const data = JSON.parse(result.content[0].text);
    expect(data.name).toBe("Test Project");
  });

  it("should return error for missing team_id", async () => {
    // Zod 検証はハンドラーが実行される前にこれをキャッチします
    // 検証レイヤーをテストします
  });
});

Claude Desktop を使用した統合テスト

サーバーが実行されたら、Claude Desktopの設定に追加します:

{
  "mcpServers": {
    "my-saas": {
      "url": "http://localhost:3001/mcp",
      "headers": {
        "Authorization": "Bearer your-test-token"
      }
    }
  }
}

それから単にClaudeと会話して、自然にツールを使おうとしてください。自動化されたテストが逃すエッジケースをすぐに見つけるでしょう。

デプロイとプロダクション環境での考慮事項

デプロイ先

MCPサーバーはただのExpressアプリです。Node.jsサービスをデプロイするどこにでもデプロイします。いくつかの良いオプション:

プラットフォーム コールドスタート コスト(概算) 最適な用途
Railway なし ~$5-20/月 小~中規模 SaaS
Fly.io <500ms ~$5-15/月 グローバル分散
AWS ECS/Fargate なし ~$15-50/月 エンタープライズ、既存 AWS
Vercel (Edge) <100ms $0-20/月 既に Vercel を使用している場合
Cloudflare Workers <5ms $0-5/月 パフォーマンス重視

レート制限

AIエージェントはおしゃべりです。単一のユーザー会話は20~30のツール呼び出しをトリガーする可能性があります。通常のAI使用に十分に寛容で、虐待を防ぐレート制限を実装します:

import rateLimit from "express-rate-limit";

const limiter = rateLimit({
  windowMs: 60 * 1000, // 1 分
  max: 60, // ユーザーあたり 1 分あたり 60 リクエスト
  keyGenerator: (req) => req.user?.id || req.ip,
});

app.use("/mcp", limiter);

モニタリング

ユーザーID、ツール名、レイテンシーと共に、すべてのツール呼び出しをログに記録します。最も使用されるツール、最も失敗するツール、レイテンシーボトルネックがどこにあるかについて可視性が必要です。Datadog、Axiom、またはCloudWatchへの構造化JSONログでも問題ありません。

バージョン管理

MCPサーバーは進化します。サーバーメタデータのversionフィールドを使用し、遷移中にパスプリフィックス(/mcp/v1/mcp/v2)の背後で複数のバージョンを実行することを検討してください。

実例:プロジェクト管理SaaS向けMCPサーバーの構築

実例を見てみましょう。プロジェクト管理ツール(簡略化されたLinearまたはAsanaのようなもの)向けMCPサーバーを構築しているとします。

公開するツールセット:

// コア CRUD ツール
server.tool("list_projects", ...);
server.tool("get_project", ...);
server.tool("create_project", ...);
server.tool("update_project", ...);

// タスク管理
server.tool("list_tasks", ...);  // ステータス、担当者、プロジェクトでフィルタリング
server.tool("create_task", ...);
server.tool("update_task", ...); // ステータス、担当者、優先度を更新
server.tool("add_comment", ...);

// 検索とレポート
server.tool("search", ...);      // プロジェクトとタスク全体で全文検索
server.tool("get_project_stats", ...); // プロジェクトの概要統計

// リソース
server.resource("workspace-info", ...); // ワークスペース設定、チームメンバー

// プロンプト
server.prompt("standup-report", ...);   // 最近のアクティビティからスタンドアップを生成
server.prompt("sprint-planning", ...);  // スプリント計画を支援

これは12個のツール、1つのリソース、2つのプロンプトです。AIのツール選択を圧倒することなく、本当に有用であるのに十分です。

ユーザーエクスペリエンスはこのようになります:誰かがClaude Desktopを開いて「Backend Rewriteプロジェクトで期日を超過したタスクは?」と言います。Claudeはステータスフィルタとプロジェクト名でlist_tasksを呼び出し、結果を取得して、自然言語で提示します。ユーザーが「auth migration タスクをSarahに割り当てて、優先度を高くして」と言います。Claudeはupdate_taskを呼び出します。それは魔法のように感じ、実は単なるプロトコルの配管です。

これのようなものを構築していて、Next.jsフロントエンドやこれらのプロジェクトに付随することが多いヘッドレスCMS層で支援が必要な場合、それはSocial Animalで大量に行うことです。ただし、MCPサーバー自体?それはこのガイドを使用して社内で絶対に構築できます。

FAQ

MCPと関数呼び出しの違いは何ですか?

関数呼び出し(OpenAIの関数呼び出しやClaudeのツール使用など)は、AIモデルが単一API呼び出し内で関数を呼び出すことを決定する方法です。MCPは、AIクライアントが外部サーバーから利用可能な関数を発見できるプロトコルです。それらは一緒に機能します。AIクライアントは内部的に関数呼び出しを使用して、MCPツールを呼び出すときを決定します。MCPをシステム間の配管として考え、関数呼び出しをモデルの意思決定プロセスとして考えてください。

MCPサーバーを構築して実行するのはどのくらいの費用がかかりますか?

サーバー自体は軽量です。10~20のツールを持つ典型的なSaaS製品の場合、数百行のTypeScriptを見ています。ホスティングコストは、トラフィックとプラットフォームに応じて、$5~50/月です。実際のコストは開発者の時間です。認証、エラーハンドリング、モニタリング、テストを備えたプロダクション品質MCPサーバーに2~4週間の予算を立てます。それが多く感じられる場合、私たちはチームがこれらをより速く提供するのを支援しました。詳細については価格ページを確認してください。

代わりにPythonを使用できますか?

絶対に。公式Python SDK(pip install mcp)は優れており、ツール定義に関してはより良いアルゴロジクスを持つと主張できます。あなたのチームが知っていることは何でも使用してください。プロトコルは言語にとらわれません。あなたのSaaSバックエンドがPython(Django、FastAPI)の場合、ツールモデルと検証ロジックを共有できるため、MCPサーバーをPythonで構築することはさらに理にかなっています。

既存のAPIを修正する必要がありますか?

いいえ。MCPサーバーは既存のAPIを呼び出す別のサービスです。アダプター層です。それはあなたが見つけるかもしれません自分自身が何か特にAI消費用のAPIエンドポイントを追加したい -- 例えば、あなたのUIが必要とするよりも多くのコンテキストを返す検索エンドポイント。それで大丈夫です。しかし、修正ではなく追加です。

長時間実行されるオペレーションをどのように処理しますか?

一部のツールは数分かかる操作をトリガーする可能性があります(レポート生成や大規模インポート処理など)。MCP の進捗通知機能を使用して、クライアントを通知します。ツール ハンドラーは操作の完了を待つ間、進捗更新を出力できます。非常に長い操作(>30秒)の場合、即座にジョブIDで戻り、別のcheck_job_statusツールを提供することを検討してください。

MCPはプロダクション向けに十分に安定していますか?

はい、2025年中盤時点では。仕様はv1.0に達し、2025-03-26改訂(Streamable HTTPを追加した)が主要クライアントで採用されています。Anthropic、Microsoft、Google、OpenAIはすべてプロトコルに投資しています。それは消えません。それはあります。ただし、仕様更新に注視してください。より良い認証フローとサーバー間通信に関するアクティブな提案があります。

MCPツールでのページネーションを処理する最良の方法は何ですか?

デフォルトで妥当なページの結果(20~50項目)を返し、cursorまたはpageパラメーターを受け入れます。AIがより多くの結果があることを知るようにレスポンスにページネーションメタデータを含めます。何か:{ results: [...], next_cursor: "abc123", total_count: 342 }。ユーザーがより多くのデータを必要とする場合、AIは自然に次のページをリクエストします。

1つのMCPサーバーが複数のAIクライアントを同時にサポートできますか?

はい、そうあるべきです。MCPサーバーはただのHTTPサーバーです。各リクエストにはユーザーの認証トークンが含まれるため、すべてを正しいテナントに対してスコープします。Streamable HTTPトランスポートを正しく使用している場合、心配するべきクライアント固有の状態はありません。それを他のステートレスAPIサーバーのように扱います。