This is page 2 of 2. Use http://codebase.md/jacksteamdev/obsidian-mcp-tools?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .clinerules ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── bug_report.md │ │ ├── config.yml │ │ ├── feature_request.md │ │ └── question.md │ ├── pull_request_template.md │ └── workflows │ └── release.yml ├── .gitignore ├── .prettierrc.yaml ├── .vscode │ └── settings.json ├── bun.lock ├── CONTRIBUTING.md ├── docs │ ├── features │ │ ├── mcp-server-install.md │ │ └── prompt-requirements.md │ ├── migration-plan.md │ └── project-architecture.md ├── LICENSE ├── manifest.json ├── mise.toml ├── package.json ├── packages │ ├── mcp-server │ │ ├── .gitignore │ │ ├── package.json │ │ ├── README.md │ │ ├── scripts │ │ │ └── install.ts │ │ ├── src │ │ │ ├── features │ │ │ │ ├── core │ │ │ │ │ └── index.ts │ │ │ │ ├── fetch │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── services │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── markdown.test.ts │ │ │ │ │ └── markdown.ts │ │ │ │ ├── local-rest-api │ │ │ │ │ └── index.ts │ │ │ │ ├── prompts │ │ │ │ │ └── index.ts │ │ │ │ ├── smart-connections │ │ │ │ │ └── index.ts │ │ │ │ ├── templates │ │ │ │ │ └── index.ts │ │ │ │ └── version │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── shared │ │ │ │ ├── formatMcpError.ts │ │ │ │ ├── formatString.ts │ │ │ │ ├── index.ts │ │ │ │ ├── logger.ts │ │ │ │ ├── makeRequest.ts │ │ │ │ ├── parseTemplateParameters.test.ts │ │ │ │ ├── parseTemplateParameters.ts │ │ │ │ └── ToolRegistry.ts │ │ │ └── types │ │ │ └── global.d.ts │ │ └── tsconfig.json │ ├── obsidian-plugin │ │ ├── .editorconfig │ │ ├── .eslintignore │ │ ├── .eslintrc │ │ ├── .gitignore │ │ ├── .npmrc │ │ ├── bun.config.ts │ │ ├── docs │ │ │ └── openapi.yaml │ │ ├── package.json │ │ ├── README.md │ │ ├── scripts │ │ │ ├── link.ts │ │ │ └── zip.ts │ │ ├── src │ │ │ ├── features │ │ │ │ ├── core │ │ │ │ │ ├── components │ │ │ │ │ │ └── SettingsTab.svelte │ │ │ │ │ └── index.ts │ │ │ │ └── mcp-server-install │ │ │ │ ├── components │ │ │ │ │ └── McpServerInstallSettings.svelte │ │ │ │ ├── constants │ │ │ │ │ ├── bundle-time.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── services │ │ │ │ │ ├── config.ts │ │ │ │ │ ├── install.ts │ │ │ │ │ ├── status.ts │ │ │ │ │ └── uninstall.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils │ │ │ │ ├── getFileSystemAdapter.ts │ │ │ │ └── openFolder.ts │ │ │ ├── main.ts │ │ │ ├── shared │ │ │ │ ├── index.ts │ │ │ │ └── logger.ts │ │ │ └── types.ts │ │ ├── svelte.config.js │ │ └── tsconfig.json │ ├── shared │ │ ├── .gitignore │ │ ├── package.json │ │ ├── README.md │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── logger.ts │ │ │ └── types │ │ │ ├── index.ts │ │ │ ├── plugin-local-rest-api.ts │ │ │ ├── plugin-smart-connections.ts │ │ │ ├── plugin-templater.ts │ │ │ ├── prompts.ts │ │ │ └── smart-search.ts │ │ └── tsconfig.json │ └── test-site │ ├── .gitignore │ ├── .npmrc │ ├── .prettierignore │ ├── .prettierrc │ ├── eslint.config.js │ ├── package.json │ ├── postcss.config.js │ ├── README.md │ ├── src │ │ ├── app.css │ │ ├── app.d.ts │ │ ├── app.html │ │ ├── lib │ │ │ └── index.ts │ │ └── routes │ │ ├── +layout.svelte │ │ ├── +layout.ts │ │ └── +page.svelte │ ├── static │ │ └── favicon.png │ ├── svelte.config.js │ ├── tailwind.config.ts │ ├── tsconfig.json │ └── vite.config.ts ├── patches │ └── [email protected] ├── README.md ├── scripts │ └── version.ts ├── SECURITY.md └── versions.json ``` # Files -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/components/McpServerInstallSettings.svelte: -------------------------------------------------------------------------------- ``` 1 | <script lang="ts"> 2 | import type McpToolsPlugin from "$/main"; 3 | import { FULL_LOGGER_FILENAME, loadDependenciesArray } from "$/shared"; 4 | import { Notice } from "obsidian"; 5 | import { dirname } from "path"; 6 | import { onMount } from "svelte"; 7 | import { 8 | removeFromClaudeConfig, 9 | updateClaudeConfig, 10 | } from "../services/config"; 11 | import { installMcpServer } from "../services/install"; 12 | import { getInstallationStatus } from "../services/status"; 13 | import { uninstallServer } from "../services/uninstall"; 14 | import type { InstallationStatus } from "../types"; 15 | import { openFolder } from "../utils/openFolder"; 16 | 17 | export let plugin: McpToolsPlugin; 18 | 19 | // Dependencies and API key status 20 | const deps = loadDependenciesArray(plugin); 21 | 22 | // Installation status 23 | let status: InstallationStatus = { 24 | state: "not installed", 25 | versions: {}, 26 | }; 27 | onMount(async () => { 28 | status = await getInstallationStatus(plugin); 29 | }); 30 | 31 | // Handle installation 32 | async function handleInstall() { 33 | try { 34 | const apiKey = await plugin.getLocalRestApiKey(); 35 | if (!apiKey) { 36 | throw new Error("Local REST API key is not configured"); 37 | } 38 | 39 | status = { ...status, state: "installing" }; 40 | const installPath = await installMcpServer(plugin); 41 | 42 | // Update Claude config 43 | await updateClaudeConfig(plugin, installPath.path, apiKey); 44 | 45 | status = await getInstallationStatus(plugin); 46 | } catch (error) { 47 | const message = 48 | error instanceof Error ? error.message : "Installation failed"; 49 | status = { ...status, state: "error", error: message }; 50 | new Notice(message); 51 | } 52 | } 53 | 54 | // Handle uninstall 55 | async function handleUninstall() { 56 | try { 57 | status = { ...status, state: "installing" }; 58 | await uninstallServer(plugin); 59 | await removeFromClaudeConfig(); 60 | status = { ...status, state: "not installed" }; 61 | } catch (error) { 62 | const message = 63 | error instanceof Error ? error.message : "Uninstallation failed"; 64 | status = { 65 | ...status, 66 | state: "error", 67 | error: message, 68 | }; 69 | new Notice(message); 70 | } 71 | } 72 | </script> 73 | 74 | <div class="installation-status"> 75 | <h3>Installation status</h3> 76 | 77 | {#if status.state === "no api key"} 78 | <div class="error-message">Please configure the Local REST API plugin</div> 79 | {:else if status.state === "not installed"} 80 | <div class="status-message"> 81 | MCP Server is not installed 82 | <button on:click={handleInstall}>Install server</button> 83 | </div> 84 | {:else if status.state === "installing"} 85 | <div class="status-message">Installing MCP server...</div> 86 | {:else if status.state === "installed"} 87 | <div class="status-message"> 88 | MCP Server v{status.versions.server} is installed 89 | <button on:click={handleUninstall}>Uninstall</button> 90 | </div> 91 | {:else if status.state === "outdated"} 92 | <div class="status-message"> 93 | Update available (v{status.versions.server} -> v{status.versions.plugin}) 94 | <button on:click={handleInstall}>Update</button> 95 | </div> 96 | {:else if status.state === "uninstalling"} 97 | <div class="status-message">Uninstalling MCP server...</div> 98 | {:else if status.state === "error"} 99 | <div class="error-message">{status.error}</div> 100 | {/if} 101 | </div> 102 | 103 | <div class="dependencies"> 104 | <h3>Dependencies</h3> 105 | 106 | {#each $deps as dep (dep.id)} 107 | <div class="dependency-item"> 108 | {#if dep.installed} 109 | ✅ {dep.name} is installed 110 | {:else} 111 | ❌ 112 | {dep.name} 113 | {dep.required ? "(Required)" : "(Optional)"} 114 | {#if dep.url}<a href={dep.url} target="_blank">How to install?</a>{/if} 115 | {/if} 116 | </div> 117 | {/each} 118 | </div> 119 | 120 | <div class="links"> 121 | <h3>Resources</h3> 122 | 123 | {#if status.path} 124 | <div class="link-item"> 125 | <!-- svelte-ignore a11y_no_static_element_interactions --> 126 | <a on:click={() => status.dir && openFolder(status.dir)}> 127 | Server install folder 128 | </a> 129 | </div> 130 | {/if} 131 | 132 | <div class="link-item"> 133 | <!-- svelte-ignore a11y_no_static_element_interactions --> 134 | <a on:click={() => openFolder(dirname(FULL_LOGGER_FILENAME))}> 135 | Server log folder 136 | </a> 137 | </div> 138 | 139 | <div class="link-item"> 140 | <a 141 | href="https://github.com/jacksteamdev/obsidian-mcp-tools" 142 | target="_blank" 143 | > 144 | GitHub repository 145 | </a> 146 | </div> 147 | </div> 148 | 149 | <style> 150 | .error-message { 151 | color: var(--text-error); 152 | margin-bottom: 1em; 153 | } 154 | 155 | .status-message { 156 | margin-bottom: 1em; 157 | } 158 | 159 | .dependency-item { 160 | margin-bottom: 0.5em; 161 | } 162 | 163 | .installed { 164 | color: var(--text-success); 165 | } 166 | 167 | .not-installed { 168 | color: var(--text-muted); 169 | } 170 | 171 | .link-item { 172 | margin-bottom: 0.5em; 173 | } 174 | 175 | button { 176 | margin-left: 0.5em; 177 | } 178 | </style> 179 | ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/services/status.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type McpToolsPlugin from "$/main"; 2 | import { logger } from "$/shared/logger"; 3 | import { exec } from "child_process"; 4 | import fsp from "fs/promises"; 5 | import { Plugin } from "obsidian"; 6 | import path from "path"; 7 | import { clean, lt, valid } from "semver"; 8 | import { promisify } from "util"; 9 | import { BINARY_NAME } from "../constants"; 10 | import type { InstallationStatus, InstallPathInfo } from "../types"; 11 | import { getFileSystemAdapter } from "../utils/getFileSystemAdapter"; 12 | import { getPlatform } from "./install"; 13 | 14 | const execAsync = promisify(exec); 15 | 16 | /** 17 | * Resolves the real path of the given file path, handling cases where the path is a symlink. 18 | * 19 | * @param filepath - The file path to resolve. 20 | * @returns The real path of the file. 21 | * @throws {Error} If the file is not found or the symlink cannot be resolved. 22 | */ 23 | async function resolveSymlinks(filepath: string): Promise<string> { 24 | try { 25 | return await fsp.realpath(filepath); 26 | } catch (error) { 27 | if ((error as NodeJS.ErrnoException).code === "ENOENT") { 28 | const parts = path.normalize(filepath).split(path.sep); 29 | let resolvedParts: string[] = []; 30 | let skipCount = 1; // Skip first segment by default 31 | 32 | // Handle the root segment differently for Windows vs POSIX 33 | if (path.win32.isAbsolute(filepath)) { 34 | resolvedParts.push(parts[0]); 35 | if (parts[1] === "") { 36 | resolvedParts.push(""); 37 | skipCount = 2; // Skip two segments for UNC paths 38 | } 39 | } else if (path.posix.isAbsolute(filepath)) { 40 | resolvedParts.push("/"); 41 | } else { 42 | resolvedParts.push(parts[0]); 43 | } 44 | 45 | // Process remaining path segments 46 | for (const part of parts.slice(skipCount)) { 47 | const partialPath = path.join(...resolvedParts, part); 48 | try { 49 | const resolvedPath = await fsp.realpath(partialPath); 50 | resolvedParts = resolvedPath.split(path.sep); 51 | } catch (err) { 52 | resolvedParts.push(part); 53 | } 54 | } 55 | 56 | return path.join(...resolvedParts); 57 | } 58 | 59 | logger.error(`Failed to resolve symlink:`, { 60 | filepath, 61 | error: error instanceof Error ? error.message : error, 62 | }); 63 | throw new Error(`Failed to resolve symlink: ${filepath}`); 64 | } 65 | } 66 | 67 | export async function getInstallPath( 68 | plugin: Plugin, 69 | ): Promise<InstallPathInfo | { error: string }> { 70 | const adapter = getFileSystemAdapter(plugin); 71 | if ("error" in adapter) return adapter; 72 | 73 | const platform = getPlatform(); 74 | const originalPath = path.join( 75 | adapter.getBasePath(), 76 | plugin.app.vault.configDir, 77 | "plugins", 78 | plugin.manifest.id, 79 | "bin", 80 | ); 81 | const realDirPath = await resolveSymlinks(originalPath); 82 | const platformSpecificBinary = BINARY_NAME[platform]; 83 | const realFilePath = path.join(realDirPath, platformSpecificBinary); 84 | return { 85 | dir: realDirPath, 86 | path: realFilePath, 87 | name: platformSpecificBinary, 88 | symlinked: originalPath === realDirPath ? undefined : originalPath, 89 | }; 90 | } 91 | 92 | /** 93 | * Gets the current installation status of the MCP server 94 | */ 95 | export async function getInstallationStatus( 96 | plugin: McpToolsPlugin, 97 | ): Promise<InstallationStatus> { 98 | // Verify plugin version is valid 99 | const pluginVersion = valid(clean(plugin.manifest.version)); 100 | if (!pluginVersion) { 101 | logger.error("Invalid plugin version:", { plugin }); 102 | return { state: "error", versions: {} }; 103 | } 104 | 105 | // Check for API key 106 | const apiKey = plugin.getLocalRestApiKey(); 107 | if (!apiKey) { 108 | return { 109 | state: "no api key", 110 | versions: { plugin: pluginVersion }, 111 | }; 112 | } 113 | 114 | // Verify server binary is present 115 | const installPath = await getInstallPath(plugin); 116 | if ("error" in installPath) { 117 | return { 118 | state: "error", 119 | versions: { plugin: pluginVersion }, 120 | error: installPath.error, 121 | }; 122 | } 123 | 124 | try { 125 | await fsp.access(installPath.path, fsp.constants.X_OK); 126 | } catch (error) { 127 | logger.error("Failed to get server version:", { installPath }); 128 | return { 129 | state: "not installed", 130 | ...installPath, 131 | versions: { plugin: pluginVersion }, 132 | }; 133 | } 134 | 135 | // Check server binary version 136 | let serverVersion: string | null | undefined; 137 | try { 138 | const versionCommand = `"${installPath.path}" --version`; 139 | const { stdout } = await execAsync(versionCommand); 140 | serverVersion = clean(stdout.trim()); 141 | if (!serverVersion) throw new Error("Invalid server version string"); 142 | } catch { 143 | logger.error("Failed to get server version:", { installPath }); 144 | return { 145 | state: "error", 146 | ...installPath, 147 | versions: { plugin: pluginVersion }, 148 | }; 149 | } 150 | 151 | return { 152 | ...installPath, 153 | state: lt(serverVersion, pluginVersion) ? "outdated" : "installed", 154 | versions: { plugin: pluginVersion, server: serverVersion }, 155 | }; 156 | } 157 | ``` -------------------------------------------------------------------------------- /packages/shared/src/types/plugin-templater.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { 2 | App, 3 | type MarkdownPostProcessorContext, 4 | TAbstractFile, 5 | TFile, 6 | TFolder, 7 | } from "obsidian"; 8 | 9 | export enum RunMode { 10 | CreateNewFromTemplate, 11 | AppendActiveFile, 12 | OverwriteFile, 13 | OverwriteActiveFile, 14 | DynamicProcessor, 15 | StartupTemplate, 16 | } 17 | 18 | export enum FunctionsMode { 19 | INTERNAL, 20 | USER_INTERNAL, 21 | } 22 | 23 | export type RunningConfig = { 24 | template_file: TFile | undefined; 25 | target_file: TFile; 26 | run_mode: RunMode; 27 | active_file?: TFile | null; 28 | }; 29 | 30 | interface TemplaterFunctions { 31 | app: App; 32 | config: RunningConfig; 33 | date: { 34 | /** 35 | * @param format "YYYY-MM-DD" 36 | * @param offset 37 | * @param reference 38 | * @param reference_format 39 | */ 40 | now( 41 | format: string, 42 | offset?: number | string, 43 | reference?: string, 44 | reference_format?: string, 45 | ): string; 46 | /** 47 | * @param format "YYYY-MM-DD" 48 | */ 49 | tomorrow(format: string): string; 50 | /** 51 | * @param format "YYYY-MM-DD" 52 | * @param weekday 53 | * @param reference 54 | * @param reference_format 55 | */ 56 | weekday( 57 | format: string, 58 | weekday: number, 59 | reference?: string, 60 | reference_format?: string, 61 | ): string; 62 | /** 63 | * @param format "YYYY-MM-DD" 64 | */ 65 | yesterday(format?: string): string; 66 | }; 67 | file: { 68 | content: string; 69 | /** 70 | * @param template TFile or string 71 | * @param filename 72 | * @param open_new Default: false 73 | * @param folder TFolder or string 74 | */ 75 | create_new( 76 | template: TFile | string, 77 | filename?: string, 78 | open_new?: boolean, 79 | folder?: TFolder | string, 80 | ): Promise<TFile>; 81 | /** 82 | * @param format Default: "YYYY-MM-DD HH:mm" 83 | */ 84 | creation_date(format?: string): string; 85 | /** 86 | * @param order 87 | */ 88 | cursor(order?: number): void; 89 | cursor_append(content: string): void; 90 | exists(filepath: string): boolean; 91 | find_tfile(filename: string): TFile; 92 | /** 93 | * @param absolute Default: false 94 | */ 95 | folder(absolute?: boolean): string; 96 | include(include_link: string | TFile): string; 97 | /** 98 | * @param format Default: "YYYY-MM-DD HH:mm" 99 | */ 100 | last_modified_date(format?: string): string; 101 | move(new_path: string, file_to_move?: TFile): Promise<void>; 102 | /** 103 | * @param relative Default: false 104 | */ 105 | path(relative?: boolean): string; 106 | rename(new_title: string): Promise<void>; 107 | selection(): string; 108 | tags: string[]; 109 | title: string; 110 | }; 111 | frontmatter: Record<string, unknown>; 112 | hooks: { 113 | on_all_templates_executed(cb: () => void): void; 114 | }; 115 | system: { 116 | /** 117 | * Retrieves the clipboard's content. 118 | */ 119 | clipboard(): Promise<string>; 120 | 121 | /** 122 | * @param prompt_text 123 | * @param default_value 124 | * @param throw_on_cancel Default: false 125 | * @param multiline Default: false 126 | */ 127 | prompt( 128 | prompt_text?: string, 129 | default_value?: string, 130 | throw_on_cancel?: boolean, 131 | multiline?: boolean, 132 | ): Promise<string>; 133 | 134 | /** 135 | * @param text_items String array or function mapping item to string 136 | * @param items Array of generic type T 137 | * @param throw_on_cancel Default: false 138 | * @param placeholder Default: "" 139 | * @param limit Default: undefined 140 | */ 141 | suggester<T>( 142 | text_items: string[] | ((item: T) => string), 143 | items: T[], 144 | throw_on_cancel?: boolean, 145 | placeholder?: string, 146 | limit?: number, 147 | ): Promise<T>; 148 | }; 149 | web: { 150 | /** 151 | * Retrieves daily quote from quotes database 152 | */ 153 | daily_quote(): Promise<string>; 154 | 155 | /** 156 | * @param size Image size specification 157 | * @param query Search query 158 | * @param include_size Whether to include size in URL 159 | */ 160 | random_picture( 161 | size: string, 162 | query: string, 163 | include_size: boolean, 164 | ): Promise<string>; 165 | 166 | /** 167 | * @param url Full URL to request 168 | * @param path Optional path parameter 169 | */ 170 | request(url: string, path?: string): Promise<string>; 171 | }; 172 | user: Record<string, unknown>; 173 | } 174 | 175 | export interface ITemplater { 176 | setup(): Promise<void>; 177 | /** Generate the config required to parse a template */ 178 | create_running_config( 179 | template_file: TFile | undefined, 180 | target_file: TFile, 181 | run_mode: RunMode, 182 | ): RunningConfig; 183 | /** I don't think this writes the file, but the config requires the file name */ 184 | read_and_parse_template(config: RunningConfig): Promise<string>; 185 | /** I don't think this writes the file, but the config requires the file name */ 186 | parse_template( 187 | config: RunningConfig, 188 | template_content: string, 189 | ): Promise<string>; 190 | create_new_note_from_template( 191 | template: TFile | string, 192 | folder?: TFolder | string, 193 | filename?: string, 194 | open_new_note?: boolean, 195 | ): Promise<TFile | undefined>; 196 | append_template_to_active_file(template_file: TFile): Promise<void>; 197 | write_template_to_file(template_file: TFile, file: TFile): Promise<void>; 198 | overwrite_active_file_commands(): void; 199 | overwrite_file_commands(file: TFile, active_file?: boolean): Promise<void>; 200 | process_dynamic_templates( 201 | el: HTMLElement, 202 | ctx: MarkdownPostProcessorContext, 203 | ): Promise<void>; 204 | get_new_file_template_for_folder(folder: TFolder): string | undefined; 205 | get_new_file_template_for_file(file: TFile): string | undefined; 206 | execute_startup_scripts(): Promise<void>; 207 | 208 | on_file_creation( 209 | templater: ITemplater, 210 | app: App, 211 | file: TAbstractFile, 212 | ): Promise<void>; 213 | 214 | current_functions_object: TemplaterFunctions; 215 | functions_generator: { 216 | generate_object( 217 | config: RunningConfig, 218 | functions_mode?: FunctionsMode, 219 | ): Promise<TemplaterFunctions>; 220 | }; 221 | } 222 | ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/main.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { type } from "arktype"; 2 | import type { Request, Response } from "express"; 3 | import { Notice, Plugin, TFile } from "obsidian"; 4 | import { shake } from "radash"; 5 | import { lastValueFrom } from "rxjs"; 6 | import { 7 | jsonSearchRequest, 8 | LocalRestAPI, 9 | searchParameters, 10 | Templater, 11 | type PromptArgAccessor, 12 | type SearchResponse, 13 | } from "shared"; 14 | import { setup as setupCore } from "./features/core"; 15 | import { setup as setupMcpServerInstall } from "./features/mcp-server-install"; 16 | import { 17 | loadLocalRestAPI, 18 | loadSmartSearchAPI, 19 | loadTemplaterAPI, 20 | type Dependencies, 21 | } from "./shared"; 22 | import { logger } from "./shared/logger"; 23 | 24 | export default class McpToolsPlugin extends Plugin { 25 | private localRestApi: Dependencies["obsidian-local-rest-api"] = { 26 | id: "obsidian-local-rest-api", 27 | name: "Local REST API", 28 | required: true, 29 | installed: false, 30 | }; 31 | 32 | async getLocalRestApiKey(): Promise<string | undefined> { 33 | // The API key is stored in the plugin's settings 34 | return this.localRestApi.plugin?.settings?.apiKey; 35 | } 36 | 37 | async onload() { 38 | // Initialize features in order 39 | await setupCore(this); 40 | await setupMcpServerInstall(this); 41 | 42 | // Check for required dependencies 43 | lastValueFrom(loadLocalRestAPI(this)).then((localRestApi) => { 44 | this.localRestApi = localRestApi; 45 | 46 | if (!this.localRestApi.api) { 47 | new Notice( 48 | `${this.manifest.name}: Local REST API plugin is required but not found. Please install it from the community plugins and restart Obsidian.`, 49 | 0, 50 | ); 51 | return; 52 | } 53 | 54 | // Register endpoints 55 | this.localRestApi.api 56 | .addRoute("/search/smart") 57 | .post(this.handleSearchRequest.bind(this)); 58 | 59 | this.localRestApi.api 60 | .addRoute("/templates/execute") 61 | .post(this.handleTemplateExecution.bind(this)); 62 | 63 | logger.info("MCP Tools Plugin loaded"); 64 | }); 65 | } 66 | 67 | private async handleTemplateExecution(req: Request, res: Response) { 68 | try { 69 | const { api: templater } = await lastValueFrom(loadTemplaterAPI(this)); 70 | if (!templater) { 71 | new Notice( 72 | `${this.manifest.name}: Templater plugin is not available. Please install it from the community plugins.`, 73 | 0, 74 | ); 75 | logger.error("Templater plugin is not available"); 76 | res.status(503).json({ 77 | error: "Templater plugin is not available", 78 | }); 79 | return; 80 | } 81 | 82 | // Validate request body 83 | const params = LocalRestAPI.ApiTemplateExecutionParams(req.body); 84 | 85 | if (params instanceof type.errors) { 86 | const response = { 87 | error: "Invalid request body", 88 | body: req.body, 89 | summary: params.summary, 90 | }; 91 | logger.debug("Invalid request body", response); 92 | res.status(400).json(response); 93 | return; 94 | } 95 | 96 | // Get prompt content from vault 97 | const templateFile = this.app.vault.getAbstractFileByPath(params.name); 98 | if (!(templateFile instanceof TFile)) { 99 | logger.debug("Template file not found", { 100 | params, 101 | templateFile, 102 | }); 103 | res.status(404).json({ 104 | error: `File not found: ${params.name}`, 105 | }); 106 | return; 107 | } 108 | 109 | const config = templater.create_running_config( 110 | templateFile, 111 | templateFile, 112 | Templater.RunMode.CreateNewFromTemplate, 113 | ); 114 | 115 | const prompt: PromptArgAccessor = (argName: string) => { 116 | return params.arguments[argName] ?? ""; 117 | }; 118 | 119 | const oldGenerateObject = 120 | templater.functions_generator.generate_object.bind( 121 | templater.functions_generator, 122 | ); 123 | 124 | // Override generate_object to inject arg into user functions 125 | templater.functions_generator.generate_object = async function ( 126 | config, 127 | functions_mode, 128 | ) { 129 | const functions = await oldGenerateObject(config, functions_mode); 130 | Object.assign(functions, { mcpTools: { prompt } }); 131 | return functions; 132 | }; 133 | 134 | // Process template with variables 135 | const processedContent = await templater.read_and_parse_template(config); 136 | 137 | // Restore original functions generator 138 | templater.functions_generator.generate_object = oldGenerateObject; 139 | 140 | // Create new file if requested 141 | if (params.createFile && params.targetPath) { 142 | await this.app.vault.create(params.targetPath, processedContent); 143 | res.json({ 144 | message: "Prompt executed and file created successfully", 145 | content: processedContent, 146 | }); 147 | return; 148 | } 149 | 150 | res.json({ 151 | message: "Prompt executed without creating a file", 152 | content: processedContent, 153 | }); 154 | } catch (error) { 155 | logger.error("Prompt execution error:", { 156 | error: error instanceof Error ? error.message : error, 157 | body: req.body, 158 | }); 159 | res.status(503).json({ 160 | error: "An error occurred while processing the prompt", 161 | }); 162 | return; 163 | } 164 | } 165 | 166 | private async handleSearchRequest(req: Request, res: Response) { 167 | try { 168 | const dep = await lastValueFrom(loadSmartSearchAPI(this)); 169 | const smartSearch = dep.api; 170 | if (!smartSearch) { 171 | new Notice( 172 | "Smart Search REST API Plugin: smart-connections plugin is required but not found. Please install it from the community plugins.", 173 | 0, 174 | ); 175 | res.status(503).json({ 176 | error: "Smart Connections plugin is not available", 177 | }); 178 | return; 179 | } 180 | 181 | // Validate request body 182 | const requestBody = jsonSearchRequest 183 | .pipe(({ query, filter = {} }) => ({ 184 | query, 185 | filter: shake({ 186 | key_starts_with_any: filter.folders, 187 | exclude_key_starts_with_any: filter.excludeFolders, 188 | limit: filter.limit, 189 | }), 190 | })) 191 | .to(searchParameters)(req.body); 192 | if (requestBody instanceof type.errors) { 193 | res.status(400).json({ 194 | error: "Invalid request body", 195 | summary: requestBody.summary, 196 | }); 197 | return; 198 | } 199 | 200 | // Perform search 201 | const results = await smartSearch.search( 202 | requestBody.query, 203 | requestBody.filter, 204 | ); 205 | 206 | // Format response 207 | const response: SearchResponse = { 208 | results: await Promise.all( 209 | results.map(async (result) => ({ 210 | path: result.item.path, 211 | text: await result.item.read(), 212 | score: result.score, 213 | breadcrumbs: result.item.breadcrumbs, 214 | })), 215 | ), 216 | }; 217 | 218 | res.json(response); 219 | return; 220 | } catch (error) { 221 | logger.error("Smart Search API error:", { error, body: req.body }); 222 | res.status(503).json({ 223 | error: "An error occurred while processing the search request", 224 | }); 225 | return; 226 | } 227 | } 228 | 229 | onunload() { 230 | this.localRestApi.api?.unregister(); 231 | } 232 | } 233 | ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/shared/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import type { App } from "obsidian"; 2 | import { getAPI, LocalRestApiPublicApi } from "obsidian-local-rest-api"; 3 | import { 4 | distinct, 5 | interval, 6 | map, 7 | merge, 8 | scan, 9 | startWith, 10 | takeUntil, 11 | takeWhile, 12 | timer, 13 | } from "rxjs"; 14 | import type { SmartConnections, Templater } from "shared"; 15 | import type McpToolsPlugin from "src/main"; 16 | 17 | export interface Dependency<ID extends keyof App["plugins"]["plugins"], API> { 18 | id: keyof Dependencies; 19 | name: string; 20 | required: boolean; 21 | installed: boolean; 22 | url?: string; 23 | api?: API; 24 | plugin?: App["plugins"]["plugins"][ID]; 25 | } 26 | 27 | export interface Dependencies { 28 | "obsidian-local-rest-api": Dependency< 29 | "obsidian-local-rest-api", 30 | LocalRestApiPublicApi 31 | >; 32 | "smart-connections": Dependency< 33 | "smart-connections", 34 | SmartConnections.SmartSearch 35 | >; 36 | "templater-obsidian": Dependency<"templater-obsidian", Templater.ITemplater>; 37 | } 38 | 39 | // Smart Connections v3.0+ uses a Smart Environment architecture instead of window.SmartSearch 40 | declare const window: { 41 | SmartSearch?: SmartConnections.SmartSearch; 42 | } & Window; 43 | 44 | export const loadSmartSearchAPI = (plugin: McpToolsPlugin) => 45 | interval(200).pipe( 46 | takeUntil(timer(5000)), 47 | map((): Dependencies["smart-connections"] => { 48 | const smartConnectionsPlugin = plugin.app.plugins.plugins[ 49 | "smart-connections" 50 | ] as any; 51 | 52 | // Check for Smart Connections v3.0+ (uses smart environment) 53 | if (smartConnectionsPlugin?.env?.smart_sources) { 54 | const smartEnv = smartConnectionsPlugin.env; 55 | 56 | // Create a compatibility wrapper that matches the old SmartSearch interface 57 | const api: SmartConnections.SmartSearch = { 58 | search: async ( 59 | search_text: string, 60 | filter?: Record<string, string>, 61 | ) => { 62 | try { 63 | // Use the new v3.0 lookup API 64 | const results = await smartEnv.smart_sources.lookup({ 65 | hypotheticals: [search_text], 66 | filter: { 67 | limit: filter?.limit, 68 | key_starts_with_any: filter?.key_starts_with_any, 69 | exclude_key_starts_with_any: 70 | filter?.exclude_key_starts_with_any, 71 | exclude_key: filter?.exclude_key, 72 | exclude_keys: filter?.exclude_keys, 73 | exclude_key_starts_with: filter?.exclude_key_starts_with, 74 | exclude_key_includes: filter?.exclude_key_includes, 75 | key_ends_with: filter?.key_ends_with, 76 | key_starts_with: filter?.key_starts_with, 77 | key_includes: filter?.key_includes, 78 | }, 79 | }); 80 | 81 | // Transform results to match expected format 82 | return results.map((result: any) => ({ 83 | item: { 84 | path: result.item.path, 85 | name: 86 | result.item.name || 87 | result.item.key?.split("/").pop() || 88 | result.item.key, 89 | breadcrumbs: result.item.breadcrumbs || result.item.path, 90 | read: () => result.item.read(), 91 | key: result.item.key, 92 | file_path: result.item.path, 93 | link: result.item.link, 94 | size: result.item.size, 95 | }, 96 | score: result.score, 97 | })); 98 | } catch (error) { 99 | console.error("Smart Connections v3.0 search error:", error); 100 | return []; 101 | } 102 | }, 103 | }; 104 | 105 | return { 106 | id: "smart-connections", 107 | name: "Smart Connections", 108 | required: false, 109 | installed: true, 110 | api, 111 | plugin: smartConnectionsPlugin, 112 | }; 113 | } 114 | 115 | // Try window.SmartSearch first (works on some platforms for v2.x) 116 | let legacyApi = window.SmartSearch; 117 | 118 | // Fallback to plugin system (fixes Linux/cross-platform detection issues) 119 | if (!legacyApi && smartConnectionsPlugin?.env) { 120 | legacyApi = smartConnectionsPlugin.env; 121 | // Cache it for future use 122 | window.SmartSearch = legacyApi; 123 | } 124 | 125 | return { 126 | id: "smart-connections", 127 | name: "Smart Connections", 128 | required: false, 129 | installed: !!legacyApi, 130 | api: legacyApi, 131 | plugin: smartConnectionsPlugin, 132 | }; 133 | }), 134 | takeWhile((dependency) => !dependency.installed, true), 135 | distinct(({ installed }) => installed), 136 | ); 137 | 138 | export const loadLocalRestAPI = (plugin: McpToolsPlugin) => 139 | interval(200).pipe( 140 | takeUntil(timer(5000)), 141 | map((): Dependencies["obsidian-local-rest-api"] => { 142 | const api = getAPI(plugin.app, plugin.manifest); 143 | return { 144 | id: "obsidian-local-rest-api", 145 | name: "Local REST API", 146 | required: true, 147 | installed: !!api, 148 | api, 149 | plugin: plugin.app.plugins.plugins["obsidian-local-rest-api"], 150 | }; 151 | }), 152 | takeWhile((dependency) => !dependency.installed, true), 153 | distinct(({ installed }) => installed), 154 | ); 155 | 156 | export const loadTemplaterAPI = (plugin: McpToolsPlugin) => 157 | interval(200).pipe( 158 | takeUntil(timer(5000)), 159 | map((): Dependencies["templater-obsidian"] => { 160 | const api = plugin.app.plugins.plugins["templater-obsidian"]?.templater; 161 | return { 162 | id: "templater-obsidian", 163 | name: "Templater", 164 | required: false, 165 | installed: !!api, 166 | api, 167 | plugin: plugin.app.plugins.plugins["templater-obsidian"], 168 | }; 169 | }), 170 | takeWhile((dependency) => !dependency.installed, true), 171 | distinct(({ installed }) => installed), 172 | ); 173 | 174 | export const loadDependencies = (plugin: McpToolsPlugin) => { 175 | const dependencies: Dependencies = { 176 | "obsidian-local-rest-api": { 177 | id: "obsidian-local-rest-api", 178 | name: "Local REST API", 179 | required: true, 180 | installed: false, 181 | url: "https://github.com/coddingtonbear/obsidian-local-rest-api", 182 | }, 183 | "smart-connections": { 184 | id: "smart-connections", 185 | name: "Smart Connections", 186 | required: false, 187 | installed: false, 188 | url: "https://smartconnections.app/", 189 | }, 190 | "templater-obsidian": { 191 | id: "templater-obsidian", 192 | name: "Templater", 193 | required: false, 194 | installed: false, 195 | url: "https://silentvoid13.github.io/Templater/", 196 | }, 197 | }; 198 | return merge( 199 | loadLocalRestAPI(plugin), 200 | loadTemplaterAPI(plugin), 201 | loadSmartSearchAPI(plugin), 202 | ).pipe( 203 | scan((acc, dependency) => { 204 | // @ts-expect-error Dynamic key assignment 205 | acc[dependency.id] = { 206 | ...dependencies[dependency.id], 207 | ...dependency, 208 | }; 209 | return acc; 210 | }, dependencies), 211 | startWith(dependencies), 212 | ); 213 | }; 214 | 215 | export const loadDependenciesArray = (plugin: McpToolsPlugin) => 216 | loadDependencies(plugin).pipe( 217 | map((deps) => Object.values(deps) as Dependencies[keyof Dependencies][]), 218 | ); 219 | 220 | export * from "./logger"; 221 | ``` -------------------------------------------------------------------------------- /docs/features/mcp-server-install.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Server Installation Feature Requirements 2 | 3 | ## Overview 4 | 5 | This feature enables users to install and manage the MCP server executable through the Obsidian plugin settings interface. The system handles the download of platform-specific binaries, Claude Desktop configuration, and provides clear user feedback throughout the process. 6 | 7 | ## Implementation Location 8 | 9 | The installation feature is implemented in the Obsidian plugin package under `src/features/mcp-server-install`. 10 | 11 | ## Installation Flow 12 | 13 | 1. User Prerequisites: 14 | 15 | - Claude Desktop installed 16 | - Local REST API plugin installed and configured with API key 17 | - (Optional) Templater plugin for enhanced functionality 18 | - (Optional) Smart Connections plugin for enhanced search 19 | 20 | 2. Installation Steps: 21 | - User navigates to plugin settings 22 | - Plugin verifies prerequisites and shows status 23 | - User initiates installation via button 24 | - Plugin retrieves API key from Local REST API plugin 25 | - Plugin downloads appropriate binary 26 | - Plugin updates Claude config file 27 | - Plugin confirms successful installation 28 | 29 | ## Settings UI Requirements 30 | 31 | The settings UI is implemented as a Svelte component in `components/SettingsTab.svelte`. 32 | 33 | 1. Component Structure: 34 | ```svelte 35 | <script lang="ts"> 36 | // Import Svelte stores for state management 37 | import { installationStatus } from '../stores/status'; 38 | import { dependencies } from '../stores/dependencies'; 39 | 40 | // Props from parent Settings.svelte 41 | export let plugin: Plugin; 42 | </script> 43 | 44 | <!-- Installation status and controls --> 45 | <div class="installation-status"> 46 | <!-- Dynamic content based on $installationStatus --> 47 | </div> 48 | 49 | <!-- Dependencies section --> 50 | <div class="dependencies"> 51 | <!-- Dynamic content based on $dependencies --> 52 | </div> 53 | 54 | <!-- Links section --> 55 | <div class="links"> 56 | <!-- External resource links --> 57 | </div> 58 | ``` 59 | 60 | 2. Display Elements: 61 | - Installation status indicator with version 62 | - Install/Update/Uninstall buttons 63 | - Dependency status and links 64 | - Links to: 65 | - Downloaded executable location (with folder access) 66 | - Log folder location (with folder access) 67 | - GitHub repository 68 | - Claude Desktop download page (when needed) 69 | - Required and recommended plugins 70 | 71 | 3. State Management: 72 | - Uses Svelte stores for reactive state 73 | - Status states: 74 | - Not Installed 75 | - Installing 76 | - Installed 77 | - Update Available 78 | 79 | ## Download Management 80 | 81 | 1. Binary Source: 82 | 83 | - GitHub latest release 84 | - Platform-specific naming conventions 85 | - Version number included in filename (e.g., mcp-server-1.2.3) 86 | 87 | 2. Installation Locations: 88 | - Binary: {vault}/.obsidian/plugins/{plugin-id}/bin/ 89 | - Logs: 90 | - macOS: ~/Library/Logs/obsidian-mcp-tools 91 | - Windows: %APPDATA%\obsidian-mcp-tools\logs 92 | - Linux: (platform-specific path) 93 | 94 | ## Claude Configuration 95 | 96 | 1. Config File: 97 | - Location: ~/Library/Application Support/Claude/claude_desktop_config.json 98 | - Create base structure if missing: { "mcpServers": {} } 99 | - Add/update only our config entry: 100 | ```json 101 | { 102 | "mcpServers": { 103 | "obsidian-mcp-tools": { 104 | "command": "(absolute path to executable)", 105 | "env": { 106 | "OBSIDIAN_API_KEY": "(stored api key)" 107 | } 108 | } 109 | } 110 | } 111 | ``` 112 | 113 | ## Version Management 114 | 115 | 1. Unified Version Approach: 116 | - Plugin and server share same version number 117 | - Version stored in plugin manifest 118 | - Server provides version via `--version` flag 119 | - Version checked during plugin initialization 120 | 121 | ## User Education 122 | 123 | 1. Documentation Requirements: 124 | - README.md must explain: 125 | - Binary download and installation process 126 | - GitHub source code location 127 | - Claude config file modifications 128 | - Log file locations and purpose 129 | - Settings page must link to full documentation 130 | 131 | ## Error Handling 132 | 133 | 1. Installation Errors: 134 | 135 | - Claude Desktop not installed 136 | - Download failures 137 | - Permission issues 138 | - Version mismatch 139 | 140 | 2. User Feedback: 141 | - Use Obsidian Notice API for progress/status 142 | - Clear error messages with next steps 143 | - Links to troubleshooting resources 144 | 145 | ## Uninstall Process 146 | 147 | 1. Cleanup Actions: 148 | - Remove executable 149 | - Remove our entry from Claude config 150 | - Clear stored plugin data 151 | 152 | ## Appendix: Implementation Insights 153 | 154 | ### Feature Organization 155 | The feature follows a modular structure: 156 | ``` 157 | src/features/mcp-server-install/ 158 | ├── components/ # Svelte components 159 | │ └── SettingsTab.svelte 160 | ├── services/ # Core functionality 161 | │ ├── config.ts # Claude config management 162 | │ ├── download.ts # Binary download 163 | │ ├── status.ts # Installation status 164 | │ └── uninstall.ts # Cleanup operations 165 | ├── stores/ # Svelte stores 166 | │ ├── status.ts # Installation status store 167 | │ └── dependencies.ts # Dependencies status store 168 | ├── utils/ # Shared utilities 169 | │ └── openFolder.ts 170 | ├── constants.ts # Configuration 171 | ├── types.ts # Type definitions 172 | └── index.ts # Feature setup & component export 173 | ``` 174 | 175 | ### Key Implementation Decisions 176 | 177 | 1. API Key Management 178 | - Removed manual API key input 179 | - Automatically retrieved from Local REST API plugin 180 | - Reduces user friction and potential errors 181 | 182 | 2. Symlink Resolution 183 | - Added robust symlink handling for binary paths 184 | - Ensures correct operation even with complex vault setups 185 | - Handles non-existent paths during resolution 186 | 187 | 3. Status Management 188 | - Unified status interface with version tracking 189 | - Real-time status updates during operations 190 | - Clear feedback for update availability 191 | 192 | 4. Error Handling 193 | - Comprehensive prerequisite validation 194 | - Detailed error messages with next steps 195 | - Proper cleanup on failures 196 | - Extensive logging for troubleshooting 197 | 198 | 5. User Experience 199 | - Reactive UI with Svelte components 200 | - One-click installation process 201 | - Direct access to logs and binaries 202 | - Clear dependency requirements 203 | - Links to all required and recommended plugins 204 | - Real-time status updates through Svelte stores 205 | 206 | ### Recommended Plugins 207 | Added information about recommended plugins that enhance functionality: 208 | - Templater: For template-based operations 209 | - Smart Connections: For enhanced search capabilities 210 | - Local REST API: Required for Obsidian communication 211 | 212 | ### Platform Compatibility 213 | Implemented robust platform detection and path handling: 214 | - Windows: Handles UNC paths and environment variables 215 | - macOS: Proper binary permissions and config paths 216 | - Linux: Flexible configuration for various distributions 217 | 218 | ### Future Considerations 219 | 1. Version Management 220 | - Consider automated update checks 221 | - Add update notifications 222 | - Implement rollback capability 223 | 224 | 2. Configuration 225 | - Add backup/restore of Claude config 226 | - Support custom binary locations 227 | - Allow custom log paths 228 | 229 | 3. Error Recovery 230 | - Add self-repair functionality 231 | - Implement health checks 232 | - Add diagnostic tools 233 | ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/src/features/mcp-server-install/services/install.ts: -------------------------------------------------------------------------------- ```typescript 1 | import fs from "fs"; 2 | import fsp from "fs/promises"; 3 | import https from "https"; 4 | import { Notice, Plugin } from "obsidian"; 5 | import os from "os"; 6 | import { Observable } from "rxjs"; 7 | import { logger } from "$/shared"; 8 | import { GITHUB_DOWNLOAD_URL, type Arch, type Platform } from "../constants"; 9 | import type { DownloadProgress, InstallPathInfo } from "../types"; 10 | import { getInstallPath } from "./status"; 11 | 12 | export function getPlatform(): Platform { 13 | const platform = os.platform(); 14 | switch (platform) { 15 | case "darwin": 16 | return "macos"; 17 | case "win32": 18 | return "windows"; 19 | default: 20 | return "linux"; 21 | } 22 | } 23 | 24 | export function getArch(): Arch { 25 | return os.arch() as Arch; 26 | } 27 | 28 | export function getDownloadUrl(platform: Platform, arch: Arch): string { 29 | if (platform === "windows") { 30 | return `${GITHUB_DOWNLOAD_URL}/mcp-server-windows.exe`; 31 | } else if (platform === "macos") { 32 | return `${GITHUB_DOWNLOAD_URL}/mcp-server-macos-${arch}`; 33 | } else { // linux 34 | return `${GITHUB_DOWNLOAD_URL}/mcp-server-linux`; // Linux binary doesn't include arch in filename 35 | } 36 | } 37 | 38 | /** 39 | * Ensures that the specified directory path exists and is writable. 40 | * 41 | * If the directory does not exist, it will be created recursively. If the directory 42 | * exists but is not writable, an error will be thrown. 43 | * 44 | * @param dirpath - The real directory path to ensure exists and is writable. 45 | * @throws {Error} If the directory does not exist or is not writable. 46 | */ 47 | export async function ensureDirectory(dirpath: string) { 48 | try { 49 | if (!fs.existsSync(dirpath)) { 50 | await fsp.mkdir(dirpath, { recursive: true }); 51 | } 52 | 53 | // Verify directory was created and is writable 54 | try { 55 | await fsp.access(dirpath, fs.constants.W_OK); 56 | } catch (accessError) { 57 | throw new Error(`Directory exists but is not writable: ${dirpath}`); 58 | } 59 | } catch (error) { 60 | logger.error(`Failed to ensure directory:`, { error }); 61 | throw error; 62 | } 63 | } 64 | 65 | export function downloadFile( 66 | url: string, 67 | outputPath: string, 68 | redirects = 0, 69 | ): Observable<DownloadProgress> { 70 | return new Observable((subscriber) => { 71 | if (redirects > 5) { 72 | subscriber.error(new Error("Too many redirects")); 73 | return; 74 | } 75 | 76 | let fileStream: fs.WriteStream | undefined; 77 | const cleanup = (err?: unknown) => { 78 | if (err) { 79 | logger.debug("Cleaning up incomplete download:", { 80 | outputPath, 81 | writableFinished: JSON.stringify(fileStream?.writableFinished), 82 | error: err instanceof Error ? err.message : String(err), 83 | }); 84 | fileStream?.destroy(); 85 | fsp.unlink(outputPath).catch((unlinkError) => { 86 | logger.error("Failed to clean up incomplete download:", { 87 | outputPath, 88 | error: 89 | unlinkError instanceof Error 90 | ? unlinkError.message 91 | : String(unlinkError), 92 | }); 93 | }); 94 | } else { 95 | fileStream?.close(); 96 | fsp.chmod(outputPath, 0o755).catch((chmodError) => { 97 | logger.error("Failed to set executable permissions:", { 98 | outputPath, 99 | error: 100 | chmodError instanceof Error 101 | ? chmodError.message 102 | : String(chmodError), 103 | }); 104 | }); 105 | } 106 | }; 107 | 108 | https 109 | .get(url, (response) => { 110 | try { 111 | if (!response) { 112 | throw new Error("No response received"); 113 | } 114 | 115 | const statusCode = response.statusCode ?? 0; 116 | 117 | // Handle various HTTP status codes 118 | if (statusCode >= 400) { 119 | throw new Error( 120 | `HTTP Error ${statusCode}: ${response.statusMessage}`, 121 | ); 122 | } 123 | 124 | if (statusCode === 302 || statusCode === 301) { 125 | const redirectUrl = response.headers.location; 126 | if (!redirectUrl) { 127 | throw new Error( 128 | `Redirect (${statusCode}) received but no location header found`, 129 | ); 130 | } 131 | 132 | // Handle redirect by creating a new observable 133 | downloadFile(redirectUrl, outputPath, redirects + 1).subscribe( 134 | subscriber, 135 | ); 136 | return; 137 | } 138 | 139 | if (statusCode !== 200) { 140 | throw new Error(`Unexpected status code: ${statusCode}`); 141 | } 142 | 143 | // Validate content length 144 | const contentLength = response.headers["content-length"]; 145 | const totalBytes = contentLength ? parseInt(contentLength, 10) : 0; 146 | if (contentLength && isNaN(totalBytes)) { 147 | throw new Error("Invalid content-length header"); 148 | } 149 | 150 | try { 151 | fileStream = fs.createWriteStream(outputPath, { 152 | flags: "w", 153 | }); 154 | } catch (err) { 155 | throw new Error( 156 | `Failed to create write stream: ${err instanceof Error ? err.message : String(err)}`, 157 | ); 158 | } 159 | 160 | let downloadedBytes = 0; 161 | 162 | fileStream.on("error", (err) => { 163 | const fileStreamError = new Error( 164 | `File stream error: ${err.message}`, 165 | ); 166 | cleanup(fileStreamError); 167 | subscriber.error(fileStreamError); 168 | }); 169 | 170 | response.on("data", (chunk: Buffer) => { 171 | try { 172 | if (!Buffer.isBuffer(chunk)) { 173 | throw new Error("Received invalid data chunk"); 174 | } 175 | 176 | downloadedBytes += chunk.length; 177 | const percentage = totalBytes 178 | ? (downloadedBytes / totalBytes) * 100 179 | : 0; 180 | 181 | subscriber.next({ 182 | bytesReceived: downloadedBytes, 183 | totalBytes, 184 | percentage: Math.round(percentage * 100) / 100, 185 | }); 186 | } catch (err) { 187 | cleanup(err); 188 | subscriber.error(err); 189 | } 190 | }); 191 | 192 | response.pipe(fileStream); 193 | 194 | fileStream.on("finish", () => { 195 | cleanup(); 196 | subscriber.complete(); 197 | }); 198 | 199 | response.on("error", (err) => { 200 | cleanup(err); 201 | subscriber.error(new Error(`Response error: ${err.message}`)); 202 | }); 203 | } catch (err) { 204 | cleanup(err); 205 | subscriber.error(err instanceof Error ? err : new Error(String(err))); 206 | } 207 | }) 208 | .on("error", (err) => { 209 | cleanup(err); 210 | subscriber.error(new Error(`Network error: ${err.message}`)); 211 | }); 212 | }); 213 | } 214 | 215 | export async function installMcpServer( 216 | plugin: Plugin, 217 | ): Promise<InstallPathInfo> { 218 | try { 219 | const platform = getPlatform(); 220 | const arch = getArch(); 221 | const downloadUrl = getDownloadUrl(platform, arch); 222 | const installPath = await getInstallPath(plugin); 223 | if ("error" in installPath) throw new Error(installPath.error); 224 | 225 | await ensureDirectory(installPath.dir); 226 | 227 | const progressNotice = new Notice("Downloading MCP server...", 0); 228 | logger.debug("Downloading MCP server:", { downloadUrl, installPath }); 229 | 230 | const download$ = downloadFile(downloadUrl, installPath.path); 231 | 232 | return new Promise((resolve, reject) => { 233 | download$.subscribe({ 234 | next: (progress: DownloadProgress) => { 235 | progressNotice.setMessage( 236 | `Downloading MCP server: ${progress.percentage}%`, 237 | ); 238 | }, 239 | error: (error: Error) => { 240 | progressNotice.hide(); 241 | new Notice(`Failed to download MCP server: ${error.message}`); 242 | logger.error("Download failed:", { error, installPath }); 243 | reject(error); 244 | }, 245 | complete: () => { 246 | progressNotice.hide(); 247 | new Notice("MCP server downloaded successfully!"); 248 | logger.info("MCP server downloaded", { installPath }); 249 | resolve(installPath); 250 | }, 251 | }); 252 | }); 253 | } catch (error) { 254 | new Notice( 255 | `Failed to install MCP server: ${error instanceof Error ? error.message : String(error)}`, 256 | ); 257 | throw error; 258 | } 259 | } 260 | ``` -------------------------------------------------------------------------------- /packages/shared/src/types/plugin-local-rest-api.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { type } from "arktype"; 2 | 3 | /** 4 | * Error response from the API 5 | * Content-Type: application/json 6 | * Used in various error responses across endpoints 7 | * @property errorCode - A 5-digit error code uniquely identifying this particular type of error 8 | * @property message - Message describing the error 9 | */ 10 | export const ApiError = type({ 11 | errorCode: "number", 12 | message: "string", 13 | }); 14 | 15 | /** 16 | * JSON representation of a note including parsed tag and frontmatter data as well as filesystem metadata 17 | * Content-Type: application/vnd.olrapi.note+json 18 | * GET /vault/{filename} or GET /active/ with Accept: application/vnd.olrapi.note+json 19 | */ 20 | export const ApiNoteJson = type({ 21 | content: "string", 22 | frontmatter: "Record<string, string>", 23 | path: "string", 24 | stat: { 25 | ctime: "number", 26 | mtime: "number", 27 | size: "number", 28 | }, 29 | tags: "string[]", 30 | }); 31 | 32 | /** 33 | * Defines the structure of a plugin manifest, which contains metadata about a plugin. 34 | * This type is used to represent the response from the API's root endpoint, providing 35 | * basic server details and authentication status. 36 | */ 37 | const ApiPluginManifest = type({ 38 | id: "string", 39 | name: "string", 40 | version: "string", 41 | minAppVersion: "string", 42 | description: "string", 43 | author: "string", 44 | authorUrl: "string", 45 | isDesktopOnly: "boolean", 46 | dir: "string", 47 | }); 48 | 49 | /** 50 | * Response from the root endpoint providing basic server details and authentication status 51 | * Content-Type: application/json 52 | * GET / - This is the only API request that does not require authentication 53 | */ 54 | export const ApiStatusResponse = type({ 55 | status: "string", 56 | manifest: ApiPluginManifest, 57 | versions: { 58 | obsidian: "string", 59 | self: "string", 60 | }, 61 | service: "string", 62 | authenticated: "boolean", 63 | certificateInfo: { 64 | validityDays: "number", 65 | regenerateRecommended: "boolean", 66 | }, 67 | apiExtensions: ApiPluginManifest.array(), 68 | }); 69 | 70 | /** 71 | * Response from searching vault files using advanced search 72 | * Content-Type: application/json 73 | * POST /search/ 74 | * Returns array of matching files and their results 75 | * Results are only returned for non-falsy matches 76 | */ 77 | export const ApiSearchResponse = type({ 78 | filename: "string", 79 | result: "string|number|string[]|object|boolean", 80 | }).array(); 81 | 82 | /** 83 | * Match details for simple text search results 84 | * Content-Type: application/json 85 | * Used in ApiSimpleSearchResult 86 | */ 87 | export const ApiSimpleSearchMatch = type({ 88 | match: { 89 | start: "number", 90 | end: "number", 91 | }, 92 | context: "string", 93 | }); 94 | 95 | /** 96 | * Result from searching vault files with simple text search 97 | * Content-Type: application/json 98 | * POST /search/simple/ 99 | * Returns matches with surrounding context 100 | */ 101 | export const ApiSimpleSearchResponse = type({ 102 | filename: "string", 103 | matches: ApiSimpleSearchMatch.array(), 104 | score: "number", 105 | }).array(); 106 | 107 | /** 108 | * Result entry from semantic search 109 | * Content-Type: application/json 110 | * Used in ApiSearchResponse 111 | */ 112 | export const ApiSmartSearchResult = type({ 113 | path: "string", 114 | text: "string", 115 | score: "number", 116 | breadcrumbs: "string", 117 | }); 118 | 119 | /** 120 | * Response from semantic search containing list of matching results 121 | * Content-Type: application/json 122 | * POST /search/smart/ 123 | */ 124 | export const ApiSmartSearchResponse = type({ 125 | results: ApiSmartSearchResult.array(), 126 | }); 127 | 128 | /** 129 | * Parameters for semantic search request 130 | * Content-Type: application/json 131 | * POST /search/smart/ 132 | * @property query - A search phrase for semantic search 133 | * @property filter.folders - An array of folder names to include. For example, ["Public", "Work"] 134 | * @property filter.excludeFolders - An array of folder names to exclude. For example, ["Private", "Archive"] 135 | * @property filter.limit - The maximum number of results to return 136 | */ 137 | export const ApiSearchParameters = type({ 138 | query: "string", 139 | filter: { 140 | folders: "string[]?", 141 | excludeFolders: "string[]?", 142 | limit: "number?", 143 | }, 144 | }); 145 | 146 | /** 147 | * Command information from Obsidian's command palette 148 | * Content-Type: application/json 149 | * Used in ApiCommandsResponse 150 | */ 151 | export const ApiCommand = type({ 152 | id: "string", 153 | name: "string", 154 | }); 155 | 156 | /** 157 | * Response containing list of available Obsidian commands 158 | * Content-Type: application/json 159 | * GET /commands/ 160 | */ 161 | export const ApiCommandsResponse = type({ 162 | commands: ApiCommand.array(), 163 | }); 164 | 165 | /** 166 | * Response containing list of files in a vault directory 167 | * Content-Type: application/json 168 | * GET /vault/ or GET /vault/{pathToDirectory}/ 169 | * Note that empty directories will not be returned 170 | */ 171 | export const ApiVaultDirectoryResponse = type({ 172 | files: "string[]", 173 | }); 174 | 175 | /** 176 | * Response containing vault file information 177 | * Content-Type: application/json 178 | * POST /vault/{pathToFile} 179 | * Returns array of matching files and their results 180 | * Results are only returned for non-falsy matches 181 | */ 182 | export const ApiVaultFileResponse = type({ 183 | frontmatter: { 184 | tags: "string[]", 185 | description: "string?", 186 | }, 187 | content: "string", 188 | path: "string", 189 | stat: { 190 | ctime: "number", 191 | mtime: "number", 192 | size: "number", 193 | }, 194 | tags: "string[]", 195 | }); 196 | 197 | /** 198 | * Parameters for patching a file or document in the Obsidian plugin's REST API. 199 | * This type defines the expected request body for the patch operation. 200 | * 201 | * @property operation - Specifies how to modify the content: append (add after), prepend (add before), or replace existing content 202 | * @property targetType - Identifies what to modify: a section under a heading, a referenced block, or a frontmatter field 203 | * @property target - The identifier - either heading path (e.g. 'Heading 1::Subheading 1:1'), block reference ID, or frontmatter field name 204 | * @property targetDelimiter - The separator used in heading paths to indicate nesting (default '::') 205 | * @property trimTargetWhitespace - Whether to remove whitespace from target identifier before matching (default: false) 206 | * @property content - The actual content to insert, append, or use as replacement 207 | * @property contentType - Format of the content - use application/json for structured data like table rows or frontmatter values 208 | */ 209 | export const ApiPatchParameters = type({ 210 | operation: type("'append' | 'prepend' | 'replace'").describe( 211 | "Specifies how to modify the content: append (add after), prepend (add before), or replace existing content", 212 | ), 213 | targetType: type("'heading' | 'block' | 'frontmatter'").describe( 214 | "Identifies what to modify: a section under a heading, a referenced block, or a frontmatter field", 215 | ), 216 | target: type("string").describe( 217 | "The identifier - either heading path (e.g. 'Heading 1::Subheading 1:1'), block reference ID, or frontmatter field name", 218 | ), 219 | "targetDelimiter?": type("string").describe( 220 | "The separator used in heading paths to indicate nesting (default '::')", 221 | ), 222 | "trimTargetWhitespace?": type("boolean").describe( 223 | "Whether to remove whitespace from target identifier before matching (default: false)", 224 | ), 225 | content: type("string").describe( 226 | "The actual content to insert, append, or use as replacement", 227 | ), 228 | "contentType?": type("'text/markdown' | 'application/json'").describe( 229 | "Format of the content - use application/json for structured data like table rows or frontmatter values", 230 | ), 231 | }); 232 | 233 | /** 234 | * Represents a response containing markdown content 235 | */ 236 | export const ApiContentResponse = type("string").describe("Content"); 237 | 238 | /** 239 | * Empty response for successful operations that don't return content 240 | * Content-Type: none (204 No Content) 241 | * Used by: 242 | * - PUT /vault/{filename} 243 | * - PUT /active/ 244 | * - PUT /periodic/{period}/ 245 | * - POST /commands/{commandId}/ 246 | * - DELETE endpoints 247 | * Returns 204 No Content 248 | */ 249 | export const ApiNoContentResponse = type("unknown").describe("No Content"); 250 | 251 | /** 252 | * Parameters for executing a template 253 | * Content-Type: application/json 254 | * POST /templates/execute/ 255 | * @property name - The name of the template to execute 256 | * @property arguments - A key-value object of arguments to pass to the template 257 | * @property createFile - Whether to create a new file from the template 258 | * @property targetPath - The path to save the file; required if createFile is true 259 | */ 260 | export const ApiTemplateExecutionParams = type({ 261 | name: type("string").describe("The full vault path to the template file"), 262 | arguments: "Record<string, string>", 263 | "createFile?": type("boolean").describe( 264 | "Whether to create a new file from the template", 265 | ), 266 | "targetPath?": type("string").describe( 267 | "Path to save the file; required if createFile is true", 268 | ), 269 | }); 270 | 271 | /** 272 | * Response from executing a template 273 | * Content-Type: application/json 274 | * POST /templates/execute/ 275 | * @property message - A message describing the result of the template execution 276 | */ 277 | export const ApiTemplateExecutionResponse = type({ 278 | message: "string", 279 | content: "string", 280 | }); 281 | 282 | // Export types for TypeScript usage 283 | export type ApiErrorType = typeof ApiError.infer; 284 | export type ApiNoteJsonType = typeof ApiNoteJson.infer; 285 | export type ApiStatusResponseType = typeof ApiStatusResponse.infer; 286 | export type ApiSearchResponseType = typeof ApiSearchResponse.infer; 287 | export type ApiSimpleSearchResponseType = typeof ApiSimpleSearchResponse.infer; 288 | export type ApiSmartSearchResultType = typeof ApiSmartSearchResult.infer; 289 | export type ApiSmartSearchResponseType = typeof ApiSmartSearchResponse.infer; 290 | export type ApiCommandType = typeof ApiCommand.infer; 291 | export type ApiCommandsResponseType = typeof ApiCommandsResponse.infer; 292 | export type ApiVaultDirectoryResponseType = 293 | typeof ApiVaultDirectoryResponse.infer; 294 | export type ApiVaultFileResponseType = typeof ApiVaultFileResponse.infer; 295 | export type ApiSearchParametersType = typeof ApiSearchParameters.infer; 296 | export type ApiNoContentResponseType = typeof ApiNoContentResponse.infer; 297 | export type ApiTemplateExecutionParamsType = 298 | typeof ApiTemplateExecutionParams.infer; 299 | export type ApiTemplateExecutionResponseType = 300 | typeof ApiTemplateExecutionResponse.infer; 301 | 302 | // Additional API response types can be added here 303 | export const MIME_TYPE_OLRAPI_NOTE_JSON = "application/vnd.olrapi.note+json"; 304 | ``` -------------------------------------------------------------------------------- /packages/mcp-server/src/features/local-rest-api/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { makeRequest, type ToolRegistry } from "$/shared"; 2 | import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; 3 | import { type } from "arktype"; 4 | import { LocalRestAPI } from "shared"; 5 | 6 | export function registerLocalRestApiTools(tools: ToolRegistry, server: Server) { 7 | // GET Status 8 | tools.register( 9 | type({ 10 | name: '"get_server_info"', 11 | arguments: "Record<string, unknown>", 12 | }).describe( 13 | "Returns basic details about the Obsidian Local REST API and authentication status. This is the only API request that does not require authentication.", 14 | ), 15 | async () => { 16 | const data = await makeRequest(LocalRestAPI.ApiStatusResponse, "/"); 17 | return { 18 | content: [{ type: "text", text: JSON.stringify(data, null, 2) }], 19 | }; 20 | }, 21 | ); 22 | 23 | // GET Active File 24 | tools.register( 25 | type({ 26 | name: '"get_active_file"', 27 | arguments: { 28 | format: type('"markdown" | "json"').optional(), 29 | }, 30 | }).describe( 31 | "Returns the content of the currently active file in Obsidian. Can return either markdown content or a JSON representation including parsed tags and frontmatter.", 32 | ), 33 | async ({ arguments: args }) => { 34 | const format = 35 | args?.format === "json" 36 | ? "application/vnd.olrapi.note+json" 37 | : "text/markdown"; 38 | const data = await makeRequest( 39 | LocalRestAPI.ApiNoteJson.or("string"), 40 | "/active/", 41 | { 42 | headers: { Accept: format }, 43 | }, 44 | ); 45 | const content = 46 | typeof data === "string" ? data : JSON.stringify(data, null, 2); 47 | return { content: [{ type: "text", text: content }] }; 48 | }, 49 | ); 50 | 51 | // PUT Active File 52 | tools.register( 53 | type({ 54 | name: '"update_active_file"', 55 | arguments: { 56 | content: "string", 57 | }, 58 | }).describe("Update the content of the active file open in Obsidian."), 59 | async ({ arguments: args }) => { 60 | await makeRequest(LocalRestAPI.ApiNoContentResponse, "/active/", { 61 | method: "PUT", 62 | body: args.content, 63 | }); 64 | return { 65 | content: [{ type: "text", text: "File updated successfully" }], 66 | }; 67 | }, 68 | ); 69 | 70 | // POST Active File 71 | tools.register( 72 | type({ 73 | name: '"append_to_active_file"', 74 | arguments: { 75 | content: "string", 76 | }, 77 | }).describe("Append content to the end of the currently-open note."), 78 | async ({ arguments: args }) => { 79 | await makeRequest(LocalRestAPI.ApiNoContentResponse, "/active/", { 80 | method: "POST", 81 | body: args.content, 82 | }); 83 | return { 84 | content: [{ type: "text", text: "Content appended successfully" }], 85 | }; 86 | }, 87 | ); 88 | 89 | // PATCH Active File 90 | tools.register( 91 | type({ 92 | name: '"patch_active_file"', 93 | arguments: LocalRestAPI.ApiPatchParameters, 94 | }).describe( 95 | "Insert or modify content in the currently-open note relative to a heading, block reference, or frontmatter field.", 96 | ), 97 | async ({ arguments: args }) => { 98 | const headers: Record<string, string> = { 99 | Operation: args.operation, 100 | "Target-Type": args.targetType, 101 | Target: args.target, 102 | "Create-Target-If-Missing": "true", 103 | }; 104 | 105 | if (args.targetDelimiter) { 106 | headers["Target-Delimiter"] = args.targetDelimiter; 107 | } 108 | if (args.trimTargetWhitespace !== undefined) { 109 | headers["Trim-Target-Whitespace"] = String(args.trimTargetWhitespace); 110 | } 111 | if (args.contentType) { 112 | headers["Content-Type"] = args.contentType; 113 | } 114 | 115 | const response = await makeRequest( 116 | LocalRestAPI.ApiContentResponse, 117 | "/active/", 118 | { 119 | method: "PATCH", 120 | headers, 121 | body: args.content, 122 | }, 123 | ); 124 | return { 125 | content: [ 126 | { type: "text", text: "File patched successfully" }, 127 | { type: "text", text: response }, 128 | ], 129 | }; 130 | }, 131 | ); 132 | 133 | // DELETE Active File 134 | tools.register( 135 | type({ 136 | name: '"delete_active_file"', 137 | arguments: "Record<string, unknown>", 138 | }).describe("Delete the currently-active file in Obsidian."), 139 | async () => { 140 | await makeRequest(LocalRestAPI.ApiNoContentResponse, "/active/", { 141 | method: "DELETE", 142 | }); 143 | return { 144 | content: [{ type: "text", text: "File deleted successfully" }], 145 | }; 146 | }, 147 | ); 148 | 149 | // POST Open File in Obsidian UI 150 | tools.register( 151 | type({ 152 | name: '"show_file_in_obsidian"', 153 | arguments: { 154 | filename: "string", 155 | "newLeaf?": "boolean", 156 | }, 157 | }).describe( 158 | "Open a document in the Obsidian UI. Creates a new document if it doesn't exist. Returns a confirmation if the file was opened successfully.", 159 | ), 160 | async ({ arguments: args }) => { 161 | const query = args.newLeaf ? "?newLeaf=true" : ""; 162 | 163 | await makeRequest( 164 | LocalRestAPI.ApiNoContentResponse, 165 | `/open/${encodeURIComponent(args.filename)}${query}`, 166 | { 167 | method: "POST", 168 | }, 169 | ); 170 | 171 | return { 172 | content: [{ type: "text", text: "File opened successfully" }], 173 | }; 174 | }, 175 | ); 176 | 177 | // POST Search via Dataview or JsonLogic 178 | tools.register( 179 | type({ 180 | name: '"search_vault"', 181 | arguments: { 182 | queryType: '"dataview" | "jsonlogic"', 183 | query: "string", 184 | }, 185 | }).describe( 186 | "Search for documents matching a specified query using either Dataview DQL or JsonLogic.", 187 | ), 188 | async ({ arguments: args }) => { 189 | const contentType = 190 | args.queryType === "dataview" 191 | ? "application/vnd.olrapi.dataview.dql+txt" 192 | : "application/vnd.olrapi.jsonlogic+json"; 193 | 194 | const data = await makeRequest( 195 | LocalRestAPI.ApiSearchResponse, 196 | "/search/", 197 | { 198 | method: "POST", 199 | headers: { "Content-Type": contentType }, 200 | body: args.query, 201 | }, 202 | ); 203 | 204 | return { 205 | content: [{ type: "text", text: JSON.stringify(data, null, 2) }], 206 | }; 207 | }, 208 | ); 209 | 210 | // POST Simple Search 211 | tools.register( 212 | type({ 213 | name: '"search_vault_simple"', 214 | arguments: { 215 | query: "string", 216 | "contextLength?": "number", 217 | }, 218 | }).describe("Search for documents matching a text query."), 219 | async ({ arguments: args }) => { 220 | const query = new URLSearchParams({ 221 | query: args.query, 222 | ...(args.contextLength 223 | ? { 224 | contextLength: String(args.contextLength), 225 | } 226 | : {}), 227 | }); 228 | 229 | const data = await makeRequest( 230 | LocalRestAPI.ApiSimpleSearchResponse, 231 | `/search/simple/?${query}`, 232 | { 233 | method: "POST", 234 | }, 235 | ); 236 | 237 | return { 238 | content: [{ type: "text", text: JSON.stringify(data, null, 2) }], 239 | }; 240 | }, 241 | ); 242 | 243 | // GET Vault Files or Directories List 244 | tools.register( 245 | type({ 246 | name: '"list_vault_files"', 247 | arguments: { 248 | "directory?": "string", 249 | }, 250 | }).describe( 251 | "List files in the root directory or a specified subdirectory of your vault.", 252 | ), 253 | async ({ arguments: args }) => { 254 | const path = args.directory ? `${args.directory}/` : ""; 255 | const data = await makeRequest( 256 | LocalRestAPI.ApiVaultFileResponse.or( 257 | LocalRestAPI.ApiVaultDirectoryResponse, 258 | ), 259 | `/vault/${path}`, 260 | ); 261 | return { 262 | content: [{ type: "text", text: JSON.stringify(data, null, 2) }], 263 | }; 264 | }, 265 | ); 266 | 267 | // GET Vault File Content 268 | tools.register( 269 | type({ 270 | name: '"get_vault_file"', 271 | arguments: { 272 | filename: "string", 273 | "format?": '"markdown" | "json"', 274 | }, 275 | }).describe("Get the content of a file from your vault."), 276 | async ({ arguments: args }) => { 277 | const isJson = args.format === "json"; 278 | const format = isJson 279 | ? "application/vnd.olrapi.note+json" 280 | : "text/markdown"; 281 | const data = await makeRequest( 282 | isJson ? LocalRestAPI.ApiNoteJson : LocalRestAPI.ApiContentResponse, 283 | `/vault/${encodeURIComponent(args.filename)}`, 284 | { 285 | headers: { Accept: format }, 286 | }, 287 | ); 288 | return { 289 | content: [ 290 | { 291 | type: "text", 292 | text: 293 | typeof data === "string" ? data : JSON.stringify(data, null, 2), 294 | }, 295 | ], 296 | }; 297 | }, 298 | ); 299 | 300 | // PUT Vault File Content 301 | tools.register( 302 | type({ 303 | name: '"create_vault_file"', 304 | arguments: { 305 | filename: "string", 306 | content: "string", 307 | }, 308 | }).describe("Create a new file in your vault or update an existing one."), 309 | async ({ arguments: args }) => { 310 | await makeRequest( 311 | LocalRestAPI.ApiNoContentResponse, 312 | `/vault/${encodeURIComponent(args.filename)}`, 313 | { 314 | method: "PUT", 315 | body: args.content, 316 | }, 317 | ); 318 | return { 319 | content: [{ type: "text", text: "File created successfully" }], 320 | }; 321 | }, 322 | ); 323 | 324 | // POST Vault File Content 325 | tools.register( 326 | type({ 327 | name: '"append_to_vault_file"', 328 | arguments: { 329 | filename: "string", 330 | content: "string", 331 | }, 332 | }).describe("Append content to a new or existing file."), 333 | async ({ arguments: args }) => { 334 | await makeRequest( 335 | LocalRestAPI.ApiNoContentResponse, 336 | `/vault/${encodeURIComponent(args.filename)}`, 337 | { 338 | method: "POST", 339 | body: args.content, 340 | }, 341 | ); 342 | return { 343 | content: [{ type: "text", text: "Content appended successfully" }], 344 | }; 345 | }, 346 | ); 347 | 348 | // PATCH Vault File Content 349 | tools.register( 350 | type({ 351 | name: '"patch_vault_file"', 352 | arguments: type({ 353 | filename: "string", 354 | }).and(LocalRestAPI.ApiPatchParameters), 355 | }).describe( 356 | "Insert or modify content in a file relative to a heading, block reference, or frontmatter field.", 357 | ), 358 | async ({ arguments: args }) => { 359 | const headers: HeadersInit = { 360 | Operation: args.operation, 361 | "Target-Type": args.targetType, 362 | Target: args.target, 363 | "Create-Target-If-Missing": "true", 364 | }; 365 | 366 | if (args.targetDelimiter) { 367 | headers["Target-Delimiter"] = args.targetDelimiter; 368 | } 369 | if (args.trimTargetWhitespace !== undefined) { 370 | headers["Trim-Target-Whitespace"] = String(args.trimTargetWhitespace); 371 | } 372 | if (args.contentType) { 373 | headers["Content-Type"] = args.contentType; 374 | } 375 | 376 | const response = await makeRequest( 377 | LocalRestAPI.ApiContentResponse, 378 | `/vault/${encodeURIComponent(args.filename)}`, 379 | { 380 | method: "PATCH", 381 | headers, 382 | body: args.content, 383 | }, 384 | ); 385 | 386 | return { 387 | content: [ 388 | { type: "text", text: "File patched successfully" }, 389 | { type: "text", text: response }, 390 | ], 391 | }; 392 | }, 393 | ); 394 | 395 | // DELETE Vault File Content 396 | tools.register( 397 | type({ 398 | name: '"delete_vault_file"', 399 | arguments: { 400 | filename: "string", 401 | }, 402 | }).describe("Delete a file from your vault."), 403 | async ({ arguments: args }) => { 404 | await makeRequest( 405 | LocalRestAPI.ApiNoContentResponse, 406 | `/vault/${encodeURIComponent(args.filename)}`, 407 | { 408 | method: "DELETE", 409 | }, 410 | ); 411 | return { 412 | content: [{ type: "text", text: "File deleted successfully" }], 413 | }; 414 | }, 415 | ); 416 | } 417 | ``` -------------------------------------------------------------------------------- /packages/obsidian-plugin/docs/openapi.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Extended from Obsidian Local REST API OpenAPI Specification 2 | # https://coddingtonbear.github.io/obsidian-local-rest-api/#/ 3 | components: 4 | schemas: 5 | Error: 6 | properties: 7 | errorCode: 8 | description: | 9 | A 5-digit error code uniquely identifying this particular type of error. 10 | example: 40149 11 | type: "number" 12 | message: 13 | description: "Message describing the error." 14 | example: "A brief description of the error." 15 | type: "string" 16 | type: "object" 17 | NoteJson: 18 | properties: 19 | content: 20 | type: "string" 21 | frontmatter: 22 | type: "object" 23 | path: 24 | type: "string" 25 | stat: 26 | properties: 27 | ctime: 28 | type: "number" 29 | mtime: 30 | type: "number" 31 | size: 32 | type: "number" 33 | required: 34 | - "ctime" 35 | - "mtime" 36 | - "size" 37 | type: "object" 38 | tags: 39 | items: 40 | type: "string" 41 | type: "array" 42 | required: 43 | - "tags" 44 | - "frontmatter" 45 | - "stat" 46 | - "path" 47 | - "content" 48 | type: "object" 49 | SearchParameters: 50 | type: object 51 | required: 52 | - query 53 | properties: 54 | query: 55 | type: string 56 | description: A search phrase for semantic search 57 | minLength: 1 58 | filter: 59 | type: object 60 | properties: 61 | folders: 62 | type: array 63 | items: 64 | type: string 65 | description: 'An array of folder names to include. For example, ["Public", "Work"]' 66 | excludeFolders: 67 | type: array 68 | items: 69 | type: string 70 | description: 'An array of folder names to exclude. For example, ["Private", "Archive"]' 71 | limit: 72 | type: number 73 | minimum: 1 74 | description: The maximum number of results to return 75 | SearchResponse: 76 | type: object 77 | required: 78 | - results 79 | properties: 80 | results: 81 | type: array 82 | items: 83 | type: object 84 | required: 85 | - path 86 | - text 87 | - score 88 | - breadcrumbs 89 | properties: 90 | path: 91 | type: string 92 | text: 93 | type: string 94 | score: 95 | type: number 96 | breadcrumbs: 97 | type: string 98 | securitySchemes: 99 | apiKeyAuth: 100 | description: | 101 | Find your API Key in your Obsidian settings 102 | in the "Local REST API" section under "Plugins". 103 | scheme: "bearer" 104 | type: "http" 105 | info: 106 | description: | 107 | You can use this interface for trying out your Local REST API in Obsidian. 108 | 109 | Before trying the below tools, you will want to make sure you press the "Authorize" button below and provide the API Key you are shown when you open the "Local REST API" section of your Obsidian settings. All requests to the API require a valid API Key; so you won't get very far without doing that. 110 | 111 | When using this tool you may see browser security warnings due to your browser not trusting the self-signed certificate the plugin will generate on its first run. If you do, you can make those errors disappear by adding the certificate as a "Trusted Certificate" in your browser or operating system's settings. 112 | title: "Local REST API for Obsidian" 113 | version: "1.0" 114 | openapi: "3.0.2" 115 | paths: 116 | /: 117 | get: 118 | description: | 119 | Returns basic details about the server as well as your authentication status. 120 | 121 | This is the only API request that does *not* require authentication. 122 | responses: 123 | "200": 124 | content: 125 | application/json: 126 | schema: 127 | properties: 128 | authenticated: 129 | description: "Is your current request authenticated?" 130 | type: "boolean" 131 | ok: 132 | description: "'OK'" 133 | type: "string" 134 | service: 135 | description: "'Obsidian Local REST API'" 136 | type: "string" 137 | versions: 138 | properties: 139 | obsidian: 140 | description: "Obsidian plugin API version" 141 | type: "string" 142 | self: 143 | description: "Plugin version." 144 | type: "string" 145 | type: "object" 146 | type: "object" 147 | description: "Success" 148 | summary: | 149 | Returns basic details about the server. 150 | tags: 151 | - "Status" 152 | /active/: 153 | delete: 154 | parameters: [] 155 | responses: 156 | "204": 157 | description: "Success" 158 | "404": 159 | content: 160 | application/json: 161 | schema: 162 | "$ref": "#/components/schemas/Error" 163 | description: "File does not exist." 164 | "405": 165 | content: 166 | application/json: 167 | schema: 168 | "$ref": "#/components/schemas/Error" 169 | description: | 170 | Your path references a directory instead of a file; this request method is valid only for updating files. 171 | summary: | 172 | Deletes the currently-active file in Obsidian. 173 | tags: 174 | - "Active File" 175 | get: 176 | description: | 177 | Returns the content of the currently active file in Obsidian. 178 | 179 | If you specify the header `Accept: application/vnd.olrapi.note+json`, will return a JSON representation of your note including parsed tag and frontmatter data as well as filesystem metadata. See "responses" below for details. 180 | parameters: [] 181 | responses: 182 | "200": 183 | content: 184 | "application/vnd.olrapi.note+json": 185 | schema: 186 | "$ref": "#/components/schemas/NoteJson" 187 | text/markdown: 188 | schema: 189 | example: | 190 | # This is my document 191 | 192 | something else here 193 | type: "string" 194 | description: "Success" 195 | "404": 196 | description: "File does not exist" 197 | summary: | 198 | Return the content of the active file open in Obsidian. 199 | tags: 200 | - "Active File" 201 | patch: 202 | description: | 203 | Inserts content into the currently-open note relative to a heading within that note. 204 | 205 | Allows you to modify the content relative to a heading, block reference, or frontmatter field in your document. 206 | 207 | Note that this API was changed in Version 3.0 of this extension and the earlier PATCH API is now deprecated. Requests made using the previous version of this API will continue to work until Version 4.0 is released. See https://github.com/coddingtonbear/obsidian-local-rest-api/wiki/Changes-to-PATCH-requests-between-versions-2.0-and-3.0 for more details and migration instructions. 208 | 209 | # Examples 210 | 211 | All of the below examples assume you have a document that looks like 212 | this: 213 | 214 | ```markdown 215 | --- 216 | alpha: 1 217 | beta: test 218 | delta: 219 | zeta: 1 220 | yotta: 1 221 | gamma: 222 | - one 223 | - two 224 | --- 225 | 226 | # Heading 1 227 | 228 | This is the content for heading one 229 | 230 | Also references some [[#^484ef2]] 231 | 232 | ## Subheading 1:1 233 | Content for Subheading 1:1 234 | 235 | ### Subsubheading 1:1:1 236 | 237 | ### Subsubheading 1:1:2 238 | 239 | Testing how block references work for a table.[[#^2c7cfa]] 240 | Some content for Subsubheading 1:1:2 241 | 242 | More random text. 243 | 244 | ^2d9b4a 245 | 246 | ## Subheading 1:2 247 | 248 | Content for Subheading 1:2. 249 | 250 | some content with a block reference ^484ef2 251 | 252 | ## Subheading 1:3 253 | | City | Population | 254 | | ------------ | ---------- | 255 | | Seattle, WA | 8 | 256 | | Portland, OR | 4 | 257 | 258 | ^2c7cfa 259 | ``` 260 | 261 | ## Append Content Below a Heading 262 | 263 | If you wanted to append the content "Hello" below "Subheading 1:1:1" under "Heading 1", 264 | you could send a request with the following headers: 265 | 266 | - `Operation`: `append` 267 | - `Target-Type`: `heading` 268 | - `Target`: `Heading 1::Subheading 1:1:1` 269 | - with the request body: `Hello` 270 | 271 | The above would work just fine for `prepend` or `replace`, too, of course, 272 | but with different results. 273 | 274 | ## Append Content to a Block Reference 275 | 276 | If you wanted to append the content "Hello" below the block referenced by 277 | "2d9b4a" above ("More random text."), you could send the following headers: 278 | 279 | - `Operation`: `append` 280 | - `Target-Type`: `block` 281 | - `Target`: `2d9b4a` 282 | - with the request body: `Hello` 283 | 284 | The above would work just fine for `prepend` or `replace`, too, of course, 285 | but with different results. 286 | 287 | ## Add a Row to a Table Referenced by a Block Reference 288 | 289 | If you wanted to add a new city ("Chicago, IL") and population ("16") pair to the table above 290 | referenced by the block reference `2c7cfa`, you could send the following 291 | headers: 292 | 293 | - `Operation`: `append` 294 | - `TargetType`: `block` 295 | - `Target`: `2c7cfa` 296 | - `Content-Type`: `application/json` 297 | - with the request body: `[["Chicago, IL", "16"]]` 298 | 299 | The use of a `Content-Type` of `application/json` allows the API 300 | to infer that member of your array represents rows and columns of your 301 | to append to the referenced table. You can of course just use a 302 | `Content-Type` of `text/markdown`, but in such a case you'll have to 303 | format your table row manually instead of letting the library figure 304 | it out for you. 305 | 306 | You also have the option of using `prepend` (in which case, your new 307 | row would be the first -- right below the table heading) or `replace` (in which 308 | case all rows except the table heading would be replaced by the new row(s) 309 | you supplied). 310 | 311 | ## Setting a Frontmatter Field 312 | 313 | If you wanted to set the frontmatter field `alpha` to `2`, you could 314 | send the following headers: 315 | 316 | - `Operation`: `replace` 317 | - `TargetType`: `frontmatter` 318 | - `Target`: `alpha` 319 | - with the request body `2` 320 | 321 | If you're setting a frontmatter field that might not already exist 322 | you may want to use the `Create-Target-If-Missing` header so the 323 | new frontmatter field is created and set to your specified value 324 | if it doesn't already exist. 325 | 326 | You may find using a `Content-Type` of `application/json` to be 327 | particularly useful in the case of frontmatter since frontmatter 328 | fields' values are JSON data, and the API can be smarter about 329 | interpreting your `prepend` or `append` requests if you specify 330 | your data as JSON (particularly when appending, for example, 331 | list items). 332 | parameters: 333 | - description: "Patch operation to perform" 334 | in: "header" 335 | name: "Operation" 336 | required: true 337 | schema: 338 | enum: 339 | - "append" 340 | - "prepend" 341 | - "replace" 342 | type: "string" 343 | - description: "Type of target to patch" 344 | in: "header" 345 | name: "Target-Type" 346 | required: true 347 | schema: 348 | enum: 349 | - "heading" 350 | - "block" 351 | - "frontmatter" 352 | type: "string" 353 | - description: "Delimiter to use for nested targets (i.e. Headings)" 354 | in: "header" 355 | name: "Target-Delimiter" 356 | required: false 357 | schema: 358 | default: "::" 359 | type: "string" 360 | - description: | 361 | Target to patch; this value can be URL-Encoded and *must* 362 | be URL-Encoded if it includes non-ASCII characters. 363 | in: "header" 364 | name: "Target" 365 | required: true 366 | schema: 367 | type: "string" 368 | - description: "Trim whitespace from Target before applying patch?" 369 | in: "header" 370 | name: "Trim-Target-Whitespace" 371 | required: false 372 | schema: 373 | default: "false" 374 | enum: 375 | - "true" 376 | - "false" 377 | type: "string" 378 | requestBody: 379 | content: 380 | application/json: 381 | schema: 382 | example: "['one', 'two']" 383 | type: "string" 384 | text/markdown: 385 | schema: 386 | example: | 387 | # This is my document 388 | 389 | something else here 390 | type: "string" 391 | description: "Content you would like to insert." 392 | required: true 393 | responses: 394 | "200": 395 | description: "Success" 396 | "400": 397 | content: 398 | application/json: 399 | schema: 400 | "$ref": "#/components/schemas/Error" 401 | description: "Bad Request; see response message for details." 402 | "404": 403 | content: 404 | application/json: 405 | schema: 406 | "$ref": "#/components/schemas/Error" 407 | description: "Does not exist" 408 | "405": 409 | content: 410 | application/json: 411 | schema: 412 | "$ref": "#/components/schemas/Error" 413 | description: | 414 | Your path references a directory instead of a file; this request method is valid only for updating files. 415 | summary: | 416 | Insert content into the currently open note in Obsidian relative to a heading within that document. 417 | tags: 418 | - "Active File" 419 | post: 420 | description: | 421 | Appends content to the end of the currently-open note. 422 | 423 | If you would like to insert text relative to a particular heading instead of appending to the end of the file, see 'patch'. 424 | parameters: [] 425 | requestBody: 426 | content: 427 | text/markdown: 428 | schema: 429 | example: | 430 | # This is my document 431 | 432 | something else here 433 | type: "string" 434 | description: "Content you would like to append." 435 | required: true 436 | responses: 437 | "204": 438 | description: "Success" 439 | "400": 440 | content: 441 | application/json: 442 | schema: 443 | "$ref": "#/components/schemas/Error" 444 | description: "Bad Request" 445 | "405": 446 | content: 447 | application/json: 448 | schema: 449 | "$ref": "#/components/schemas/Error" 450 | description: | 451 | Your path references a directory instead of a file; this request method is valid only for updating files. 452 | summary: | 453 | Append content to the active file open in Obsidian. 454 | tags: 455 | - "Active File" 456 | put: 457 | requestBody: 458 | content: 459 | "*/*": 460 | schema: 461 | type: "string" 462 | text/markdown: 463 | schema: 464 | example: | 465 | # This is my document 466 | 467 | something else here 468 | type: "string" 469 | description: "Content of the file you would like to upload." 470 | required: true 471 | responses: 472 | "204": 473 | description: "Success" 474 | "400": 475 | content: 476 | application/json: 477 | schema: 478 | "$ref": "#/components/schemas/Error" 479 | description: | 480 | Incoming file could not be processed. Make sure you have specified a reasonable file name, and make sure you have set a reasonable 'Content-Type' header; if you are uploading a note, 'text/markdown' is likely the right choice. 481 | "405": 482 | content: 483 | application/json: 484 | schema: 485 | "$ref": "#/components/schemas/Error" 486 | description: | 487 | Your path references a directory instead of a file; this request method is valid only for updating files. 488 | summary: | 489 | Update the content of the active file open in Obsidian. 490 | tags: 491 | - "Active File" 492 | /commands/: 493 | get: 494 | responses: 495 | "200": 496 | content: 497 | application/json: 498 | example: 499 | commands: 500 | - id: "global-search:open" 501 | name: "Search: Search in all files" 502 | - id: "graph:open" 503 | name: "Graph view: Open graph view" 504 | schema: 505 | properties: 506 | commands: 507 | items: 508 | properties: 509 | id: 510 | type: "string" 511 | name: 512 | type: "string" 513 | type: "object" 514 | type: "array" 515 | type: "object" 516 | description: "A list of available commands." 517 | summary: | 518 | Get a list of available commands. 519 | tags: 520 | - "Commands" 521 | "/commands/{commandId}/": 522 | post: 523 | parameters: 524 | - description: "The id of the command to execute" 525 | in: "path" 526 | name: "commandId" 527 | required: true 528 | schema: 529 | type: "string" 530 | responses: 531 | "204": 532 | description: "Success" 533 | "404": 534 | content: 535 | application/json: 536 | schema: 537 | "$ref": "#/components/schemas/Error" 538 | description: "The command you specified does not exist." 539 | summary: | 540 | Execute a command. 541 | tags: 542 | - "Commands" 543 | "/open/{filename}": 544 | post: 545 | description: | 546 | Opens the specified document in Obsidian. 547 | 548 | Note: Obsidian will create a new document at the path you have 549 | specified if such a document did not already exist. 550 | parameters: 551 | - description: | 552 | Path to the file to return (relative to your vault root). 553 | in: "path" 554 | name: "filename" 555 | required: true 556 | schema: 557 | format: "path" 558 | type: "string" 559 | - description: "Open this as a new leaf?" 560 | in: "query" 561 | name: "newLeaf" 562 | required: false 563 | schema: 564 | type: "boolean" 565 | responses: 566 | "200": 567 | description: "Success" 568 | summary: | 569 | Open the specified document in Obsidian 570 | tags: 571 | - "Open" 572 | "/periodic/{period}/": 573 | delete: 574 | description: | 575 | Deletes the periodic note for the specified period. 576 | parameters: 577 | - description: "The name of the period for which you would like to grab the current note." 578 | in: "path" 579 | name: "period" 580 | required: true 581 | schema: 582 | default: "daily" 583 | enum: 584 | - "daily" 585 | - "weekly" 586 | - "monthly" 587 | - "quarterly" 588 | - "yearly" 589 | type: "string" 590 | responses: 591 | "204": 592 | description: "Success" 593 | "404": 594 | content: 595 | application/json: 596 | schema: 597 | "$ref": "#/components/schemas/Error" 598 | description: "File does not exist." 599 | "405": 600 | content: 601 | application/json: 602 | schema: 603 | "$ref": "#/components/schemas/Error" 604 | description: | 605 | Your path references a directory instead of a file; this request method is valid only for updating files. 606 | summary: | 607 | Delete a periodic note. 608 | tags: 609 | - "Periodic Notes" 610 | get: 611 | parameters: 612 | - description: "The name of the period for which you would like to grab the current note." 613 | in: "path" 614 | name: "period" 615 | required: true 616 | schema: 617 | default: "daily" 618 | enum: 619 | - "daily" 620 | - "weekly" 621 | - "monthly" 622 | - "quarterly" 623 | - "yearly" 624 | type: "string" 625 | responses: 626 | "200": 627 | content: 628 | "application/vnd.olrapi.note+json": 629 | schema: 630 | "$ref": "#/components/schemas/NoteJson" 631 | text/markdown: 632 | schema: 633 | example: | 634 | # This is my document 635 | 636 | something else here 637 | type: "string" 638 | description: "Success" 639 | "404": 640 | description: "File does not exist" 641 | summary: | 642 | Get current periodic note for the specified period. 643 | tags: 644 | - "Periodic Notes" 645 | patch: 646 | description: | 647 | Inserts content into an existing note relative to a heading within your note. 648 | 649 | Allows you to modify the content relative to a heading, block reference, or frontmatter field in your document. 650 | 651 | Note that this API was changed in Version 3.0 of this extension and the earlier PATCH API is now deprecated. Requests made using the previous version of this API will continue to work until Version 4.0 is released. See https://github.com/coddingtonbear/obsidian-local-rest-api/wiki/Changes-to-PATCH-requests-between-versions-2.0-and-3.0 for more details and migration instructions. 652 | 653 | # Examples 654 | 655 | All of the below examples assume you have a document that looks like 656 | this: 657 | 658 | ```markdown 659 | --- 660 | alpha: 1 661 | beta: test 662 | delta: 663 | zeta: 1 664 | yotta: 1 665 | gamma: 666 | - one 667 | - two 668 | --- 669 | 670 | # Heading 1 671 | 672 | This is the content for heading one 673 | 674 | Also references some [[#^484ef2]] 675 | 676 | ## Subheading 1:1 677 | Content for Subheading 1:1 678 | 679 | ### Subsubheading 1:1:1 680 | 681 | ### Subsubheading 1:1:2 682 | 683 | Testing how block references work for a table.[[#^2c7cfa]] 684 | Some content for Subsubheading 1:1:2 685 | 686 | More random text. 687 | 688 | ^2d9b4a 689 | 690 | ## Subheading 1:2 691 | 692 | Content for Subheading 1:2. 693 | 694 | some content with a block reference ^484ef2 695 | 696 | ## Subheading 1:3 697 | | City | Population | 698 | | ------------ | ---------- | 699 | | Seattle, WA | 8 | 700 | | Portland, OR | 4 | 701 | 702 | ^2c7cfa 703 | ``` 704 | 705 | ## Append Content Below a Heading 706 | 707 | If you wanted to append the content "Hello" below "Subheading 1:1:1" under "Heading 1", 708 | you could send a request with the following headers: 709 | 710 | - `Operation`: `append` 711 | - `Target-Type`: `heading` 712 | - `Target`: `Heading 1::Subheading 1:1:1` 713 | - with the request body: `Hello` 714 | 715 | The above would work just fine for `prepend` or `replace`, too, of course, 716 | but with different results. 717 | 718 | ## Append Content to a Block Reference 719 | 720 | If you wanted to append the content "Hello" below the block referenced by 721 | "2d9b4a" above ("More random text."), you could send the following headers: 722 | 723 | - `Operation`: `append` 724 | - `Target-Type`: `block` 725 | - `Target`: `2d9b4a` 726 | - with the request body: `Hello` 727 | 728 | The above would work just fine for `prepend` or `replace`, too, of course, 729 | but with different results. 730 | 731 | ## Add a Row to a Table Referenced by a Block Reference 732 | 733 | If you wanted to add a new city ("Chicago, IL") and population ("16") pair to the table above 734 | referenced by the block reference `2c7cfa`, you could send the following 735 | headers: 736 | 737 | - `Operation`: `append` 738 | - `TargetType`: `block` 739 | - `Target`: `2c7cfa` 740 | - `Content-Type`: `application/json` 741 | - with the request body: `[["Chicago, IL", "16"]]` 742 | 743 | The use of a `Content-Type` of `application/json` allows the API 744 | to infer that member of your array represents rows and columns of your 745 | to append to the referenced table. You can of course just use a 746 | `Content-Type` of `text/markdown`, but in such a case you'll have to 747 | format your table row manually instead of letting the library figure 748 | it out for you. 749 | 750 | You also have the option of using `prepend` (in which case, your new 751 | row would be the first -- right below the table heading) or `replace` (in which 752 | case all rows except the table heading would be replaced by the new row(s) 753 | you supplied). 754 | 755 | ## Setting a Frontmatter Field 756 | 757 | If you wanted to set the frontmatter field `alpha` to `2`, you could 758 | send the following headers: 759 | 760 | - `Operation`: `replace` 761 | - `TargetType`: `frontmatter` 762 | - `Target`: `beep` 763 | - with the request body `2` 764 | 765 | If you're setting a frontmatter field that might not already exist 766 | you may want to use the `Create-Target-If-Missing` header so the 767 | new frontmatter field is created and set to your specified value 768 | if it doesn't already exist. 769 | 770 | You may find using a `Content-Type` of `application/json` to be 771 | particularly useful in the case of frontmatter since frontmatter 772 | fields' values are JSON data, and the API can be smarter about 773 | interpreting yoru `prepend` or `append` requests if you specify 774 | your data as JSON (particularly when appending, for example, 775 | list items). 776 | parameters: 777 | - description: "Patch operation to perform" 778 | in: "header" 779 | name: "Operation" 780 | required: true 781 | schema: 782 | enum: 783 | - "append" 784 | - "prepend" 785 | - "replace" 786 | type: "string" 787 | - description: "Type of target to patch" 788 | in: "header" 789 | name: "Target-Type" 790 | required: true 791 | schema: 792 | enum: 793 | - "heading" 794 | - "block" 795 | - "frontmatter" 796 | type: "string" 797 | - description: "Delimiter to use for nested targets (i.e. Headings)" 798 | in: "header" 799 | name: "Target-Delimiter" 800 | required: false 801 | schema: 802 | default: "::" 803 | type: "string" 804 | - description: | 805 | Target to patch; this value can be URL-Encoded and *must* 806 | be URL-Encoded if it includes non-ASCII characters. 807 | in: "header" 808 | name: "Target" 809 | required: true 810 | schema: 811 | type: "string" 812 | - description: "Trim whitespace from Target before applying patch?" 813 | in: "header" 814 | name: "Trim-Target-Whitespace" 815 | required: false 816 | schema: 817 | default: "false" 818 | enum: 819 | - "true" 820 | - "false" 821 | type: "string" 822 | - description: "The name of the period for which you would like to grab the current note." 823 | in: "path" 824 | name: "period" 825 | required: true 826 | schema: 827 | default: "daily" 828 | enum: 829 | - "daily" 830 | - "weekly" 831 | - "monthly" 832 | - "quarterly" 833 | - "yearly" 834 | type: "string" 835 | requestBody: 836 | content: 837 | application/json: 838 | schema: 839 | example: "['one', 'two']" 840 | type: "string" 841 | text/markdown: 842 | schema: 843 | example: | 844 | # This is my document 845 | 846 | something else here 847 | type: "string" 848 | description: "Content you would like to insert." 849 | required: true 850 | responses: 851 | "200": 852 | description: "Success" 853 | "400": 854 | content: 855 | application/json: 856 | schema: 857 | "$ref": "#/components/schemas/Error" 858 | description: "Bad Request; see response message for details." 859 | "404": 860 | content: 861 | application/json: 862 | schema: 863 | "$ref": "#/components/schemas/Error" 864 | description: "Does not exist" 865 | "405": 866 | content: 867 | application/json: 868 | schema: 869 | "$ref": "#/components/schemas/Error" 870 | description: | 871 | Your path references a directory instead of a file; this request method is valid only for updating files. 872 | summary: | 873 | Insert content into a periodic note relative to a heading within that document. 874 | tags: 875 | - "Periodic Notes" 876 | post: 877 | description: | 878 | Appends content to the periodic note for the specified period. This will create the relevant periodic note if necessary. 879 | parameters: 880 | - description: "The name of the period for which you would like to grab the current note." 881 | in: "path" 882 | name: "period" 883 | required: true 884 | schema: 885 | default: "daily" 886 | enum: 887 | - "daily" 888 | - "weekly" 889 | - "monthly" 890 | - "quarterly" 891 | - "yearly" 892 | type: "string" 893 | requestBody: 894 | content: 895 | text/markdown: 896 | schema: 897 | example: | 898 | # This is my document 899 | 900 | something else here 901 | type: "string" 902 | description: "Content you would like to append." 903 | required: true 904 | responses: 905 | "204": 906 | description: "Success" 907 | "400": 908 | content: 909 | application/json: 910 | schema: 911 | "$ref": "#/components/schemas/Error" 912 | description: "Bad Request" 913 | "405": 914 | content: 915 | application/json: 916 | schema: 917 | "$ref": "#/components/schemas/Error" 918 | description: | 919 | Your path references a directory instead of a file; this request method is valid only for updating files. 920 | summary: | 921 | Append content to a periodic note. 922 | tags: 923 | - "Periodic Notes" 924 | put: 925 | parameters: 926 | - description: "The name of the period for which you would like to grab the current note." 927 | in: "path" 928 | name: "period" 929 | required: true 930 | schema: 931 | default: "daily" 932 | enum: 933 | - "daily" 934 | - "weekly" 935 | - "monthly" 936 | - "quarterly" 937 | - "yearly" 938 | type: "string" 939 | requestBody: 940 | content: 941 | "*/*": 942 | schema: 943 | type: "string" 944 | text/markdown: 945 | schema: 946 | example: | 947 | # This is my document 948 | 949 | something else here 950 | type: "string" 951 | description: "Content of the file you would like to upload." 952 | required: true 953 | responses: 954 | "204": 955 | description: "Success" 956 | "400": 957 | content: 958 | application/json: 959 | schema: 960 | "$ref": "#/components/schemas/Error" 961 | description: | 962 | Incoming file could not be processed. Make sure you have specified a reasonable file name, and make sure you have set a reasonable 'Content-Type' header; if you are uploading a note, 'text/markdown' is likely the right choice. 963 | "405": 964 | content: 965 | application/json: 966 | schema: 967 | "$ref": "#/components/schemas/Error" 968 | description: | 969 | Your path references a directory instead of a file; this request method is valid only for updating files. 970 | summary: | 971 | Update the content of a periodic note. 972 | tags: 973 | - "Periodic Notes" 974 | /search/: 975 | post: 976 | description: | 977 | Evaluates a provided query against each file in your vault. 978 | 979 | This endpoint supports multiple query formats. Your query should be specified in your request's body, and will be interpreted according to the `Content-type` header you specify from the below options.Additional query formats may be added in the future. 980 | 981 | # Dataview DQL (`application/vnd.olrapi.dataview.dql+txt`) 982 | 983 | Accepts a `TABLE`-type Dataview query as a text string. See [Dataview](https://blacksmithgu.github.io/obsidian-dataview/query/queries/)'s query documentation for information on how to construct a query. 984 | 985 | # JsonLogic (`application/vnd.olrapi.jsonlogic+json`) 986 | 987 | Accepts a JsonLogic query specified as JSON. See [JsonLogic](https://jsonlogic.com/operations.html)'s documentation for information about the base set of operators available, but in addition to those operators the following operators are available: 988 | 989 | - `glob: [PATTERN, VALUE]`: Returns `true` if a string matches a glob pattern. E.g.: `{"glob": ["*.foo", "bar.foo"]}` is `true` and `{"glob": ["*.bar", "bar.foo"]}` is `false`. 990 | - `regexp: [PATTERN, VALUE]`: Returns `true` if a string matches a regular expression. E.g.: `{"regexp": [".*\.foo", "bar.foo"]` is `true` and `{"regexp": [".*\.bar", "bar.foo"]}` is `false`. 991 | 992 | Returns only non-falsy results. "Non-falsy" here treats the following values as "falsy": 993 | 994 | - `false` 995 | - `null` or `undefined` 996 | - `0` 997 | - `[]` 998 | - `{}` 999 | 1000 | Files are represented as an object having the schema described 1001 | in the Schema named 'NoteJson' at the bottom of this page. 1002 | Understanding the shape of a JSON object from a schema can be 1003 | tricky; so you may find it helpful to examine the generated metadata 1004 | for individual files in your vault to understand exactly what values 1005 | are returned. To see that, access the `GET` `/vault/{filePath}` 1006 | route setting the header: 1007 | `Accept: application/vnd.olrapi.note+json`. See examples below 1008 | for working examples of queries performing common search operations. 1009 | requestBody: 1010 | content: 1011 | "application/vnd.olrapi.dataview.dql+txt": 1012 | examples: 1013 | find_fields_by_tag: 1014 | summary: "List data from files having the #game tag." 1015 | value: | 1016 | TABLE 1017 | time-played AS "Time Played", 1018 | length AS "Length", 1019 | rating AS "Rating" 1020 | FROM #game 1021 | SORT rating DESC 1022 | schema: 1023 | externalDocs: 1024 | url: "https://blacksmithgu.github.io/obsidian-dataview/query/queries/" 1025 | type: "object" 1026 | "application/vnd.olrapi.jsonlogic+json": 1027 | examples: 1028 | find_by_frontmatter_url_glob: 1029 | summary: "Find notes having URL or a matching URL glob frontmatter field." 1030 | value: | 1031 | { 1032 | "or": [ 1033 | {"===": [{"var": "frontmatter.url"}, "https://myurl.com/some/path/"]}, 1034 | {"glob": [{"var": "frontmatter.url-glob"}, "https://myurl.com/some/path/"]} 1035 | ] 1036 | } 1037 | find_by_frontmatter_value: 1038 | summary: "Find notes having a certain frontmatter field value." 1039 | value: | 1040 | { 1041 | "==": [ 1042 | {"var": "frontmatter.myField"}, 1043 | "myValue" 1044 | ] 1045 | } 1046 | find_by_tag: 1047 | summary: "Find notes having a certain tag" 1048 | value: | 1049 | { 1050 | "in": [ 1051 | "myTag", 1052 | {"var": "tags"} 1053 | ] 1054 | } 1055 | schema: 1056 | externalDocs: 1057 | url: "https://jsonlogic.com/operations.html" 1058 | type: "object" 1059 | required: true 1060 | responses: 1061 | "200": 1062 | content: 1063 | application/json: 1064 | schema: 1065 | items: 1066 | properties: 1067 | filename: 1068 | description: "Path to the matching file" 1069 | type: "string" 1070 | result: 1071 | oneOf: 1072 | - type: "string" 1073 | - type: "number" 1074 | - type: "array" 1075 | - type: "object" 1076 | - type: "boolean" 1077 | required: 1078 | - "filename" 1079 | - "result" 1080 | type: "object" 1081 | type: "array" 1082 | description: "Success" 1083 | "400": 1084 | content: 1085 | application/json: 1086 | schema: 1087 | "$ref": "#/components/schemas/Error" 1088 | description: | 1089 | Bad request. Make sure you have specified an acceptable 1090 | Content-Type for your search query. 1091 | summary: | 1092 | Search for documents matching a specified search query 1093 | tags: 1094 | - "Search" 1095 | /search/simple/: 1096 | post: 1097 | parameters: 1098 | - description: "Your search query" 1099 | in: "query" 1100 | name: "query" 1101 | required: true 1102 | schema: 1103 | type: "string" 1104 | - description: "How much context to return around the matching string" 1105 | in: "query" 1106 | name: "contextLength" 1107 | required: false 1108 | schema: 1109 | default: 100 1110 | type: "number" 1111 | responses: 1112 | "200": 1113 | content: 1114 | application/json: 1115 | schema: 1116 | items: 1117 | properties: 1118 | filename: 1119 | description: "Path to the matching file" 1120 | type: "string" 1121 | matches: 1122 | items: 1123 | properties: 1124 | context: 1125 | type: "string" 1126 | match: 1127 | properties: 1128 | end: 1129 | type: "number" 1130 | start: 1131 | type: "number" 1132 | required: 1133 | - "start" 1134 | - "end" 1135 | type: "object" 1136 | required: 1137 | - "match" 1138 | - "context" 1139 | type: "object" 1140 | type: "array" 1141 | score: 1142 | type: "number" 1143 | type: "object" 1144 | type: "array" 1145 | description: "Success" 1146 | summary: | 1147 | Search for documents matching a specified text query 1148 | tags: 1149 | - "Search" 1150 | /search/smart: 1151 | post: 1152 | summary: "Perform a smart search" 1153 | description: "Handles semantic search requests." 1154 | requestBody: 1155 | required: true 1156 | content: 1157 | application/json: 1158 | schema: 1159 | $ref: "#/components/schemas/SearchParameters" 1160 | responses: 1161 | "200": 1162 | description: "Successful search" 1163 | content: 1164 | application/json: 1165 | schema: 1166 | $ref: "#/components/schemas/SearchResponse" 1167 | "400": 1168 | description: "Invalid request body" 1169 | content: 1170 | application/json: 1171 | schema: 1172 | $ref: "#/components/schemas/Error" 1173 | "503": 1174 | description: "Service unavailable" 1175 | content: 1176 | application/json: 1177 | schema: 1178 | $ref: "#/components/schemas/Error" 1179 | /vault/: 1180 | get: 1181 | description: | 1182 | Lists files in the root directory of your vault. 1183 | 1184 | Note: that this is exactly the same API endpoint as the below "List files that exist in the specified directory." and exists here only due to a quirk of this particular interactive tool. 1185 | responses: 1186 | "200": 1187 | content: 1188 | application/json: 1189 | example: 1190 | files: 1191 | - "mydocument.md" 1192 | - "somedirectory/" 1193 | schema: 1194 | properties: 1195 | files: 1196 | items: 1197 | type: "string" 1198 | type: "array" 1199 | type: "object" 1200 | description: "Success" 1201 | "404": 1202 | content: 1203 | application/json: 1204 | schema: 1205 | "$ref": "#/components/schemas/Error" 1206 | description: "Directory does not exist" 1207 | summary: | 1208 | List files that exist in the root of your vault. 1209 | tags: 1210 | - "Vault Directories" 1211 | "/vault/{filename}": 1212 | delete: 1213 | parameters: 1214 | - description: | 1215 | Path to the relevant file (relative to your vault root). 1216 | in: "path" 1217 | name: "filename" 1218 | required: true 1219 | schema: 1220 | format: "path" 1221 | type: "string" 1222 | responses: 1223 | "204": 1224 | description: "Success" 1225 | "404": 1226 | content: 1227 | application/json: 1228 | schema: 1229 | "$ref": "#/components/schemas/Error" 1230 | description: "File does not exist." 1231 | "405": 1232 | content: 1233 | application/json: 1234 | schema: 1235 | "$ref": "#/components/schemas/Error" 1236 | description: | 1237 | Your path references a directory instead of a file; this request method is valid only for updating files. 1238 | summary: | 1239 | Delete a particular file in your vault. 1240 | tags: 1241 | - "Vault Files" 1242 | get: 1243 | description: | 1244 | Returns the content of the file at the specified path in your vault should the file exist. 1245 | 1246 | If you specify the header `Accept: application/vnd.olrapi.note+json`, will return a JSON representation of your note including parsed tag and frontmatter data as well as filesystem metadata. See "responses" below for details. 1247 | parameters: 1248 | - description: | 1249 | Path to the relevant file (relative to your vault root). 1250 | in: "path" 1251 | name: "filename" 1252 | required: true 1253 | schema: 1254 | format: "path" 1255 | type: "string" 1256 | responses: 1257 | "200": 1258 | content: 1259 | "application/vnd.olrapi.note+json": 1260 | schema: 1261 | "$ref": "#/components/schemas/NoteJson" 1262 | text/markdown: 1263 | schema: 1264 | example: | 1265 | # This is my document 1266 | 1267 | something else here 1268 | type: "string" 1269 | description: "Success" 1270 | "404": 1271 | description: "File does not exist" 1272 | summary: | 1273 | Return the content of a single file in your vault. 1274 | tags: 1275 | - "Vault Files" 1276 | patch: 1277 | description: | 1278 | Inserts content into an existing note relative to a heading within your note. 1279 | 1280 | Allows you to modify the content relative to a heading, block reference, or frontmatter field in your document. 1281 | 1282 | Note that this API was changed in Version 3.0 of this extension and the earlier PATCH API is now deprecated. Requests made using the previous version of this API will continue to work until Version 4.0 is released. See https://github.com/coddingtonbear/obsidian-local-rest-api/wiki/Changes-to-PATCH-requests-between-versions-2.0-and-3.0 for more details and migration instructions. 1283 | 1284 | # Examples 1285 | 1286 | All of the below examples assume you have a document that looks like 1287 | this: 1288 | 1289 | ```markdown 1290 | --- 1291 | alpha: 1 1292 | beta: test 1293 | delta: 1294 | zeta: 1 1295 | yotta: 1 1296 | gamma: 1297 | - one 1298 | - two 1299 | --- 1300 | 1301 | # Heading 1 1302 | 1303 | This is the content for heading one 1304 | 1305 | Also references some [[#^484ef2]] 1306 | 1307 | ## Subheading 1:1 1308 | Content for Subheading 1:1 1309 | 1310 | ### Subsubheading 1:1:1 1311 | 1312 | ### Subsubheading 1:1:2 1313 | 1314 | Testing how block references work for a table.[[#^2c7cfa]] 1315 | Some content for Subsubheading 1:1:2 1316 | 1317 | More random text. 1318 | 1319 | ^2d9b4a 1320 | 1321 | ## Subheading 1:2 1322 | 1323 | Content for Subheading 1:2. 1324 | 1325 | some content with a block reference ^484ef2 1326 | 1327 | ## Subheading 1:3 1328 | | City | Population | 1329 | | ------------ | ---------- | 1330 | | Seattle, WA | 8 | 1331 | | Portland, OR | 4 | 1332 | 1333 | ^2c7cfa 1334 | ``` 1335 | 1336 | ## Append Content Below a Heading 1337 | 1338 | If you wanted to append the content "Hello" below "Subheading 1:1:1" under "Heading 1", 1339 | you could send a request with the following headers: 1340 | 1341 | - `Operation`: `append` 1342 | - `Target-Type`: `heading` 1343 | - `Target`: `Heading 1::Subheading 1:1:1` 1344 | - with the request body: `Hello` 1345 | 1346 | The above would work just fine for `prepend` or `replace`, too, of course, 1347 | but with different results. 1348 | 1349 | ## Append Content to a Block Reference 1350 | 1351 | If you wanted to append the content "Hello" below the block referenced by 1352 | "2d9b4a" above ("More random text."), you could send the following headers: 1353 | 1354 | - `Operation`: `append` 1355 | - `Target-Type`: `block` 1356 | - `Target`: `2d9b4a` 1357 | - with the request body: `Hello` 1358 | 1359 | The above would work just fine for `prepend` or `replace`, too, of course, 1360 | but with different results. 1361 | 1362 | ## Add a Row to a Table Referenced by a Block Reference 1363 | 1364 | If you wanted to add a new city ("Chicago, IL") and population ("16") pair to the table above 1365 | referenced by the block reference `2c7cfa`, you could send the following 1366 | headers: 1367 | 1368 | - `Operation`: `append` 1369 | - `TargetType`: `block` 1370 | - `Target`: `2c7cfa` 1371 | - `Content-Type`: `application/json` 1372 | - with the request body: `[["Chicago, IL", "16"]]` 1373 | 1374 | The use of a `Content-Type` of `application/json` allows the API 1375 | to infer that member of your array represents rows and columns of your 1376 | to append to the referenced table. You can of course just use a 1377 | `Content-Type` of `text/markdown`, but in such a case you'll have to 1378 | format your table row manually instead of letting the library figure 1379 | it out for you. 1380 | 1381 | You also have the option of using `prepend` (in which case, your new 1382 | row would be the first -- right below the table heading) or `replace` (in which 1383 | case all rows except the table heading would be replaced by the new row(s) 1384 | you supplied). 1385 | 1386 | ## Setting a Frontmatter Field 1387 | 1388 | If you wanted to set the frontmatter field `alpha` to `2`, you could 1389 | send the following headers: 1390 | 1391 | - `Operation`: `replace` 1392 | - `TargetType`: `frontmatter` 1393 | - `Target`: `beep` 1394 | - with the request body `2` 1395 | 1396 | If you're setting a frontmatter field that might not already exist 1397 | you may want to use the `Create-Target-If-Missing` header so the 1398 | new frontmatter field is created and set to your specified value 1399 | if it doesn't already exist. 1400 | 1401 | You may find using a `Content-Type` of `application/json` to be 1402 | particularly useful in the case of frontmatter since frontmatter 1403 | fields' values are JSON data, and the API can be smarter about 1404 | interpreting yoru `prepend` or `append` requests if you specify 1405 | your data as JSON (particularly when appending, for example, 1406 | list items). 1407 | parameters: 1408 | - description: "Patch operation to perform" 1409 | in: "header" 1410 | name: "Operation" 1411 | required: true 1412 | schema: 1413 | enum: 1414 | - "append" 1415 | - "prepend" 1416 | - "replace" 1417 | type: "string" 1418 | - description: "Type of target to patch" 1419 | in: "header" 1420 | name: "Target-Type" 1421 | required: true 1422 | schema: 1423 | enum: 1424 | - "heading" 1425 | - "block" 1426 | - "frontmatter" 1427 | type: "string" 1428 | - description: "Delimiter to use for nested targets (i.e. Headings)" 1429 | in: "header" 1430 | name: "Target-Delimiter" 1431 | required: false 1432 | schema: 1433 | default: "::" 1434 | type: "string" 1435 | - description: | 1436 | Target to patch; this value can be URL-Encoded and *must* 1437 | be URL-Encoded if it includes non-ASCII characters. 1438 | in: "header" 1439 | name: "Target" 1440 | required: true 1441 | schema: 1442 | type: "string" 1443 | - description: "Trim whitespace from Target before applying patch?" 1444 | in: "header" 1445 | name: "Trim-Target-Whitespace" 1446 | required: false 1447 | schema: 1448 | default: "false" 1449 | enum: 1450 | - "true" 1451 | - "false" 1452 | type: "string" 1453 | - description: | 1454 | Path to the relevant file (relative to your vault root). 1455 | in: "path" 1456 | name: "filename" 1457 | required: true 1458 | schema: 1459 | format: "path" 1460 | type: "string" 1461 | requestBody: 1462 | content: 1463 | application/json: 1464 | schema: 1465 | example: "['one', 'two']" 1466 | type: "string" 1467 | text/markdown: 1468 | schema: 1469 | example: | 1470 | # This is my document 1471 | 1472 | something else here 1473 | type: "string" 1474 | description: "Content you would like to insert." 1475 | required: true 1476 | responses: 1477 | "200": 1478 | description: "Success" 1479 | "400": 1480 | content: 1481 | application/json: 1482 | schema: 1483 | "$ref": "#/components/schemas/Error" 1484 | description: "Bad Request; see response message for details." 1485 | "404": 1486 | content: 1487 | application/json: 1488 | schema: 1489 | "$ref": "#/components/schemas/Error" 1490 | description: "Does not exist" 1491 | "405": 1492 | content: 1493 | application/json: 1494 | schema: 1495 | "$ref": "#/components/schemas/Error" 1496 | description: | 1497 | Your path references a directory instead of a file; this request method is valid only for updating files. 1498 | summary: | 1499 | Insert content into an existing note relative to a heading within that document. 1500 | tags: 1501 | - "Vault Files" 1502 | post: 1503 | description: | 1504 | Appends content to the end of an existing note. If the specified file does not yet exist, it will be created as an empty file. 1505 | 1506 | If you would like to insert text relative to a particular heading instead of appending to the end of the file, see 'patch'. 1507 | parameters: 1508 | - description: | 1509 | Path to the relevant file (relative to your vault root). 1510 | in: "path" 1511 | name: "filename" 1512 | required: true 1513 | schema: 1514 | format: "path" 1515 | type: "string" 1516 | requestBody: 1517 | content: 1518 | text/markdown: 1519 | schema: 1520 | example: | 1521 | # This is my document 1522 | 1523 | something else here 1524 | type: "string" 1525 | description: "Content you would like to append." 1526 | required: true 1527 | responses: 1528 | "204": 1529 | description: "Success" 1530 | "400": 1531 | content: 1532 | application/json: 1533 | schema: 1534 | "$ref": "#/components/schemas/Error" 1535 | description: "Bad Request" 1536 | "405": 1537 | content: 1538 | application/json: 1539 | schema: 1540 | "$ref": "#/components/schemas/Error" 1541 | description: | 1542 | Your path references a directory instead of a file; this request method is valid only for updating files. 1543 | summary: | 1544 | Append content to a new or existing file. 1545 | tags: 1546 | - "Vault Files" 1547 | put: 1548 | description: | 1549 | Creates a new file in your vault or updates the content of an existing one if the specified file already exists. 1550 | parameters: 1551 | - description: | 1552 | Path to the relevant file (relative to your vault root). 1553 | in: "path" 1554 | name: "filename" 1555 | required: true 1556 | schema: 1557 | format: "path" 1558 | type: "string" 1559 | requestBody: 1560 | content: 1561 | "*/*": 1562 | schema: 1563 | type: "string" 1564 | text/markdown: 1565 | schema: 1566 | example: | 1567 | # This is my document 1568 | 1569 | something else here 1570 | type: "string" 1571 | description: "Content of the file you would like to upload." 1572 | required: true 1573 | responses: 1574 | "204": 1575 | description: "Success" 1576 | "400": 1577 | content: 1578 | application/json: 1579 | schema: 1580 | "$ref": "#/components/schemas/Error" 1581 | description: | 1582 | Incoming file could not be processed. Make sure you have specified a reasonable file name, and make sure you have set a reasonable 'Content-Type' header; if you are uploading a note, 'text/markdown' is likely the right choice. 1583 | "405": 1584 | content: 1585 | application/json: 1586 | schema: 1587 | "$ref": "#/components/schemas/Error" 1588 | description: | 1589 | Your path references a directory instead of a file; this request method is valid only for updating files. 1590 | summary: | 1591 | Create a new file in your vault or update the content of an existing one. 1592 | tags: 1593 | - "Vault Files" 1594 | "/vault/{pathToDirectory}/": 1595 | get: 1596 | parameters: 1597 | - description: | 1598 | Path to list files from (relative to your vault root). Note that empty directories will not be returned. 1599 | 1600 | Note: this particular interactive tool requires that you provide an argument for this field, but the API itself will allow you to list the root folder of your vault. If you would like to try listing content in the root of your vault using this interactive tool, use the above "List files that exist in the root of your vault" form above. 1601 | in: "path" 1602 | name: "pathToDirectory" 1603 | required: true 1604 | schema: 1605 | format: "path" 1606 | type: "string" 1607 | responses: 1608 | "200": 1609 | content: 1610 | application/json: 1611 | example: 1612 | files: 1613 | - "mydocument.md" 1614 | - "somedirectory/" 1615 | schema: 1616 | properties: 1617 | files: 1618 | items: 1619 | type: "string" 1620 | type: "array" 1621 | type: "object" 1622 | description: "Success" 1623 | "404": 1624 | content: 1625 | application/json: 1626 | schema: 1627 | "$ref": "#/components/schemas/Error" 1628 | description: "Directory does not exist" 1629 | summary: | 1630 | List files that exist in the specified directory. 1631 | tags: 1632 | - "Vault Directories" 1633 | security: 1634 | - apiKeyAuth: [] 1635 | servers: 1636 | - description: "HTTPS (Secure Mode)" 1637 | url: "https://{host}:{port}" 1638 | variables: 1639 | host: 1640 | default: "127.0.0.1" 1641 | description: "Binding host" 1642 | port: 1643 | default: "27124" 1644 | description: "HTTPS port" 1645 | - description: "HTTP (Insecure Mode)" 1646 | url: "http://{host}:{port}" 1647 | variables: 1648 | host: 1649 | default: "127.0.0.1" 1650 | description: "Binding host" 1651 | port: 1652 | default: "27123" 1653 | description: "HTTP port" 1654 | ```