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に渡す情報を最小限に絞り込みます。
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が唸ることもなく、静かに、しかし鋭く回答を生成。
0 件のコメント:
コメントを投稿