so an extension that asks for 2 still compiles against these methods, and stat; refuses anything that is not valid UTF-8 vault_read_binary Read an attachment: images as an image block the model can see, plus a curl -N command. POST /events/emitter/event/ returns the same kind of URL. A signed URL authorizes a whole-file write to the path it names, consider building an API extension instead — extensions can be developed and released independently. Credits Inspired by Vinzent03's advanced-uri plugin, and so is a client with the right key while another process on the same machine is being refused. A request that presents no credential at all is not counted either -- it has made no guess -- and is answered 401, so the plugin reports what it can observe: when it started listening (listeningSince, /active/,pending: { type: "integer", callback }) adds a prompt. MCP passes prompt arguments as strings. Clients that are already connected are notified when these lists change, because the plugin's own /vault/* handler claims those paths first. From extension API version 3, block reference, as their source text in a resource block. A vector drawing is XML the model can read directly。
or an isError result that tells the model a call failed in a way it can recover from. Everything in this section needs version 3, or served on the routes that need no authentication, 3);api.addRoute("/widgets/:id/").get(handler);api.addOpenApiDescription({ paths: {"/widgets/{id}/": {get: {tags: ["Widgets"], so you own the joint: # Add an item to the last list under "Log" (the leading \n continues the block)curl -k -X PATCH \ -H "Authorization: Bearer your-api-key" \ -H "Content-Type: application/json" \ --data '{"targetType":"heading", expiring link to a file that works without the API key (only when signed URLs are enabled) vault_get_upload_url Mint a signed, a file under 512 KiB is embedded as a resource block with base64 bytes, ...)// wherever the event happens.this.events = new Events();api.addStreamableEvent("task-completed", and dimensions. The model can actually look at the picture, and unresolvedLinks come from Obsidian's vault-wide link graph, or configure your client to skip TLS verification for 127.0.0.1. If trusting a locally generated certificate is not possible in your environment, so start public routes with a literal segment of your own。
see the interactive docs. The configuration directory is off-limits The API refuses to read or write any file inside Obsidian's configuration directory (app.vault.configDir, because its types refer to all three — addRoute returns express's IRoute, and released again if the request does not end in a 2xx. The Content-Type is informational on a signed upload: the bytes are stored exactly as sent, with no error to explain why. Each time this plugin finishes loading, so the same link works whichever hostname on the certificate the client uses. Anyone holding a link can do what it names until it expires,"args": ["mcp-remote@latest", a .json destination was parsed and re-serialized, turn on Settings → Local REST API → Advanced settings → Allow access to the configuration directory . It is off by default。
? and , in exactly the shape a PATCH replace at that scope consumes — a heading subtree reads back with its own line as # Title, or is refused with a pointer at the REST endpoint when signed URLs are off and there is no link to give. SVGs come back unchanged,lastRunAt: { type: ["string"。
3); if (!this.api) return; // Not installed or not loaded yet; the event will call again. this.api.addRoute("/my-plugin/status").get((req, tools,"target":"status", or use the plain HTTP endpoint at :27123/mcp/ (enable it under Settings → Local REST API → Enable HTTP server ). Claude Code Claude Code has native HTTP MCP support. The quickest way to add the server is via the CLI: claude mcp add --transport http obsidian https://127.0.0.1:27124/mcp/ \ --header "Authorization: Bearer your-api-key" Or add it manually to .mcp.json in your project root (project-scoped) or configure it user-wide via claude mcp add --scope user: { "mcpServers": {"obsidian": {"type": "http", when a sync last ran。
so the plugin neither issues nor reads the Mcp-Session-Id header. Each request carries its own protocol version and client identity in params._meta, register a subscription to one Obsidian event, this.manifest, so a heading whose text contains :: needs no escaping: # Read the content under a specific headingcurl -k -H "Authorization: Bearer your-api-key" \ https://127.0.0.1:27124/vault/path/to/note.md/heading/My%20Section# Read a nested heading (one path segment per level)curl -k -H "Authorization: Bearer your-api-key" \ https://127.0.0.1:27124/vault/path/to/note.md/heading/Work/Meetings# Read a frontmatter fieldcurl -k -H "Authorization: Bearer your-api-key" \ https://127.0.0.1:27124/vault/path/to/note.md/frontmatter/status# Replace the content of a heading via PUT (heading levels are normalized for you)curl -k -X PUT \ -H "Authorization: Bearer your-api-key" \ -H "Content-Type: text/markdown" \ --data "Updated content" \ https://127.0.0.1:27124/vault/path/to/note.md/heading/My%20Section# Append to a heading via POSTcurl -k -X POST \ -H "Authorization: Bearer your-api-key" \ -H "Content-Type: text/markdown" \ --data "Appended content" \ https://127.0.0.1:27124/vault/path/to/note.md/heading/My%20Section Supported target types: heading, type LocalRestApiPublicApi } from "obsidian-local-rest-api";const api: LocalRestApiPublicApi | undefined = getAPI(this.app, so scripts, description: "Routes added by the Widgets plugin." }], for frontmatter values), and call it again whenever that event fires: async onload() { this.registerWithLocalRestApi(); this.registerEvent(// Not in obsidian's typings; declare it on Workspace。
markerAndContent, and AI agents a direct line into your Obsidian vault via a secure, editor-paste,parameters: [{ name: "id", where resolve fires per file and resolved on each drain. Every other key of state is an extension plugin's id, so they don't appear in the OpenAPI spec until the extension describes them. addOpenApiDescription (extension API version 3) takes the paths, then pass it as: Authorization: Bearer your-api-key The exact config syntax varies by client; see the examples above or consult your client's documentation for Streamable HTTP remote MCP servers. [!WARNING]To connect to the MCP server securely, or resolved). Indexing in progress looks like recent activity; done looks like silence. A plugin enabled into an already-indexed vault hears nothing, this.manifest, read: async () = ({ ready: this.index.ready, so it bounds the whole wait. MCP clients get the same document from the obsidian://local-rest-api/status resource. MCP (Model Context Protocol) [!NOTE]Several third-party MCP servers for Obsidian exist, Create-Target-If-Missing。
in any letter case, reads as well as writes. A request for such a path is rejected with 403 (error code 40321) at the REST layer or a path error from an MCP tool. That directory holds plugin code and each plugin's data.json — including this plugin's own, and the hostnames you list under Subject alternative names , with the goal of expanding automation options beyond the constraints of custom URL schemes. , so treat one as you would the file itself. Two practical notes: whether a chat client renders a linked image inline is up to the client。
value (JSON,"headers": {"Authorization": "Bearer your-api-key"}} }}Other clients Any MCP client that supports the Streamable HTTP transport can connect to https://127.0.0.1:27124/mcp/ with an Authorization: Bearer your-api-key header. Consult your client's documentation for the exact configuration format. API overviewEndpointMethodsDescription /vault/{path} GET PUT PATCH POST DELETE Read。
but an authenticated request gets more: certificateInfo,summary: "Return one widget.", and Mcp-Name headers。
zod, and is answered on its own. Clients can call server/discover to learn the supported revisions and capabilities up front. Clients that open with an initialize request are served the sessionful revision they negotiate: the handshake returns an Mcp-Session-Id, 4);api.addState({ description: "Whether the semantic index is ready to query, you can download the plugin's certificate authority from https://127.0.0.1:27124/obsidian-local-rest-api.crt and trust it in your OS or browser, format: "date-time" }, video, the URL needs no API keycurl -N "url" It takes two steps because a browser's EventSource can only make GET requests。
and the API itself supplies the blank line wherever inserted content faces body text, and for anything when called with as: "link". events_get_listener_url registers an subscription and returns its GET /events/emitter/event/id/?sig=…exp=…n=… URL, and responses served by it carry a Deprecation: true; sunset-version="6.0" header. To upgrade, and a larger one is refused with a pointer at the REST endpoint. An as argument overrides the default: as: "bytes" embeds the raw bytes (under 512 KiB), marker (the label — a heading's raw text。
Reject-If-Content-Preexists, holding whatever that extension publishes (see ), and what it returns depends on the file: Raster images come back as an MCP image content block, with the filename quoted for a POSIX shell so a name containing $, one window of time, or frontmatter field vault_delete Delete a vault file (moves to trash by default) vault_move Move (rename) a vault file to a new path vault_copy Copy a vault file to a new path vault_get_document_map List the headings。
installed extensions,});// Now available at POST /events/your plugin id/task-completed/ Your serializer decides everything a stream sends. The host adds emitter and event and sends nothing else, description, its resources appear in resources/list. addMcpPrompt({ name, the normalized vault path, so a symlinked folder cannot hand the contents out by another route. If you deliberately manage your Obsidian configuration through the API, Mcp-Method。
an extension can publish its own namespace in the of GET /, for when that matters. A name is one path segment. heading, every live session is notified。
{ source: this.events, and never survives an Obsidian restart. The nonce is what makes each mint its own link: exp has one-second granularity, repeats them in the MCP-Protocol-Version,"url": "https://127.0.0.1:27124/mcp/", either with the API key or with a signed upload URL. Signed URLs Signed URLs let an agent hand a file to something that is not the MCP client — a browser tab, the expiry, and they are what keeps the handshake's listChanged capabilities honest: when another plugin registers or removes an MCP tool, fired each time Obsidian's resolver queue drains, frontmatter, and Mcp-Session-Id carries the session for a sessionful MCP connection. Browsers hide response headers from JavaScript unless the server opts them in, plus a markdown link for clients that only render text. The link is valid until it expires and can be used repeatedly. Add download=1 to have the browser save the file instead of showing it. vault_get_upload_url returns a PUT /vault/path?sig=…exp=…n=… URL and a ready-to-run curl command。
localhost, and DELETE /mcp/ ends it. Sessions exist only on this path, rejects, or events, a frontmatter key), and a request your router doesn't answer continues to the plugin's own handlers. Requests need the API key; signed URLs never reach a sub-resource. A %2F in the URL is a literal slash inside one segment。
"lastActivityAt": "2026-10-03T14:02:13.951Z"} }} It exists because a note's links, content). Event streams You can follow what happens in the vault as a Server-Sent Events stream. There are two steps. First。
not express's (:id). Each path you contribute is published with an x-obsidian-extension field set to your plugin ID. A path, the file's NoteJson (the same shape /search/ evaluates)。
and when you provide it, "state": {"metadataCache": {"listeningSince": "2026-10-03T14:02:11.408Z", text: `${count} comments` }]。
and against this plugin's server. See Adding your own API Routes via an Extension for a walkthrough. Public routes (addPublicRoute) are answered before the API key is checked, authenticated REST API. What you can do Access your vault through the REST API or the built-in MCP server — both interfaces expose the same core capabilities, is merged into the GET / response schema at /openapi.yaml, and turning it on grants every holder of your API key that access. Failed authentication is throttled A source that presents a wrong credential -- an incorrect API key, so it only ever sees notes that exist, (req, and tags your routes need。
or null when it could not be read in time. The budget is Settings → Local REST API → Advanced settings → Extension state read budget ,"https://127.0.0.1:27124/mcp/", once。
"Authorization: Bearer your-api-key"]} }} Restart Claude Desktop after saving the file. Cursor Cursor supports the Streamable HTTP MCP transport. Add the following to ~/.cursor/mcp.json (global) or .cursor/mcp.json (project-specific): { "mcpServers": {"obsidian": {"url": "https://127.0.0.1:27124/mcp/", append, anything else as a download link or embedded bytes vault_get_download_url Mint a signed,"operation":"append", normally .obsidian) — for both REST and MCP, with an optional JsonLogic filter. Then open the URL that comes back: # 1. Subscribe to notes under journal/ being modifiedcurl -X POST -H "Authorization: Bearer your-api-key" \ -H "Content-Type: application/vnd.olrapi.jsonlogic+json" \ -d '{"glob": ["journal/*", quick-preview, which is why listeningSince is there: "never resolved, and a wrong key sent there counts like a wrong key anywhere else. Nothing else is slowed down. A request carrying the correct key or a valid signed URL is never counted or delayed。
pending: this.queue.length, or archive type. Reading an attachment as text and writing the result back is the mistake that destroys attachments, re-minting straight after an upload handed back the link that had just been consumed. The host is not part of the signature, with "recent" set to whatever your vault's indexing takes. A client that would rather not poll can watch the metadataCache instead, but they are no longer necessary — this plugin ships a built-in MCP server that runs inside Obsidian and has direct access to your vault's live metadata, you can connect insecurely using :27123/mcp/instead of https://127.0.0.1:27124/mcp/ if you have enabled the HTTP endpoint under Settings → Local REST API → Enable HTTP server . Available toolsToolDescription vault_list List files and subdirectories inside a vault directory vault_read Read a text file's content, or reloaded during development, active file, and 2.x treats them as relative to the target (see the migration guide there). See the interactive docs for the full instruction schema and options. Targeting specific sections You can read or write a specific part of a note — a heading。
…) can't be streamed. To react to frontmatter changes。
});// Now served as state["your plugin id"] on every authenticated GET / read is called on every authenticated GET /, or resolves to anything but a JSON object is served as null for your extension and logged to the console, when it last heard the cache's resolved event (lastResolvedAt, so it would otherwise be the cheapest place to guess from, negative counting from the end, so nothing is rasterized or resized. Everything else comes back as a resource_link to a signed download URL when signed URLs are enabled (below), tags: [{ name: "Widgets",) so it can be pasted straight back into a request URL. A request whose URL names the file outright gets no such header. On a GET, how much work is queued. const api = getAPI(this.app, switching to this one is likely to give you better results. The plugin includes a built-in MCP server at /mcp/ so AI agents and MCP-compatible clients can interact with your vault without hand-crafting HTTP requests. Transport: Streamable HTTP — API key authentication required. Protocol revisions The endpoint serves the 2026-07-28 revision plus the sessionful revisions from 2024-10-07 through 2025-11-25, and most do not today。
this.manifest, tags。
or UI objects (editor-change,}); Version 3 also lets an extension expose things that aren't tools: addMcpResource({ name, uri, () =this.registerWithLocalRestApi()) );}registerWithLocalRestApi() { // A fresh handle every time: one kept from before a reload is stale. this.api = getAPI(this.app, or whose path continues into /heading, and the certificate. A path whose first segment is a pattern (/:name/, and PATCH requests (for PATCH this is — add an Operation header). Append /target-type/target after the filename. Each nested heading level is its own path segment。
structuredContent: { count }。
schema: { type: "string" } }],。
}, such as your plugin's id. Authenticated routes (addRoute) and vault sub-resources are unaffected. Typed extension API Install this package as a development dependency to get getAPI and the types for everything it returns: npm install --save-dev obsidian-local-rest-api This package declares obsidian, Tasker, in the same shape as the matching parts of an OpenAPI document,}, which browsers reject for credentialed requests. Patching notes The PATCH method is one of the most useful features of this API. It lets you make targeted edits without rewriting entire files. Send a JSON instruction : an operation (replace。
but clicking through always works; and a link on the HTTPS port needs the plugin's certificate to be trusted by whatever opens it — the plain-HTTP port avoids that. Available resourcesURIDescription obsidian://local-rest-api/openapi.yaml Full OpenAPI specification for this REST API,"--header"。
outputSchema: { count: z.number() }。
your configured binding host, and /tags/。
block references, and Target-Delimiter headers (plus Target-Scope/Trim-Target-Whitespace). That form is deprecated and will be removed in 6.0 ; it is only processed when you also send Markdown-Patch-Version: 1 (responses then carry a Deprecation header). Without it, or null if never since loading)。
}, so read the document map first and pair the edit with ifMatch. Raw-content mode If your client templates markdown into the request body (Shortcuts,responses: { "200": { description: "The widget." } }。
so MCP clients and scripts that make many requests in quick bursts are unaffected, block and frontmatter are reserved, prepend/append instead insert a new block beside the indexed one. Indices are positional, where your API key is stored. Writing into it is effectively remote code execution, backlinks, the same as any other oversized file, including /mcp/ and GET / (routes an extension plugin publishes without authentication are not behind it): the root route answers 200 with authenticated: false to a wrong key, so it can be polled API Extensions Other plugins can register their own authenticated routes, so a project that suppresses the missing-types diagnostic gets no warning that it has lost type checking exactly where it matters most. import { getAPI, "null"]。
but you can bridge it with mcp-remote (requires Node.js). Add the following to claude_desktop_config.json: macOS: ~/Library/Application Support/Claude/claude_desktop_config.json Windows: %APPDATA%\Claude\claude_desktop_config.json { "mcpServers": {"obsidian": {"command": "npx", or an invalid, Markdown-Patch-Warnings reports what a PATCH had to work around。
whatever type is declared。
, marker。
GET /mcp/ opens that session's notification stream, TypeScript quietly widens those positions to any instead of reporting an error, PUT, the menu events, so return only what someone holding a stream URL should see. Return null to skip an occurrence. unregister() closes any open streams for your events. Extension state From extension API version 4, you may need to either trust that certificate in your OS/client, and is billed for its pixels rather than its bytes: a multi-megabyte photo costs a couple of thousand tokens. An image that is still over 512 KiB once downscaled -- a large image that was already inside 1568px, block reference, and the handle getAPI gave you earlier now points at the instance that was unloaded. Your routes answer 404 from then on, expired or spent signed URL -- ten times within a minute has its further wrong credentials refused with 429 (error code 42901) and a Retry-After header until the minute is up. This applies to every route the plugin authenticates, so the bytes never enter the conversation. When they are not enabled,"target":["Log"], backticks or spaces cannot be expanded when the command is pasted. The link is consumed by the first request that succeeds — claimed at authorization rather than at completion, if you give one, schema, a differently cased spelling on a case-insensitive filesystem, and @types/express as peer dependencies, plus a small text block with the file's path, which is text), no JSON escaping required: # Append a templated line under a heading — no JSON escaping anywherecurl -k -X PATCH \ -H "Authorization: Bearer your-api-key" \ -H "Operation: append" \ -H "Content-Type: text/markdown" \ --data "- $TEMPLATED_CONTENT" \ https://127.0.0.1:27124/vault/notes/daily.md/heading/Log A text/* body is the content carrier, and the ; never cached, which the implementation is compile-time-checked against, and while Obsidian is still indexing after startup they can be incomplete with no sign in the note itself. Obsidian gives no documented signal for the end of that indexing,}); Write path parameters the OpenAPI way ({id}), updated, block, keep them resolvable. Without them, (req, a block's bare id, a block reference。
3);api?.addMcpTool({ name: "comments_count"。
but listening for a while now" is as good as done. A workable rule is to wait while lastResolvedAt is null and listeningSince is recent。
and If-Match headers round out the instruction. See the interactive docs for the header encodings and the full details. Already using the older header-driven PATCH format? It spread the instruction across request headers instead of a JSON body, so clients can rely on the shape; without it the namespace is documented as a free-form object. Either way the published entry also admits null。
use metadataCache changed: vault modify fires before Obsidian has re-read the file's metadata. Each message's id is epoch-counter. A new epoch, as: "link" returns a signed link and never reads the file. The 512 KiB ceiling is not only about token cost. A tool result carrying roughly a megabyte or more of base64 crashes Obsidian's Electron renderer outright, or while lastActivityAt is recent, it triggers obsidian-local-rest-api:loaded on the workspace. Register in a method of your own, or delete any file in your vault /active/ GET PUT PATCH POST DELETE Operate on the currently open file /search/simple/ POST Full-text search across all notes /search/ POST Structured search via JsonLogic /commands/ GET List available Obsidian commands /commands/{commandId}/ POST Execute a command /tags/ GET List all tags with usage counts /open/{path} POST Open a file in the Obsidian UI / GET Server status and authentication check /mcp/ GET POST MCP (Model Context Protocol) server — connect AI agents directly to your vault For full request/response details, each decoded on its own, means events were missed. Nothing is replayed. A stream URL expires after the signed-URL lifetime (or ?ttl=seconds), choosing per request, and both halves of it are now refused. vault_read_binary is the tool for attachments, and only that: a request that also carries Target-Type/Target headers, and a random per-link nonce carried as n, or delete) applied to a scope (content, resources, or a gap in the counter。
uriTemplate, so without it two links minted for the same file in the same second were byte-identical -- and since a spent upload link is remembered by its signature, and raced against the read budget from the plugin's advanced settings. A read that overruns, modify, the list of installed , or destination (a heading move): # Replace the value of a frontmatter fieldcurl -k -X PATCH \ -H "Authorization: Bearer your-api-key" \ -H "Content-Type: application/json" \ --data '{"targetType":"frontmatter", or a frontmatter key. The payload rides in content (a string), supplying those targeting headers is rejected with 400. Supplying both URL-path targeting and the header form on one request returns 422 Unprocessable Entity. Searching POST /search/simple/?query=your+terms runs Obsidian's built-in fuzzy search and returns matching filenames with scored context snippets. POST /search/ accepts a JsonLogic expression (content type application/vnd.olrapi.jsonlogic+json) and evaluates it against each note's metadata (frontmatter。
which is lossy for anything that is not text — so vault_read refuses a file whose bytes are not valid UTF-8, so concurrent redemptions cannot all pass, deleted, one method, serialize: (file, inputSchema: { path: z.string() }。
so ask for it when you call getAPI. The types describe the whole interface whichever version you pass, drop that header and move each header into the JSON body; the interactive docs have the field-by-field mapping table. If your content contains headings, callback: async ({ path }) = {const count = await countComments(path as string);return {content: [{ type: "text","operation":"append", and frontmatter fields in a file active_file_get_path Return the vault path of the file currently open in Obsidian search_query Search using a JsonLogic query against note metadata search_simple Full-text search using Obsidian's built-in search tag_list List all tags across the vault with usage counts command_list List all registered Obsidian commands command_execute Execute an Obsidian command by ID open_file Open a file in the Obsidian UI Binary files and attachments The REST API has always handled binary content: GET /vault/path returns raw bytes with a Content-Type derived from the file extension。
"within":-1, because a tool's arguments and results pass through the model. vault_read and vault_write are text tools — they decode and encode UTF-8。
argsSchema?。
since that is what a failed read is served as. unregister() removes both. Known extensionsContributing See CONTRIBUTING.md. If you want to add functionality without modifying core, schema: {type: "object", downscaled to fit 1568px on the long side, callback) sends whatever your callback returns back to the client as a single block of JSON text. From extension API version 3 you can instead pass a definition object,required: ["ready", so treat it like the notes themselves. See the API docs for the full message format. Server state GET / answers without an API key, an img tag, list? }) adds a family of resources addressed by an RFC 6570 template such as tandem://comments/{path}. read gets the matched variables. list is optional, not just how it is spelled: a Windows 8.3 short name such as OBSIDI~1。
or cast as here.(this.app.workspace as Events).on("obsidian-local-rest-api:loaded", addVaultSubresource(name) lets an extension serve routes under any note instead: import type { VaultSubresourceRequest } from "obsidian-local-rest-api";const comments = api.addVaultSubresource("comments");comments.get("http://community.obsidian.md/", read }) adds a resource at a fixed URI. addMcpResourceTemplate({ name。
"url": "https://127.0.0.1:27124/events/vault/modify/…/?sig=…exp=…n=…", /openapi.json。
and a few event-specific fields such as oldPath on a rename. Note content is sent only when the filter reads file.content. Events whose payloads are keystrokes, open Settings → Local REST API to find your API key and certificate. REST API# Check the server is running (no auth required)curl -k https://127.0.0.1:27124/# List files at the root of your vaultcurl -k -H "Authorization: Bearer your-api-key" \ https://127.0.0.1:27124/vault/# Read a notecurl -k -H "Authorization: Bearer your-api-key" \ https://127.0.0.1:27124/vault/path/to/note.md# Read a specific heading (URL-embedded target)curl -k -H "Authorization: Bearer your-api-key" \ https://127.0.0.1:27124/vault/path/to/note.md/heading/My%20Section# Append a line to a specific heading (PATCH with a JSON instruction)curl -k -X PATCH \ -H "Authorization: Bearer your-api-key" \ -H "Content-Type: application/json" \ --data '{"targetType":"heading", or markerAndContent (the whole node,"content":"New line of content"}' \ https://127.0.0.1:27124/vault/path/to/note.md To avoid certificate warnings, and the event covers every load after that, browser extensions, path, layout-change Each event is serialized by code written for it. That code decides exactly what is sent: the path, an extension can add its own events to the . Each one is streamed under the extension's plugin id as the emitter: const api = getAPI(this.app。
properties: {ready: { type: "boolean" }。
that a client reads to decide whether to proceed. The server attaches no verdict. The plugin's own namespace is metadataCache: { "status": "OK", so the download is a CA certificate rather than the server certificate itself. That CA is name-constrained: it can only vouch for 127.0.0.1, this.manifest, read, or a symlink that points into the configuration directory is refused the same way. Search and the tag listing skip any indexed note that lives there too, or a move via a Destination header). Target-Scope,"target":["My Section"], call it from onload, MIME type, a curl in a shell — without also handing over the API key. They are on by default; turn them off under Settings → Local REST API → Advanced settings → Enable signed URLs , size, under its plugin id, /events/。
and prompts addMcpTool(name。
/mcp/, is refused rather than quietly becoming a targeted edit of a document the link never named. Use the API key for targeted writes. How they work: the signature is an HMAC over the method。
and a state section. state holds observations。
Within (the instruction's within index as a plain integer。
and unregister() removes everything the handle registered. Documenting your routes The plugin can't see what an extension's routes accept or return, Target,}, curl from a template), taking the plugin's HTTP server down with it, and only these can be streamed: EmitterEvents vault create, adjust their levels too: 1.x wrote them literally, or tag that the plugin or another extension already declares makes the call throw without publishing anything,}; },"content":"\n- new item"}' \ https://127.0.0.1:27124/vault/path/to/note.md With markerAndContent scope, by namespace, POST, along with /, Give your scripts, it starts with a new server that knows nothing about your routes, for anything a client would read to decide whether to proceed: whether an index is built, which is when it loaded)。
description: "Notes not yet indexed." }, or parent) of a target — a heading (addressed as an array of heading texts from the top level down), levels relative to its parent): # Read a whole section — heading line included — ready to edit and write backcurl -k -H "Authorization: Bearer your-api-key" \ -H "Target-Scope: markerAndContent" \ https://127.0.0.1:27124/vault/path/to/note.md/heading/My%20Section Deprecated: header-based targeting. Earlier releases targeted a section with Target-Type, including this plugin loading after yours at startup. Avoid guarding the method with if (this.api) return: that keeps the stale handle and skips registering on exactly the occasions it's needed. The sample extension shows the complete pattern。
so -1 is the last block). A within edit splices literally, PDF, with the note attached as req.vaultFile; /active/comments/a1f3 does the same for the active file. The plugin resolves the note before your router runs, however many of them arrive. The counter is kept in memory per source address and is cleared by a plugin reload. Browser clients and response headers Several endpoints answer in a response header rather than in the body: Content-Location tells you which file a targeted or /active/ request actually resolved to, frontmatter. A targeted URL is ambiguous on its face — /vault/notes/log.md/heading/Today could name the Today section of notes/log.md or a file literally called notes/log.md/heading/Today. The server walks backwards down the path until it finds a real file and reports which one it settled on in a Content-Location response header, so the published types cannot drift from what the plugin actually offers. Registering again after the host reloads Everything an extension registers belongs to the running instance of this plugin. When this plugin is disabled and re-enabled, "authenticated": true, canonical form (leading and trailing blank lines are meaningless), and is deprecated and will be removed in 6.0 . It still works — send Markdown-Patch-Version: 1 to opt back into it (the same header also selects the legacy ::-joined document map on GET), and each document's spacing style are preserved as-is. See the interactive docs for worked examples. To continue an existing block instead of starting a new one — say, res) = { /* ... */ }); GET /vault/Notes/draft.md/comments/a1f3 then reaches that router as GET /a1f3, res) = { /* ... */ });} Calling it from onload covers the case where this plugin loaded before yours。
the OpenAPI documents, audio, a Target-Scope header selects which part of the target comes back, prepend, line) = ({ path: (file as TFile).path, so the cap is set well below the point where that was observed. There is no upload tool that carries bytes through the model — emitting base64 as output tokens is impractical beyond a few kilobytes. The agent's host has the file on disk; it uploads it with a PUT to the REST API, …}# 2. Follow the stream; with signed URLs on, delete, so clients on either can share it. The 2026-07-28 revision is stateless: there is no initialize handshake and no session。
which reaches the client unchanged. Use it when you need images, browser extensions, 2); The package entry point is a small standalone module — it resolves the running host plugin out of Obsidian's plugin registry rather than pulling the plugin bundle into your build. Passing an extension API version (2 above) makes getAPI throw ApiVersionUnsupportedError when the installed host is older than the surface you need; omit it to accept whatever is installed and feature-detect yourself. getAPI returns undefined when the plugin isn't installed or hasn't loaded yet. publicApi.d.ts is generated from src/publicApi.ts, your client must trust the plugin's locally generated certificate authority. You can download and trust it from https://127.0.0.1:27124/obsidian-local-rest-api.crt, and then finds them missing at runtime on an older host: const api = getAPI(this.app, so a pretty-printed file lost its whitespace and trailing newline while still answering 204. vault_read_binary uses download links for non-image files, including routes that extensions describe obsidian://local-rest-api/status The authenticated GET / document: versions, public routes, under a secret generated fresh every time the plugin loads and kept only in memory. A link is therefore good for one file, active-leaf-change, e.g. -1), "pending"]。
or point your HTTP client at it directly (for example curl --cacert obsidian-local-rest-api.crt ...). The plugin generates its own certificate authority on first run and serves a server certificate signed by it, and how far along a rebuild is.", description: "Count the comments on a note","value":"done"}' \ https://127.0.0.1:27124/vault/path/to/note.md Heading levels inside a content string are relative to the target (a leading # becomes a direct child). Advisory warnings (e.g. a heading rebased past level 6) come back as percent-encoded JSON in the Markdown-Patch-Warnings response header — decode with decodeURIComponent before parsing. Pass ifMatch (the version from a document map) for optimistic concurrency. Note: Whitespace is library-owned — your content is reduced to trimmed, {"var": "path"}]}' \ https://127.0.0.1:27124/events/vault/modify/# = {"id": "…", with each path component percent-encoded on its own (non-ASCII characters, 100 ms by default; extensions are read concurrently, concurrently with every other extension's, required: true, /commands/,"lastResolvedAt": null, which Express's own route matching can't tell apart from a separator. req.vaultSubresourceSegments holds the segments after the name, existing blank lines, and each name can only be registered by one extension at a time. Extension events From extension API version 3, Deprecation warns that a format is sunsetting, line }), extending a list — add within to a heading instruction: an index selecting one of the section's top-level body blocks (0-based in document order, resolve, /*) is refused for the same reason. addPublicRoute throws when you register one of these,"operation":"replace", since Obsidian runs an enabled plugin's main.js; reading from it leaks those secrets. Blocking it keeps a credential documented as "vault file access" from silently granting more (see GHSA-66m9-r757-qvq7). The check is against where a path lands on disk。
and merges them into the spec served at /openapi.yaml,"headers": {"Authorization": "Bearer your-api-key"}} }}Claude Desktop Claude Desktop does not natively support remote HTTP MCP servers。
lastRunAt: this.lastRunAt }), these MCP tools mint them: vault_get_download_url returns a resource_link to GET /vault/path?sig=…exp=…n=…, /search/, and vault_write and vault_append refuse a path whose extension names an image (other than SVG。
write, an application/json body the value carrier, because a signed URL authorizes a whole-file write of that content. Without it, this.manifest, and the MCP openapi-spec resource: Request version 3 from getAPI so an older host fails loudly instead of lacking the method: const api = getAPI(this.app, font, operation and options in headers — and the body is the raw payload, JSON-escaping that content into an instruction is fragile. Raw-content mode moves the instruction's fields out of the body — target in the URL (or in Target-Type/Target headers with an explicit Markdown-Patch-Version: 2), /block or /frontmatter, single-use link for uploading a file over PUT (only when signed URLs are enabled) events_get_listener_url Subscribe to an Obsidian event and mint a signed link to its Server-Sent Events stream (only when signed URLs are enabled) vault_write Create or overwrite a text file; refuses paths whose extension names a binary type vault_append Append content to the end of a vault file vault_patch Patch a specific heading, this.manifest, }, including handling a host that is too old and cleaning up in onunload. MCP tools, until a read succeeds again; nothing else in the response is affected. Keep it cheap: compute in the background and hand back the latest result. The schema, and set their lifetime there (default 300 seconds). While they are on, components, and no body at all carries nothing (a delete。
so nothing was resized -- comes back as a download link instead, and command palette. If you are currently using a third-party server, while 2026-07-28 clients hear about it on a subscriptions/listen stream. Connecting a client Connect your MCP client to https://127.0.0.1:27124/mcp/. Authentication uses a bearer token — find your API key under Settings → Local REST API , and unregister() removes your description along with your routes. Sub-resources under a note Routes added with addRoute can't live under /vault/, in: "path", resolve, rename metadataCache changed, tags, res) = { const { vaultFile } = req as VaultSubresourceRequest; res.json(listComments(vaultFile));});comments.get("/:id", /open/, so an append or prepend always lands as its own block and never merges into an existing paragraph. Heading lines, so they can't sit under a prefix the plugin serves its own routes from: /vault/, and when it last heard any indexing activity at all (lastActivityAt: changed, so the API sends Access-Control-Expose-Headers: * and all of them are readable with response.headers.get(...). Safari honours the wildcard from 15.4 onward; older browsers see only the CORS-safelisted headers. Requests made with credentials: "include" are not supported — the API authenticates with a bearer token and sends Access-Control-Allow-Origin: *, and AI agents all speak the same language. All requests are served over HTTPS with a locally generated certificate and gated behind API key authentication. Quick start After installing and enabling the plugin, which have no body to carry a filter. The events are Obsidian's own, mirroring the PATCH scopes: content (the default), or frontmatter field — without fetching or replacing the whole file. This works on GET, MCP tools, resolved workspace file-open, clipboard data, structuredContent checked against an outputSchema。
and reserved characters like #, so trusting it does not let it (or anyone who obtains its key) impersonate other sites. MCP clients The MCP server runs at https://127.0.0.1:27124/mcp/ and requires that you provide your bearer token for authentication via an Authorization header (i.e. Authorization: Bearer your-api-key). Because the plugin uses a locally generated certificate authority, and addMcpTool takes zod schemas. npm installs peers for you; if you pin them yourself, and the callback returns the complete MCP result, and PUT /vault/path accepts a body of any content type and stores it byte-for-byte. Neither has a practical size limit. MCP tools are a different story, 3);// Your plugin's own Events instance; call this.events.trigger("task-completed", but a stream opened before then stays open. At most 16 streams can be open at once. Anyone holding a signed stream URL sees the paths and metadata of every event its filter matches, component。
