ウェブ運用の工数削減に役立つ
AI活用術39選
ウェブ運用の膨大なタスクを「CMS×AI」で削減する39個のユースケースをご紹介します。単なるテキスト生成を超え、AIがCMS操作そのものを支援する次世代の運用フローをご覧ください。
39個のAI活用術を見る最終更新日:2026.8.5
ウェブ運用の工数削減に役立つ
AI活用術39選
ウェブ運用の膨大なタスクを「CMS×AI」で削減する39個のユースケースをご紹介します。単なるテキスト生成を超え、AIがCMS操作そのものを支援する次世代の運用フローをご覧ください。
39個のAI活用術を見るObsidianの開発元は、公式のMCPサーバーを公開していません(2026年8月時点)。そのため、この記事で扱う「Obsidian MCP」とは、有志が公開しているツールや自分で用意する仕組みを含めた、ObsidianとAIをつなぐ連携全般を指す呼び方です。正式な製品名ではありません。
MCP(Model Context Protocol)は、AIと外部のツールやデータをつなぐ共通規格です。この規格に対応したサーバー(MCPサーバー)を用意すると、対応するAIアプリからObsidianのノートを操作できます。仕組みそのものはMCPサーバーとはで解説しています。
Obsidianは、ノートをまとめたフォルダを1つの単位として扱います。これを保管庫と呼びます(海外版ではVault)。初回起動時の「保管庫を作成」から作り、用途ごとに複数持つこともできます。実体はただのフォルダで、中にはMarkdownファイルがそのまま並びます。
ノートがファイルとして置かれている以上、多くの用途はMCPなしで操作できます。
分かれ目は、Obsidianというアプリを介さないと届かない領域に用があるかどうかです。コマンドの実行、アプリ内蔵の検索、リンクの解決、タグの索引がそれにあたります。ここに用がなければ、MCPは要りません。
この記事では、やりたいことに応じて必要な段階まで進む形で解説します。
上から順に、できることが増える代わりに、準備と権限も増えます。
段階Bで使うのは、有志が個人で開発しているプラグインです。公式のコミュニティプラグインストアで配布されていますが、導入すると保管庫全体を読み書きできる権限を渡すことになります。次の点は、進む前に把握しておく必要があります。
これらを受け入れられない場合の選択肢が段階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は挙動が変わる可能性があります。
@modelcontextprotocol/server 2.0.0obsidian 1.13.1段階Cは、掲載したコードをそのままビルドして動作することを確認済みです。異常系も、想定どおりのステータスコードが返ることを確認しています。
保管庫フォルダをファイル操作できるAIツールに渡すだけで、次ができます。
対応するツールの例:Claude Code、Claude Desktop、Cursor、Codex、Claude Cowork。
作業ディレクトリを保管庫にしてAIツールを起動します。Claude Codeなら次のようになります。
cd C:/Users/<ユーザー名>/Documents/<保管庫フォルダ>この状態で「Notes/アイデア.md を読んで要点を教えて」「Daily/2026-08-03.md に今日の要点を追記して」と頼めば動きます。設定ファイルもAPIキーも不要です。
AIがファイルを直接書き換えるため構造の保証ができず、検索もファイルを実際に読むので、ノートが増えるほど重くなります。
AIによるファイル操作はObsidianのリンク更新機構を通りません。Obsidianのアプリ内でノートをリネーム・移動すると [[リンク]] が自動追従しますが、AIがファイルシステム経由で同じことをすると、リンクが壊れたまま残ります。
段階Aでは、リネームと移動はAIにやらせないのが安全です。
保管庫全体から「会議」を含むノートを探して、パスの一覧にしてNotes/アイデア.md を読んで、要点を3つにまとめて先月のデイリーノートを読んで、まだ着手していないタスクを拾い出していまの会話の結論を Notes/決定事項.md として新規作成してDaily/2026-08-03.md の末尾に、今日やったことを箇条書きで追記して検索はAIが実際にファイルを読んで探すので、ノートが数千件あると時間がかかります。フォルダを指定して範囲を狭めると安定します。
## タスク の下にだけ追記して」「フロントマターの status だけ done に変えて」を確実にやりたい症状 | 原因と対処 |
|---|---|
ノートが見つからない | AIツールの作業ディレクトリが保管庫になっているか確認します |
動作が重い | 隠しフォルダ( |
起動中のObsidian本体に接続します。ファイルを外から書き換えるのではなく、Obsidianに処理させる形になるため、アプリの機能がそのまま使えます。
リンクの整合性もObsidian側が面倒を見るため、段階Aで抱えていたリンク破損のリスクも下がります。
公式のMCPサーバーがない以上、実質的な標準はコミュニティプラグインの「Local REST API with MCP」です。
冒頭で触れたとおり、これは有志が個人で開発しているプラグインで、導入すると保管庫全体への読み書き権限を渡すことになります。業務のノートを置いている保管庫で判断がつかない場合は、AI専用の保管庫を別に用意し、渡してよいノートだけをそこに置いたうえで導入する方法もあります。
なお、以前はこのプラグインのREST APIを叩くMCPサーバーを自作する必要がありましたが、v5.0.0(2026年7月24日)以降、プラグイン自体がMCPサーバーを内蔵しています。
既定ではHTTPSのポート 27124 で待ち受けます。
Obsidian 1.13.1 以上が必要です。それ未満のバージョンでは、コミュニティストアからMCP非対応の旧版(4.1.7)しかインストールできません。
APIキーは、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版を入れ、ターミナルを開き直してから設定します。
%APPDATA%\Claude\claude_desktop_config.json~/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の場合
設定ファイルを開いて、次の内容を書きます。ファイルが無ければ新規作成します。
%USERPROFILE%\.cursor\mcp.json(C:\Users\<ユーザー名>\.cursor\mcp.json)~/.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アプリを完全に終了して起動し直します。
ポート27124はHTTPSで、プラグインは自己署名証明書を使います。クライアントが接続を拒否する場合は、次のいずれかで対応します。
https://127.0.0.1:27124/obsidian-local-rest-api.crt から証明書を取得し、端末に信頼させるhttp://127.0.0.1:27123/mcp/ に接続する(ローカル限定の切り分け用)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が合っているか確認します |
ポートが競合する | 複数の保管庫を同時に開いていないか確認します |
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サーバーを内蔵することもできますが、プラグインは素の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.ts と server.mjs は書き換えずに使えます。そのままコピーして保存します。
STEP 3〜5は繰り返します。コードを直すたびに、ビルド → コピー → Obsidianで読み込み直し、という流れをたどります。
プラグインを置くフォルダを作ります。場所は次のとおりです。
<保管庫フォルダ>/.obsidian/plugins/my-bridge/<保管庫フォルダ> は、いま使っている保管庫のフォルダです。Obsidianを起動して「設定 → ファイルとリンク」などの画面を開くか、ノートを右クリックして「エクスプローラーで表示」を選ぶと場所を確認できます。
my-bridge はプラグインの識別名で、自分で決められます。ただし公式の規約があり、小文字とハイフンだけを使い、obsidian を含めず、plugin で終わらせない必要があります。他のプラグインとの重複も避けます。この記事では以降 my-bridge で進めます。別の名前にする場合は、フォルダ名と manifest.json の id を同じ名前に揃えます。この2つがずれるとObsidianはプラグインを読み込みません。
plugins フォルダは最初から存在しません。コミュニティプラグインを一度も入れていない保管庫には .obsidian の下に plugins が無いので、plugins と my-bridge を続けて作ります。また .obsidian は先頭がドットで始まる隠しフォルダなので、エクスプローラーやFinderで隠しファイルの表示をONにします。
このフォルダに manifest.json と main.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 は設定画面に表示される名前で、日本語も使えます。isDesktopOnly は true のままにします。HTTPサーバーの起動にNode.jsの機能を使うため、Node.jsを持たないモバイル版では動きません。
プラグインのコードはTypeScriptで書き、main.js という1つのファイルに変換(ビルド)してから保管庫に置きます。この変換作業をする場所が必要です。
保管庫の中では作業しません。ビルドに使う部品が数千ファイル単位で作られ、Obsidianがそれを全部ノートとして読み込もうとして重くなります。保管庫とは関係のない場所に、作業用のフォルダを1つ作ります。
たとえばドキュメントの下に obsidian-plugin-dev という名前で作ります。場所と名前は自由です。
C:/Users/<ユーザー名>/Documents/obsidian-plugin-dev/以降のコマンドは、すべてこの作業用フォルダの中で実行します。
ターミナルに、作ったフォルダのパスが表示されていることを確認します。
ここからの作業にはNode.jsが必要となります。まず、入っているかとバージョンを確認します。
node --version19以下だった場合は、最新版のLTSを nodejs.org からダウンロードしてインストールします。
完了したら、インストールしたNode.jsを認識させるために、開いているターミナルをいったん全部閉じて開き直します。
次の2つを順に実行します。1つ目は設定ファイルを作るコマンド、2つ目はビルドに使う部品をダウンロードするコマンドです。2つ目は少し時間がかかります。
npm init -ynpm install -D esbuild typescript obsidian @types/node終わると、フォルダの中に package.json と node_modules ができます。node_modules は部品置き場なので、中身を気にする必要はありません。
Obsidianの中でHTTPサーバーを動かし、次の12個のURLでObsidianの機能を呼び出せるようにします。
URL | できること |
|---|---|
| ノートのパス一覧を返す |
| 指定したノートの本文を返す |
| 本文とファイル名からノートを検索する( |
| ノートを新規作成する |
| ノートの末尾に追記する(無ければ作成する) |
| ノートの見出し構造を返す |
| 保管庫全体のタグ一覧を返す(非公開API) |
| コマンドの一覧を返す(非公開API。実行可否は |
| コマンドを実行する |
| ノートをリネームする(リンクも追従) |
| ノートを削除する(ゴミ箱へ移動) |
| フロントマターの項目を書き換える |
作業用フォルダに 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 で件数を絞ります。
作業用フォルダを開いたターミナルで、次を実行します。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 ができます。コードを直したときも、このコマンドを再実行します。
ビルドした 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 から直接読むので、控える必要はありません。同じ画面の「再生成」で作り直せます。
/rename を使う場合、設定 → ファイルとリンク → 「内部リンクを毎回更新する」をONにしておきます。

OFFのままだと、リネーム時にObsidianが「リンクを更新しますか?」という確認ダイアログを出します。ONにすれば確認なしで即座に完了します。
プラグインの作業用フォルダとは別に、新しいフォルダを作ります。名前と場所は自由です。ここでは my-obsidian-mcp とします。
C:/Users/<ユーザー名>/Documents/my-obsidian-mcp/プラグインのときと同じ手順で、このフォルダを開いてターミナルを起動し、次を順に実行します。
npm init -ynpm 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で動作確認しています。
登録には server.mjs のフルパスと、保管庫のフルパスが要ります。トークンは server.mjs が data.json から直接読むので、渡す必要はありません。
Claude Codeの場合
ターミナルで次を実行します。どのフォルダで実行しても動きます。
claude mcp add my-obsidian node C:/Users/<ユーザー名>/Documents/my-obsidian-mcp/server.mjs --env VAULT_PATH=C:/Users/<ユーザー名>/Documents/<保管庫フォルダ> -s userAdded stdio MCP server my-obsidian ... と表示されれば登録できています。
-s user を付けると、どのフォルダからでも使えます。省くとコマンドを実行したフォルダ専用になります。
--env は node とスクリプトパスより後ろに置きます。前に置くとWindowsで失敗します。
Claude Desktopの場合
設定ファイルを開いて、次の内容を書きます。ファイルが無ければ新規作成します。
%APPDATA%\Claude\claude_desktop_config.json~/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で使えるコマンドを一覧して」と頼めば分かります。一覧が返ればつながっています。
まずプラグイン単体で確認します。
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以外の手段を使っています。
症状 | 原因と対処 |
|---|---|
直したのに挙動が変わらない | 反映漏れです。ビルド → |
| 登録した |
|
|
登録したツールが出てこない |
|
importエラー | SDKのバージョン違いです。v1は |
| 前のサーバーが残っています。プラグインをOFF/ONします。別のアプリが27200番を使っている場合は、 |
プラグインが動かない | Obsidianの開発者コンソール(Ctrl+Shift+I)にエラーが出ます |
切り分けはプラグイン側から始めます。MCPを介さずcurlで叩けば、どちらの層の問題かが分かります。
curl -H "Authorization: Bearer <トークン>" http://127.0.0.1:27200/commands最初の一歩は、段階Aで「保管庫のノートを検索して要約して」と頼んでみることです。足りないものが見えたら、そのとき次の段階に進みます。
次世代ヘッドレスCMS「NILTO」を活用し、
AIによる運用効率化とチームでのスムーズな
更新体験を最短で実現します。