Obsidian × MCP × Blogger — 無頭發佈組合技實錄
本文由 AI(Shadow Monarch)協助撰寫,內容基於實際程式碼修改與端到端測試結果。
當 Obsidian 插件不跟你對話時,那就自己開一條 MCP 專屬通道。
如果你需要在 Obsidian 外(透過 AI Agent 或自動化腳本)觸發插件功能,很可能會撞到一個尷尬的牆:Obsidian 的指令執行 API 只傳 commandId,不帶任何參數,也無法處理對話框(dialog)。
這篇文章記錄了我們如何繞過這個限制,實作一套從 MCP → REST API → Obsidian 插件 → Blogger 的完整發佈管線。
背景
obsidian-blogger 是一個能將 Obsidian 筆記直接發佈到 Google Blogger 的插件。它原本的發佈指令 google-blogger2:defaultPublish 是這樣註冊的:
// 原始實作 — 使用 editorCallback
this.addCommand({
id: 'defaultPublish',
name: 'Publish current note to Blogger',
editorCallback: (_editor, _view) => {
doClientPublish(this);
},
});
當你在 Obsidian 內按快捷鍵或點指令面板時,這完全沒問題。但當我們想透過 MCP(Model Context Protocol) 來觸發它時,問題出現了。
問題:command_execute 的先天限制
Obsidian Local REST API 提供了一個 command_execute 工具,可以透過程式執行任何 Obsidian 指令。但它的簽章長這樣:
command_execute({ commandId: string })
只有 commandId,沒有第二個參數。
這意味著:
- 無法傳遞資料 — 不能指定要發佈哪個檔案,插件只能自己抓 active file
- 無法處理對話框 — 如果指令彈出 modal 或 prompt,Agent 無法回應
- 無法處理
checkCallback — 需要條件判斷才能執行的指令會直接失敗
實測結果
測試 google-blogger2:defaultPublish 時,雖然 API 回傳了 { "message": "OK" },但實際上什麼都沒發生。沒有發佈,沒有錯誤訊息,什麼都沒有。
偵查:到底差在哪裡?
比對 Obsidian 的 addCommand API 文件後,發現 editorCallback 和 callback 在執行上有微妙的差異:
| 回呼類型 |
何時可用 |
簽章 |
適用場景 |
callback |
任何時候 |
() => void |
不需編輯器也可執行 |
editorCallback |
僅有 Markdown 編輯器焦點時 |
(editor: Editor, view: MarkdownView) => void |
需要操作編輯器內容 |
checkCallback |
條件滿足時 |
(checking: boolean) => boolean |
需要動態判斷是否可用 |
關鍵洞察:command_execute 在觸發指令時,並不會自動給予編輯器焦點。因此使用 editorCallback 註冊的指令,在透過 REST API 執行時,Obsidian 內部可能無法正確將它視為「可執行」狀態。
解法很簡單:新增一個專門給 MCP 用的指令,改用 callback 註冊。
解法:開一條 MCP 專屬通道
在 src/main.ts 中加入一個新的指令:
this.addCommand({
id: 'mcpPublish',
name: 'Publish via MCP (no dialogs)',
callback: () => {
doClientPublish(this);
},
});
關鍵差異只有一行:callback: () => { ... } 取代了 editorCallback: (_editor, _view) => { ... }。
由於 doClientPublish 內部已經是透過 plugin.app.workspace.getActiveFile() 取得當前檔案,而不是靠 editorCallback 的參數,所以兩者的執行邏輯完全一樣 — 差別只在 Obsidian 願不願意讓它在無編輯器焦點的狀態下執行。
重構後生效
重新建置外掛:
pnpm run build
然後在 Obsidian 中重新載入插件。新的指令 google-blogger2:mcpPublish 就會出現在指令清單中。
組合技:端到端流程
AI Agent / MCP Client
│
├─ command_execute("google-blogger2:mcpPublish")
│
▼
Obsidian Local REST API (HTTP port 27123)
│
├─ 接收 commandId
│
▼
Obsidian 插件系統
│
├─ 找到 blogger plugin 的 mcpPublish 指令
│
▼
callback() → doClientPublish(plugin)
│
├─ getActiveFile() → 取得目前開啟的檔案
│
▼
publishPost(file)
│
├─ 解析 frontmatter、轉換 Markdown
│
▼
Blogger API (Google)
│
├─ POST /blogger/v3/blogs/{blogId}/posts
│
▼
結果回寫至 frontmatter
├─ postId, url, blogger.published/updated
├─ tags: [obsidian-blogger/post, obsidian-blogger/draft]
│
▼
✅ 發佈完成
實測驗證
執行 command_execute("google-blogger2:mcpPublish") 後,檔案的 frontmatter 自動更新為:
profileName: Just E!
postId: "7373519934638137794"
url: https://elf-here-us.blogspot.com/
blogger:
published: 2026-05-21T02:25:41+08:00
updated: 2026-05-21T02:25:41+08:00
tags:
- obsidian-blogger/post
- obsidian-blogger/draft
所有欄位都正確寫入,表示發佈流程完全成功。
這個組合技的適用範圍
這套「加一條 MCP 專屬指令」的模式,不只適用於 obsidian-blogger。任何 Obsidian 插件只要遇到以下情境,都可以套用:
| 情境 |
說明 |
| 指令需要對話框 |
插件彈出 modal 要求使用者輸入 → 拆成兩個指令:一個是原本的 UI 版,另一個是 MCP 版跳過對話框,使用預設值或 active file 的內容 |
指令使用 editorCallback |
如同本文案例,改成 callback 讓 REST API 能正常觸發 |
指令有 checkCallback |
檢查條件無法滿足 → 新增無條件執行的 callback 版本 |
| 需要傳遞資料 |
command_execute 無法帶參數 → 改用 vault_patch 先在 frontmatter 寫入設定,再觸發指令讀取 |
重要原則
- 保留原始指令 — 不要刪除原本的
defaultPublish,因為使用者在 Obsidian UI 內還是需要它
- 命名清楚 — 新的 MCP 指令名稱要標明用途,例如
mcpPublish、mcpPublishLive 等
- 跳過對話框 — MCP 版的指令不應彈出任何 UI,所有的決策應該基於 frontmatter 或預設值
已知限制
- 一次只能發佈一個檔案 — 因為依賴
getActiveFile(),無法批次發佈
- 無法選擇目標 Blogger 部落格 — 使用上次設定的 profile,無法臨時切換
- 如果沒有開啟任何檔案 —
getActiveFile() 回傳 null,會拋出錯誤
這些限制在一般使用場景下問題不大,但如果需要更進階的批次發佈或動態設定,就需要更深入的改造了。
總結
這次探索的核心收穫其實很簡單:
command_execute 不能帶參數、不能處理對話框、也不能保證編輯器焦點。但我們可以用一條 callback 指令,繞過所有這些限制。
如果你正在開發 Obsidian 插件,想要讓它可以被外部工具(AI Agent、自動化腳本、快捷鍵啟動器)呼叫,只要在 onload 裡面多註冊一個 callback 版本的指令就好 — 不需要動到原本的架構,也不需要引入額外的相依套件。
這就是 Obsidian × MCP 組合技的精髓:看懂 API 的限制,找到最小的繞道路徑。
相關連結