MCPサーバーを自作した夜、AIにローカルメモを渡す怖さと便利さを同時に知った

READMEが読めるAIに、次は自分のメモを読ませたくなりました。

でも、最初の10分で怖くなりました。便利そうな連携ほど、渡してはいけない情報まで渡しそうになるからです。夜中の1時半、コーヒーを片手に「これは窓口設計の話だ」と気づきました。

MCPサーバー自作は、AIを賢くする作業というより、AIに見せる世界を3センチずつ広げる作業でした。

目次

何を作ったのか

作ったのは、ローカルのMarkdownメモを検索するだけの小さなサーバーです。

対象は検証用の3ファイルだけ。秘密情報、個人情報、契約情報は入れません。ここを曖昧にすると、あとで必ず怖くなります。

notes/
  python.md
  wordpress.md
  automation.md

最初から何でも検索できる道具にしない。これが一番大事でした。

まず普通のPython関数で作る

MCPの前に、検索関数として動くか確認しました。

from pathlib import Path

NOTES_DIR = Path("notes")


def search_notes(keyword: str) -> list[dict[str, str]]:
    results: list[dict[str, str]] = []

    for path in NOTES_DIR.glob("*.md"):
        text = path.read_text(encoding="utf-8")
        if keyword.lower() in text.lower():
            results.append({
                "file": path.name,
                "preview": text[:160],
            })

    return results


if __name__ == "__main__":
    print(search_notes("Python"))

ハマりポイントは、いきなりAI連携に行かないことです。普通の関数として動かないものは、MCPにしてもだいたい動きません。

最初の失敗は「全部見せる」設計

最初、私はメモフォルダ全体を検索対象にしようとしました。

これが危なかったです。フォルダの中には、古い作業メモ、下書き、公開するつもりのない断片が混ざっていました。AIが悪いのではありません。こちらが窓口を広げすぎていました。

そこで、対象ディレクトリを固定し、拡張子も .md だけにしました。さらに返す文字数を160文字に制限しました。

この制限のおかげで、検索結果は「本文全部」ではなく「候補のチラ見せ」になります。人間が必要なら追加で読む。AIには最初から全部渡さない。かなり安心感が違います。

MCPらしい形にする

実装は環境によって変わりますが、考え方はシンプルです。関数をツールとして公開し、引数を受け取り、結果を返します。

以下は概念をつかむための最小例です。

from pathlib import Path
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("local-notes")
NOTES_DIR = Path(__file__).parent / "notes"


@mcp.tool()
def search_notes(keyword: str) -> list[dict[str, str]]:
    """Search safe local markdown notes."""
    if len(keyword.strip()) < 2:
        return []

    results: list[dict[str, str]] = []
    for path in NOTES_DIR.glob("*.md"):
        text = path.read_text(encoding="utf-8")
        if keyword.lower() in text.lower():
            results.append({
                "file": path.name,
                # ハマりポイント: 全文を返さず、プレビューだけにする
                "preview": text[:160],
            })
    return results[:5]


if __name__ == "__main__":
    mcp.run()

このコードの肝は、検索できる場所、返す件数、返す量を絞っているところです。AI連携では、便利さより先に境界線を決めるべきでした。

30分ハマった設定ミス

一番時間を溶かしたのは、コードではなく起動設定でした。

ファイル名を変えたのに設定側のパスを直していませんでした。エラーは地味で、最初はMCPの理解不足だと思い込みました。30分後、ただのパス違いだと気づいた瞬間、コーヒーが少し冷めていました。

こういう時は、AIに聞く前にまず単体起動です。

python server.py

ここで落ちるならClaude Code以前の問題です。AI連携のデバッグは、普通のデバッグに戻すと急に進みます。

使ってみて変わったこと

メモ検索をMCP化すると、Claude Codeとの会話が少し変わります。

以前は「この前のWordPressメモには何を書いたっけ」と自分で探していました。今は「WordPress REST APIのメモを検索して」と頼めます。たった1手減るだけですが、作業中には大きいです。

ただし、全部をAIに渡したくなる誘惑も強くなります。便利さは、だいたい安全性と綱引きになります。私はここで、公開してよい検証メモだけを対象にする方針にしました。

ログを残しておく

MCPサーバーを触るなら、最初からログを残したほうがいいです。

私は最初、呼び出されたキーワードを何も記録していませんでした。そのせいで、AIが何を検索したのか、結果が0件だったのか、そもそも呼ばれていないのか分からなくなりました。ここで20分ほど迷いました。

最低限なら、標準エラーに出すだけでも助かります。

import sys


def log(message: str) -> None:
    print(message, file=sys.stderr)


def normalize_keyword(keyword: str) -> str:
    value = keyword.strip()
    # ハマりポイント: 空文字検索を通すと、意図せず大量ヒットする
    if len(value) < 2:
        return ""
    log(f"search keyword={value!r}")
    return value

本番運用にするなら、ログの保存先や個人情報の扱いも決める必要があります。ただ、検証段階でも「呼ばれたかどうか」だけは見えるようにしておくと、デバッグがかなり楽になります。

まとめ

MCPサーバー自作は、難しい魔法ではありません。小さなPython関数を、AIから呼べる窓口にする作業です。

ただし、窓口を作るということは、何を見せないかも決めるということです。3ファイル、5件、160文字。このくらい狭く始めると、怖さより便利さが勝ちます。

Claude Codeに外部ツールをつなぐなら、最初の設計対象は機能ではなく境界線です。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

目次