2026年4月14日火曜日

ローカルLLMにObsidianの「書き込み権限」を与えて真の相棒にしてみた

 

はじめに

ついに導入した M4 Max Mac Studio。36GBのメモリを活かしてローカルLLM(Qwen 3.5 9Bなど)をぶん回しているが、ただチャットするだけではもったいない。 自分の外部脳である Obsidian と MCP (Model Context Protocol) で連携させ、AIに「メモを読ませる」だけでなく、ついに「メモを書き込ませる」ところまで進化したので、その備忘録を残しておく。


今回の課題:AIに「権限がない」と拒否される

当初、LM Studio経由で自作のMCPサーバーを介してObsidianと連携させていたが、AIに「この手順書を更新しておいて」と頼むと、こんな冷たい返事が返ってきた。

「申し訳ありませんが、メモファイルを直接編集・更新する権限を持っておりません…」

インフラ屋として、権限(Permission)で弾かれるほど悔しいことはない。 原因は、MCPサーバー(index.mjs)に search(検索) と read(読み込み) のツールしか実装していなかったからだ。


解決策:index.mjs を改造して「書く力」を与える

Node.jsで書いたMCPサーバーの index.mjs に、fs.writeFile と fs.appendFile を叩くためのツールを追加実装した。

実装した主なツール

  • write_note: 指定したパスに新規作成、または上書き。

  • append_note: 既存のメモの末尾に追記。

これで、AIが「あ、この情報は重要だな」と思ったら、自動的にナレッジベースを更新できるようになった。


実際のコード(抜粋)

JavaScript

-----------------------------------------------

import { Server } from "@modelcontextprotocol/sdk/server/index.js";

import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";

import * as fs from "fs/promises";

import * as path from "path";

import { glob } from "glob";


const VAULT_PATH = path.resolve(process.env.OBSIDIAN_VAULT_PATH || "");

const OMNISEARCH_HOST = process.env.OMNISEARCH_HOST || "http://localhost:51361";


const server = new Server(

    { name: "obsidian-local-server", version: "1.4.1" },

    { capabilities: { tools: {} } }

);


server.setRequestHandler(ListToolsRequestSchema, async () => ({

    tools: [

        {

            name: "search_vault",

            description: "ファイル名と中身を検索します。",

            inputSchema: {

                type: "object",

                properties: { query: { type: "string" } },

                required: ["query"],

            },

        },

        {

            name: "read_note",

            description: "指定された相対パスのファイルを読み込みます。",

            inputSchema: {

                type: "object",

                properties: { relativePath: { type: "string" } },

                required: ["relativePath"],

            },

        },

        {

            name: "write_note",

            description: "指定された相対パスにファイルを作成、または上書き保存します。",

            inputSchema: {

                type: "object",

                properties: { 

                    relativePath: { type: "string", description: "例: tech/k8s.md" },

                    content: { type: "string", description: "書き込む内容" }

                },

                required: ["relativePath", "content"],

            },

        },

        {

            name: "append_note",

            description: "指定された相対パスのファイルの末尾に追記します。",

            inputSchema: {

                type: "object",

                properties: { 

                    relativePath: { type: "string" },

                    content: { type: "string", description: "追記する内容" }

                },

                required: ["relativePath", "content"],

            },

        }

    ],

}));


server.setRequestHandler(CallToolRequestSchema, async (request) => {

    const { name, arguments: args } = request.params;


    // ヘルパー: 安全なパス解決

    const getSafePath = (rel) => {

        const fullPath = path.join(VAULT_PATH, rel);

        if (!fullPath.startsWith(VAULT_PATH)) throw new Error("Access denied: Outside of Vault");

        return fullPath;

    };


    if (name === "search_vault") {

        try {

            const q = (args?.query || "").replace(/[_-]/g, ' ');

            const fileNameMatches = await glob(`**/*${q}*.md`, { cwd: VAULT_PATH, nocase: true });

            const url = `${OMNISEARCH_HOST}/search?q=${encodeURIComponent(q)}&limit=10`;

            const response = await fetch(url).catch(() => null);

            const contentMatches = response?.ok ? await response.json() : [];


            let report = `[System Log] BasePath: ${VAULT_PATH}\n[Found]: ${fileNameMatches.length}件\n`;

            const results = [

                ...fileNameMatches.map(f => `Path: ${f} (Filename Match)`),

                ...contentMatches.map(c => `Path: ${c.path}\nExcerpt: ${c.excerpt}`)

            ].join("\n---\n");

            return { content: [{ type: "text", text: report + (results || "No results found.") }] };

        } catch (e) { return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true }; }

    }


    if (name === "read_note") {

        try {

            const targetPath = getSafePath(args.relativePath);

            const content = await fs.readFile(targetPath, "utf-8");

            return { content: [{ type: "text", text: content }] };

        } catch (e) { return { content: [{ type: "text", text: `Read Error: ${e.message}` }], isError: true }; }

    }


    if (name === "write_note") {

        try {

            const targetPath = getSafePath(args.relativePath);

            await fs.mkdir(path.dirname(targetPath), { recursive: true });

            await fs.writeFile(targetPath, args.content, "utf-8");

            return { content: [{ type: "text", text: `Successfully wrote to ${args.relativePath}` }] };

        } catch (e) { return { content: [{ type: "text", text: `Write Error: ${e.message}` }], isError: true }; }

    }


    if (name === "append_note") {

        try {

            const targetPath = getSafePath(args.relativePath);

            await fs.appendFile(targetPath, "\n" + args.content, "utf-8");

            return { content: [{ type: "text", text: `Successfully appended to ${args.relativePath}` }] };

        } catch (e) { return { content: [{ type: "text", text: `Append Error: ${e.message}` }], isError: true }; }

    }


    throw new Error("Tool not found");

});


const transport = new StdioServerTransport();

await server.connect(transport);





LM StudioのMCPが遅い・嘘をつく?インフラ屋がコード1つで爆速化した話

 

1. 課題:ローカルLLM+MCPが「爆熱」で「5分待ち」

LM StudioのMCP経由でObsidianを読み込ませると、以下の問題が発生しました。

  • レイテンシ地獄: 回答まで5分以上かかる。

  • ハードウェア負荷: M4 MaxのGPUが唸りを上げ、本体が熱を持つ。

  • ハルシネーション: 検索が空振ると、AIが脳内の一般常識で「嘘のファイル構成」を捏造し始める。

2. 原因:インデックスなしの「フルスキャン」

原因は、MCPサーバーが質問のたびに数千のファイルをスキャンし、膨大なテキストをLLMのコンテキストに流し込んでいたこと。DBでいう「インデックスなしのフルテーブルスキャン」を毎秒回しているような状態でした。

3. 解決策:検索と推論の「疎結合」化

「AIに全部読ませて探させる」のをやめ、**「インデックス(Omnisearch)と軽量なglobで絞り込んでから渡す」**構造にインフラを再構築しました。


4. 最終版:爆速・低負荷 Obsidian MCP Server

このコードは、ファイル名の部分一致(glob)と中身の全文検索(Omnisearch API)をハイブリッドで実行し、LLMに渡す情報を最小限に絞り込みます。

JavaScript
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import * as fs from "fs/promises";
import * as path from "path";
import { glob } from "glob";

// 環境変数からパスとOmnisearchのURLを取得
const VAULT_PATH = path.resolve(process.env.OBSIDIAN_VAULT_PATH || "");
const OMNISEARCH_HOST = process.env.OMNISEARCH_HOST || "http://localhost:51361";

const server = new Server(
    { name: "obsidian-local-server", version: "1.4.0" },
    { capabilities: { tools: {} } }
);

// 1. ツールの定義:AIに「全件リスト」を見せず、検索ツールだけを渡すのがコツ
server.setRequestHandler(ListToolsRequestSchema, async () => ({
    tools: [
        {
            name: "search_vault",
            description: "ファイル名と中身を両方検索し、関連しそうな内容をピックアップします。",
            inputSchema: {
                type: "object",
                properties: { query: { type: "string", description: "検索キーワード" } },
                required: ["query"],
            },
        },
        {
            name: "read_note",
            description: "指定されたパスのファイルを読み込みます。",
            inputSchema: {
                type: "object",
                properties: { relativePath: { type: "string" } },
                required: ["relativePath"],
            },
        }
    ],
}));

// 2. ツールの実装
server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;

    if (name === "search_vault") {
        try {
            // クエリの正規化(アンダースコア等をスペースに変換してヒット率を上げる)
            const q = (args?.query || "").replace(/[_-]/g, ' ');
            
            // A. ファイル名検索 (大文字小文字無視、一瞬で終わる)
            const fileNameMatches = await glob(`**/*${q}*.md`, {
                cwd: VAULT_PATH,
                nocase: true,
                absolute: false
            });

            // B. Omnisearch API による中身の全文検索
            const url = `${OMNISEARCH_HOST}/search?q=${encodeURIComponent(q)}&limit=10`;
            const response = await fetch(url).catch(() => null);
            const contentMatches = response?.ok ? await response.json() : [];

            // デバッグログをAIに返し、嘘(ハルシネーション)を防止する
            let report = `[System Log] BasePath: ${VAULT_PATH}\n`;
            report += `[Found Files]: ${fileNameMatches.length}件\n`;
            
            const results = [
                ...fileNameMatches.map(f => `Path: ${f} (Filename Match)`),
                ...contentMatches.map(c => `Path: ${c.path}\nExcerpt: ${c.excerpt}`)
            ].join("\n---\n");

            return { content: [{ type: "text", text: report + (results || "No results found.") }] };
        } catch (e) {
            return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
        }
    }

    if (name === "read_note") {
        const targetPath = path.join(VAULT_PATH, args?.relativePath);
        try {
            const content = await fs.readFile(targetPath, "utf-8");
            return { content: [{ type: "text", text: content }] };
        } catch (e) {
            return { content: [{ type: "text", text: `Read Error: ${e.message}` }], isError: true };
        }
    }
    throw new Error("Tool not found");
});

const transport = new StdioServerTransport();
await server.connect(transport);

5. 導入後の効果:5分が5秒に

  • 高速レスポンス: LLMに渡すテキスト量が激減したため、回答開始まで数秒に短縮。

  • 正確性の向上: 自分のObsidianにある「本物のメモ」に基づいた回答が返るようになった。

  • 省エネ: M4 Maxが唸ることもなく、静かに、しかし鋭く回答を生成。

2026年4月12日日曜日

【ブログ追記セクション】自作MCPサーバーによる「AIとObsidian」の接続

特に、AI とローカル環境を繋ぐブリッジとして MCP (Model Context Protocol) サーバーを自作した過程を記録します。

1. 構築したアーキテクチャ

単に Obsidian を使うだけでなく、AI(LLM)がローカルのファイルを自在に検索・閲覧できる「外部脳」としての構成を目指しました。

  • 全文検索エンジン: Omnisearch (Obsidian Plugin)

  • テキスト抽出: Text Extractor (OCR対応)

  • AI ブリッジ: 自作 MCP サーバー (Node.js / TypeScript)

2. 【核心】自作 MCP サーバーの実装

AI が Obsidian の保管庫(Vault)にアクセスできるよう、専用の MCP サーバーを実装しました。特に、Excel や PDF の解析精度を高めるために Omnisearch API との連携に注力しています。

MCP サーバーのソースコード (index.mjs)

----------------------------------
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import * as fs from "fs/promises";
import * as path from "path";
import { glob } from "glob";

const VAULT_PATH = process.env.OBSIDIAN_VAULT_PATH;
const OMNISEARCH_HOST = process.env.OMNISEARCH_HOST || "http://localhost:51361";

const server = new Server(
    { name: "obsidian-local-server", version: "1.0.0" },
    { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
    tools: [
        {
            name: "list_notes",
            description: "Vault内のノート一覧を取得します(.mdファイルのみ)",
            inputSchema: { type: "object", properties: {} },
        },
        {
            name: "read_note",
            description: "指定されたパスのノート内容を読み込みます",
            inputSchema: {
                type: "object",
                properties: { relativePath: { type: "string" } },
                required: ["relativePath"],
            },
        },
        {
            name: "search_vault",
            description: "Omnisearchを使用してPDFやOfficeファイルを含む保管庫全体を検索し、内容の抜粋を返します",
            inputSchema: {
                type: "object",
                properties: { query: { type: "string" } },
                required: ["query"],
            },
        }
    ],
}));

// ツール実行時のロジック(search_vault が今回の肝)
server.setRequestHandler(CallToolRequestSchema, async (request) => {
    const { name, arguments: args } = request.params;

    switch (name) {
        case "search_vault": {
            try {
                // limitを100に増やし、AIが一度に多くの抜粋(Excerpt)を受け取れるように最適化
                const url = `${OMNISEARCH_HOST}/search?q=${encodeURIComponent(args?.query)}&limit=100`;
                const response = await fetch(url);
                const data = await response.json();

                // ファイルパスと抜粋を整形してAIに渡す
                const results = data.map((res) =>
                    `File: ${res.path}\nContent_Found: ${res.excerpt}\n---`
                ).join("\n");

                return {
                    content: [{ type: "text", text: results || "該当情報なし" }]
                };
            } catch (error) {
                return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
            }
        }
        // ... 他のツール(list_notes, read_note)の実装
    }
});

const transport = new StdioServerTransport();
await server.connect(transport);
-----------------

こだわりのチューニングポイント

  • limit=100 の設定: デフォルトの検索数では、Excel 内の複数のシートに散らばった情報を拾いきれないことがあります。リミットを広げることで、AI がコンテキストを一度に把握しやすくしました。

  • 抜粋(Excerpt)の整形File: path と Content_Found を明示的に分けることで、AI が「どのファイルのどの部分を読んでいるのか」を誤認しないようにしています。

3. 遭遇したトラブルと解決策:Excel 解析の壁

構築中、Excel ファイルが物理的に存在(ls で確認済み)するのに、AI が「0 results」と答える事象に遭遇しました。

原因と対策:

  1. インデックスの未更新: Omnisearch と Text Extractor の設定変更後、キャッシュクリア(Clear cache data)と Obsidian の再起動が必要でした。

  2. バイナリ解析のラグ: Excel ファイルは PDF 以上にパースに時間がかかる場合があるため、バックグラウンドでのインデックス完了を待つ必要がありました。

4. 導入後の効果

開通後は、AI に対して以下のような自然な指示が可能になりました。

「K8S のまとめ Excel の内容から、Docker ネットワークとの関連性を整理して」

AI は自作の search_vault ツールを叩き、Excel 内の「k8sまとめ」「Dockerまとめ」といった複数シートの構造まで正確に把握して回答を生成します。


ObsidianでExcelの中身まで爆速検索!インフラエンジニアのためのドキュメント管理術

 エンジニアなら誰しも、k8sのパラメータシートやIP管理表など、どうしてもExcelで管理せざるを得ないドキュメントを抱えているものです。

今回は、最強のナレッジベースツール「Obsidian」を使って、**「Excelファイルを認識させ、さらにその中身(セル内のテキスト)まで検索対象にする」**ための設定手順をまとめます。


1. 課題:ObsidianはデフォルトでExcelを無視する

Obsidianは基本的にMarkdownファイルを扱うツールのため、そのままでは.xlsxファイルはサイドバーに表示すらされません。まずはこれを「ファイル」として認識させるところからスタートします。

設定手順

  1. 設定 (Settings) > ファイルとリンク (Files & Links) を開く。

  2. 「すべてのファイル拡張子を認識 (Detect all file extensions)」 を ON にする。

これで、Excelファイルがファイルエクスプローラーに現れ、クリック一つでExcelアプリを立ち上げられるようになります。


2. 検索の強化:Omnisearch × Text Extractor の導入

ファイルが見えるようになっても、そのままでは「ファイル名」しか検索にヒットしません。「あの設定、どのExcelのどのシートに書いたっけ?」を解決するために、以下の2つのプラグインを導入します。

  • Omnisearch: 標準検索より強力な検索プラグイン。

  • Text Extractor: PDFや画像、Officeファイルからテキストを抽出するエンジン。


3. Excelの中身をインデックス化する設定

ここが一番のキモです。Omnisearchの設定を変更し、Excel(Officeファイル)の中身を読み取れるようにします。

設定手順

  1. Omnisearchの設定 を開く。

  2. 「Documents content indexing」 を ON に切り替える。

    • これにより、.docx や .xlsx がインデックス対象になります。

  3. キャッシュのクリアと再構築

    • 設定画面の最下部にある 「Clear cache」 を実行し、Obsidianを再起動します。


4. 実行結果:k8sまとめシートが「76箇所」ヒット!

設定完了後、Omnisearch(Cmd + Option + O)でキーワードを入力してみます。

例えば「k8s」と検索すると、ファイル名だけでなく、Excel内のシート名やセルに書き込まれた概要、詳細テキストまでがプレビュー付きで表示されるようになります。

画像のように「76 matches」と表示されれば成功です。検索結果から直接該当のファイルへアクセスできるため、ドキュメントを探す時間がゼロになります。


5. まとめ:Obsidianをドキュメントの「ハブ」にする

インフラエンジニアの業務では、構成図(PNG)、設計書(PDF)、パラメータ表(Excel)とフォーマットが散らばりがちです。

今回の設定を行うことで、「形式を問わず、とりあえずObsidianで検索すれば答えが見つかる」 という環境が構築できます。

  • テキスト情報の管理: Markdown

  • バイナリデータの検索: Omnisearch + Text Extractor

この組み合わせで、ナレッジ管理を一段上のレベルへ引き上げてみてください。

Obsidian × AI で Excel 解析を自動化した話:インフラエンジニアのデバッグ記録

 業務効率化を進める中で、ObsidianとAIを連携させてドキュメント解析を行える環境を構築しました。PDFの解析はスムーズにいきましたが、エクセル(xlsx)の解析で少しハマったので、その解決までのプロセスを記録します。

1. 構築した環境

  • Hardware: Mac Studio M4 Max (36GB RAM)

  • Software: Obsidian v1.28.2

  • Plugins:

    • Omnisearch: ローカルファイルの全文検索エンジン

    • Text Extractor: バイナリファイル(PDF/Excel)からテキストを抽出するエンジン

  • AI Bridgemcp/obsidian-local (Model Context Protocol経由でAIがVaultにアクセス)

2. 発生した問題

特定のディレクトリ(doc_test)に配置したエクセルファイルが、物理的には存在するのにAI(Omnisearch)から見えず、検索結果が「0 results」になるという「サイレントな失敗」が発生しました。

3. デバッグと解決のステップ

ステップ1:インデックス対象の有効化

まず、Omnisearchがエクセルを読み取るように設定を確認しました。

  • Documents content indexing をオンに設定。これにより、.docx や .xlsx がスキャン対象に含まれます。

ステップ2:キャッシュのクリアと再起動

設定変更を反映させるため、以下の「インフラの再デプロイ」に近い作業を実施しました。

  • 設定画面の Danger Zone にある Clear cache data を実行。

  • その後、Obsidianアプリ自体を再起動。これによりインデックスDBがゼロから再構築されます。

ステップ3:OCRと言語設定の最適化

Text Extractor側で日本語を正しく認識させるため、言語設定に jpn を追加し、システムのOCRを利用するように設定しました。

4. 最終的な成果

疎通確認の結果、AIがエクセルの中身を完璧に把握できるようになりました。

  • 複数シートの認識: エクセル内の「k8sまとめ」「Dockerまとめ」「Linux_DBまとめ」といった個別のタブ構造をAIが自動で識別しました。

  • 論理構造の解析: 単なるテキスト抽出にとどまらず、シート間の関連性や詳細な項目(リソース種類、概要、自己検証結果など)をリストアップして回答できるようになりました。

5. エンジニアとしての振り返り

バイナリファイル(Excel)をAIに読ませる場合、プラグインによるテキスト抽出の「インデックス更新」がボトルネックになりやすいことが分かりました。

一度開通してしまえば、**「エクセルをフォルダに放り込むだけでAIが仕様を理解する」**という、インフラエンジニアにとって最強のドキュメント管理環境が手に入ります。今後はAWSの請求書や構築手順書の解析にも活用していく予定です。

2026年4月5日日曜日

M4 MaxのRoo Codeから自宅ラズパイk8sクラスターをMCPで「完落ち」させるまで③

完成後、MCP経由で自作のraspberry piにリソースの作成や削除などの実験をしてみました。


現時点で、思うこととして、障害時にログの分析や作成や削除に関する助言として受け入れるには頼りになるなって思いました。


[頼りにならない動作について]

削除についてですが、namespaceをargocdとargo-cdと2つ作成した時があって

不要なargo-cdの削除をお願いしたのに、argocdを消されてしまい困ったことをされました。。 

所詮LLMなので慎重に確認するとかの発想がないので、運用保守観点だと、

少なくても修正、削除については、人が行った方が無難かもしれませんね。

または、高性能なLLMだと、この辺についてもクリアーするんでしょうかね。。


LLM(gpt-oss-20b)のバカたれ。。



【AI×GitOps】Docker Composeから始めるRaspberry Pi 5 Kubernetesクラスタへの自動デプロイ設計

現在構築を進めている動画管理システム開発において、いきなりKubernetes(K8s)に展開するのではなく、 Docker Composeを中間地点に挟んで段階的にK8s+Argo CD(GitOps)へ移行するアーキテクチャ設計 をまとめました。 Bionic(AIアシスタン...