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に外部ツールをつなぐなら、最初の設計対象は機能ではなく境界線です。

コメント