mynote-mcp
by Yatty1
README.md
# mynote-mcp
Obsidian の vault にノートを読み書きするための、ローカル専用 MCP サーバです。
**どのディレクトリで開いた Claude Code / Codex セッションからでも、同じ vault にノートを残せる**
ことを目的にしています。ノートはフォルダで分類せず、すべて vault ルート直下に
`<prefix> YYYY-MM-DD HHmm.md` という名前で作られ、分類は**ファイル先頭のハッシュタグ行**で行います。
```markdown
#ideas #jcat
本文...
```
> **YAML frontmatter は使いません。** タグは 1 行目の `#tag1 #tag2` に書きます
> (既存 vault の実態に合わせています)。frontmatter を持つ古いノートも読み取りだけは対応します。
> **走査範囲は vault ルート直下のみです。**
> `create_note` はフォルダを作らず、`search_notes` / `list_tags` / `read_note` も
> ルート直下の `*.md` しか見ません。既存のサブフォルダに入っているノートは検索対象外です。
- ランタイムは **Bun**。TypeScript を直接実行するのでビルド不要
- MCP SDK の `McpServer` + `WebStandardStreamableHTTPServerTransport` (Web 標準の `Request`/`Response` で完結)
- HTTP 本体は Hono + `Bun.serve`
- 依存は `@modelcontextprotocol/sdk` / `hono` / `zod` のみ
- テストは `bun test` (アサーションは `node:assert/strict`)
- `bun build --compile` で **単一バイナリ**にできる (常駐用)
### Node.js でも動きます
サーバ本体 (`buildHttpApp`) は Web 標準 API のみに依存しているため、ランタイムを選びません。
`Bun.serve` を使うのは listen 部分だけで、Node では `@hono/node-server`
(`optionalDependencies`) へ自動的に切り替わります。
ただし**テストは `bun test` に移行済みで Node では走りません**。
Node 経路が壊れていないことは `bun run test:node` (`scripts/node-smoke.sh`) が
実際の Node プロセスで serve / stdio を叩いて確認します。
## なぜ HTTP 常駐 + stdio ブリッジなのか
MCP サーバを stdio で登録すると、クライアントは**セッションごとにプロセスを起動**します。
それでも動きますが、次の点で困ります。
- vault パスなどの設定を、セッションを開くディレクトリごとに用意したくない
- ノート件数の走査やタグ集計を、セッション起動のたびに一から行いたくない
- 「今どのプロセスが vault を触っているのか」を 1 つに寄せたい
そこで **vault を触る本体は HTTP で 1 プロセスだけ常駐** させ (`mynote-mcp serve`)、
クライアントはそこへ接続する構成にしています。
- **Claude Code**: HTTP トランスポートを直接サポートするので、常駐先の URL を登録するだけです。
- **Codex CLI**: stdio しか扱えないため、`mynote-mcp stdio` を薄いブリッジとして挟みます。
ブリッジは `initialize` / `tools/list` / `tools/call` を常駐プロセスへ透過するだけで、
vault には触りません。
常駐プロセスが落ちていた場合、`mynote-mcp stdio` は **in-process モードへ自動フォールバック**
します (ブリッジ自身が同じツール群を組み立てて直接 vault を触る)。
そのため `serve` を起動し忘れていても、Codex 側は動作します。
```
Claude Code ──── HTTP ───────────────┐
├─→ mynote-mcp serve ──→ Obsidian vault
Codex CLI ──stdio──→ mynote-mcp stdio┘ (127.0.0.1:7391)
└─ 接続不可なら in-process で直接 vault を触る
```
## セットアップ
**Bun 1.2 以降**が必要です (`curl -fsSL https://bun.sh/install | bash`)。
```sh
git clone <this repo> mynote-mcp
cd mynote-mcp
bun install
cp .env.example .env # MYNOTE_VAULT_PATH を自分の vault に書き換える
```
ビルド手順はありません。Bun が `src/index.ts` を直接実行します。
`.env` の最低限の内容です。**iCloud 上の Obsidian vault はパスにスペースを含むので、
必ずクォートしてください。**
```sh
MYNOTE_VAULT_PATH="$HOME/Library/Mobile Documents/iCloud~md~obsidian/Documents/MyVault"
```
動作確認します。
```sh
bun start # = bun src/index.ts serve
curl http://127.0.0.1:7391/health
# {"ok":true,"vault":"MyVault","notes":123}
```
開発中は `bun run dev` (`bun --watch src/index.ts serve`) が使えます。
### 単一バイナリを作る (常駐向け・任意)
```sh
bun run compile # → dist/mynote-mcp (約 60MB, Bun ランタイム同梱)
```
`node_modules` も Bun 本体も不要な自己完結バイナリになります。
launchd / systemd から起動する場合、PATH やランタイムのバージョンに左右されないので確実です。
生成後は一度手で動作確認してください。
```sh
./dist/mynote-mcp serve
```
### インストール構成の例 (常駐 + PATH)
開発用チェックアウトと、常駐させる実体を分けておくと運用が楽です。
```sh
git clone git@github.com:Yatty1/mynote-mcp.git ~/.local/share/mynote-mcp
cd ~/.local/share/mynote-mcp
bun install
bun run compile # → dist/mynote-mcp
ln -sf ~/.local/share/mynote-mcp/dist/mynote-mcp ~/.local/bin/mynote-mcp
cp .env.example .env # MYNOTE_VAULT_PATH を設定
```
`.env` は `dist/mynote-mcp` の 1 つ上 (= インストールルート) からも読まれるので、
launchd / systemd のように cwd が異なる環境からでも設定が効きます。
更新するときは pull して作り直します (`.env` は git 管理外なので残ります)。
```sh
cd ~/.local/share/mynote-mcp && git pull && bun install && bun run compile
launchctl kickstart -k gui/$(id -u)/com.mynote.mcp # 常駐している場合
```
### `.env` の探索場所
`.env` は **パッケージルート** (このリポジトリの `.env`) か、
`MYNOTE_ENV_FILE` で明示したパスからのみ読み込みます。
**カレントディレクトリの `.env` は読みません。**
任意のリポジトリで開いたセッションから使うサーバなので、
第三者のリポジトリに置かれた `.env` に `MYNOTE_VAULT_PATH` や `MYNOTE_URL` を
差し替えられないようにしています。読み込むキーも `MYNOTE_` で始まるものだけです。
## 環境変数
| 変数 | 必須 | 既定値 | 説明 |
| --- | --- | --- | --- |
| `MYNOTE_VAULT_PATH` | ○ | — | Obsidian vault のルート。`~` 展開に対応。スペースを含む場合はクォートする。起動時に存在確認し、無ければエラー終了する |
| `MYNOTE_ATTACHMENTS_DIR` | | `attachments` | 添付の保存先 (vault 相対)。無ければ自動作成 |
| `MYNOTE_PORT` | | `7391` | HTTP の待ち受けポート |
| `MYNOTE_HOST` | | `127.0.0.1` | バインドするホスト。既定はループバックのみ |
| `MYNOTE_URL` | | `http://127.0.0.1:${MYNOTE_PORT}/mcp` | stdio ブリッジの接続先。ループバック以外は拒否して in-process にフォールバックする |
| `MYNOTE_ENV_FILE` | | — | 読み込む `.env` のパス |
CLI では `--port` / `--host` が環境変数を上書きします。
```sh
mynote-mcp serve --port 7500
mynote-mcp stdio
mynote-mcp --help
```
## クライアントへの登録
### Claude Code
常駐している HTTP エンドポイントを user スコープで登録します。
これで**どのディレクトリで開いたセッションからでも**同じ vault が使えます。
```sh
claude mcp add --transport http mynote http://127.0.0.1:7391/mcp --scope user
```
確認と削除。
```sh
claude mcp list
claude mcp remove mynote --scope user
```
### Codex CLI
`~/.codex/config.toml` に stdio ブリッジを登録します。パスは自分の環境に合わせてください。
```toml
[mcp_servers.mynote]
command = "/Users/shinya/.bun/bin/bun"
args = ["/Users/shinya/Desktop/products/mynote-mcp/src/index.ts", "stdio"]
[mcp_servers.mynote.env]
MYNOTE_VAULT_PATH = "/Users/shinya/Library/Mobile Documents/iCloud~md~obsidian/Documents/MyVault"
MYNOTE_URL = "http://127.0.0.1:7391/mcp"
```
`MYNOTE_VAULT_PATH` は in-process フォールバック時に必要です
(`.env` をパッケージルートに置いてあるなら省略できます)。
`env` に `$HOME` などのシェル変数は書けないので、絶対パスを直接書いてください。
`command` も同様に `bun` の絶対パス (`which bun`) が必要です。
単一バイナリを作った場合は、そのパス 1 つで済むので確実です。
```toml
[mcp_servers.mynote]
command = "/Users/shinya/Desktop/products/mynote-mcp/dist/mynote-mcp"
args = ["stdio"]
```
## 常駐させる
### macOS (launchd)
`~/Library/LaunchAgents/com.mynote.mcp.plist` を次の内容で作成します。
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.mynote.mcp</string>
<key>ProgramArguments</key>
<array>
<string>/Users/shinya/Desktop/products/mynote-mcp/dist/mynote-mcp</string>
<string>serve</string>
</array>
<key>WorkingDirectory</key>
<string>/Users/shinya/Desktop/products/mynote-mcp</string>
<key>EnvironmentVariables</key>
<dict>
<key>MYNOTE_VAULT_PATH</key>
<string>/Users/shinya/Library/Mobile Documents/iCloud~md~obsidian/Documents/MyVault</string>
<key>MYNOTE_PORT</key>
<string>7391</string>
<key>MYNOTE_HOST</key>
<string>127.0.0.1</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/shinya/Library/Logs/mynote-mcp.log</string>
<key>StandardErrorPath</key>
<string>/Users/shinya/Library/Logs/mynote-mcp.error.log</string>
</dict>
</plist>
```
読み込みと操作。
```sh
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.mynote.mcp.plist
launchctl print gui/$(id -u)/com.mynote.mcp # 状態確認
launchctl kickstart -k gui/$(id -u)/com.mynote.mcp # 再起動
launchctl bootout gui/$(id -u)/com.mynote.mcp # 停止・解除
```
**launchd はログインシェルの `PATH` を引き継がないため、絶対パスが必須です。**
上記は `bun run compile` で作った単一バイナリを指しています
(ランタイムを同梱するので、bun のインストール先やバージョンに影響されません)。
バイナリを作らずソースを直接動かす場合は、`bun` の絶対パス (`which bun`、
通常 `/Users/<you>/.bun/bin/bun`) を第 1 引数にしてください。
```xml
<key>ProgramArguments</key>
<array>
<string>/Users/shinya/.bun/bin/bun</string>
<string>/Users/shinya/Desktop/products/mynote-mcp/src/index.ts</string>
<string>serve</string>
</array>
```
### Linux (systemd --user)
`~/.config/systemd/user/mynote-mcp.service` を次の内容で作成します。
```ini
[Unit]
Description=mynote-mcp (Obsidian vault MCP server)
After=default.target
[Service]
Type=simple
WorkingDirectory=/home/shinya/mynote-mcp
ExecStart=/home/shinya/mynote-mcp/dist/mynote-mcp serve
# 単一バイナリを作らない場合:
# ExecStart=/home/shinya/.bun/bin/bun /home/shinya/mynote-mcp/src/index.ts serve
Environment=MYNOTE_VAULT_PATH=/home/shinya/Obsidian/MyVault
Environment=MYNOTE_PORT=7391
Environment=MYNOTE_HOST=127.0.0.1
Restart=on-failure
RestartSec=3
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
```
有効化と操作。
```sh
systemctl --user daemon-reload
systemctl --user enable --now mynote-mcp
systemctl --user status mynote-mcp
journalctl --user -u mynote-mcp -f
loginctl enable-linger "$USER" # ログアウト後も動かす場合
```
値にスペースを含む場合は `Environment="MYNOTE_VAULT_PATH=/path/with space"` のように
行全体をクォートしてください。
## エンドポイント (serve)
| メソッド | パス | 説明 |
| --- | --- | --- |
| `POST` | `/mcp` | MCP Streamable HTTP。`initialize` で新規セッションを作り、以降は `mcp-session-id` ヘッダで継続 |
| `GET` | `/mcp` | 既存セッションの SSE ストリーム |
| `DELETE` | `/mcp` | セッション終了 |
| `GET` | `/health` | `{ ok: true, vault: <vault のディレクトリ名>, notes: <ノート件数> }` |
SIGINT / SIGTERM で全セッションを閉じてから HTTP を停止します (graceful shutdown)。
無通信のセッションは 30 分で破棄されます。
## ツール一覧
すべてのツールは JSON 文字列を text コンテンツとして返します。
エラーは例外ではなく `isError: true` のツール結果として返ります。
### 書き込み
| ツール | 入力 | 戻り値 |
| --- | --- | --- |
| `create_note` | `prefix` (必須), `content` (必須), `tags?: string[]`, `datetime?` (ISO8601) | `{ path, filename, title, tags }` |
| `append_to_note` | `filename` または `title` (どちらか必須), `content` (必須), `heading?` | `{ path, filename, title, heading }` |
| `update_tags` | `filename` (必須), `addTags?`, `removeTags?`, `setTags?` | `{ path, filename, title, changed, tags }` |
- `create_note` は命名規則でファイル名を決め、タグがあれば 1 行目に `#tag1 #tag2` を書き、
空行を挟んで本文を続けます。タグが無ければ本文だけを書きます。
日時はファイル名が持つので `created` のようなメタデータは書きません。
- `append_to_note` は `heading` を指定するとその見出しセクションの末尾に挿入します
(見出しが無ければ本文末尾に `## <heading>` を作ります)。タグ行は変更しません。
- `update_tags` は 1 行目のタグ行だけを書き換え、**本文は 1 バイトも変えません**。
タグ行が無いノートには先頭に追加し、タグを全て消すとタグ行ごと削除します。
`setTags` を渡すと集合を置き換えます (`addTags`/`removeTags` より優先)。
タグに使えない文字 (空白・改行・`#` など) は除去するので、書いたタグ行は必ず読み戻せます。
### 読み取り・検索
| ツール | 入力 | 戻り値 |
| --- | --- | --- |
| `search_notes` | `tags?`, `tagMode?: "and" \| "or"` (既定 `and`), `query?`, `from?`, `to?`, `prefix?`, `limit?` (既定 50, 最大 500) | `{ total, count, limit, truncated, results: { filename, title, prefix, datetime, date, time, tags, snippet }[] }` |
| `read_note` | `filename` または `title` | `{ path, filename, title, prefix, datetime, tags, frontmatter, body }` (`body` に先頭タグ行は含みません) |
| `list_tags` | なし | `{ total, tags: { tag, count }[] }` (count 降順) |
- `query` は本文の大文字小文字を無視した部分一致です。
- `from` / `to` は **ファイル名の日付**で絞り込みます (`YYYY-MM-DD` または ISO8601)。
- `prefix` は前方一致です。
- タグは 先頭の `#tag` 行 ∪ 本文中の `#tag` ∪ (古いノートに残る) frontmatter の `tags` を拾います。
- `truncated` が `true` なら `limit` で切られています (`total` が実ヒット数)。
### 添付
| ツール | 入力 | 戻り値 |
| --- | --- | --- |
| `save_attachment` | `filename` (必須), `base64?`, `sourcePath?` (`base64` と排他) | `{ path, relativePath, filename, wikilink, size }` |
`MYNOTE_ATTACHMENTS_DIR` 配下に保存し (必要なら再帰的に作成)、
`![[<filename>]]` 形式の wikilink を返します。
同名ファイルがあれば連番を付けます。1 件あたり 64MB までです。
### デイリー
| ツール | 入力 | 戻り値 |
| --- | --- | --- |
| `append_to_daily` | `content` (必須), `prefix?` (既定 `日記`), `heading?`, `tags?` (既定 `["Diary"]`) | `{ path, filename, title, created, date, prefix }` |
今日の日付を持つ `<prefix> YYYY-MM-DD *.md` を探し、あれば**最も新しいもの**に追記します。
無ければ `create_note` 相当で新規作成 (タグ `daily` 付き) してから追記します。
`created` は新規作成したかどうかを表します。
## ファイル命名規則
```
<prefix> YYYY-MM-DD HHmm.md
議事録 2026-07-30 1145.md
Idea 2026-07-30 0902.md
Daily 2026-07-30 0800.md
```
- `prefix` は自由入力です。許可リストによる検証はありません。
- 日時は**ローカルタイムゾーン**です。`datetime` に ISO 文字列を渡せば任意の日時を使えます
(日付のみ / オフセットなしの日時もローカルタイムとして解釈します)。
- ファイル名として不正な文字 `/ \ : * ? " < > |` と制御文字は除去し、
前後の空白を trim、連続空白を 1 つに圧縮します。
サニタイズ後に空になった場合は `Note` にフォールバックします。
- 同名衝突時は `議事録 2026-07-30 1145-2.md` のように連番サフィックスを付けます。
- **フォルダは作りません。** ノートはすべて vault ルート直下です。分類はタグで行います。
例外は添付だけで、`MYNOTE_ATTACHMENTS_DIR` 配下に入ります。
## セキュリティ
- すべての読み書きパスが vault ルート配下に解決されることを検証します。
シンボリックリンクは実在しない末尾コンポーネントも含めて 1 段ずつ解決するため、
リンク経由の脱出もできません。vault 外を指すパスは必ず例外になります。
- HTTP は既定でループバック (`127.0.0.1`) にのみバインドします。
- DNS rebinding 対策 (`allowedHosts` / `allowedOrigins`) を Hono ミドルウェアで一本化しています。
`/mcp` と `/health` の両方に同じ検証を適用します
(SDK 側の同名オプションは deprecated で、外部ミドルウェアが推奨されているため)。
- `/health` は vault の絶対パスを返さず、ディレクトリ名だけを返します。
- stdio ブリッジはループバック以外の `MYNOTE_URL` へは接続しません
(乗っ取られた URL にノート内容を送らないため)。
- `.env` はパッケージルートか `MYNOTE_ENV_FILE` からのみ、`MYNOTE_` で始まるキーのみ読み込みます。
## 開発
```sh
bun run typecheck # tsc --noEmit
bun test ./test # 253 tests
bun run test:node # Node 互換スモーク (serve / stdio を実プロセスで確認)
bun run test:binary # 単一バイナリスモーク (バンドル後しか出ない不具合を拾う)
bun run compile # dist/mynote-mcp (単一バイナリ)
bun run build:node # tsc で dist/ を生成し bin の shebang を node に書き換える
```
`bin` (`dist/src/index.js`) は npm 経由でも動くよう tsc 出力を指しています
(`prepare` が shebang を `#!/usr/bin/env node` に書き換えて実行権限を付けます)。
Bun で直接動かす場合はソースの `src/index.ts` (`#!/usr/bin/env bun`) を使います。
`bun test` を引数なしで実行すると `dist/` 配下のビルド成果物まで拾ってしまうので、
必ず `./test` を指定してください (`bun run test` は指定済みです)。
```
src/
index.ts # CLI エントリ (serve | stdio)
config.ts # env 読み込み + 検証 + .env ローダー
vault/
paths.ts # vault ルート解決 / パス封じ込め / ファイル名サニタイズ / 命名規則
hashtags.ts # 先頭 #tag 行の parse / edit (分類タグの正)
frontmatter.ts # 自前 frontmatter parse / stringify (古いノートの読み取り用)
notes.ts # ノート CRUD + 一覧走査
tools/
index.ts # registerTools / createServer
write.ts # create_note / append_to_note / update_tags
read.ts # search_notes / read_note / list_tags
attachment.ts # save_attachment
daily.ts # append_to_daily
server/
http.ts # Hono アプリ + Web 標準 StreamableHTTP 配線 (listen だけ Bun/Node 分岐)
stdio.ts # stdio ブリッジ
test/ # bun test による単体・統合テスト
scripts/
node-smoke.sh # Node 互換スモーク
```
frontmatter は依存を増やさないため自前の最小パーサです。
`tags` / `aliases` のような単純なリストとスカラーだけを扱い、
書き戻しの際に既存行を壊さないようにしています。
## トラブルシューティング
### vault が見つからない
```
設定の読み込みに失敗しました: vault が見つかりません: /path/to/vault (MYNOTE_VAULT_PATH=...)
```
- パスにスペースが含まれている場合、`.env` でクォートしていますか。
`MYNOTE_VAULT_PATH="$HOME/Library/Mobile Documents/iCloud~md~obsidian/Documents/MyVault"`
- launchd の `EnvironmentVariables` や Codex の `env` では `$HOME` や `~` が展開されません。
絶対パスを書いてください (CLI から渡す `~` は展開されます)。
- iCloud vault はまだローカルにダウンロードされていない可能性があります。
Finder で vault を開いて実体化されているか確認してください。
- カレントディレクトリの `.env` は読みません。パッケージルートの `.env` か
`MYNOTE_ENV_FILE` を使ってください。
確認コマンド:
```sh
ls -d "$HOME/Library/Mobile Documents/iCloud~md~obsidian/Documents/MyVault"
```
### ポートが衝突する
```
mynote-mcp の起動に失敗しました: listen EADDRINUSE: address already in use 127.0.0.1:7391
```
使用中のプロセスを確認します。
```sh
lsof -nP -iTCP:7391 -sTCP:LISTEN # macOS / Linux
```
すでに `mynote-mcp` が常駐しているだけなら、二重起動は不要です
(`curl http://127.0.0.1:7391/health` で確認)。
別のプロセスが使っている場合はポートを変えます。変更したら**クライアント側の登録も
合わせて更新**してください。
```sh
MYNOTE_PORT=7500 bun start
claude mcp remove mynote --scope user
claude mcp add --transport http mynote http://127.0.0.1:7500/mcp --scope user
```
### stdio が繋がらない
`mynote-mcp stdio` は stderr に状況を出します。
| stderr | 意味 |
| --- | --- |
| `http://127.0.0.1:7391/mcp に接続しました (プロキシモード)` | 正常。常駐プロセスへ転送しています |
| `... への接続に失敗しました: ...` → `in-process モードで起動します` | 常駐プロセスが居ないので、ブリッジが直接 vault を触ります |
| `MYNOTE_URL がループバックではないため接続しません` | `MYNOTE_URL` を `127.0.0.1` / `localhost` / `[::1]` に直してください |
in-process モードでも機能はすべて使えますが、`MYNOTE_VAULT_PATH` が
そのプロセスから見えている必要があります。Codex の `[mcp_servers.mynote.env]` に
書いたか、パッケージルートに `.env` があるかを確認してください。
Codex 側でツールが 1 つも見えない場合:
- `command` の `node` が PATH で解決できているか (絶対パスにすると確実です)
- `command` の `bun` (または単一バイナリ) が絶対パスで、実行可能か
- `~/.codex/config.toml` の変更後に Codex を再起動したか
Claude Code 側で繋がらない場合は、まず `curl http://127.0.0.1:7391/health` が
`{"ok":true,...}` を返すか確認してください。
`403 Invalid Host header` が返る場合は、登録した URL のホスト名が
`127.0.0.1` / `localhost` / `[::1]` のいずれかになっているか確認してください
(DNS rebinding 対策で他のホスト名は拒否されます)。
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing