NILTOナレッジ

AI時代のウェブサイト運用ノウハウ

最終更新日:2026.8.5

Obsidian MCPの使い方|導入手順、できること、注意点を解説

Obsidian MCPの使い方|導入手順、できること、注意点を解説
Obsidian MCPを使うと、Obsidianに貯めたノートをAIアシスタントから直接扱えます。ただ、いきなりMCPサーバーを立てる必要はありません。 - ノートが増えすぎて、必要な情報にたどり着けない - AIに相談するたび、ノートの内容をコピペしている - 「Obsidian MCP」を試したいが、どこから手を付ければよいか分からない - 手順どおり設定したのに動かず、原因が分からない - 見せたくないノートまでAIに渡すのが不安 この記事を読むと、やりたいことに必要な段階だけを選び、無駄な準備をせずにObsidianとAIをつなげます。

具体的な活用イメージを39個収録 具体的な活用イメージを39個収録

AI活用術39選

ウェブ運用の工数削減に役立つ

AI活用術39選

ウェブ運用の膨大なタスクを「CMS×AI」で削減する39個のユースケースをご紹介します。単なるテキスト生成を超え、AIがCMS操作そのものを支援する次世代の運用フローをご覧ください。

39個のAI活用術を見る

Obsidian MCPとは

Obsidianの開発元は、公式のMCPサーバーを公開していません(2026年8月時点)。そのため、この記事で扱う「Obsidian MCP」とは、有志が公開しているツールや自分で用意する仕組みを含めた、ObsidianとAIをつなぐ連携全般を指す呼び方です。正式な製品名ではありません。

MCP(Model Context Protocol)は、AIと外部のツールやデータをつなぐ共通規格です。この規格に対応したサーバー(MCPサーバー)を用意すると、対応するAIアプリからObsidianのノートを操作できます。仕組みそのものはMCPサーバーとはで解説しています。

ObsidianにMCPは必要か?

Obsidianは、ノートをまとめたフォルダを1つの単位として扱います。これを保管庫と呼びます(海外版ではVault)。初回起動時の「保管庫を作成」から作り、用途ごとに複数持つこともできます。実体はただのフォルダで、中にはMarkdownファイルがそのまま並びます。

ノートがファイルとして置かれている以上、多くの用途はMCPなしで操作できます。

分かれ目は、Obsidianというアプリを介さないと届かない領域に用があるかどうかです。コマンドの実行、アプリ内蔵の検索、リンクの解決、タグの索引がそれにあたります。ここに用がなければ、MCPは要りません。

この記事では、やりたいことに応じて必要な段階まで進む形で解説します。

  • 段階A: MCPを使わず、AIに保管庫のファイルを直接読み書きさせます
  • 段階B: 有志のプラグインで、起動中のObsidianの機能まで使います
  • 段階C: 段階Bと同じことを、自分のコードで用意します

上から順に、できることが増える代わりに、準備と権限も増えます。

段階Bで使うのは、有志が個人で開発しているプラグインです。公式のコミュニティプラグインストアで配布されていますが、導入すると保管庫全体を読み書きできる権限を渡すことになります。次の点は、進む前に把握しておく必要があります。

  • Obsidianのプラグインは、アプリの中で任意のコードを実行できます。仕組みのうえでは、ノートの内容を外部へ送信することも可能です
  • ストアへの登録時には審査がありますが、その後に配信される更新のすべてについて、中身が保証されるわけではありません
  • 開発と保守は個人に依存します。更新が止まる、Obsidian本体の変更で動かなくなる、といった事態は起こりえます
  • 業務のノートを扱う場合、第三者が作ったプラグインに全権限を渡す構成は、社内の情報管理ルールで認められないことがあります

これらを受け入れられない場合の選択肢が段階Cです。段階Bの主要機能を自分のコードで再現するもので、違いは「そのコードを誰が書いたか」と「どこまで作り込むか」です。

まず、どこまで必要かを判定する

やりたいこと

必要な段階

ノートを読ませて要約・相談したい

A

AIに新しいノートを書かせたい

A

既存ノートに追記させたい

A

ノートが数千件あり、全文読み込みが重い

B

Obsidianのコマンドや、Templater・Dataviewなどのプラグインのコマンドを実行したい

B

「## タスク の下だけ」など見出し単位で正確に編集したい

B

フロントマターの特定プロパティだけ書き換えたい

B

タグ一覧やデイリーノートの場所など、アプリの文脈を使いたい

B

コマンド実行やフロントマター編集を、第三者製プラグインに頼らず実現したい

C

AIに渡すツールや対象ノートを自分で限定したい

C

判断がつかない場合は、段階Aから始めます。不足を感じた時点で次に進めば、無駄がありません。

事前に用意するもの

段階によって必要なものが変わります。自分が進む段階までのものだけ用意します。

A

B

C

Obsidian本体と保管庫

ファイル操作できるAIツール(Claude Code、Cursor など)

Node.js

Local REST API with MCP プラグイン

TypeScriptのビルド環境(Obsidianプラグイン用)

Node.jsは、段階Cと、段階BでClaude Desktopを使う場合に必要です。導入手順は、それぞれ必要になる箇所で説明します。段階Aだけなら不要です。

あわせて、AIにノートを書き換えさせる前にバックアップを用意します。git、クラウド同期、フォルダのコピーのいずれでも使えます。クラウド同期を使っている場合、AIの書き込みは同期先にも反映されます。

この記事の検証環境

手順は次の環境で実際に動作を確認しています(2026年8月3日)。バージョンが大きく違う場合、特にMCP SDKは挙動が変わる可能性があります。

  • Windows 11 / Obsidian 1.13.4 / Node.js v22.17.0 / npm 11.6.0
  • MCP TypeScript SDK @modelcontextprotocol/server 2.0.0
  • Obsidian型定義 obsidian 1.13.1
  • Local REST API with MCP 5.1.0

段階Cは、掲載したコードをそのままビルドして動作することを確認済みです。異常系も、想定どおりのステータスコードが返ることを確認しています。

段階A|MCPを使わずにファイルを直接扱う

できること

保管庫フォルダをファイル操作できるAIツールに渡すだけで、次ができます。

  • ノートの検索
  • ノートの読み取りと要約
  • 新規ノートの作成
  • 既存ノートへの追記・編集

対応するツールの例:Claude Code、Claude Desktop、Cursor、Codex、Claude Cowork。

手順

作業ディレクトリを保管庫にしてAIツールを起動します。Claude Codeなら次のようになります。

cd C:/Users/<ユーザー名>/Documents/<保管庫フォルダ>

この状態で「Notes/アイデア.md を読んで要点を教えて」「Daily/2026-08-03.md に今日の要点を追記して」と頼めば動きます。設定ファイルもAPIキーも不要です。

できないこと

  • Obsidianのコマンド実行
  • 見出し・ブロック単位を保証した編集
  • アプリ内蔵の高速検索
  • タグ索引やリンク解決などのアプリ側の情報

AIがファイルを直接書き換えるため構造の保証ができず、検索もファイルを実際に読むので、ノートが増えるほど重くなります。

注意点

AIによるファイル操作はObsidianのリンク更新機構を通りません。Obsidianのアプリ内でノートをリネーム・移動すると [[リンク]] が自動追従しますが、AIがファイルシステム経由で同じことをすると、リンクが壊れたまま残ります。

段階Aでは、リネームと移動はAIにやらせないのが安全です。

指示の例

保管庫全体から「会議」を含むノートを探して、パスの一覧にして
Notes/アイデア.md を読んで、要点を3つにまとめて
先月のデイリーノートを読んで、まだ着手していないタスクを拾い出して
いまの会話の結論を Notes/決定事項.md として新規作成して
Daily/2026-08-03.md の末尾に、今日やったことを箇条書きで追記して

検索はAIが実際にファイルを読んで探すので、ノートが数千件あると時間がかかります。フォルダを指定して範囲を狭めると安定します。

次の段階に進む目安

  • ノートが多く、AIがファイルを読み込むだけで時間とトークンを消費する
  • ## タスク の下にだけ追記して」「フロントマターの status だけ done に変えて」を確実にやりたい
  • デイリーノートへの追記や、Templater・Dataviewの起動をAIに任せたい
  • リネームや移動もAIに任せたい

うまくいかないとき

症状

原因と対処

ノートが見つからない

AIツールの作業ディレクトリが保管庫になっているか確認します

動作が重い

隠しフォルダ(.obsidian)を除外するよう指示すると、設定ファイルの読み込みが減ります

段階B|Obsidianアプリごとつなぐ

何が変わるか

起動中のObsidian本体に接続します。ファイルを外から書き換えるのではなく、Obsidianに処理させる形になるため、アプリの機能がそのまま使えます。

  • Obsidianのコマンド実行
  • 見出し・ブロック参照・フロントマターを対象にした正確な編集
  • アプリ内蔵の検索。AIは全ファイルを読み込みません
  • タグ一覧、アクティブファイル、保管庫の構造情報

リンクの整合性もObsidian側が面倒を見るため、段階Aで抱えていたリンク破損のリスクも下がります。

使うのはLocal REST API with MCPプラグイン

公式のMCPサーバーがない以上、実質的な標準はコミュニティプラグインの「Local REST API with MCP」です。

冒頭で触れたとおり、これは有志が個人で開発しているプラグインで、導入すると保管庫全体への読み書き権限を渡すことになります。業務のノートを置いている保管庫で判断がつかない場合は、AI専用の保管庫を別に用意し、渡してよいノートだけをそこに置いたうえで導入する方法もあります。

なお、以前はこのプラグインのREST APIを叩くMCPサーバーを自作する必要がありましたが、v5.0.0(2026年7月24日)以降、プラグイン自体がMCPサーバーを内蔵しています。

STEP 1|プラグインの導入とAPIキーの取得

  1. Obsidianの 設定 → コミュニティプラグイン を開き、「制限モードを終了する」を押します。初期状態のままではコミュニティプラグインを使えません。
  2. Local REST API with MCP を開き、「Add to Obsidian」を押します。Obsidianが起動して、インストール画面が開きます。
  3. インストールしたら、トグルをONにして有効化します。
  4. 設定の左サイドバーに現れた「Local REST API」を開くと、APIキーが表示されます。これを控えます。この画面が、以降のポートや証明書の設定場所にもなります。

既定ではHTTPSのポート 27124 で待ち受けます。

Obsidian 1.13.1 以上が必要です。それ未満のバージョンでは、コミュニティストアからMCP非対応の旧版(4.1.7)しかインストールできません。

APIキーは、AIがノートにアクセスするための鍵です。外部に漏らさないよう扱います。

STEP 2|AIアプリに登録

接続先は https://127.0.0.1:27124/mcp/、認証に使うのは STEP 1で控えたAPIキーです。以下の <APIキー> を、その文字列に置き換えます。

Claude Codeの場合

ターミナルで次を実行します。どのフォルダで実行しても動きます。

claude mcp add --transport http obsidian https://127.0.0.1:27124/mcp/ --header "Authorization: Bearer <APIキー>"

Added HTTP MCP server obsidian ... と表示されれば登録できています。どこからでも使いたい場合は、末尾に -s user を付けます。

Claude Desktopの場合

設定ファイルを開いて、次の内容を書きます。ファイルが無ければ新規作成します(HTTP接続を中継する mcp-remote を使います)。

この方法だけはNode.jsが必要です。mcp-remote の起動に、Node.jsに同梱の npx を使うためです。未導入の場合は nodejs.org からLTS版を入れ、ターミナルを開き直してから設定します。

  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • mac: ~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "obsidian": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://127.0.0.1:27124/mcp/",
        "--header",
        "Authorization:${AUTH_HEADER}"
      ],
      "env": {
        "AUTH_HEADER": "Bearer <APIキー>"
      }
    }
  }
}

Authorization: の後にスペースがないのは意図的です。Windows版では npx に渡す引数のスペースが壊れることがあるため、キー本体を環境変数へ逃がしています。

Cursorの場合

設定ファイルを開いて、次の内容を書きます。ファイルが無ければ新規作成します。

  • Windows: %USERPROFILE%\.cursor\mcp.jsonC:\Users\<ユーザー名>\.cursor\mcp.json
  • mac / Linux: ~/.cursor/mcp.json
{
  "mcpServers": {
    "obsidian": {
      "url": "https://127.0.0.1:27124/mcp/",
      "headers": { "Authorization": "Bearer <APIキー>" }
    }
  }
}

.cursor は隠しフォルダなので、無ければ自分で作ります。

Codexの場合

ターミナルで次を実行します。CodexはAPIキーを環境変数から読むので、キーを直接コマンドに書きません。

codex mcp add obsidian --url https://127.0.0.1:27124/mcp/ --bearer-token-env-var OBSIDIAN_API_KEY

そのうえで、環境変数 OBSIDIAN_API_KEY にAPIキーを設定します。Windowsなら次のコマンドで永続的に登録できます(設定後はターミナルを開き直します)。

setx OBSIDIAN_API_KEY "<APIキー>"

登録内容は ~/.codex/config.toml(Windowsは C:\Users\<ユーザー名>\.codex\config.toml)に次の形で保存されます。手で書くこともできます。

[mcp_servers.obsidian]
url = "https://127.0.0.1:27124/mcp/"
bearer_token_env_var = "OBSIDIAN_API_KEY"

いずれの場合も、保存したら Obsidianを起動したままAIアプリを完全に終了して起動し直します。

STEP 3|証明書について

ポート27124はHTTPSで、プラグインは自己署名証明書を使います。クライアントが接続を拒否する場合は、次のいずれかで対応します。

  • https://127.0.0.1:27124/obsidian-local-rest-api.crt から証明書を取得し、端末に信頼させる
  • 設定 → Local REST API でHTTPサーバー(ポート27123)を有効にし、http://127.0.0.1:27123/mcp/ に接続する(ローカル限定の切り分け用)

STEP 4|動作確認

Obsidianを起動したまま頼みます。まず、段階Aでもできていたことが同じように動くか確認します。

Obsidianで「会議」を含むノートを検索して
Notes/アイデア.md を読んで、要点を教えて

AIは保管庫の全ファイルを読み込まないため、消費トークンを抑えられます。

次が、段階Bで初めてできるようになることです。TemplaterとDataviewは別途インストールするプラグインなので、入れていない場合は該当の例を飛ばします。

Notes/アイデア.md の「## タスク」の下にだけ、いまの会話のToDoを追記して
Projects/新機能.md のフロントマターの status を done に変えて
今日のデイリーノートを開いて、この会話の要点を追記して
保管庫で使われているタグを一覧して、使用数の多い順に並べて
Obsidianで使えるコマンドを一覧して
Templater のテンプレート挿入コマンドを実行して
Projects フォルダだけ「設計」で検索して、上位5件を見せて

見出しを指定した追記は、本文の他の部分に触れずに実行されます。フロントマターの書き換えも、本文やYAMLの構造を壊しません。デイリーノートの位置やタグ索引は、Obsidianが持っている情報をそのまま使っています。いずれもファイルを直接書き換える方式では保証できなかった部分です。

うまく動かないときは、この章の末尾にある「うまくいかないとき」を確認します。

足りないツールを足す

プラグインは他のプラグインが独自のAPIルートを登録できる拡張APIを備えています。標準のMCPツールで足りない操作が出てきたら、この仕組みで自分用のツールを追加できます。ここまで来ると自作プラグインの領域ですが、ゼロから作るより手間はかかりません。

うまくいかないとき

症状

原因と対処

つながらない

Obsidianが起動しているか確認します。プラグインはObsidianのプロセス内で動くため、閉じていると接続できません

接続を拒否される

証明書が原因のことがあります。切り分けのためHTTP(27123)を試します

認証に失敗する

APIキーと、ポート27124が合っているか確認します

ポートが競合する

複数の保管庫を同時に開いていないか確認します

段階C|段階Bを、自分のコードで実現する

AIアプリは、Claude CodeとClaude Desktopの2つを扱います。CursorとCodexは、STEP 7の設定例を用意していません。

また、一部にObsidianの非公開APIを使うため、本体のアップデートで動かなくなる可能性があります。その対応も自分で行うことになります。

段階Bは、個人開発者のコードに保管庫全体の読み書き権限を渡す構成です。仕事のノートや機微な記録を置いている保管庫で、それを受け入れられないことはあります。その場合は、同じことを自分で作ります。

アプリの機能をMCPツールとして公開するには、Obsidianのプロセス内で動くコードが要ります。外部のプログラムからObsidianの内部APIを直接呼ぶ方法はないためです。つまり段階Cは、段階Bで有志のプラグインがやっていることを、自分で書き直す作業になります。

カバーするのは段階Bの主要な機能です。ノートの読み書きも、コマンド実行も、すべて自分のコード経由で行います。実装しない範囲はSTEP 6で示します。ファイル操作まで含めてObsidianを通すのがポイントで、段階Aのようにファイルシステムを直接触るのとは違い、リンクやキャッシュの整合性がアプリ側で保たれます。

プラグインは薄く、MCPは外に置く

プラグイン自体にMCPサーバーを内蔵することもできますが、プラグインは素のHTTPエンドポイントのみを出し、MCP化は外側の小さなサーバーで行う構成にします。プラグインに依存を持ち込まずに済み、curlでプラグイン側だけを切り分けてデバッグできます。

AIアプリ  ──stdio──>  自作MCPサーバー
                          │
                    HTTP(127.0.0.1のみ)
                          ↓
                   自作プラグイン ── Obsidian本体

全体の流れ

作業は8ステップです。STEP 1〜5でObsidian側のプラグインを、STEP 6〜7でAIとつなぐMCPサーバーを作り、STEP 8で動作を確認します。

やること

作業場所

STEP 1

保管庫にプラグイン用フォルダを作成

保管庫

STEP 2

作業用フォルダの用意

作業用フォルダ

STEP 3

プラグインのコードを記述

作業用フォルダ

STEP 4

ビルド

作業用フォルダ

STEP 5

Obsidianでプラグインを有効化

Obsidian

STEP 6

MCPサーバーを作成

MCPサーバー用フォルダ

STEP 7

AIアプリに登録

ターミナル

STEP 8

動作確認

AIアプリ

main.tsserver.mjs は書き換えずに使えます。そのままコピーして保存します。

STEP 3〜5は繰り返します。コードを直すたびに、ビルド → コピー → Obsidianで読み込み直し、という流れをたどります。

STEP 1|保管庫にプラグイン用フォルダを作成

STEP 1-1|フォルダを作成

プラグインを置くフォルダを作ります。場所は次のとおりです。

<保管庫フォルダ>/.obsidian/plugins/my-bridge/

<保管庫フォルダ> は、いま使っている保管庫のフォルダです。Obsidianを起動して「設定 → ファイルとリンク」などの画面を開くか、ノートを右クリックして「エクスプローラーで表示」を選ぶと場所を確認できます。

my-bridge はプラグインの識別名で、自分で決められます。ただし公式の規約があり、小文字とハイフンだけを使い、obsidian を含めず、plugin で終わらせない必要があります。他のプラグインとの重複も避けます。この記事では以降 my-bridge で進めます。別の名前にする場合は、フォルダ名と manifest.jsonid を同じ名前に揃えます。この2つがずれるとObsidianはプラグインを読み込みません。

plugins フォルダは最初から存在しません。コミュニティプラグインを一度も入れていない保管庫には .obsidian の下に plugins が無いので、plugins と my-bridge を続けて作ります。また .obsidian は先頭がドットで始まる隠しフォルダなので、エクスプローラーやFinderで隠しファイルの表示をONにします。

STEP 1-2|manifest.jsonを作成

このフォルダに manifest.jsonmain.js の2つを置くと、プラグインとして認識されます。main.js はSTEP 4で作るので、ここでは manifest.json だけ用意します。

my-bridge フォルダに manifest.json というファイルを作成し、次の内容を保存します。

{
  "id": "my-bridge",
  "name": "My Bridge",
  "version": "1.0.0",
  "minAppVersion": "1.8.0",
  "description": "自分専用のローカルAPI",
  "author": "あなた",
  "isDesktopOnly": true
}

id はフォルダ名と同じにします。name は設定画面に表示される名前で、日本語も使えます。isDesktopOnlytrue のままにします。HTTPサーバーの起動にNode.jsの機能を使うため、Node.jsを持たないモバイル版では動きません。

STEP 2|作業用フォルダの用意

プラグインのコードはTypeScriptで書き、main.js という1つのファイルに変換(ビルド)してから保管庫に置きます。この変換作業をする場所が必要です。

保管庫の中では作業しません。ビルドに使う部品が数千ファイル単位で作られ、Obsidianがそれを全部ノートとして読み込もうとして重くなります。保管庫とは関係のない場所に、作業用のフォルダを1つ作ります。

たとえばドキュメントの下に obsidian-plugin-dev という名前で作ります。場所と名前は自由です。

C:/Users/<ユーザー名>/Documents/obsidian-plugin-dev/

STEP 2-1|ターミナルの起動

以降のコマンドは、すべてこの作業用フォルダの中で実行します。

  • Windows: エクスプローラーでフォルダを開き、何もないところを右クリック →「ターミナルで開く」
  • mac: Finderでフォルダを右クリック →「フォルダに新規ターミナル」

ターミナルに、作ったフォルダのパスが表示されていることを確認します。

STEP 2-2|Node.jsの導入

ここからの作業にはNode.jsが必要となります。まず、入っているかとバージョンを確認します。

node --version

19以下だった場合は、最新版のLTSを nodejs.org からダウンロードしてインストールします。

完了したら、インストールしたNode.jsを認識させるために、開いているターミナルをいったん全部閉じて開き直します。

STEP 2-3|部品の導入

次の2つを順に実行します。1つ目は設定ファイルを作るコマンド、2つ目はビルドに使う部品をダウンロードするコマンドです。2つ目は少し時間がかかります。

npm init -y
npm install -D esbuild typescript obsidian @types/node

終わると、フォルダの中に package.jsonnode_modules ができます。node_modules は部品置き場なので、中身を気にする必要はありません。

STEP 3|プラグインのコードを記述

Obsidianの中でHTTPサーバーを動かし、次の12個のURLでObsidianの機能を呼び出せるようにします。

URL

できること

/notes

ノートのパス一覧を返す

/note

指定したノートの本文を返す

/search

本文とファイル名からノートを検索する(limit / folder で絞れる)

/create

ノートを新規作成する

/append

ノートの末尾に追記する(無ければ作成する)

/headings

ノートの見出し構造を返す

/tags

保管庫全体のタグ一覧を返す(非公開API)

/commands

コマンドの一覧を返す(非公開API。実行可否は available で示す)

/command

コマンドを実行する

/rename

ノートをリネームする(リンクも追従)

/delete

ノートを削除する(ゴミ箱へ移動)

/frontmatter

フロントマターの項目を書き換える

作業用フォルダに main.ts というファイルを作成し、以下のコードを保存します。

書き換える箇所はありません。認証に使うトークンは、初回起動時に自動生成されます。

import { App, Notice, Plugin, PluginSettingTab, Setting, TFile } from "obsidian";
import * as http from "node:http";

const PORT = 27200;

interface BridgeSettings {
  token: string;
}

// 48文字のランダムな16進文字列
function newToken(): string {
  const bytes = new Uint8Array(24);
  crypto.getRandomValues(bytes);
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
}

// 返すHTTPステータスを持たせたエラー。クライアントの誤りと想定外の不具合を区別する
class HttpError extends Error {
  constructor(readonly status: number, message: string) {
    super(message);
  }
}

export default class MyBridge extends Plugin {
  settings: BridgeSettings = { token: "" };
  private server?: http.Server;

  async onload() {
    // トークンはコードに書かず、プラグイン設定(data.json)に保存する。
    // 初回だけ自動生成するので、利用者が用意する必要はない。
    const saved = (await this.loadData()) as Partial<BridgeSettings> | null;
    this.settings = { token: saved?.token || newToken() };
    if (!saved?.token) await this.saveData(this.settings);
    this.addSettingTab(new BridgeSettingTab(this.app, this));

    this.server = http.createServer(async (req, res) => {
      // 認証。ローカル限定でも、他のローカルプロセスから叩かれ得るので付ける
      if (req.headers.authorization !== `Bearer ${this.settings.token}`) {
        res.writeHead(401).end();
        return;
      }

      const url = new URL(req.url ?? "/", `http://127.0.0.1:${PORT}`);
      const json = (data: unknown) => {
        res.writeHead(200, { "Content-Type": "application/json" });
        res.end(JSON.stringify(data));
      };

      // 想定外のHTTPメソッドは405。GET用URLへのPOSTなどを素通りさせない
      const only = (method: string) => {
        if (req.method !== method) {
          throw new HttpError(405, `${url.pathname} は ${method} で呼んでください`);
        }
      };

      // 必須クエリパラメータ。無ければ400
      const param = (name: string): string => {
        const v = url.searchParams.get(name);
        if (!v) throw new HttpError(400, `パラメータ ${name} が必要です`);
        return v;
      };

      // 存在するノートを取得。無ければ404
      const noteAt = (path: string): TFile => {
        const f = this.app.vault.getAbstractFileByPath(path);
        if (!(f instanceof TFile)) throw new HttpError(404, `ノートがありません: ${path}`);
        return f;
      };

      // POSTのJSON本文。壊れていれば400
      const body = async (...required: string[]): Promise<Record<string, string>> => {
        let parsed: Record<string, string>;
        try {
          parsed = JSON.parse(await readBody(req));
        } catch {
          throw new HttpError(400, "本文がJSONとして読めません");
        }
        for (const k of required) {
          // 「無い」と「型が違う」は原因が別なので、メッセージを分ける
          if (!(k in (parsed ?? {}))) throw new HttpError(400, `${k} が必要です`);
          if (typeof parsed[k] !== "string") throw new HttpError(400, `${k} は文字列である必要があります`);
        }
        return parsed;
      };

      try {
        // --- ノートの読み書き(すべてObsidian経由なのでキャッシュと整合する) ---

        if (url.pathname === "/notes") {
          only("GET");
          return json(this.app.vault.getMarkdownFiles().map((f) => f.path));
        }

        if (url.pathname === "/note") {
          only("GET");
          // cachedReadは表示目的の読み取り。編集前提ならreadを使う
          return json({ content: await this.app.vault.cachedRead(noteAt(param("path"))) });
        }

        if (url.pathname === "/search") {
          only("GET");
          const q = param("query").toLowerCase();
          const folder = url.searchParams.get("folder") ?? "";
          // 「日記」で「日記帳/」に当たらないよう、区切り文字まで含めて比較する
          const prefix = folder.endsWith("/") ? folder : folder + "/";
          // 不正な値でNaNになると打ち切りが効かなくなるので、数値として妥当なときだけ採用する
          const rawLimit = Number(url.searchParams.get("limit"));
          const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? rawLimit : 20;
          const hits = [];
          for (const f of this.app.vault.getMarkdownFiles()) {
            if (hits.length >= limit) break; // 上限に達したら打ち切る
            if (folder && !f.path.startsWith(prefix)) continue;
            // ファイル名も検索対象にする。本文に語が出てこないノートを取りこぼさない
            const inName = f.path.toLowerCase().includes(q);
            const text = await this.app.vault.cachedRead(f);
            const i = text.toLowerCase().indexOf(q);
            if (i < 0 && !inName) continue;
            hits.push({
              path: f.path,
              matchedIn: i >= 0 ? (inName ? "both" : "content") : "name",
              excerpt: i >= 0 ? text.slice(Math.max(0, i - 80), i + 120) : text.slice(0, 120),
            });
          }
          return json(hits);
        }

        if (url.pathname === "/create") {
          only("POST");
          const { path, content } = await body("path", "content");
          if (this.app.vault.getAbstractFileByPath(path)) {
            throw new HttpError(409, `同名のノートが既にあります: ${path}`);
          }
          await this.app.vault.create(path, content);
          return json({ ok: true });
        }

        if (url.pathname === "/append") {
          only("POST");
          const { path, content } = await body("path", "content");
          const file = this.app.vault.getAbstractFileByPath(path);
          if (file instanceof TFile) await this.app.vault.append(file, content);
          else await this.app.vault.create(path, content);
          return json({ ok: true });
        }

        // --- ここから先が、ファイル直接操作では代替できない領域 ---

        if (url.pathname === "/headings") {
          only("GET");
          // アプリのメタデータキャッシュ。ファイルを読まずに見出し構造が取れる
          const cache = this.app.metadataCache.getFileCache(noteAt(param("path")));
          return json(cache?.headings ?? []);
        }

        if (url.pathname === "/tags") {
          only("GET");
          // 保管庫全体のタグ索引。自力で集計すると全ファイル読み込みになる
          return json((this.app.metadataCache as any).getTags());
        }

        if (url.pathname === "/commands") {
          only("GET");
          // listCommands()は「いまの状況で実行できるもの」しか返さない。
          // ノートを開いていないとエディタ系が丸ごと消えるので、登録済みの全件を返す。
          const reg = (this.app as any).commands;
          const runnable = new Set((reg.listCommands() as any[]).map((c) => c.id));
          const all: any[] = reg.commands ? Object.values(reg.commands) : reg.listCommands();
          return json(all.map((c) => ({ id: c.id, name: c.name, available: runnable.has(c.id) })));
        }

        if (url.pathname === "/command") {
          only("POST");
          const { id } = await body("id");
          const reg = (this.app as any).commands;
          if (reg.commands && !reg.commands[id]) {
            throw new HttpError(404, `コマンドがありません: ${id}`);
          }
          // 存在していても、いまの状況では実行できないことがある(例: ノートを開いていない
          // ときのエディタ系)。executeCommandById はこの場合の成否が当てにならないので、
          // 実行可能かを先に確かめる
          const runnable = new Set((reg.listCommands() as any[]).map((c) => c.id));
          if (!runnable.has(id)) {
            throw new HttpError(409, `いまの状況では実行できません: ${id}(対象のノートを開いてから試してください)`);
          }
          const ok = reg.executeCommandById(id);
          if (!ok) throw new HttpError(409, `実行できませんでした: ${id}`);
          return json({ ok: true });
        }

        if (url.pathname === "/rename") {
          only("POST");
          // fileManager経由なら、他ノートの [[リンク]] が自動追従する
          const { from, to } = await body("from", "to");
          await this.app.fileManager.renameFile(noteAt(from), to);
          return json({ ok: true });
        }

        if (url.pathname === "/delete") {
          only("POST");
          // trashFileはObsidianの「削除済みファイル」設定に従う。既定はシステムのゴミ箱なので復元できる
          const { path } = await body("path");
          await this.app.fileManager.trashFile(noteAt(path));
          return json({ ok: true });
        }

        if (url.pathname === "/frontmatter") {
          only("POST");
          // フロントマターだけを安全に書き換える
          const { path, key, value } = await body("path", "key", "value");
          await this.app.fileManager.processFrontMatter(noteAt(path), (fm) => { fm[key] = value; });
          return json({ ok: true });
        }

        throw new HttpError(404, `そのURLはありません: ${url.pathname}`);
      } catch (e) {
        // HttpErrorはこちらの想定内。それ以外は500(プラグイン側の不具合)
        const status = e instanceof HttpError ? e.status : 500;
        const message = e instanceof Error ? e.message : String(e);
        res.writeHead(status, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ error: message }));
      }
    });

    // 127.0.0.1に限定。0.0.0.0でlistenしないこと
    this.server.listen(PORT, "127.0.0.1");
  }

  onunload() {
    // 閉じ忘れるとプラグイン再読み込みのたびにポートが埋まる
    this.server?.close();
  }
}

// 設定画面。トークンの確認と再生成ができる
class BridgeSettingTab extends PluginSettingTab {
  constructor(app: App, private plugin: MyBridge) {
    super(app, plugin);
  }

  display(): void {
    this.containerEl.empty();

    new Setting(this.containerEl)
      .setName("トークン")
      .setDesc("MCPサーバーがこの値を自動で読みます。通常は触る必要はありません")
      .addText((text) =>
        text.setValue(this.plugin.settings.token).onChange(async (v) => {
          this.plugin.settings.token = v.trim();
          await this.plugin.saveData(this.plugin.settings);
        })
      )
      .addButton((btn) =>
        btn.setButtonText("再生成").onClick(async () => {
          this.plugin.settings.token = newToken();
          await this.plugin.saveData(this.plugin.settings);
          this.display();
          new Notice("トークンを再生成しました。MCPサーバー側の設定も更新してください。", 10000);
        })
      );
  }
}

function readBody(req: http.IncomingMessage): Promise<string> {
  return new Promise((resolve, reject) => {
    let b = "";
    req.on("data", (c) => (b += c));
    req.on("end", () => resolve(b));
    // 購読しないと、送信中に切断されたときPromiseが永久に解決されず応答が止まる
    req.on("error", reject);
    req.on("aborted", () => reject(new Error("リクエストが中断されました")));
  });
}

検索は全ノートを順に読む方式です。ノートが多い場合は、folder で範囲を、limit で件数を絞ります。

STEP 4|ビルド

作業用フォルダを開いたターミナルで、次を実行します。main.ts が、Obsidianの読める main.js に変換されます。

npx esbuild main.ts --bundle --outfile=main.js --format=cjs --platform=node --target=es2018 --external:obsidian --external:electron --external:node:*

エラーが出なければ、同じフォルダに main.js ができます。コードを直したときも、このコマンドを再実行します。

STEP 5|Obsidianでプラグインを有効化

ビルドした main.js を、先ほど作った my-bridge フォルダにコピーします。manifest.json と合わせて、フォルダの中身は次の2ファイルだけになります。

<保管庫フォルダ>/.obsidian/plugins/my-bridge/
├── manifest.json
└── main.js

ファイルを置くだけでは動きません。Obsidian側で2つの操作が必要です。

1. 設定 → コミュニティプラグイン を開き、「コミュニティプラグインを許可」欄の 「制限モードを終了する」を押します。制限モードが有効な間、自作プラグインは読み込まれません。

2. 「インストールされたプラグイン」欄の再読み込みアイコンを押すと My Bridge が現れるので、トグルをONにします。この時点で onload() が実行され、HTTPサーバーが起動します。

コードを修正して main.js を差し替えたときも、同じ再読み込み操作が必要です。

トークンについて

有効化すると、認証に使うトークンが自動生成されます。設定の左サイドバーに現れた「My Bridge」を開くと確認できます。

MCPサーバーが <保管庫フォルダ>/.obsidian/plugins/my-bridge/data.json から直接読むので、控える必要はありません。同じ画面の「再生成」で作り直せます。

あわせて変更するObsidianの設定

/rename を使う場合、設定 → ファイルとリンク → 「内部リンクを毎回更新する」をONにしておきます。

OFFのままだと、リネーム時にObsidianが「リンクを更新しますか?」という確認ダイアログを出します。ONにすれば確認なしで即座に完了します。

STEP 6|MCPサーバーを作成

プラグインの作業用フォルダとは別に、新しいフォルダを作ります。名前と場所は自由です。ここでは my-obsidian-mcp とします。

C:/Users/<ユーザー名>/Documents/my-obsidian-mcp/

プラグインのときと同じ手順で、このフォルダを開いてターミナルを起動し、次を順に実行します。

npm init -y
npm install @modelcontextprotocol/server@2 zod

@2 を付けているのは、次のコードがv2系のAPIで書かれているためです。省略すると将来v3が入って動かなくなる可能性があります。

同じフォルダに server.mjs というファイルを作成し、以下のコードを保存します。ビルドは不要です。

import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import * as z from "zod";
import fs from "node:fs/promises";
import path from "node:path";

const BRIDGE = "http://127.0.0.1:27200";

// トークンはプラグインが保存したファイルから読む。コピーを持たないので、
// AIアプリの設定ファイルに認証情報が残らず、再生成しても食い違わない
const DATA_JSON = path.join(process.env.VAULT_PATH, ".obsidian", "plugins", "my-bridge", "data.json");

async function readToken() {
  try {
    return JSON.parse(await fs.readFile(DATA_JSON, "utf8")).token;
  } catch {
    throw new Error(`トークンを読めません: ${DATA_JSON}(VAULT_PATH は合っていますか?プラグインを一度有効化しましたか?)`);
  }
}
let token = await readToken();

async function bridge(p, init = {}) {
  const call = () =>
    fetch(BRIDGE + p, { ...init, headers: { Authorization: `Bearer ${token}`, ...init.headers } });
  let res = await call();
  if (res.status === 401) {
    token = await readToken(); // 設定画面で再生成された場合に追随する
    res = await call();
  }
  if (!res.ok) throw new Error(`bridge ${res.status}: ${await res.text()}`);
  return res.json();
}

const post = (path, body) => bridge(path, { method: "POST", body: JSON.stringify(body) });
const q = (v) => encodeURIComponent(v);
const text = (v) => ({ content: [{ type: "text", text: typeof v === "string" ? v : JSON.stringify(v, null, 2) }] });

const server = new McpServer({ name: "my-obsidian", version: "1.0.0" });

// --- 読み取り ---

server.registerTool(
  "list_notes",
  { description: "保管庫内のノートのパス一覧を返す", inputSchema: z.object({}) },
  async () => text(await bridge("/notes"))
);

server.registerTool(
  "read_note",
  { description: "ノートの本文を返す", inputSchema: z.object({ path: z.string() }) },
  async ({ path }) => text((await bridge(`/note?path=${q(path)}`)).content)
);

server.registerTool(
  "search_notes",
  {
    description: "ノートを本文とファイル名から検索する。folderで範囲を、limitで件数を絞れる",
    inputSchema: z.object({
      query: z.string(),
      folder: z.string().optional(),
      limit: z.number().int().positive().optional(),
    }),
  },
  async ({ query, folder, limit }) => {
    const params = new URLSearchParams({ query });
    if (folder) params.set("folder", folder);
    if (limit) params.set("limit", String(limit));
    return text(await bridge(`/search?${params}`));
  }
);

server.registerTool(
  "get_headings",
  { description: "ノートの見出し構造を返す", inputSchema: z.object({ path: z.string() }) },
  async ({ path }) => text(await bridge(`/headings?path=${q(path)}`))
);

server.registerTool(
  "list_tags",
  { description: "保管庫全体で使われているタグの一覧を返す", inputSchema: z.object({}) },
  async () => text(await bridge("/tags"))
);

server.registerTool(
  "list_commands",
  { description: "Obsidianで実行できるコマンドの一覧を返す", inputSchema: z.object({}) },
  async () => text(await bridge("/commands"))
);

// --- 書き込み。読み取り専用にしたい場合はここから下を消す ---

server.registerTool(
  "create_note",
  {
    description: "ノートを新規作成する",
    inputSchema: z.object({ path: z.string(), content: z.string() }),
  },
  async (args) => { await post("/create", args); return text(`${args.path} を作成しました`); }
);

server.registerTool(
  "append_note",
  {
    description: "ノートの末尾に追記する。無ければ作成する",
    inputSchema: z.object({ path: z.string(), content: z.string() }),
  },
  async (args) => { await post("/append", args); return text(`${args.path} に追記しました`); }
);

server.registerTool(
  "set_frontmatter",
  {
    description: "ノートのフロントマターの項目を書き換える",
    inputSchema: z.object({ path: z.string(), key: z.string(), value: z.string() }),
  },
  async (args) => { await post("/frontmatter", args); return text(`${args.path} の ${args.key} を更新しました`); }
);

server.registerTool(
  "rename_note",
  {
    description: "ノートをリネームする。他ノートからのリンクも追従する",
    inputSchema: z.object({ from: z.string(), to: z.string() }),
  },
  async (args) => { await post("/rename", args); return text(`${args.from} を ${args.to} に変更しました`); }
);

server.registerTool(
  "run_command",
  { description: "Obsidianのコマンドを実行する", inputSchema: z.object({ id: z.string() }) },
  async ({ id }) => { await post("/command", { id }); return text(`実行しました: ${id}`); }
);

// 削除。不要ならこのツールだけ消せば、AIは削除できなくなる
server.registerTool(
  "delete_note",
  {
    description: "ノートを削除する。Obsidianの設定に従いゴミ箱へ移動する",
    inputSchema: z.object({ path: z.string() }),
  },
  async ({ path }) => { await post("/delete", { path }); return text(`${path} を削除しました`); }
);

await server.connect(new StdioServerTransport());

プラグインの12個のエンドポイントを、そのまま12個のMCPツールとして公開しています。これで段階Bの主要機能をカバーできます。

ただし完全に同じではありません。この記事のコードで実装していないものは次のとおりです。必要なら同じ形で足せます。

  • 見出し単位の編集
  • ブロック参照を指定した編集
  • アクティブファイルの取得
  • デイリーノートの場所の解決

公開するツールを選べます。ここが段階Bとの一番の違いです。

  • 削除させたくない → delete_note の登録ブロックを消す
  • 一切書き換えさせたくない → create_note 以降をまるごと消す
  • 使わないツールがある → その登録ブロックを消す

消したツールはAIから見えなくなるため、指示されても実行できません。

SDKのバージョンについて:MCPのTypeScript SDKはv2で、パッケージ名が @modelcontextprotocol/sdk(v1)から @modelcontextprotocol/server に変わり、stdioトランスポートは @modelcontextprotocol/server/stdio に分かれました。v1系の記事やサンプルはimportが違うので、そのままでは動きません。importエラーが出たら、まず参照しているドキュメントがv1かv2かを確認します。上のコードは2.0.0で動作確認しています。

STEP 7|AIアプリに登録

登録には server.mjs のフルパスと、保管庫のフルパスが要ります。トークンは server.mjsdata.json から直接読むので、渡す必要はありません。

Claude Codeの場合

ターミナルで次を実行します。どのフォルダで実行しても動きます。

claude mcp add my-obsidian node C:/Users/<ユーザー名>/Documents/my-obsidian-mcp/server.mjs --env VAULT_PATH=C:/Users/<ユーザー名>/Documents/<保管庫フォルダ> -s user

Added stdio MCP server my-obsidian ... と表示されれば登録できています。

-s user を付けると、どのフォルダからでも使えます。省くとコマンドを実行したフォルダ専用になります。

--envnode とスクリプトパスより後ろに置きます。前に置くとWindowsで失敗します。

Claude Desktopの場合

設定ファイルを開いて、次の内容を書きます。ファイルが無ければ新規作成します。

  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • mac: ~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "my-obsidian": {
      "command": "node",
      "args": ["C:\\Users\\<ユーザー名>\\Documents\\my-obsidian-mcp\\server.mjs"],
      "env": {
        "VAULT_PATH": "C:\\Users\\<ユーザー名>\\Documents\\<保管庫フォルダ>"
      }
    }
  }
}

書き終えたら、Claude Desktopを完全に終了してから起動し直します。再起動しないと登録が反映されません。

登録できたかは、AIに「Obsidianで使えるコマンドを一覧して」と頼めば分かります。一覧が返ればつながっています。

STEP 8|動作確認

まずプラグイン単体で確認します。

curl -H "Authorization: Bearer <トークン>" http://127.0.0.1:27200/commands

コマンド一覧のJSONが返れば、Obsidian側は正常です。次にAIから頼みます。

Obsidianで使えるコマンドを一覧して
「会議」を含むノートを探して
Notes/アイデア.md の見出し構成を教えて
Daily/2026-08-03.md に、いまの会話の要点を追記して
Projects/新機能.md の status を done にして

削除も試せます。Obsidianの「削除済みファイル」設定に従うので、既定ならゴミ箱から戻せます。

テスト用に作った 不要メモ.md を削除して

最後に、外したツールが使えないことを確認します。server.mjs から delete_note の登録ブロックを消し、AIアプリを再起動してから同じ削除を頼むと、AIは実行できないと答えます。ここで削除できてしまう場合は、AIがMCP以外の手段を使っています。

うまくいかないとき

症状

原因と対処

直したのに挙動が変わらない

反映漏れです。ビルド → main.js を保管庫にコピー → プラグインをOFF/ON、まで済ませます

bridge 401

登録した VAULT_PATH が別の保管庫を指しているか、プラグインをまだ有効化していません

error: missing required argument

--env の位置が原因です。node とスクリプトパスより後ろに置きます

登録したツールが出てこない

claude mcp list で確認し、-s user を付けて登録し直します

importエラー

SDKのバージョン違いです。v1は @modelcontextprotocol/sdk、v2は @modelcontextprotocol/server です

EADDRINUSE

前のサーバーが残っています。プラグインをOFF/ONします。別のアプリが27200番を使っている場合は、main.tsPORTserver.mjsBRIDGE を同じ番号に変えます

プラグインが動かない

Obsidianの開発者コンソール(Ctrl+Shift+I)にエラーが出ます

切り分けはプラグイン側から始めます。MCPを介さずcurlで叩けば、どちらの層の問題かが分かります。

curl -H "Authorization: Bearer <トークン>" http://127.0.0.1:27200/commands

まとめ

  • 段階A: 保管庫フォルダをAIツールに渡すと、ノートの読み取り・作成・追記ができます
  • 段階B: Local REST API with MCPを入れると、Obsidianのコマンド実行や見出し単位の編集ができます
  • 段階C: 同じことを自分のコードで用意すると、AIに渡すツールを自分で選べます。保守は自分の責任になります

最初の一歩は、段階Aで「保管庫のノートを検索して要約して」と頼んでみることです。足りないものが見えたら、そのとき次の段階に進みます。

おすすめ情報

タグ一覧