Xでシェア

Writing / MCP

N° 12

【実装】既存REST APIをMCPサーバー化する:Pythonでタスク管理APIをAIから操作する

公開GitHubサンプルを使い、既存のFastAPI製タスク管理APIをMCP Python SDKとhttpxでAIから操作できるようにします。cloneから起動、入力Schema、構造化出力、安全なエラー変換、stdio、Inspector、Codex、3層のpytestまで、実装と再現手順を日英で解説します。

このガイドに含まれます

MCP完全ガイド

この記事を読むとできるようになること

  • 既存REST APIを用途別MCP Toolとして公開できる
  • 認証・エラー・データ変換の境界を実装できる

はじめに:REST APIがあるのに、なぜMCPが必要なのか

REST APIは、プログラムがシステムを操作するための優れたインターフェースです。それでは、既存APIがある場合、AIにもそのAPIを直接呼ばせれば十分なのでしょうか。

技術的には、OpenAPI定義などをAIへ渡してHTTPリクエストを生成させる方法もあります。しかしAIクライアントが「利用可能な操作」「必要な引数」「戻り値の形」を共通の方法で発見し、実行するには、そのためのインターフェースが必要です。

MCPのToolは、名前、説明、入力Schemaをクライアントへ公開し、tools/callによって実行されます。MCPのTools仕様では、Tool定義に説明とJSON Schemaベースの入力定義が含まれます。

この記事で伝えたい中心メッセージは、次の一文です。

MCPはREST APIを置き換えるものではなく、AIクライアントが既存APIを利用するためのインターフェースを追加するものである。

MCPサーバーへ保存処理や業務ルールを複製するのではなく、既存REST APIのAI向けアダプターとして薄く実装します。

この記事と対応する実行可能な完成コードは、GitHubのtask-api-mcpリポジトリで公開しています。以降のファイル名、コマンド、テストはこのリポジトリの実装に合わせています。

1. 今回作るシステムの全体像

ユーザーは自然言語でタスク作成を依頼します。AIクライアントは依頼を構造化し、stdioでMCPサーバーのcreate_task Toolを呼びます。MCPサーバーはHTTPリクエストへ変換し、既存REST APIへ送信します。

flowchart LR
    accTitle: タスク作成システムの全体構成
    accDescr: ユーザーの自然言語による依頼をAIクライアントがMCP Tool呼び出しへ変換し、MCPサーバーがREST APIへHTTPリクエストを送り、APIがJSONファイルへ保存する。
    User[ユーザー] -->|自然言語| AI[AIクライアント]
    AI -->|MCP / stdio| MCP[MCPサーバー]
    MCP -->|HTTP POST /tasks| API[タスク管理REST API]
    API --> JSON[(data/tasks.json)]

システム全体のデータフロー

各コンポーネントの責務は次のとおりです。

コンポーネント

責務

AIクライアント

自然言語をToolの引数へ変換する

MCPサーバー

Toolを公開し、MCPとHTTPの間を変換する

REST API

業務ルールを検証し、タスクを作成する

JSONファイル

タスクを永続化する

実際の呼び出しでは、2種類の通信が連続して発生します。AIクライアントとMCPサーバーの間はMCP、MCPサーバーと既存システムの間はHTTPです。

sequenceDiagram
    accTitle: create_task Toolの実行順序
    accDescr: ユーザーの依頼を受けたAIクライアントがcreate_taskを呼び、MCPサーバーが入力を検証してREST APIへ送信し、APIがtasks.jsonへ保存した結果を同じ経路で返す。
    actor User as ユーザー
    participant AI as AIクライアント
    participant MCP as MCPサーバー
    participant API as REST API
    participant File as tasks.json
    User->>AI: 明日までのタスクを作成して
    AI->>MCP: tools/call create_task
    MCP->>MCP: 入力Schemaを検証
    MCP->>API: POST /tasks
    API->>File: タスクを保存
    File-->>API: 保存完了
    API-->>MCP: 201 Created
    MCP-->>AI: 構造化された作成結果
    AI-->>User: タスクを作成しました

create_taskの実行シーケンス

ここで重要なのは、MCPサーバーを新しい業務システムにしないことです。タスク作成の正規の入り口は、引き続きREST APIです。

2. 既存タスク管理REST APIを確認する

既存APIには次のエンドポイントがあるものとします。

plaintext
POST /tasks
GET /tasks
GET /tasks/{task_id}

この記事でMCP Toolとして公開するのはPOST /tasksに対応するcreate_taskだけです。

リクエスト例は次のとおりです。

json
{
  "title": "MCPの記事を公開する",
  "description": "図とコードを最終確認する",
  "priority": "high",
  "due_date": "2026-07-16"
}

成功時には201 Createdと作成済みタスクを返します。

json
{
  "id": "b48b8da3-f80b-40e3-a78d-8d70b11e58c4",
  "title": "MCPの記事を公開する",
  "description": "図とコードを最終確認する",
  "priority": "high",
  "due_date": "2026-07-16",
  "status": "todo",
  "created_at": "2026-07-15T21:30:00+09:00"
}

REST API側では、タイトルの必須・文字数、説明文の最大長、優先度、日付形式、過去日付の禁止などを検証します。この検証はMCPサーバーを追加した後も残します。CLI、Web UI、バッチ処理など、MCP以外の呼び出し元にも同じルールを適用するためです。

MCP実装を始める前に、API単体で確認しておきます。

bash
curl -X POST http://localhost:8000/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "title": "MCPの記事を公開する",
    "priority": "high",
    "due_date": "2026-07-16"
  }'

ここで失敗する場合は、先にREST APIを修正します。MCPを介したデバッグと既存APIのデバッグを同時に始めると、障害箇所を切り分けにくくなります。

3. REST APIをMCP化する設計

create_taskの内部から直接data/tasks.jsonを書き換える実装は、一見すると簡単です。しかし、次の問題が生じます。

  • RESTとMCPで保存処理が重複する
  • バリデーションや業務ルールが二重管理になる
  • 認証、監査ログ、レート制限を迂回する可能性がある
  • JSONからDBへ移行したときに両方を修正する必要がある
  • 呼び出し経路によって結果が変わり得る

そこで、MCP ToolはTaskApiClient.create_task()を呼び、そのクライアントがPOST /tasksを実行する三層構造にします。

Toolが担当するのは、MCPの入力と出力、および意味のあるエラーへの変換だけです。タスクを作成できるかという最終判断はREST APIへ残します。

REST APIとMCPサーバーが同じコードベースにあり、同一プロセスで動かすなら、ControllerとToolから共通のService層を呼ぶ設計も可能です。ただし今回は「既存システムを外部からMCP化する」という条件を明確にするため、HTTP境界を維持します。

4. 公開リポジトリからプロジェクトを準備する

完成版はtask-api-mcpリポジトリから取得できます。Python 3.10以上とuvを用意し、次の手順で依存関係を再現します。Inspectorを使う場合だけNode.js 18以上、Codexで試す場合だけCodex CLIも必要です。

bash
git clone https://github.com/yunosuke-github/task-api-mcp.git
cd task-api-mcp
uv sync --dev

uv.lockが含まれているため、記事と同じ検証済みの依存関係をインストールできます。2026年7月16日時点では、MCP Python SDKの安定版は1.28.1で、v2はプレリリースです。本サンプルはv1系の挙動を固定するためmcp[cli]==1.28.1を指定しています。PyPI公式v1.xリポジトリも併せて確認してください。

パッケージ設定にはREST APIとMCPサーバーの実行コマンドも定義しています。

pyproject.tomlplaintext
[project]
name = "task-api-mcp"
version = "0.1.0"
description = "A local task REST API with a thin MCP adapter"
readme = "README.md"
requires-python = ">=3.10"
license = "MIT"
dependencies = [
    "fastapi>=0.115,<1",
    "httpx>=0.27,<1",
    "mcp[cli]==1.28.1",
    "pydantic>=2.10,<3",
    "uvicorn>=0.30,<1",
]

[project.scripts]
task-api = "task_api.app:main"
task-mcp = "task_mcp.server:main"

[dependency-groups]
dev = [
    "pytest>=8.3,<9",
    "pytest-asyncio>=0.24,<2",
]

リポジトリは、REST APIとMCPアダプターを別パッケージに分けています。

plaintext
task-api-mcp/
├── data/tasks.json
├── src/
│   ├── task_api/
│   │   ├── app.py
│   │   ├── models.py
│   │   └── repository.py
│   └── task_mcp/
│       ├── api_client.py
│       ├── errors.py
│       ├── models.py
│       └── server.py
├── tests/
├── .env.example
├── pyproject.toml
└── uv.lock

task_apiは入力検証とJSON永続化を担当し、task_mcpはToolとHTTPの変換だけを担当します。MCPサーバーはdata/tasks.jsonを直接操作しません。

接続先、タイムアウト、保存先は環境変数へ分離しています。

.env.exampleplaintext
TASK_API_BASE_URL=http://localhost:8000
TASK_API_TIMEOUT_SECONDS=10
TASK_API_DATA_FILE=data/tasks.json

.envを使う場合は、uvへ明示的に渡します。

bash
cp .env.example .env
uv run --env-file .env task-api
# 別ターミナルでMCPサーバーを直接起動する場合
uv run --env-file .env task-mcp

5. REST APIクライアントを実装する

HTTP固有の処理をToolから分離し、REST APIの結果を3種類のアプリケーションエラーへ変換します。

src/task_mcp/errors.pypython
"""Application-level errors raised by the task REST API client."""


class TaskApiValidationError(Exception):
    """The API rejected a request that the caller can correct."""


class TaskApiUnavailableError(Exception):
    """The API could not be reached or returned a temporary failure."""


class TaskApiUnexpectedError(Exception):
    """The API returned an unexpected status or response body."""

httpxの例外や生のレスポンス本文をToolへ漏らさず、モデルが次の行動を判断できる意味へ変換します。

src/task_mcp/api_client.pypython
"""Async HTTP client used by the MCP adapter."""

from typing import Any

import httpx

from .errors import (
    TaskApiUnexpectedError,
    TaskApiUnavailableError,
    TaskApiValidationError,
)
from .models import CreateTaskRequest, TaskResult


class TaskApiClient:
    """Translate HTTP outcomes from the task API into domain-level errors."""

    def __init__(
        self,
        base_url: str,
        timeout_seconds: float,
        transport: httpx.AsyncBaseTransport | None = None,
    ) -> None:
        self._base_url = base_url.rstrip("/")
        self._timeout = timeout_seconds
        self._transport = transport

    async def create_task(self, request: CreateTaskRequest) -> TaskResult:
        """Create a task through the REST API and validate its response."""

        try:
            async with httpx.AsyncClient(
                base_url=self._base_url,
                timeout=self._timeout,
                transport=self._transport,
            ) as client:
                response = await client.post(
                    "/tasks",
                    json=request.model_dump(mode="json"),
                )
        except (httpx.TimeoutException, httpx.RequestError) as exc:
            raise TaskApiUnavailableError from exc

        if response.status_code == 201:
            try:
                return TaskResult.model_validate(response.json())
            except (ValueError, TypeError) as exc:
                raise TaskApiUnexpectedError from exc

        if response.status_code in {400, 409, 422}:
            raise TaskApiValidationError(self._safe_detail(response))

        if response.status_code >= 500:
            raise TaskApiUnavailableError

        raise TaskApiUnexpectedError

    @staticmethod
    def _safe_detail(response: httpx.Response) -> str:
        """Extract only a bounded, user-correctable API error message."""

        try:
            body: Any = response.json()
        except ValueError:
            return "The task request was rejected."

        detail = body.get("detail") if isinstance(body, dict) else None
        if isinstance(detail, str):
            return detail[:300]
        return "The task request was rejected."

明示したタイムアウト、201レスポンスの出力検証、修正可能な4xx、一時障害の5xx、想定外レスポンスを分けています。APIのdetailは文字列の場合だけ最大300文字まで採用し、配列や内部構造はそのまま返しません。

6. MCPサーバーを初期化する

v1.28.1ではFastMCPを次の場所からインポートします。

src/task_mcp/server.pypython
from mcp.server.fastmcp import FastMCP

mcp = FastMCP(
    name="task-manager",
    json_response=True,
)

FastMCPは、関数の型ヒントとdocstringからTool定義を生成します。Pydanticモデルなど互換性のある戻り値型を指定すれば、構造化出力のSchemaも生成できます。v1.xの公式READMEには、FastMCPの初期化、@mcp.tool()、Inspectorの利用例が掲載されています。

サーバー名、Tool名、docstringには異なる役割があります。

  • サーバー名は、クライアント上で接続先を識別する
  • Tool名は、モデルが実行する操作を識別する
  • docstringは、いつ何のために使う操作かを説明する
  • 引数descriptionは、各値の意味や制約を説明する

AIによる選択精度を高めるには、createexecuteではなく、対象を含むcreate_taskのような名前を使います。

7. create_task Toolを実装する

MCP境界で使う入出力モデルを定義します。REST API側にも独立したモデルがあり、最終的な業務ルールはそちらで検証します。

src/task_mcp/models.pypython
"""Models at the boundary between MCP and the task REST API."""

from datetime import date, datetime
from typing import Literal

from pydantic import BaseModel

Priority = Literal["low", "medium", "high"]


class CreateTaskRequest(BaseModel):
    """JSON body sent to ``POST /tasks``."""

    title: str
    description: str | None = None
    priority: Priority = "medium"
    due_date: date | None = None


class TaskResult(BaseModel):
    """Structured task returned to the MCP client."""

    id: str
    title: str
    description: str | None = None
    priority: Priority
    due_date: date | None = None
    status: str
    created_at: datetime

Toolの引数はモデルが組み立てやすいフラットな形にし、各引数の制約と説明をSchemaへ載せます。次は公開リポジトリのserver.py全体です。

src/task_mcp/server.pypython
"""stdio MCP server exposing the task API as a model-friendly tool."""

import logging
import os
import sys
from datetime import date
from typing import Annotated

from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.exceptions import ToolError
from pydantic import Field

from .api_client import TaskApiClient
from .errors import (
    TaskApiUnexpectedError,
    TaskApiUnavailableError,
    TaskApiValidationError,
)
from .models import CreateTaskRequest, Priority, TaskResult

logger = logging.getLogger(__name__)


def _timeout_seconds() -> float:
    raw_timeout = os.getenv("TASK_API_TIMEOUT_SECONDS", "10")
    try:
        timeout = float(raw_timeout)
    except ValueError as exc:
        raise RuntimeError("TASK_API_TIMEOUT_SECONDS must be a number.") from exc

    if timeout <= 0:
        raise RuntimeError("TASK_API_TIMEOUT_SECONDS must be greater than zero.")
    return timeout


mcp = FastMCP(name="task-manager", json_response=True)
client = TaskApiClient(
    base_url=os.getenv("TASK_API_BASE_URL", "http://localhost:8000"),
    timeout_seconds=_timeout_seconds(),
)


@mcp.tool()
async def create_task(
    title: Annotated[
        str,
        Field(
            min_length=1,
            max_length=100,
            description="Short task title.",
        ),
    ],
    description: Annotated[
        str | None,
        Field(
            max_length=1000,
            description="Optional details about the task.",
        ),
    ] = None,
    priority: Annotated[
        Priority,
        Field(description="Task priority: low, medium, or high."),
    ] = "medium",
    due_date: Annotated[
        date | None,
        Field(description="Optional due date in YYYY-MM-DD format."),
    ] = None,
) -> TaskResult:
    """Create one task in the task management service."""

    request = CreateTaskRequest(
        title=title,
        description=description,
        priority=priority,
        due_date=due_date,
    )

    try:
        return await client.create_task(request)
    except TaskApiValidationError as exc:
        raise ToolError(f"The task could not be created: {exc}") from exc
    except TaskApiUnavailableError as exc:
        raise ToolError(
            "The task service is temporarily unavailable. Try again later."
        ) from exc
    except TaskApiUnexpectedError as exc:
        logger.exception("Unexpected task API response")
        raise ToolError("The task service returned an unexpected response.") from exc


def main() -> None:
    """Run the MCP server over stdio without writing logs to stdout."""

    logging.basicConfig(
        level=logging.INFO,
        stream=sys.stderr,
        format="%(asctime)s %(levelname)s %(name)s: %(message)s",
    )
    mcp.run(transport="stdio")


if __name__ == "__main__":
    main()

_timeout_seconds()は環境変数を起動時に検証し、0以下や数値でない値を早期に拒否します。main()はログをstderrへ向け、stdioのstdoutを汚しません。Tool本体は入力の変換、APIクライアント呼び出し、構造化結果または安全なToolErrorの返却だけを担当します。

8. 入力Schemaとバリデーションを設計する

型ヒントとFieldから生成される入力Schemaは、概ね次の形になります。実際の表現はSDKとPydanticのバージョンで細部が変わるため、Inspectorで最終確認してください。

json
{
  "type": "object",
  "properties": {
    "title": {
      "type": "string",
      "minLength": 1,
      "maxLength": 100
    },
    "description": {
      "anyOf": [{"type": "string", "maxLength": 1000}, {"type": "null"}],
      "default": null
    },
    "priority": {
      "type": "string",
      "enum": ["low", "medium", "high"],
      "default": "medium"
    },
    "due_date": {
      "anyOf": [{"type": "string", "format": "date"}, {"type": "null"}],
      "default": null
    }
  },
  "required": ["title"]
}

バリデーションは二種類に分けて考えます。

種類

主な検証場所

形式的な検証

必須、型、日付形式、enum、文字数

MCP SchemaとREST API

業務ルール

過去日付、重複、作成数の上限

REST API

MCP側の検証は、不要なHTTP呼び出しを減らし、モデルへ早くフィードバックするために役立ちます。ただしREST API側の検証を削除してはいけません。MCPは唯一の呼び出し元ではないからです。

エラーは、モデルが次の操作を判断できる具体性を持たせます。

plaintext
The task could not be created: due_date must be today or later.

Validation failedだけでは、どの値を直すべきか分かりません。

9. 成功結果とエラー結果を返す

成功時には文章だけでなくTaskResultを返します。これにより、AIは作成結果をユーザーへ説明できるだけでなく、後続処理でidstatusdue_dateなどを利用できます。

MCP仕様は、Toolの失敗を二種類に分けています。

  • 存在しないToolや不正なリクエスト構造はプロトコルエラー
  • API失敗、入力値、業務ルールの失敗はisError: trueのTool Execution Error

後者にはモデルが自己修正できる情報を含めることが推奨されています。Tools仕様のエラー処理を参照してください。v1 SDKにはToolErrorが定義されており、Tool内の想定内エラーを表現できます。v1.xの例外定義も確認できます。

REST APIの状態

MCP側の扱い

400 / 422

修正可能な入力エラー

409

重複などの業務エラー

タイムアウト・接続失敗

一時的な利用不能

500系

一時的なサーバー障害

不正JSON・未知の状態

汎用エラーを返し、詳細はログへ記録

モデルへ返すメッセージには、スタックトレース、ローカルパス、APIキー、内部ホスト名、生のレスポンス、ライブラリ例外全文を含めません。原因調査に必要な詳細はstderr側のログへ残します。

10. stdioで起動してInspectorから確認する

cloneしたリポジトリのルートでREST APIを起動します。

bash
uv run task-api

APIはhttp://localhost:8000で待ち受けます。ブラウザでhttp://localhost:8000/docsを開くとSwagger UIも確認できます。コード変更時の自動リロードが必要なら、代わりに次を使います。

bash
uv run uvicorn task_api.app:app --reload --port 8000

APIを起動したまま、別ターミナルからMCP Inspectorを起動します。

bash
TASK_API_BASE_URL=http://localhost:8000 \
npx -y @modelcontextprotocol/inspector \
uv run python -m task_mcp.server

Inspectorでは次を確認します。

  • create_taskがTool一覧に表示される
  • titleだけが必須で、1〜100文字になっている
  • prioritylowmediumhighのenumになっている
  • due_dateが日付として表現される
  • 正常なタスクを作成でき、data/tasks.jsonへ保存される
  • 過去日付とAPI停止時に、安全で修正可能なエラーが返る

stdioではクライアントとサーバーがstdin/stdoutでJSON-RPCを交換するため、通常の出力をstdoutへ書けません。公開実装はlogging.basicConfig(..., stream=sys.stderr)を使い、診断ログをプロトコルから分離しています。Transport仕様も参照してください。

11. AIクライアントから実際に呼び出す

リポジトリのルートで、stdioサーバーをCodex CLIへ登録します。

bash
codex mcp add task-manager \
  --env TASK_API_BASE_URL=http://localhost:8000 \
  --env TASK_API_TIMEOUT_SECONDS=10 \
  -- uv run python -m task_mcp.server

登録結果はcodex mcp list、Codex TUIでは/mcpから確認できます。CodexのMCP公式ドキュメントも参照してください。

次のように依頼してみます。

plaintext
明日までに「MCPの記事を公開する」というタスクを作成してください。
優先度はhighにしてください。

モデルは自然言語を次のようなTool引数へ変換します。

json
{
  "title": "MCPの記事を公開する",
  "description": null,
  "priority": "high",
  "due_date": "2026-07-17"
}

due_dateは日付だけを受け取ります。時刻まで必要なら、REST APIとToolの両方にdatetimeまたは時刻フィールドを設計してください。MCPアダプターだけでAPIが表現できない情報を暗黙に保存してはいけません。

最後にdata/tasks.jsonを確認し、自然言語の依頼がREST APIを経由して保存されたことを確かめます。

bash
cat data/tasks.json

12. 自動テストを実行する

公開リポジトリにはREST API、HTTPクライアント、MCP Toolの3層を確認するテストが含まれています。

bash
uv run pytest

コミット2b343d2では全26テストが成功します。テストは一時ディレクトリとhttpx.MockTransportを使うため、起動中のAPIを必要とせず、手元のdata/tasks.jsonも変更しません。

たとえばMCP層では、実際に公開される入力Schemaを取得して制約を検証します。

tests/test_mcp_server.pypython
import pytest

from task_mcp import server


@pytest.mark.asyncio
async def test_create_task_schema_is_model_friendly() -> None:
    tools = await server.mcp.list_tools()
    create_task_tool = next(tool for tool in tools if tool.name == "create_task")
    schema = create_task_tool.inputSchema

    assert schema["required"] == ["title"]
    assert schema["properties"]["title"]["minLength"] == 1
    assert schema["properties"]["title"]["maxLength"] == 100
    assert schema["properties"]["priority"]["enum"] == [
        "low",
        "medium",
        "high",
    ]

テスト対象は次の3段階です。

  1. REST API:正常作成、空白除去、未知フィールド、過去日付、404、保存障害
  2. APIクライアント:201、422、タイムアウト、接続失敗、5xx、不正JSON、未知のHTTP状態
  3. MCP Tool:構造化結果、Schema、検証エラー、一時障害、想定外レスポンスのToolError変換

すべてのテストと最新の実装はGitHubリポジトリで確認できます。

13. 実運用に向けて考えること

認証情報はTool引数にしない

実運用ではAPIキー、Bearer Token、ユーザーID、テナントID、トレースIDなどをREST APIへ渡す場合があります。認証情報をAIが生成するTool引数に含めず、MCPサーバーの環境変数、シークレットストア、または認証済みセッションから取得してください。

作成系Toolは二重実行へ備える

AIクライアントや通信層で再試行が起きる可能性があります。REST API側でIdempotency-Key、リクエストID、重複検出、監査ログなどを検討します。MCP側だけで重複を防ぐと、別のAPIクライアントには効果がありません。

stdioとStreamable HTTPを使い分ける

stdioは、ローカルのAIクライアントがローカルプロセスを起動する構成に適しています。複数ユーザーがネットワーク越しに利用する場合は、Streamable HTTP、認証、セッション管理、Origin検証などが必要です。本記事ではそのリモート構成を扱いません。

RESTエンドポイントを機械的にTool化しない

REST APIが次の操作を持っていても、すべてを同じ粒度でToolにする必要はありません。

plaintext
POST   /tasks
GET    /tasks
GET    /tasks/{id}
PATCH  /tasks/{id}
DELETE /tasks/{id}

AIの利用目的に合わせて、次のような操作単位に再設計できます。

plaintext
create_task
search_tasks
complete_task

REST APIはシステムのリソースと業務境界を表現します。一方、MCP Toolはモデルが理解しやすく、安全に実行できる操作単位を表現します。特に削除や外部送信などの高リスク操作では、Toolを分離し、説明、承認、監査を設計する価値があります。

まとめ

既存REST APIをMCP対応にするために、APIを削除したり、AI専用の保存処理を作り直したりする必要はありません。

MCPサーバーはAIクライアント向けの薄いアダプターとして追加できます。Toolでは入力をREST API用のリクエストへ変換し、構造化された結果を返し、APIの失敗をモデルが理解できる安全なエラーへ変換します。業務ルールと永続化は既存REST APIへ残します。

この責務分担なら、REST APIを利用する既存クライアントを維持しながら、自然言語という新しい操作経路を追加できます。ローカルのstdio構成で確認した後は、認証、冪等性、監査ログ、Streamable HTTPへ段階的に拡張できます。

まずは公開サンプルをcloneし、REST API、Inspector、Codex、pytestの順に動かして、境界ごとの責務を確認してみてください。

次のステップ

【実装】データベースに接続するMCPサーバー:SQLiteとMySQLで安全なCRUD Toolを作る

任意SQLをAIへ公開せず、目的別MCP Tool、入力検証、パラメータ化クエリ、最小権限、トランザクション、署名付き確認トークンを組み合わせて、安全なCRUDをSQLiteとMySQLで実装します。

次の記事を読む