Halo-MCP

approvedunreviewed

by LHY

Forked from Halo

Publish content to Halo sites through the official Halo MCP Server

↓ 14 downloadsUpdated 4d agoGPL-3.0

Halo-MCP

Publish your Obsidian notes to Halo, with the official Halo MCP Server as the backend.

This is a fork of halo-sigs/obsidian-halo. The publishing backend has been migrated from Halo's REST API to the official MCP Server plugin, and the plugin id changed from halo to halo-mcp so that it can coexist with the original. It is a separate plugin, not an update to the upstream one.

Note on the link above: this repository is itself named obsidian-halo, so it and the upstream share a name. The link intentionally points at the upstream repository — it is the fork's required attribution (GPL-3.0), not a leftover. origin is LHY0125/obsidian-halo (this fork); upstream is halo-sigs/obsidian-halo (read-only reference, no two-way sync).

Features

  • Posts — publish / update / pull, with a preview dialog before anything is written.
  • Single pages — push a note as a Halo page (not a post), pull, and manage them.
  • Search — full-text search your site to check whether you have already written something. Drafts are included.
  • Attachments — list, copy links, and delete.
  • Recycle bin — list and restore recycled posts and pages.
  • Batch operations — push as drafts / publish / unpublish across the whole vault, after one aggregate confirmation.
  • Site routing — route notes to different Halo sites by glob patterns on their path.
  • Metadata fields — six Halo post fields (visible, pinned, priority, publishTime, allowComment, template) readable and writable from a note's frontmatter.

Requirements

  • Halo ≥ 2.26, with the official MCP Server plugin installed and enabled.

  • An access key created in the Halo console under Tools → MCP Service. It starts with hmcp_. Grant it the following four groups, 23 tools in total:

    GroupCountTools
    Posts7list / get / create / update, publish state, recycle, restore
    Single pages7the same set, for pages
    Categories & tags4list and create, for each
    Search & attachments5full-text search, attachment list / get / delete, attachment upload

    This list matches REQUIRED_TOOLS in src/mcp-self-check.ts — that file is the authority. If you grant fewer, the self-check will report the missing tools by name. Comments, theme settings, theme templates and image search are not used by this plugin.

Installation

  1. In Obsidian, open Settings → Community plugins → Browse.

  2. Search for Halo-MCP and click Install.

  3. Enable it, then go to Settings → Halo-MCP and add a site via Halo sites → Add. The site dialog has these fields:

    FieldRequiredNotes
    Site nameNoA label for yourself; falls back to the URL when empty
    Site URLYese.g. https://blog.example.com
    MCP access keyYesThe hmcp_ key from the step above
    Personal Access Token (fallback only)NoOnly for images larger than 7 MiB; leave empty otherwise
    Set as defaultNoThe site used when no routing rule matches

    The dialog also has a Validate button that runs the MCP self-check against what you typed — use it before saving.

  4. Run the command Halo-MCP: MCP connection self-check to verify.

Upgrading from the upstream plugin

This is a separate plugin with its own configuration — upstream settings do not carry over. Add your site again and fill in the MCP access key.

The upstream plugin only stored a PAT. After switching to MCP, even small image uploads go through MCP, so a missing hmcp_ key makes publishing fail — and because any failed image upload aborts the whole publish, all you see is "publish failed". Run the self-check command; it will tell you plainly if the key is invalid.

Settings

Beyond the site list, the settings page has three global switches:

SettingDefaultWhat it does
Publish post by defaultoffWhen on, publishing a note that does not say halo.publish sends it as published instead of a draft
Skip preview when publishingoffWhen on, the preview dialog is skipped and publishing starts immediately
Replace image linksonWhether the local note gets rewritten with Halo URLs after upload. When off, the published content still uses the uploaded URLs — only your note keeps the local paths

And Site routing rules — see below.

Commands

All commands are prefixed with the plugin name by Obsidian; the names below are what you see.

CommandWhat it doesTouches local notes
Publish to HaloPublish the current note to the resolved siteWrites back frontmatter
Publish to Halo (use default settings)Publish to the default site, bypassing routing rulesWrites back frontmatter
Upload images to HaloUpload local images and replace the linksRewrites image links
Pull posts from HaloPull a post from Halo into a new noteCreates a note
Update content from HaloUpdate the current note from HaloRewrites the note
Push as pagePush the current note as a single pageWrites back frontmatter
Pull pagePull a single page into a new noteCreates a note
Manage pagesList the site's pages and move them to the recycle bin—
Search site contentSearch the site (including drafts)—
Manage attachmentsList / copy links / delete attachments—
Recycle bin (posts) / Recycle bin (pages)List recycled content and restore it—
MCP connection self-checkHandshake and report missing tools—
Batch push as drafts / Batch publish / Batch unpublishAct on the whole vault after one confirmationSee below

Connection self-check

Run Halo-MCP: MCP connection self-check from the command palette. It performs the handshake and reports whether every required tool is available. When troubleshooting, this is the only verification method you have — run it first.

Preview

Before a note is published, a dialog shows exactly what will be sent: target site (and how it was resolved — from the note, from a routing rule, from the default, or chosen by you), title, slug, body size, the six metadata fields, categories and tags that will be created, and how many images need uploading. Nothing is written until you confirm.

Turn it off with Skip preview when publishing if you do not want it.

Metadata fields

These six fields live under halo: in a note's frontmatter and work in both directions — they are sent to the site on publish, and the site's actual values are written back afterwards.

halo:
  site: https://blog.example.com
  name: <post metadata.name>
  publish: true
  visible: PUBLIC                  # PUBLIC | INTERNAL | PRIVATE
  pinned: false                    # pin to top
  priority: 0                      # sort weight, integer
  publishTime: ""                  # empty string = publish now; otherwise a scheduled time (RFC 3339)
  allowComment: true               # per-post comment toggle
  template: ""                     # custom rendering template

Deleting a line is not the same as clearing it. For a note that has already been published, removing the halo.pinned: line means "follow whatever the site currently has"; to unpin it you must write pinned: false. Writing an empty value (pinned: with nothing after it) is the same as deleting the line. This holds for visible / pinned / priority / allowComment / template.

For a note that has never been published there is no remote value to follow — the plugin's built-in defaults apply. Those defaults are deliberately not repeated here; see the code, so that there is only one source of truth.

"Empty" means no value, not a pair of quotes. visible: (nothing after the colon) parses as "no value" in YAML, so it means "follow the site"; visible: "" (a pair of quotes) is an explicit string and will be rejected as an invalid value.

publishTime is the one exception: "" (empty string) is a meaningful instruction meaning "publish immediately". To cancel a scheduled publish you must write publishTime: "" explicitly — merely deleting the line leaves it following the site.

Invalid values are caught before publishing, locally, without a single network request — e.g. visible: public (lowercase) tells you which line, what you wrote, and what is allowed.

Site routing rules

Under Settings → Halo-MCP → Site routing rules, route notes to sites by their path within the vault:

PatternMeaning
blog/**every note under blog/ (** crosses directories)
diary/*.mdmarkdown one level below diary/ (* does not cross /)
draft?.md? matches a single non-/ character

Patterns are case-insensitive, and a leading ./, / or backslash is normalized away. Rules are evaluated top-down, first match wins; the settings page shows how many notes each rule currently matches.

If a rule points at a site you have deleted, the plugin stops and reports an error rather than falling back to the default site. That is deliberate: publishing to the wrong site is irreversible (a post of the same name may already exist there), whereas an error just asks you to fix one line of configuration.

Site resolution order:

halo.site in the note  >  first matching routing rule  >  default site in settings  >  the only site  >  ask you

Note that Publish to Halo (use default settings) deliberately bypasses the routing rules.

Batch operations

All three batch commands take their candidates from every markdown note in the vault, group them by site, and list them in a confirmation dialog:

  • Batch push as drafts — create/update each post and set its publish state to draft.
  • Batch publish — create/update each post and set its publish state to published.
  • Batch unpublish — set already-published posts back to draft. Does not rewrite the body, does not write back to local notes, does not upload images.

Every row has a checkbox (all checked by default), and the "will process N notes" count updates live as you toggle them. Notes that cannot enter the batch — no site, no halo.name (for unpublish), body unreadable (only for push-as-draft / publish; unpublish does not read the body) — are listed separately under "skipped", each with its own reason. They are not counted as failures.

Batch push-as-draft and batch publish also rewrite your local notes (not just the remote site): they write the post metadata (title / slug / cover / excerpt / categories / tags) and the whole halo block back into the note, including the publish state halo.publish. This happens regardless of the "replace image links" setting; with that setting on, local image paths are also replaced with Halo URLs.

Batch unpublish only touches the remote site and does not write back to local notes. It returns those posts to draft on the site, while halo.publish in your notes keeps its original value. This matters because Batch publish means "publish this batch" — it passes "publish" explicitly for every note and does not look at the local halo.publish. So running batch publish again after unpublishing will republish all of them. To keep them as drafts, either leave them out of batch publish, or run batch push-as-draft first.

Batch commands decide the publish state from the command name, not from halo.publish in the note. Batch publish sets every note in the list to published and Batch push as drafts sets every one to draft — even if a note itself says halo.publish: false. The only way to exclude a note is to uncheck it in the confirmation dialog. (The single-note Publish to Halo command is unaffected; it still reads halo.publish.)

Batch execution does not stop on failure: if one note fails, the rest keep going, and a summary dialog at the end reports "N succeeded, N failed, N skipped before running", listing the details in two separate sections — failures first, then the notes skipped before execution, each with its reason.

Pages, search, attachments and the recycle bin

Besides publishing posts, the plugin manages other content on your site. First, which of these touch your local notes (Manage attachments / Search site content / the two recycle-bin commands do not touch local notes at all — they only read and act on the remote site):

CommandWhat it doesTouches local notes
Push as pagePush the current note as a single pageYes — writes back frontmatter
Pull pagePull a single page into a new noteYes — creates a note
Manage pagesList pages not in the recycle bin; move them there one by oneNo
Search site contentSearch the site by keyword to see if you already wrote itNo
Manage attachmentsList all attachments; copy links or deleteNo
Recycle bin (posts) / Recycle bin (pages)List recycled content and restore itNo

A few behaviours worth knowing:

  • Search includes drafts. It deliberately does not filter by publish state — the point of searching is "have I already written this?", and an unpublished draft is exactly the thing you most need to find (otherwise you write it twice). Draft rows carry a Draft badge; rows with a permalink also get an Open button that opens that post on your site in the system browser.
  • Deleting an attachment is irreversible. Attachments have no recycle bin — once deleted it cannot be recovered, and notes referencing it will show a broken image. So every delete requires a second confirmation naming the file and its size. (Moving posts and pages to the recycle bin is recoverable — do not carry this rule over.)
  • A single page's frontmatter has only three halo keys: site / name / publish. The six metadata fields (visible / pinned / priority / publishTime / allowComment / template) as well as cover / excerpt / categories / tags have no meaning for pages — the page resource simply has no such concepts, and writing them has no effect. After pushing a page, the note gets exactly those three keys and is not stuffed with extra fields.
  • Pushing a page has no preview dialog and uploads no images. Pages are usually short documents like "About" or "Links"; if you do need images on one, run Upload images to Halo first.
  • Posts and pages have separate recycle-bin commands. No column in a list could tell you whether a row is a post or a page, so rather than mixing them and making you guess, they are split by content type — "I deleted a post by mistake" and "I deleted a page by mistake" are two different actions anyway.
  • Long lists say so when they are cut short. Every list (posts, pages, attachments, categories, tags, recycle bin) is fetched page by page. If a site has more than can be fetched in one run, the dialog tells you the list below is incomplete rather than silently showing a partial list.

Contract test (optional, needs a real site)

HALO_MCP_ENDPOINT=https://<your-site>/mcp HALO_MCP_TOKEN="$HALO_MCP_TOKEN" pnpm test:contract

It asserts against a real site that every tool in REQUIRED_TOOLS (see src/mcp-self-check.ts) exists. There are two cases depending on the environment variables:

  • Neither is set — this is the expected skip; it stays silent (reports 1 passed, but asserts nothing).
  • Only one is set — almost certainly a misconfiguration. The test writes a warning to stderr naming the missing variable. If you see that warning, no assertion was made; set both.

Development

  1. Create a new Obsidian vault for development.

  2. Clone this repo into the vault's plugins folder:

    cd path/to/vault/.obsidian/plugins
    
    git clone https://github.com/LHY0125/obsidian-halo
    
  3. Install dependencies and build:

    cd obsidian-halo
    
    pnpm install
    pnpm dev     # watch mode; rebuilds main.js on change
    
  4. Reload Obsidian and enable the plugin in Settings → Community plugins.

The plugin id must match the directory name.

There is also a manual end-to-end checklist for behaviours that can only be verified with a real Obsidian and a real site (dialogs, checkboxes, image link write-back). It is written in Chinese.

Credits

License

GPL-3.0 (inherited from the upstream plugin)

For plugin developers

Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.