技術資料

PythonでMCPサーバーを作り、Codexから別プロジェクトの文書を検索する

作成日:2026.08.11

別プロジェクトのファイルを検索する読み取り専用MCPサーバーを、Pythonの標準ライブラリで実装します。Codexアプリから呼び出すためのconfig.toml設定、MCPのSTDIO通信、標準出力へログを出さない注意点、PYTHONIOENCODING未設定時のトラブル対処まで紹介します。

複数のローカルプロジェクトを扱っていると、あるプロジェクトの情報を別のプロジェクトから参照したくなることがあります。

例えば、プロジェクトAでCodexを使って実装しながら、プロジェクトBに保存しているドキュメントや設定情報を検索したい場合です。

今回は、プロジェクトBに読み取り専用のMCPサーバーを用意し、プロジェクトAのCodexアプリから、プロジェクトBのインデックスを検索してみます。MCP専用SDKは使わず、Pythonの標準ライブラリでJSON-RPCとSTDIOの処理を実装します。

今回の構成

プロジェクトAをMCPクライアント、プロジェクトBをMCPサーバーとして扱います。

D:\projectA
└─ .codex
   └─ config.toml

D:\projectB
├─ index.json
└─ mcp_server.py

Codexアプリは、プロジェクトAのconfig.tomlに登録されたMCPサーバーを起動します。MCPサーバーはプロジェクトBのindex.jsonを読み込み、検索結果だけを返します。

今回のサーバーは、任意のファイルを読み取ったり、ファイルを書き換えたりしません。検索対象をインデックスに限定することで、MCP経由で公開する機能を小さくしています。

MCPとSTDIO

MCP(Model Context Protocol)は、AIアプリケーションと外部のデータや機能を接続するためのプロトコルです。MCPサーバーは、検索やデータ取得などの機能をツールとして公開します。

今回は、MCPのSTDIOトランスポートを使用します。クライアントがMCPサーバーを子プロセスとして起動し、標準入力と標準出力を使ってJSON-RPCメッセージをやり取りする方式です。

MCPのSTDIOでは、標準出力へMCPメッセージ以外の文字列を出力しないことが重要です。デバッグ用のログを出力する場合は、標準エラー出力へ出すようにします。詳しくはMCP公式仕様のTransportsを参照してください。

プロジェクトBにインデックスを用意する

まず、プロジェクトBのルートにindex.jsonを作成します。今回は、検索対象のパス、タイトル、本文をインデックスへ保存します。

[
    {
        "path": "docs/mcp-overview.md",
        "title": "MCPの概要",
        "text": "MCPはAIアプリケーションと外部のデータや機能を接続するためのプロトコルです。"
    },
    {
        "path": "docs/setup.md",
        "title": "ローカル開発環境の設定",
        "text": "CodexアプリからMCPサーバーを呼び出す場合は、config.tomlへサーバーを登録します。"
    }
]

実際のプロジェクトでは、Markdownなどのファイルからインデックスを生成する処理を別途用意します。この記事ではMCPサーバーの仕組みに集中するため、インデックスを手動で作成します。

PythonでMCPサーバーを作成する

プロジェクトBにmcp_server.pyを作成します。このサーバーは、search_filesという検索ツールを1つだけ公開します。

検索処理はindex.jsonの本文を対象にします。

from __future__ import annotations

import json
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional


INDEX_PATH = Path(__file__).with_name("index.json")
PROTOCOL_VERSION = "2025-11-25"


def load_index() -> List[Dict[str, Any]]:
    """インデックスを読み込む。"""
    with INDEX_PATH.open(encoding="utf-8") as index_file:
        index = json.load(index_file)

    if not isinstance(index, list):
        raise ValueError("index.json must contain an array")

    return index


def search_files(query: str, limit: int = 10) -> Dict[str, Any]:
    """インデックス内の本文を検索する。"""
    normalized_query = query.strip().casefold()
    if not normalized_query:
        raise ValueError("query is required")

    result_limit = max(1, min(limit, 10))
    results = []

    for item in load_index():
        text = str(item.get("text", ""))
        if normalized_query not in text.casefold():
            continue

        results.append({
            "path": item.get("path", ""),
            "title": item.get("title", ""),
            "snippet": text[:160],
        })

        if len(results) >= result_limit:
            break

    return {"results": results}


TOOLS = [
    {
        "name": "search_files",
        "description": "インデックス化されたプロジェクト内の文書を検索します。",
        "inputSchema": {
            "type": "object",
            "properties": {
                "query": {"type": "string"},
                "limit": {"type": "integer", "minimum": 1, "maximum": 10},
            },
            "required": ["query"],
            "additionalProperties": False,
        },
    }
]


def handle(message: Dict[str, Any]) -> Optional[Dict[str, Any]]:
    """MCPのリクエストを処理する。"""
    request_id = message.get("id")
    method = message.get("method")

    if method in {"notifications/initialized", "notifications/cancelled"}:
        return None

    if method == "initialize":
        return {
            "jsonrpc": "2.0",
            "id": request_id,
            "result": {
                "protocolVersion": PROTOCOL_VERSION,
                "capabilities": {"tools": {}},
                "serverInfo": {
                    "name": "projectb-search",
                    "version": "0.1.0",
                },
            },
        }

    if method == "tools/list":
        return {
            "jsonrpc": "2.0",
            "id": request_id,
            "result": {"tools": TOOLS},
        }

    if method == "tools/call":
        params = message.get("params", {})
        name = params.get("name")
        arguments = params.get("arguments", {})

        if name != "search_files":
            return tool_error(request_id, "unknown tool")

        try:
            result = search_files(**arguments)
        except (TypeError, ValueError) as error:
            return tool_error(request_id, str(error))

        return {
            "jsonrpc": "2.0",
            "id": request_id,
            "result": {
                "content": [
                    {
                        "type": "text",
                        "text": json.dumps(result, ensure_ascii=False),
                    }
                ],
                "isError": False,
            },
        }

    return {
        "jsonrpc": "2.0",
        "id": request_id,
        "error": {"code": -32601, "message": "method not found"},
    }


def tool_error(request_id: Any, message: str) -> Dict[str, Any]:
    return {
        "jsonrpc": "2.0",
        "id": request_id,
        "result": {
            "content": [{"type": "text", "text": message}],
            "isError": True,
        },
    }


def serve() -> None:
    """標準入力からJSON-RPCを受け取り、標準出力へ応答する。"""
    for line in sys.stdin:
        if not line.strip():
            continue

        try:
            message = json.loads(line)
            response = handle(message)
        except json.JSONDecodeError:
            response = {
                "jsonrpc": "2.0",
                "id": None,
                "error": {"code": -32700, "message": "invalid JSON"},
            }

        if response is not None:
            print(json.dumps(response, ensure_ascii=False), flush=True)


if __name__ == "__main__":
    serve()
MCP SDKを使わない理由

今回はMCPの通信の流れを確認するため、専用SDKを使わず、標準ライブラリでプロトコルの最小部分を実装しています。

実際のアプリケーションでは、プロトコルの実装漏れや仕様変更への対応を考えると、対象言語のMCP SDKを利用する方が保守しやすい場合があります。この記事の例は、MCPサーバーの最小構成を理解するためのサンプルとして扱います。

標準出力へログを出さない

このサーバーは標準入力からJSON-RPCを受け取り、標準出力へJSON-RPCの応答を返します。標準出力へ次のようなデバッグ文字列を出すと、CodexがMCPメッセージとして解釈できなくなる可能性があります。

print("MCP server started")

ログを出したい場合は、標準エラー出力へ出します。

print("MCP server started", file=sys.stderr)

CodexアプリへMCPサーバーを登録する

次に、プロジェクトAの.codex/config.tomlへ、プロジェクトBのMCPサーバーを登録します。

[mcp_servers.projectb_search]
command = "python"
args = ["D:\\projectB\\mcp_server.py"]
enabled = true
enabled_tools = ["search_files"]
env = { PYTHONIOENCODING = "utf-8" }

ここでは、ローカルでのみ使用する前提でプロジェクトBの絶対パスを指定しています。プロジェクトAをGitHubなどで共有する場合は、利用者ごとにパスが異なるため、このまま設定ファイルへ保存する方法は向きません。

共有する場合は、環境変数からプロジェクトBの場所を読み込むランチャーを用意する方法があります。Codexの設定にはランチャーを指定し、ランチャー側で環境変数を読み込んでからMCPサーバーを起動します。

設定項目の名前や利用できる項目は、Codexアプリのバージョンによって変更される可能性があります。利用時は、Codex公式リポジトリのMCP説明と、使用しているCodexアプリの設定を確認してください。

Codexから検索を依頼する

config.tomlを保存したら、Codexアプリを再起動して設定を読み込ませます。MCPサーバーの一覧や利用可能なツールを確認できる画面が表示される場合は、projectb_searchsearch_filesが読み込まれていることを確認します。

Codexへ、次のように依頼します。

projectBのMCPサーバーを使って、
「Codex」という語を検索してください。
まだファイルは変更しないでください。

検索結果として、インデックスに登録したパス、タイトル、本文の一部が返れば、プロジェクトAのCodexからプロジェクトBの検索ツールを呼び出せています。

{
    "results": [
        {
            "path": "docs/setup.md",
            "title": "ローカル開発環境の設定",
            "snippet": "CodexアプリからMCPサーバーを呼び出す場合は、config.tomlへサーバーを登録します。"
        }
    ]
}

この例では、CodexがプロジェクトBのファイルを直接読み取っているわけではありません。MCPサーバーがインデックスを検索し、検索結果をツールの応答として返しています。

応答がなく数分間固まる場合

今回の環境では、env = { PYTHONIOENCODING = "utf-8" }を設定していない状態で、CodexからMCPサーバーを呼び出しても応答がなく、数分間固まる症状が発生しました。

この設定がないことがすべての環境で同じ症状を起こすとは限りません。Python、Windows、Codexアプリのバージョンや標準入出力の扱いによって結果が変わる可能性があります。ただ、Windows上でPythonのSTDIOサーバーを起動する場合は、まず次の設定を試す価値があります。

env = { PYTHONIOENCODING = "utf-8" }

設定を追加しても応答しない場合は、次の順番で確認します。

  1. pythonが通常のPowerShellから起動できるか確認する。
  2. D:\projectB\mcp_server.pyindex.jsonが存在するか確認する。
  3. Codexの設定ファイルが、現在開いているプロジェクトの.codex/config.tomlか確認する。
  4. MCPサーバーの標準出力へデバッグログを出していないか確認する。
  5. サーバーを単独で起動し、MCPの初期化要求を受け取れる状態か確認する。

サーバーを単独で確認する場合も、標準入力と標準出力はMCPの通信に使われます。確認用のメッセージやログを標準出力へ追加しないようにします。

読み取り専用にする理由

MCPサーバーへ多くの機能を追加すると便利になる一方、Codexから実行できる操作の範囲も広がります。

今回のサーバーは、検索対象をインデックスに限定し、検索結果だけを返します。次のような機能は追加していません。

  • クライアントから指定された任意パスのファイル読み取り
  • ファイルの作成、変更、削除
  • Gitコマンドやシェルコマンドの実行
  • プロジェクト外のディレクトリへのアクセス

検索対象の文書にAPIキー、パスワード、秘密鍵、個人情報などが含まれている場合は、それらをインデックスへ入れないようにします。MCPサーバーを読み取り専用にしても、公開したデータの内容までは自動的に安全になりません。

まとめ

今回は、プロジェクトBのインデックスを検索するPython製のMCPサーバーを作成し、プロジェクトAのCodexアプリから呼び出しました。

MCP専用SDKを使わず、Python標準ライブラリでJSON-RPCとSTDIOの処理を実装すると、MCPサーバーの基本的な流れを確認できます。実際の運用では、読み取り専用のツールから始め、必要な機能だけを段階的に追加するのがよいかと思います。

今回の環境では、Codexのconfig.tomlPYTHONIOENCODINGを設定しない場合、応答がなく数分間固まる症状が発生しました。MCPサーバーが応答しない場合は、設定ファイル、Pythonの起動、標準出力の内容、インデックスのパスを順番に確認します。

この記事を書いた人

※上が私です。

奈良市を拠点に、27年以上の経験を持つフリーランスWebエンジニア、阿部辰也です。

これまで、ECサイトのバックエンド開発や業務効率化システム、公共施設の予約システムなど、多彩なプロジェクトを手がけ、企業様や制作会社様のパートナーとして信頼を築いてまいりました。

【制作会社・企業様向けサポート】
  • 専任エンジニアのいない企業様に対するシステム面の不安を解消
  • 柔軟な契約形態や短納期での対応により、急なニーズにも迅速にサポート
  • システムの企画段階から運用まで、ワンストップでのサービスを提供

Webシステムの開発やサイト改善でお困りの際は、どうぞお気軽にご相談ください。小さな疑問から大規模プロジェクトまで、最適なご提案を心を込めてさせていただきます。

ぜひ、プロフィールWeb制作会社様向け業務案内一般企業様向け業務案内もご覧くださいね。

Codexを使い始めた人向けGitHub入門:リポジトリ作成からPull Requestまで

2026.08.10

Windows版ChatGPTデスクトップアプリのCodexを主な例として、GitHubのアカウント・リポジトリ作成から、作業用ブランチでの変更、commit・push、Pull Request、mergeまでの流れを解説します。GitやGitHubの基本用語を丁寧に説明し、Codexの変更内容を確認してから反映するための注意点も紹介します。

Codex GitHub

ChatGPT・GitHub・Codexで設計から実装・レビューまで進める方法

2026.08.09

Codexにいきなりアプリ開発を依頼するのではなく、ChatGPTで設計と作業分解を行い、GitHub Issueへ整理してからCodexで実装する流れを紹介します。pushやPull Request、レビュー、手戻りとトークン消費を抑える考え方も説明します。

ChatGPT Codex GitHub VS Code

Windows版Codexで「CreateProcessAsUserW failed: 5」が出たときのPowerShell確認と対処

2026.08.07

Windows 11のCodexで、Get-Contentなどの読み取りコマンド実行時に「CreateProcessAsUserW failed: 5」が発生した事例をもとに、エラー内のpwsh.exeのパスやwhere.exe pwsh、Get-Commandの結果を確認し、PowerShellの導入形態を見直して改善するまでの切り分け手順を紹介します。

Codex PowerShell

CodexのAGENTS.mdとSkillsを使い分けて開発ルールを整える

2026.08.04

Codexを継続的な開発に活用するために、プロジェクト全体の前提や規約をAGENTS.mdへ、文書更新・実装・テストなどの作業手順をSkillsへ分けて記述する方法を紹介します。実際の運用例をもとに、ルールを少しずつ育てる考え方も整理します。

Codex

阿部辰也へのお仕事の依頼・お問い合わせ

軽いご相談もお気軽にどうぞ!

個人情報の取り扱いについて *必須 プライバシーポリシーをご確認いただき、同意いただける場合は「同意する」にチェックをしてください。

keyboard_double_arrow_up
TOP