From f1526abb6968ae1c4651218fd4dbc475ac34e262 Mon Sep 17 00:00:00 2001 From: John Pruitt Date: Tue, 21 Apr 2026 12:22:55 -0500 Subject: [PATCH 1/3] chore: ignore scratch dir --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9476c4b..0f6b53b 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json repos tmp +scratch .mcp.json opencode.json .gemini From 228764bc950cf821bc734779a019a54a3db8798d Mon Sep 17 00:00:00 2001 From: John Pruitt Date: Tue, 21 Apr 2026 12:38:02 -0500 Subject: [PATCH 2/3] feat: add local CLI install script --- DEVELOPMENT.md | 1 + package.json | 1 + scripts/install-local.ts | 82 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 scripts/install-local.ts diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 986fa5d..86dffff 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -194,6 +194,7 @@ After login, the server URL is stored as the default in `~/.config/me/credential | `bun run check` | Format + lint + typecheck | | `bun run build` | Compile CLI binary (current platform) | | `bun run build:all` | Cross-compile CLI for all platforms | +| `bun run install:local` | Build and install local CLI binary to your PATH | | `bun run clean` | Remove build artifacts | | `bun run generate:master-key` | Generate a new encryption master key | diff --git a/package.json b/package.json index df24615..a2c65ef 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "build": "./bun run --filter '@memory.build/cli' build", "server": "./bun run packages/server/index.ts", "build:all": "./bun scripts/build-all.ts", + "install:local": "./bun scripts/install-local.ts", "clean": "rm -rf packages/cli/dist dist", "release": "./bun scripts/release.ts", "docs": "uvx --with mkdocs-material --with 'mkdocs<2' mkdocs serve", diff --git a/scripts/install-local.ts b/scripts/install-local.ts new file mode 100644 index 0000000..5cadfc6 --- /dev/null +++ b/scripts/install-local.ts @@ -0,0 +1,82 @@ +import { constants } from "node:fs"; +import { access } from "node:fs/promises"; +import path from "node:path"; +import { $ } from "bun"; + +const repoRoot = path.resolve(import.meta.dir, ".."); +const source = path.join(repoRoot, "packages/cli/dist/me"); +const installDir = await resolveInstallDir(); +const dest = path.join(installDir, "me"); + +console.log("Building local CLI binary...\n"); +await $`./bun run build`.cwd(repoRoot); + +await $`mkdir -p ${installDir}`; +await $`cp ${source} ${dest}`; +await $`chmod +x ${dest}`; + +if (process.platform === "darwin") { + const entitlements = path.join( + repoRoot, + "packages/cli/dist/.entitlements.plist", + ); + await Bun.write( + entitlements, + ` + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-executable-page-protection + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.disable-library-validation + + +`, + ); + + await $`codesign --remove-signature ${dest}`.quiet().nothrow(); + await $`codesign --entitlements ${entitlements} -f --deep -s - ${dest}` + .quiet() + .nothrow(); + await $`xattr -d com.apple.quarantine ${dest}`.quiet().nothrow(); + await $`rm -f ${entitlements}`; +} + +console.log(`Installed local build to ${dest}`); + +const pathEntries = (process.env.PATH ?? "").split(":"); +if (!pathEntries.includes(installDir)) { + console.log(`\nAdd ${installDir} to your PATH:`); + console.log(` export PATH="${installDir}:$PATH"`); +} + +console.log("\nRun 'me --help' to test the installed local binary."); + +async function resolveInstallDir() { + if (process.env.ME_INSTALL_DIR) { + return process.env.ME_INSTALL_DIR; + } + + const localDir = path.join(process.env.HOME ?? "", ".local"); + const localBin = path.join(localDir, "bin"); + if ((await exists(localBin)) || (await exists(localDir))) { + return localBin; + } + + return path.join(process.env.HOME ?? "", "bin"); +} + +async function exists(filePath: string) { + try { + await access(filePath, constants.F_OK); + return true; + } catch { + return false; + } +} From 4ecc269a89bdc5d344207cdec1f3330519979703 Mon Sep 17 00:00:00 2001 From: John Pruitt Date: Tue, 21 Apr 2026 14:53:06 -0500 Subject: [PATCH 3/3] mcp: accept omission or null for optional tool params Switch MCP inputSchema fields that represent "no value" from .nullable()-only to .optional().nullable() so tool calls work for agents that omit unused fields (Claude Code, OpenCode, Gemini CLI) and agents that send explicit null (Codex). Handlers coalesce null to undefined before forwarding to the engine client. Docs for the six affected tools (create, update, search, tree, import, export) updated to describe optional params as \`T | null\` with "omit or pass null" semantics. --- docs/mcp/me_memory_create.md | 14 ++-- docs/mcp/me_memory_export.md | 33 ++++------ docs/mcp/me_memory_import.md | 15 ++--- docs/mcp/me_memory_search.md | 55 ++++++---------- docs/mcp/me_memory_tree.md | 8 +-- docs/mcp/me_memory_update.md | 19 +++--- packages/cli/mcp/server.ts | 120 +++++++++++++++++++++++++++-------- 7 files changed, 145 insertions(+), 119 deletions(-) diff --git a/docs/mcp/me_memory_create.md b/docs/mcp/me_memory_create.md index 92f529b..3fcd449 100644 --- a/docs/mcp/me_memory_create.md +++ b/docs/mcp/me_memory_create.md @@ -6,18 +6,18 @@ Store a new memory. | Name | Type | Required | Description | |------|------|----------|-------------| -| `id` | `string \| null` | yes | UUIDv7 for idempotent creates. Pass `null` to auto-generate. | +| `id` | `string \| null` | no | UUIDv7 for idempotent creates. Omit or pass `null` to auto-generate. | | `content` | `string` | yes | The content of the memory. Must be non-empty. | -| `meta` | `object \| null` | yes | Key-value metadata pairs. Pass `null` to omit. | -| `tree` | `string \| null` | yes | Hierarchical path using dot-separated labels (e.g., `work.projects.me`). Pass `null` to store at the root. | -| `temporal` | `object \| null` | yes | Time range for the memory. Pass `null` to omit. | +| `meta` | `object \| null` | no | Key-value metadata pairs. Omit or pass `null` to skip. | +| `tree` | `string \| null` | no | Hierarchical path using dot-separated labels (e.g., `work.projects.me`). Omit or pass `null` to store at the root. | +| `temporal` | `object \| null` | no | Time range for the memory. Omit or pass `null` to skip. | ### temporal | Name | Type | Required | Description | |------|------|----------|-------------| | `start` | `string` | yes | ISO 8601 timestamp for the start of the time range. | -| `end` | `string \| null` | yes | ISO 8601 timestamp for the end. Pass `null` for a point-in-time memory. | +| `end` | `string \| null` | no | ISO 8601 timestamp for the end. Omit or pass `null` for a point-in-time memory. | ## Returns @@ -53,13 +53,11 @@ The full memory object as created: ```json { - "id": null, "content": "Use ltree for hierarchical path queries in PostgreSQL.", "meta": { "source": "docs", "confidence": "high" }, "tree": "research.postgres", "temporal": { - "start": "2025-04-15T00:00:00Z", - "end": null + "start": "2025-04-15T00:00:00Z" } } ``` diff --git a/docs/mcp/me_memory_export.md b/docs/mcp/me_memory_export.md index 61a5350..cacc91f 100644 --- a/docs/mcp/me_memory_export.md +++ b/docs/mcp/me_memory_export.md @@ -8,20 +8,20 @@ Prefer `path` to write directly to a file instead of returning content through t | Name | Type | Required | Description | |------|------|----------|-------------| -| `tree` | `string \| null` | yes | Tree path filter. Pass `null` for all memories. | -| `meta` | `object \| null` | yes | Metadata filter. Pass `null` to skip. | -| `temporal` | `object \| null` | yes | Temporal filter. Pass `null` to skip. | +| `tree` | `string \| null` | no | Tree path filter. Omit or pass `null` for all memories. | +| `meta` | `object \| null` | no | Metadata filter. Omit or pass `null` to skip. | +| `temporal` | `object \| null` | no | Temporal filter. Omit or pass `null` to skip. | | `format` | `string` | yes | Output format: `"json"`, `"yaml"`, or `"md"`. | -| `limit` | `integer` | yes | Maximum memories to export. Pass `0` for default (1000). | -| `path` | `string \| null` | yes | Absolute file or directory path. For `md` format, use a directory path to write one `.md` file per memory. Pass `null` to return content inline. | +| `limit` | `integer \| null` | no | Maximum memories to export. Omit or pass `null` for default (1000). | +| `path` | `string \| null` | no | Absolute file or directory path. For `md` format, use a directory path to write one `.md` file per memory. Omit or pass `null` to return content inline. | ### temporal | Name | Type | Required | Description | |------|------|----------|-------------| -| `contains` | `string \| null` | yes | Find memories containing this point in time. | -| `overlaps` | `object \| null` | yes | Find memories overlapping this range (`{start, end}`). | -| `within` | `object \| null` | yes | Find memories fully within this range (`{start, end}`). | +| `contains` | `string \| null` | no | Find memories containing this point in time. | +| `overlaps` | `object \| null` | no | Find memories overlapping this range (`{start, end}`). | +| `within` | `object \| null` | no | Find memories fully within this range (`{start, end}`). | ## Returns @@ -49,7 +49,7 @@ For `md` format with a directory path: | `path` | `string` | The file path that was written to (JSON/YAML). | | `directory` | `string` | The directory that `.md` files were written to (Markdown). | -### When `path` is null (inline output) +### When `path` is omitted (inline output) ```json { @@ -70,10 +70,7 @@ For `md` format with a directory path: ```json { "tree": "me.design.*", - "meta": null, - "temporal": null, "format": "yaml", - "limit": 0, "path": "/Users/me/memories/design-export.yaml" } ``` @@ -83,10 +80,7 @@ For `md` format with a directory path: ```json { "tree": "me.design.*", - "meta": null, - "temporal": null, "format": "md", - "limit": 0, "path": "/Users/me/memories/design-export" } ``` @@ -97,20 +91,17 @@ Each memory is written as `{id}.md` with YAML frontmatter. The directory is crea ```json { - "tree": null, "meta": { "type": "decision" }, - "temporal": null, "format": "json", - "limit": 10, - "path": null + "limit": 10 } ``` ## Notes -- **Prefer `path` for large exports** to avoid returning large payloads through the conversation. Use inline (`path: null`) only for small result sets or when you need to inspect the content. +- **Prefer `path` for large exports** to avoid returning large payloads through the conversation. Omit `path` only for small result sets or when you need to inspect the content. - The exported content is directly compatible with [me_memory_import](me_memory_import.md). Exported files and directories can be re-imported directly. -- **Markdown format**: use a directory path for multi-memory export. Each memory is written as `{id}.md`. Inline Markdown export (`path: null`) is only supported for single-memory results. +- **Markdown format**: use a directory path for multi-memory export. Each memory is written as `{id}.md`. Inline Markdown export (omitting `path`) is only supported for single-memory results. - Results are sorted in ascending order by creation time. - The `tree` filter supports exact match, wildcards, negation, and label search. See [Tree filter syntax](../concepts.md#tree-filter-syntax) for the full reference. Use `me.!archived.*{0,}` to export everything under `me` except archived content. - See [File Formats](../formats.md) for full schema documentation and format details. diff --git a/docs/mcp/me_memory_import.md b/docs/mcp/me_memory_import.md index 6f9adda..49a2fae 100644 --- a/docs/mcp/me_memory_import.md +++ b/docs/mcp/me_memory_import.md @@ -8,9 +8,9 @@ Parses the input according to the specified format and creates all memories in o | Name | Type | Required | Description | |------|------|----------|-------------| -| `path` | `string \| null` | yes | Absolute path to a file or directory. Directories are imported recursively. Format is inferred from extension (`.json`, `.yaml`, `.yml`, `.md`, `.ndjson`, `.jsonl`). Mutually exclusive with `content`. | -| `content` | `string \| null` | yes | Raw content to import (JSON array, YAML array, or Markdown with frontmatter). Mutually exclusive with `path`. | -| `format` | `string \| null` | yes | Content format: `"json"`, `"yaml"`, or `"md"`. Required when using `content`. Optional when using `path` (inferred from file extension). | +| `path` | `string \| null` | no | Absolute path to a file or directory. Directories are imported recursively. Format is inferred from extension (`.json`, `.yaml`, `.yml`, `.md`, `.ndjson`, `.jsonl`). Mutually exclusive with `content`. Omit or pass `null` if providing `content`. | +| `content` | `string \| null` | no | Raw content to import (JSON array, YAML array, or Markdown with frontmatter). Mutually exclusive with `path`. Omit or pass `null` if providing `path`. | +| `format` | `string \| null` | no | Content format: `"json"`, `"yaml"`, or `"md"`. Required when using `content`. Optional when using `path` (inferred from file extension). Omit or pass `null` to skip. | One of `path` or `content` must be provided. @@ -45,9 +45,7 @@ See [File Formats](../formats.md) for full schema documentation, examples, and f ```json { - "path": "/Users/me/memories/export.yaml", - "content": null, - "format": null + "path": "/Users/me/memories/export.yaml" } ``` @@ -57,9 +55,7 @@ Format is inferred from the `.yaml` extension. ```json { - "path": "/Users/me/memories/export-dir", - "content": null, - "format": null + "path": "/Users/me/memories/export-dir" } ``` @@ -69,7 +65,6 @@ Recursively imports all supported files (`.json`, `.yaml`, `.yml`, `.md`, `.ndjs ```json { - "path": null, "content": "[{\"content\": \"Hello world\", \"tree\": \"test\"}]", "format": "json" } diff --git a/docs/mcp/me_memory_search.md b/docs/mcp/me_memory_search.md index 2ee3716..bad9858 100644 --- a/docs/mcp/me_memory_search.md +++ b/docs/mcp/me_memory_search.md @@ -8,16 +8,16 @@ Supports three search modes: **semantic** (meaning-based), **fulltext** (keyword | Name | Type | Required | Description | |------|------|----------|-------------| -| `semantic` | `string \| null` | yes | Natural language query for semantic search. Pass `null` to skip. | -| `fulltext` | `string \| null` | yes | Keywords/phrases for BM25 exact matching. Pass `null` to skip. | -| `grep` | `string \| null` | yes | POSIX regex pattern filter on content (case-insensitive). Applied as a WHERE filter alongside other filters. Pass `null` to skip. | -| `meta` | `object \| null` | yes | Filter by metadata attributes. Pass `null` to skip. | -| `tree` | `string \| null` | yes | Filter by tree path. Pass `null` to skip. | -| `temporal` | `object \| null` | yes | Temporal filter. Pass `null` to skip. | -| `weights` | `object \| null` | yes | Weights for hybrid search ranking. Pass `null` for defaults. | -| `candidateLimit` | `integer` | yes | Candidates per search mode before RRF fusion. Pass `0` for default (30). | -| `limit` | `integer` | yes | Maximum number of results. Pass `0` for default (10). Max: 1000. | -| `order_by` | `string \| null` | yes | Sort direction for filter-only searches: `"asc"` or `"desc"`. Default: `"desc"`. Pass `null` for default. | +| `semantic` | `string \| null` | no | Natural language query for semantic search. Omit or pass `null` to skip. | +| `fulltext` | `string \| null` | no | Keywords/phrases for BM25 exact matching. Omit or pass `null` to skip. | +| `grep` | `string \| null` | no | POSIX regex pattern filter on content (case-insensitive). Applied as a WHERE filter alongside other filters. Omit or pass `null` to skip. | +| `meta` | `object \| null` | no | Filter by metadata attributes. Omit or pass `null` to skip. | +| `tree` | `string \| null` | no | Filter by tree path. Omit or pass `null` to skip. | +| `temporal` | `object \| null` | no | Temporal filter. Omit or pass `null` to skip. | +| `weights` | `object \| null` | no | Weights for hybrid search ranking. Omit or pass `null` for defaults. | +| `candidateLimit` | `integer \| null` | no | Candidates per search mode before RRF fusion. Omit or pass `null` for default (30). | +| `limit` | `integer \| null` | no | Maximum number of results. Omit or pass `null` for default (10). Max: 1000. | +| `order_by` | `string \| null` | no | Sort direction for filter-only searches: `"asc"` or `"desc"`. Default: `"desc"`. Omit or pass `null` for default. | ### tree syntax @@ -36,16 +36,16 @@ See [Tree filter syntax](../concepts.md#tree-filter-syntax) for the full referen | Name | Type | Required | Description | |------|------|----------|-------------| -| `contains` | `string \| null` | yes | Find memories whose time range contains this point in time. | -| `overlaps` | `object \| null` | yes | Find memories overlapping this range (`{start, end}`). | -| `within` | `object \| null` | yes | Find memories fully within this range (`{start, end}`). | +| `contains` | `string \| null` | no | Find memories whose time range contains this point in time. | +| `overlaps` | `object \| null` | no | Find memories overlapping this range (`{start, end}`). | +| `within` | `object \| null` | no | Find memories fully within this range (`{start, end}`). | ### weights | Name | Type | Required | Description | |------|------|----------|-------------| -| `fulltext` | `number \| null` | yes | Weight for BM25 keyword matching (0-1). | -| `semantic` | `number \| null` | yes | Weight for semantic similarity (0-1). | +| `fulltext` | `number \| null` | no | Weight for BM25 keyword matching (0-1). | +| `semantic` | `number \| null` | no | Weight for semantic similarity (0-1). | ## Returns @@ -83,15 +83,7 @@ See [Tree filter syntax](../concepts.md#tree-filter-syntax) for the full referen ```json { "semantic": "how does authentication work", - "fulltext": null, - "grep": null, - "meta": null, - "tree": null, - "temporal": null, - "weights": null, - "candidateLimit": 0, - "limit": 10, - "order_by": null + "limit": 10 } ``` @@ -101,14 +93,8 @@ See [Tree filter syntax](../concepts.md#tree-filter-syntax) for the full referen { "semantic": "embedding performance", "fulltext": "nomic ollama", - "grep": null, - "meta": null, "tree": "me.design.*", - "temporal": null, - "weights": null, - "candidateLimit": 0, - "limit": 5, - "order_by": null + "limit": 5 } ``` @@ -116,14 +102,8 @@ See [Tree filter syntax](../concepts.md#tree-filter-syntax) for the full referen ```json { - "semantic": null, - "fulltext": null, - "grep": null, "meta": { "type": "decision" }, "tree": "me.strategy.*", - "temporal": null, - "weights": null, - "candidateLimit": 0, "limit": 20, "order_by": "desc" } @@ -132,6 +112,7 @@ See [Tree filter syntax](../concepts.md#tree-filter-syntax) for the full referen ## Notes - Provide at least one of `semantic`, `fulltext`, or a filter (`tree`, `meta`, `temporal`, `grep`) -- otherwise the search has no criteria. +- Optional parameters may be omitted or explicitly passed as `null` — both are treated as "no value". - When both `semantic` and `fulltext` are provided, results are ranked using Reciprocal Rank Fusion (hybrid mode). - `order_by` only applies to filter-only searches (no `semantic`/`fulltext`). Ranked searches are always sorted by score. - `score` ranges from 0 to 1, where 1 is the best match. diff --git a/docs/mcp/me_memory_tree.md b/docs/mcp/me_memory_tree.md index 3403c6d..454ff86 100644 --- a/docs/mcp/me_memory_tree.md +++ b/docs/mcp/me_memory_tree.md @@ -8,8 +8,8 @@ Shows how memories are organized and how many exist at each level. Use this to u | Name | Type | Required | Description | |------|------|----------|-------------| -| `tree` | `string \| null` | yes | Root path to display from (e.g., `work.projects`). Pass `null` for the full tree. | -| `levels` | `integer` | yes | Maximum depth to display. Pass `0` for unlimited. | +| `tree` | `string \| null` | no | Root path to display from (e.g., `work.projects`). Omit or pass `null` for the full tree. | +| `levels` | `integer \| null` | no | Maximum depth to display. Omit or pass `null` for unlimited. | ## Returns @@ -37,7 +37,6 @@ Shows how memories are organized and how many exist at each level. Use this to u ```json { - "tree": null, "levels": 2 } ``` @@ -46,8 +45,7 @@ Shows how memories are organized and how many exist at each level. Use this to u ```json { - "tree": "me.design", - "levels": 0 + "tree": "me.design" } ``` diff --git a/docs/mcp/me_memory_update.md b/docs/mcp/me_memory_update.md index 7f96459..e781524 100644 --- a/docs/mcp/me_memory_update.md +++ b/docs/mcp/me_memory_update.md @@ -2,24 +2,24 @@ Modify an existing memory. -Provide the ID and any fields to change. Fields set to `null` remain unchanged. +Provide the ID and any fields to change. Omitted fields remain unchanged. ## Parameters | Name | Type | Required | Description | |------|------|----------|-------------| | `id` | `string` | yes | The UUID of the memory to update. | -| `content` | `string \| null` | yes | New content. Pass `null` to keep existing. | -| `meta` | `object \| null` | yes | New metadata. Pass `null` to keep existing. | -| `tree` | `string \| null` | yes | New tree path. Pass `null` to keep existing. | -| `temporal` | `object \| null` | yes | New time range. Pass `null` to keep existing. | +| `content` | `string \| null` | no | New content. Omit or pass `null` to keep existing. | +| `meta` | `object \| null` | no | New metadata. Omit or pass `null` to keep existing. | +| `tree` | `string \| null` | no | New tree path. Omit or pass `null` to keep existing. | +| `temporal` | `object \| null` | no | New time range. Omit or pass `null` to keep existing. | ### temporal | Name | Type | Required | Description | |------|------|----------|-------------| | `start` | `string` | yes | ISO 8601 timestamp for the start of the time range. | -| `end` | `string \| null` | yes | ISO 8601 timestamp for the end. Pass `null` for a point-in-time memory. | +| `end` | `string \| null` | no | ISO 8601 timestamp for the end. Omit or pass `null` for a point-in-time memory. | ## Returns @@ -48,10 +48,7 @@ Update only the content, keep everything else: ```json { "id": "0194a000-0001-7000-8000-000000000001", - "content": "PostgreSQL 18 supports native UUIDv7 via the uuidv7() function.", - "meta": null, - "tree": null, - "temporal": null + "content": "PostgreSQL 18 supports native UUIDv7 via the uuidv7() function." } ``` @@ -67,5 +64,5 @@ Update only the content, keep everything else: - Always fetch the memory first (`me_memory_get`) to see the current state before updating. - **`meta` is fully replaced, not merged.** If you want to add a key, fetch the current meta first, merge locally, then send the full object. - Updating `content` triggers a new embedding computation -- this is automatic, no action needed. `hasEmbedding` may temporarily become `false`. -- Omitted fields (set to `null`) are preserved -- you can update just `tree` without touching `content`. +- Omitted fields are preserved -- you can update just `tree` without touching `content`. - Returns an error if the memory does not exist. diff --git a/packages/cli/mcp/server.ts b/packages/cli/mcp/server.ts index 65168c4..ba04f9a 100644 --- a/packages/cli/mcp/server.ts +++ b/packages/cli/mcp/server.ts @@ -52,29 +52,38 @@ Docs: ${docUrl("me_memory_create")}`, inputSchema: { id: z .string() + .optional() .nullable() - .describe("UUIDv7 for idempotent creates (null to auto-generate)"), + .describe( + "UUIDv7 for idempotent creates (omit or null to auto-generate)", + ), content: z.string().min(1).describe("The content of the memory"), meta: z .record(z.string(), z.any()) + .optional() .nullable() - .describe("Key-value metadata pairs (null to omit)"), + .describe("Key-value metadata pairs"), tree: z .string() + .optional() .nullable() .describe( - "Hierarchical path (e.g., work.projects.me). Null defaults to root", + "Hierarchical path (e.g., work.projects.me). Omit or null to store at the root.", ), temporal: z .object({ start: z.string().describe("ISO timestamp for start of time range"), end: z .string() + .optional() .nullable() - .describe("ISO timestamp for end (null for point-in-time)"), + .describe( + "ISO timestamp for end (omit or null for point-in-time)", + ), }) + .optional() .nullable() - .describe("Time range for the memory (null to omit)"), + .describe("Time range for the memory"), }, annotations: { title: "Create Memory", @@ -89,7 +98,12 @@ Docs: ${docUrl("me_memory_create")}`, content: args.content, meta: args.meta ?? undefined, tree: args.tree ?? undefined, - temporal: args.temporal ?? undefined, + temporal: args.temporal + ? { + start: args.temporal.start, + end: args.temporal.end ?? undefined, + } + : undefined, }); return { content: [ @@ -112,24 +126,29 @@ Docs: ${docUrl("me_memory_search")}`, inputSchema: { semantic: z .string() + .optional() .nullable() .describe("Natural language query for semantic/meaning search"), fulltext: z .string() + .optional() .nullable() .describe("Keywords/phrases for BM25 exact matching"), grep: z .string() + .optional() .nullable() .describe( "Regex pattern filter on content (POSIX, case-insensitive). Applied as WHERE filter alongside other filters.", ), meta: z .record(z.string(), z.any()) + .optional() .nullable() - .describe("Filter by metadata attributes (null to omit)"), + .describe("Filter by metadata attributes"), tree: z .string() + .optional() .nullable() .describe( "Filter by tree path. Bare path (work.projects) matches exactly \u2014 use work.projects.* to include descendants. Supports lquery patterns (*.api.*) and ltxtquery label search (api & v2).", @@ -138,6 +157,7 @@ Docs: ${docUrl("me_memory_search")}`, .object({ contains: z .string() + .optional() .nullable() .describe("Find memories containing this point in time"), overlaps: z @@ -145,6 +165,7 @@ Docs: ${docUrl("me_memory_search")}`, start: z.string().describe("Start of range"), end: z.string().describe("End of range"), }) + .optional() .nullable() .describe("Find memories overlapping this range"), within: z @@ -152,36 +173,46 @@ Docs: ${docUrl("me_memory_search")}`, start: z.string().describe("Start of range"), end: z.string().describe("End of range"), }) + .optional() .nullable() .describe("Find memories fully within this range"), }) + .optional() .nullable() - .describe("Temporal filter for search (null to omit)"), + .describe("Temporal filter for search"), weights: z .object({ fulltext: z .number() + .optional() .nullable() .describe("Weight for BM25 keyword matching (0-1)"), semantic: z .number() + .optional() .nullable() .describe("Weight for semantic similarity (0-1)"), }) + .optional() .nullable() - .describe("Weights for hybrid search ranking (null to omit)"), + .describe("Weights for hybrid search ranking"), candidateLimit: z .number() .int() + .optional() + .nullable() .describe( "Candidates per search mode before RRF fusion (0 = default 30)", ), limit: z .number() .int() + .optional() + .nullable() .describe("Maximum results (0 = default 10, max: 1000)"), order_by: z .string() + .optional() .nullable() .describe( "Sort direction for filter-only searches (no semantic/fulltext). Default: desc", @@ -215,9 +246,12 @@ Docs: ${docUrl("me_memory_search")}`, } : undefined, candidateLimit: - args.candidateLimit > 0 ? args.candidateLimit : undefined, - limit: args.limit > 0 ? args.limit : undefined, - orderBy: (args.order_by as "asc" | "desc") ?? undefined, + args.candidateLimit && args.candidateLimit > 0 + ? args.candidateLimit + : undefined, + limit: args.limit && args.limit > 0 ? args.limit : undefined, + orderBy: + (args.order_by as "asc" | "desc" | null | undefined) ?? undefined, }); return { content: [ @@ -271,26 +305,33 @@ Docs: ${docUrl("me_memory_update")}`, id: z.string().describe("The UUID of the memory to update"), content: z .string() + .optional() .nullable() - .describe("New content (null to keep existing)"), + .describe("New content (omit or null to keep existing)"), meta: z .record(z.string(), z.any()) + .optional() .nullable() - .describe("New metadata (null to keep existing)"), + .describe("New metadata (omit or null to keep existing)"), tree: z .string() + .optional() .nullable() - .describe("New tree path (null to keep existing)"), + .describe("New tree path (omit or null to keep existing)"), temporal: z .object({ start: z.string().describe("ISO timestamp for start of time range"), end: z .string() + .optional() .nullable() - .describe("ISO timestamp for end (null for point-in-time)"), + .describe( + "ISO timestamp for end (omit or null for point-in-time)", + ), }) + .optional() .nullable() - .describe("Time range for the memory (null to omit)"), + .describe("Time range for the memory"), }, annotations: { title: "Update Memory", @@ -305,7 +346,12 @@ Docs: ${docUrl("me_memory_update")}`, content: args.content ?? undefined, meta: args.meta ?? undefined, tree: args.tree ?? undefined, - temporal: args.temporal ?? undefined, + temporal: args.temporal + ? { + start: args.temporal.start, + end: args.temporal.end ?? undefined, + } + : undefined, }); return { content: [ @@ -437,14 +483,17 @@ Docs: ${docUrl("me_memory_tree")}`, inputSchema: { tree: z .string() + .optional() .nullable() .describe( - "Root path to display from (e.g., work.projects). Null for full tree", + "Root path to display from (e.g., work.projects). Omit or null for full tree.", ), levels: z .number() .int() - .describe("Maximum depth to display (0 = unlimited)"), + .optional() + .nullable() + .describe("Maximum depth to display (omit or null for unlimited)"), }, annotations: { title: "Memory Tree", @@ -456,7 +505,7 @@ Docs: ${docUrl("me_memory_tree")}`, async (args) => { const result = await client.memory.tree({ tree: args.tree ?? undefined, - levels: args.levels > 0 ? args.levels : undefined, + levels: args.levels && args.levels > 0 ? args.levels : undefined, }); return { content: [ @@ -479,18 +528,21 @@ Docs: ${docUrl("me_memory_import")}`, inputSchema: { path: z .string() + .optional() .nullable() .describe( "Absolute path to a file or directory. Directories are imported recursively. Format is inferred from extension (.json, .yaml, .yml, .md, .ndjson, .jsonl). Mutually exclusive with content.", ), content: z .string() + .optional() .nullable() .describe( "Raw content to import (JSON array, YAML array, or Markdown with frontmatter). Mutually exclusive with path.", ), format: z .string() + .optional() .nullable() .describe( "Content format: json, yaml, or md. Required when using content, optional when using path (inferred from extension).", @@ -620,15 +672,21 @@ Token-efficient: use \`path\` to write directly to a file instead of returning c Docs: ${docUrl("me_memory_export")}`, inputSchema: { - tree: z.string().nullable().describe("Tree path filter (null for all)"), + tree: z + .string() + .optional() + .nullable() + .describe("Tree path filter (omit or null for all)"), meta: z .record(z.string(), z.any()) + .optional() .nullable() - .describe("Metadata filter (null to omit)"), + .describe("Metadata filter"), temporal: z .object({ contains: z .string() + .optional() .nullable() .describe("Find memories containing this point in time"), overlaps: z @@ -636,6 +694,7 @@ Docs: ${docUrl("me_memory_export")}`, start: z.string().describe("Start of range"), end: z.string().describe("End of range"), }) + .optional() .nullable() .describe("Find memories overlapping this range"), within: z @@ -643,21 +702,28 @@ Docs: ${docUrl("me_memory_export")}`, start: z.string().describe("Start of range"), end: z.string().describe("End of range"), }) + .optional() .nullable() .describe("Find memories fully within this range"), }) + .optional() .nullable() - .describe("Temporal filter (null to omit)"), + .describe("Temporal filter"), format: z.string().describe("Output format: json, yaml, or md"), limit: z .number() .int() - .describe("Maximum memories to export (0 = default 1000)"), + .optional() + .nullable() + .describe( + "Maximum memories to export (omit or null for default 1000)", + ), path: z .string() + .optional() .nullable() .describe( - "Absolute file or directory path to write to. For md format, use a directory path to write one .md file per memory. Null to return content inline.", + "Absolute file or directory path to write to. For md format, use a directory path to write one .md file per memory. Omit or null to return content inline.", ), }, annotations: { @@ -669,7 +735,7 @@ Docs: ${docUrl("me_memory_export")}`, }, async (args) => { const searchParams: Record = { - limit: args.limit > 0 ? args.limit : 1000, + limit: args.limit && args.limit > 0 ? args.limit : 1000, orderBy: "asc", }; if (args.tree) searchParams.tree = args.tree;