Skip to main content
Glama

Replace in Note

replace_in_note
Destructive

Perform search-and-replace in a note using literal strings or regex patterns, with an option to confirm expected match count to prevent unintended over-replacement.

Instructions

Search-and-replace within a single note. Supports literal strings or regex patterns. With expectedCount, the operation refuses to commit unless that many matches are present, guarding against accidental over-replacement when an LLM drafts a pattern that's too broad. Returns the count of replacements made.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
pathYesVault-relative path to the note.
findYesLiteral string (default) or regex pattern to match.
replaceYesReplacement text. With `regex: true`, supports $1, $2 backreferences.
regexNoTreat `find` as a JavaScript regex (multi-line, case-sensitive by default).
flagsNoRegex flags (e.g., 'gi'). Defaults to 'g' so all matches are replaced.
expectedCountNoIf set, abort unless exactly this many matches are found.

Implementation Reference

  • The async handler function for the 'replace_in_note' tool. It uses updateNote to atomically read/modify/write the note file. If 'regex' is false, it escapes the find string and uses a global regex. If true, applies the provided flags (must include 'g'). Supports expectedCount guard to prevent accidental over-replacement. Returns the count of replacements made.
      async ({ path: notePath, find, replace, regex, flags, expectedCount }) => {
        try {
          let count = 0;
          await updateNote(vaultPath, notePath, (existing) => {
            let pattern: RegExp;
            if (regex) {
              const f = flags ?? "g";
              if (!f.includes("g")) {
                throw new Error("regex flags must include 'g' for replace_in_note");
              }
              pattern = new RegExp(find, f);
            } else {
              const escaped = find.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
              pattern = new RegExp(escaped, "g");
            }
            const matches = existing.match(pattern);
            count = matches ? matches.length : 0;
            if (expectedCount !== undefined && count !== expectedCount) {
              throw new Error(
                `Match-count check failed: expected ${expectedCount}, found ${count}. No changes written.`,
              );
            }
            if (count === 0) return existing;
            return existing.replace(pattern, replace);
          });
          return textResult(
            count === 0
              ? `No matches in ${notePath} — file unchanged.`
              : `Replaced ${count} match(es) in ${notePath}.`,
          );
        } catch (err) {
          log.error("replace_in_note failed", { tool: "replace_in_note", err: err as Error });
          return errorResult(`Error replacing in note: ${sanitizeError(err)}`);
        }
      },
    );
  • Input schema for 'replace_in_note' using Zod. Defines parameters: path (string), find (string), replace (string), regex (boolean, default false), flags (optional string), expectedCount (optional integer).
    inputSchema: {
      path: z.string().min(1).describe("Vault-relative path to the note."),
      find: z.string().min(1).describe("Literal string (default) or regex pattern to match."),
      replace: z.string().describe("Replacement text. With `regex: true`, supports $1, $2 backreferences."),
      regex: z.boolean().default(false).describe("Treat `find` as a JavaScript regex (multi-line, case-sensitive by default)."),
      flags: z.string().optional().describe("Regex flags (e.g., 'gi'). Defaults to 'g' so all matches are replaced."),
      expectedCount: z.number().int().min(0).optional().describe("If set, abort unless exactly this many matches are found."),
    },
  • Registration of the 'replace_in_note' tool on the MCP server via server.registerTool() call inside registerSectionTools().
    server.registerTool(
      "replace_in_note",
      {
        title: "Replace in Note",
        description:
          "Search-and-replace within a single note. Supports literal strings or regex patterns. With `expectedCount`, the operation refuses to commit unless that many matches are present, guarding against accidental over-replacement when an LLM drafts a pattern that's too broad. Returns the count of replacements made.",
        annotations: {
          readOnlyHint: false,
          destructiveHint: true,
          idempotentHint: false,
          openWorldHint: false,
        },
        inputSchema: {
          path: z.string().min(1).describe("Vault-relative path to the note."),
          find: z.string().min(1).describe("Literal string (default) or regex pattern to match."),
          replace: z.string().describe("Replacement text. With `regex: true`, supports $1, $2 backreferences."),
          regex: z.boolean().default(false).describe("Treat `find` as a JavaScript regex (multi-line, case-sensitive by default)."),
          flags: z.string().optional().describe("Regex flags (e.g., 'gi'). Defaults to 'g' so all matches are replaced."),
          expectedCount: z.number().int().min(0).optional().describe("If set, abort unless exactly this many matches are found."),
        },
      },
      async ({ path: notePath, find, replace, regex, flags, expectedCount }) => {
        try {
          let count = 0;
          await updateNote(vaultPath, notePath, (existing) => {
            let pattern: RegExp;
            if (regex) {
              const f = flags ?? "g";
              if (!f.includes("g")) {
                throw new Error("regex flags must include 'g' for replace_in_note");
              }
              pattern = new RegExp(find, f);
            } else {
              const escaped = find.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
              pattern = new RegExp(escaped, "g");
            }
            const matches = existing.match(pattern);
            count = matches ? matches.length : 0;
            if (expectedCount !== undefined && count !== expectedCount) {
              throw new Error(
                `Match-count check failed: expected ${expectedCount}, found ${count}. No changes written.`,
              );
            }
            if (count === 0) return existing;
            return existing.replace(pattern, replace);
          });
          return textResult(
            count === 0
              ? `No matches in ${notePath} — file unchanged.`
              : `Replaced ${count} match(es) in ${notePath}.`,
          );
        } catch (err) {
          log.error("replace_in_note failed", { tool: "replace_in_note", err: err as Error });
          return errorResult(`Error replacing in note: ${sanitizeError(err)}`);
        }
      },
    );
  • The updateNote helper used by replace_in_note. Provides atomic read-modify-write with per-file locking. Skips writes when content is unchanged (e.g., zero matches) to avoid mtime bumps.
    * Atomic read-modify-write: reads existing content, applies `transform`, and
    * writes the result while holding the per-file lock for the full sequence.
    * Prevents lost updates when concurrent callers would otherwise read the same
    * base and overwrite each other's changes.
    *
    * Skips the write when the transform returns the existing content unchanged.
    * Without this guard, no-op tools (e.g. `replace_in_note` with zero matches,
    * `rename_tag` on a note that contains no occurrences) would still call
    * `atomicWriteFile`, bumping mtime and invalidating downstream caches
    * (index-cache, embedding-store) for files we didn't actually modify.
    */
Behavior5/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Beyond the annotations (destructiveHint=true), the description reveals key behavioral traits: supports literal or regex, expectedCount as a safety guard, returns the count of replacements, and implies a commit-or-abort pattern. This adds significant value beyond what annotations and schema provide.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is three sentences (about 50 words), front-loaded with the core action, and every sentence adds distinct value (overview, feature, safety detail). No redundancy or filler.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description covers the operation, return value, and key safety feature. It doesn't explain error cases (e.g., invalid path, no matches without expectedCount) or that the tool modifies the vault permanently, but annotations and schema partially fill those gaps. Slightly incomplete for a destructive tool with no output schema.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3. The description adds meaning by explaining the purpose of expectedCount (safety guard), hinting at regex backreferences, and clarifying the behavior of regex flags. This elevates the score above baseline.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states 'search-and-replace within a single note', specifying the verb (replace) and resource (note). It distinguishes itself from siblings like append_to_note or edit_block by focusing on pattern-based replacement, and adds unique features like regex support and expectedCount guard.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description explicitly mentions the use of expectedCount to guard against over-replacement, providing context for safe usage. It does not directly compare to alternative tools, but the operation is specific enough that the agent can infer when to use it (when replacement is needed). Slight deduction for missing explicit when-not-to-use guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rps321321/obsidian-mcp-pro'

If you have feedback or need assistance with the MCP directory API, please join our Discord server