diff --git a/.env.example b/.env.example index a7e26839..83753a55 100644 --- a/.env.example +++ b/.env.example @@ -113,6 +113,10 @@ TZ=UTC # Set to false to hide memory tools, skip auto-initialization, and omit # memory references from server metadata. MEMORY_DIR is ignored when false. # MEMORY_ENABLED=true +# Enable or disable file tools — vault_read_file and vault_list_files (default: true). +# Set to false to hide file tools entirely. Useful for remote deployments where +# Obsidian Sync has attachment syncing disabled (no files on disk). +# FILE_TOOLS_ENABLED=true # Memory folder name in your vault (default: "About Me") MEMORY_DIR=About Me # Comma-separated folders protected from deletion (default: MEMORY_DIR, Daily Notes). diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d0ab257b..1e29f1ca 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -249,6 +249,7 @@ jobs: EMBEDDING_ENABLED: ${{ vars.EMBEDDING_ENABLED }} RERANK_MODE: ${{ vars.RERANK_MODE }} MEMORY_ENABLED: ${{ vars.MEMORY_ENABLED }} + FILE_TOOLS_ENABLED: ${{ vars.FILE_TOOLS_ENABLED }} MEMORY_DIR: ${{ vars.MEMORY_DIR }} PROTECTED_PATHS: ${{ vars.PROTECTED_PATHS }} ORPHAN_EXCLUDE_FOLDERS: ${{ vars.ORPHAN_EXCLUDE_FOLDERS }} @@ -270,6 +271,7 @@ jobs: EMBEDDING_ENABLED=${EMBEDDING_ENABLED:-true} RERANK_MODE=${RERANK_MODE:-blended} MEMORY_ENABLED=${MEMORY_ENABLED:-true} + FILE_TOOLS_ENABLED=${FILE_TOOLS_ENABLED:-true} MEMORY_DIR=${MEMORY_DIR:-About Me} PUID=1000 PGID=1000 diff --git a/.github/workflows/test_deploy.yml b/.github/workflows/test_deploy.yml index 16df3cae..f8d549b6 100644 --- a/.github/workflows/test_deploy.yml +++ b/.github/workflows/test_deploy.yml @@ -194,6 +194,7 @@ jobs: EMBEDDING_ENABLED: ${{ vars.EMBEDDING_ENABLED }} RERANK_MODE: ${{ vars.RERANK_MODE }} MEMORY_ENABLED: ${{ vars.MEMORY_ENABLED }} + FILE_TOOLS_ENABLED: ${{ vars.FILE_TOOLS_ENABLED }} MEMORY_DIR: ${{ vars.MEMORY_DIR }} PROTECTED_PATHS: ${{ vars.PROTECTED_PATHS }} ORPHAN_EXCLUDE_FOLDERS: ${{ vars.ORPHAN_EXCLUDE_FOLDERS }} @@ -215,6 +216,7 @@ jobs: EMBEDDING_ENABLED=${EMBEDDING_ENABLED:-true} RERANK_MODE=${RERANK_MODE:-blended} MEMORY_ENABLED=${MEMORY_ENABLED:-true} + FILE_TOOLS_ENABLED=${FILE_TOOLS_ENABLED:-true} MEMORY_DIR=${MEMORY_DIR:-About Me} PUID=1000 PGID=1000 diff --git a/AGENTS.md b/AGENTS.md index d0516904..e703b912 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -202,7 +202,8 @@ on**, not just its topic: orchestrator that composes `TOOL_NAMES` from the domain group modules under `mcp-core/tools/` (vault-crud, search, memory, daily-note, task, asset) and calls each register function — conditionally skipping memory tools when - `MEMORY_ENABLED` is `false`. Each group module is self-contained: its own tool + `MEMORY_ENABLED` is `false` and file tools when `FILE_TOOLS_ENABLED` is + `false`. Each group module is self-contained: its own tool name constants, register function, and data-layer imports. Shared helpers (`safeHandler`, `formatNoteMetadata`, `ToolRegistrationContext` type) live in `tool-helpers.ts`. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index bd3a0ae2..4ec334c2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -303,6 +303,8 @@ The extension-to-representation routing above is implemented by the `vault-opera `vault_list_files` is the discovery surface (also `vault-operations/asset-operations.ts`): a filesystem walk (`vaultFs.listAssets` — filesystem truth, deliberately not the index), folder and case-insensitive extension filters, per-extension counts computed over the full filtered set, and byte sizes statted only for the returned slice. Files are readable and browsable but not yet searchable — content indexing is a possible future tier. +**Opt-out:** File tools are opt-out: set `FILE_TOOLS_ENABLED=false` to hide `vault_read_file` and `vault_list_files`, and strip file tool references from server metadata and other tool descriptions. The `vault_get_outgoing_links` tool continues to report file links (it indexes from the links table, not the file tools). File config vars (`MAX_FILE_BYTES`, `MAX_IMAGE_OUTPUT_BYTES`, `MAX_PDF_RENDER_PAGES`) are still parsed when disabled. + ### Tasks (R9) | Tool | Input | Annotation | diff --git a/DEPLOY.md b/DEPLOY.md index fba1f380..1a476801 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -61,7 +61,7 @@ Then open `~/.config/vault-cortex/.env` and fill in the remaining values: | `VAULT_PASSWORD` | Only if vault has E2E encryption | | `OBSIDIAN_AUTH_TOKEN` | Generate with the command below | -The [`.env.example`](./.env.example) file also includes optional configuration for the embedding pipeline (`EMBEDDING_ENABLED`), the reranker (`RERANK_MODE`), the memory system (`MEMORY_ENABLED`, `MEMORY_DIR`, `PROTECTED_PATHS`, `ORPHAN_EXCLUDE_FOLDERS`), timezone (`TZ`), and OAuth metadata (`SERVICE_DOCUMENTATION_URL`). All have sensible defaults — see the [Configuration](./README.md#configuration) section in the README. +The [`.env.example`](./.env.example) file also includes optional configuration for the embedding pipeline (`EMBEDDING_ENABLED`), the reranker (`RERANK_MODE`), the memory system (`MEMORY_ENABLED`, `MEMORY_DIR`, `PROTECTED_PATHS`, `ORPHAN_EXCLUDE_FOLDERS`), file tools (`FILE_TOOLS_ENABLED`), timezone (`TZ`), and OAuth metadata (`SERVICE_DOCUMENTATION_URL`). All have sensible defaults — see the [Configuration](./README.md#configuration) section in the README. ```bash npx vault-cortex@latest get-sync-token @@ -329,6 +329,7 @@ To find your stage: `cat .sst/stage` (after your first deploy). | `EMBEDDING_ENABLED` | Optional. Set `false` to disable the embedding pipeline — skips model download, vector tables, embedding passes, and hybrid search. Search falls back to FTS5 keyword matching. Default: `true`. | | `RERANK_MODE` | Optional. Cross-encoder reranking mode: `blended` (default) applies position-aware score blending after RRF fusion, `none` skips reranking for lower latency. Only takes effect when `EMBEDDING_ENABLED` is true. | | `MEMORY_ENABLED` | Optional. Set `false` to disable the memory layer entirely — hides memory tools, skips bootstrap, omits memory from server metadata. Default: `true`. | +| `FILE_TOOLS_ENABLED` | Optional. Set `false` to hide file tools (`vault_read_file`, `vault_list_files`) for deployments without synced attachments. Default: `true`. | | `MEMORY_DIR` | Optional. Memory folder name in the vault (default: `About Me`). See the [Configuration](./README.md#configuration) section. | | `PROTECTED_PATHS` | Optional. Comma-separated folders protected from deletion (default: `MEMORY_DIR, Daily Notes`). Overrides the default entirely when set. | | `ORPHAN_EXCLUDE_FOLDERS` | Optional. Comma-separated folders excluded from orphan detection (default: `Daily Notes, Templates, MEMORY_DIR`). Overrides the default entirely when set. | diff --git a/DOCKERHUB.md b/DOCKERHUB.md index 4cec7cbf..86423c99 100644 --- a/DOCKERHUB.md +++ b/DOCKERHUB.md @@ -144,6 +144,7 @@ All settings are environment variables with sensible defaults. | `EMBEDDING_ENABLED` | — | `true` | Set `false` to disable the embedding pipeline — skips model download, vector tables, embedding passes, and hybrid search. Search falls back to FTS5 keyword matching. | | `RERANK_MODE` | — | `blended` | Cross-encoder reranking mode: `blended` applies position-aware score blending after RRF fusion (~200ms added latency), `none` skips reranking. Only takes effect when `EMBEDDING_ENABLED` is true. | | `MEMORY_ENABLED` | — | `true` | Set `false` to fully disable the memory layer — hides memory tools, skips bootstrap, omits memory from server metadata. `MEMORY_DIR` is ignored when `false`. | +| `FILE_TOOLS_ENABLED` | — | `true` | Set `false` to hide file tools (`vault_read_file`, `vault_list_files`) — useful for remote deployments where Obsidian Sync has attachment syncing disabled. | | `MEMORY_DIR` | — | `About Me` | Vault folder for structured memory files | | `PROTECTED_PATHS` | — | `MEMORY_DIR, Daily Notes` | Folders that `vault_delete_note` refuses to touch | | `ORPHAN_EXCLUDE_FOLDERS` | — | `Daily Notes, Templates, MEMORY_DIR` | Folders excluded from orphan detection | diff --git a/README.md b/README.md index 923baf20..4630a568 100644 --- a/README.md +++ b/README.md @@ -304,6 +304,7 @@ All settings are environment variables with sensible defaults. | `EMBEDDING_ENABLED` | — | `true` | Set `false` to disable the embedding pipeline — skips model download, vector tables, embedding passes, and hybrid search. Search falls back to FTS5 keyword matching. | | `RERANK_MODE` | — | `blended` | Cross-encoder reranking mode: `blended` applies position-aware score blending after RRF fusion (~200ms added latency), `none` skips reranking. Only takes effect when `EMBEDDING_ENABLED` is true. | | `MEMORY_ENABLED` | — | `true` | Set `false` to fully disable the memory layer — hides memory tools, skips bootstrap, omits memory from server metadata. `MEMORY_DIR` is ignored when `false`. | +| `FILE_TOOLS_ENABLED` | — | `true` | Set `false` to hide file tools (`vault_read_file`, `vault_list_files`) — useful for remote deployments where Obsidian Sync has attachment syncing disabled. | | `MEMORY_DIR` | — | `About Me` | Vault folder for structured memory files | | `PROTECTED_PATHS` | — | `MEMORY_DIR, Daily Notes` | Folders that `vault_delete_note` refuses to touch | | `ORPHAN_EXCLUDE_FOLDERS` | — | `Daily Notes, Templates, MEMORY_DIR` | Folders excluded from orphan detection | @@ -317,7 +318,7 @@ All settings are environment variables with sensible defaults. | `MAX_IMAGE_OUTPUT_BYTES` | — | `49152` (48 KiB) | Byte budget for images delivered by `vault_read_file`, in binary bytes before base64 encoding. Images exceeding this are downscaled and recompressed to fit. Sized for the tightest mainstream MCP client cap; raise for clients that accept larger responses. | | `MAX_PDF_RENDER_PAGES` | — | `5` | Maximum PDF pages to render as images when `raw: true` is set on `vault_read_file`. The per-page byte budget is `MAX_IMAGE_OUTPUT_BYTES` divided evenly across the rendered pages — fewer pages means higher quality each. | -**Smart defaults:** Setting `MEMORY_DIR` automatically updates the defaults for `PROTECTED_PATHS` and `ORPHAN_EXCLUDE_FOLDERS`. You only set those explicitly for a fully custom list. When `MEMORY_ENABLED` is `false`, the memory layer is fully disabled — memory tools are hidden and the memory folder is not auto-created. +**Smart defaults:** Setting `MEMORY_DIR` automatically updates the defaults for `PROTECTED_PATHS` and `ORPHAN_EXCLUDE_FOLDERS`. You only set those explicitly for a fully custom list. When `MEMORY_ENABLED` is `false`, the memory layer is fully disabled — memory tools are hidden and the memory folder is not auto-created. When `FILE_TOOLS_ENABLED` is `false`, file tools are hidden entirely — useful when Obsidian Sync has attachment syncing disabled and no files exist on disk. See [`templates/memory/`](./templates/memory/) for memory file examples and the dated-entry design philosophy. diff --git a/cli/src/env.ts b/cli/src/env.ts index 6bc8ab47..6bbbb989 100644 --- a/cli/src/env.ts +++ b/cli/src/env.ts @@ -70,6 +70,9 @@ RERANK_MODE=blended # Enable or disable the memory layer (default: true). # Set to false to hide memory tools and skip About Me/ creation. MEMORY_ENABLED=true +# Enable or disable file tools — vault_read_file and vault_list_files (default: true). +# Set to false when Obsidian Sync has attachment syncing disabled. +FILE_TOOLS_ENABLED=true # Memory folder name in your vault (default: About Me). MEMORY_DIR=About Me @@ -161,6 +164,9 @@ MAX_PDF_RENDER_PAGES=5 # Enable or disable the memory layer (default: true). # Set to false to hide memory tools and skip About Me/ creation. MEMORY_ENABLED=true +# Enable or disable file tools — vault_read_file and vault_list_files (default: true). +# Set to false when Obsidian Sync has attachment syncing disabled. +FILE_TOOLS_ENABLED=true # Memory folder name in your vault (default: About Me). MEMORY_DIR=About Me diff --git a/deploy/local/.env.example b/deploy/local/.env.example index 8fa09988..050c9dde 100644 --- a/deploy/local/.env.example +++ b/deploy/local/.env.example @@ -53,6 +53,9 @@ RERANK_MODE=blended # Enable or disable the memory layer (default: true). # Set to false to hide memory tools and skip About Me/ creation. MEMORY_ENABLED=true +# Enable or disable file tools — vault_read_file and vault_list_files (default: true). +# Set to false when Obsidian Sync has attachment syncing disabled. +FILE_TOOLS_ENABLED=true # Memory folder name in your vault (default: About Me). MEMORY_DIR=About Me diff --git a/deploy/local/README.md b/deploy/local/README.md index 28da72a3..c1ca0874 100644 --- a/deploy/local/README.md +++ b/deploy/local/README.md @@ -3,7 +3,7 @@ Run Vault Cortex on your machine against a local Obsidian vault. No cloud, no Obsidian Sync — just Docker and a folder of `.md` files. -**Contents** — [Prerequisites](#prerequisites) · [Setup](#setup) · [Connect](#connect-your-mcp-client) · [Verify](#verify) · [Updating](#updating) · [Stop](#stop) · [Windows](#windows-docker-desktop) · [Memory](#memory) · [Config](#configuration) · [Troubleshooting](#troubleshooting) +**Contents** — [Prerequisites](#prerequisites) · [Setup](#setup) · [Connect](#connect-your-mcp-client) · [Verify](#verify) · [Updating](#updating) · [Stop](#stop) · [Windows](#windows-docker-desktop) · [Memory](#memory) · [File Tools](#file-tools) · [Config](#configuration) · [Troubleshooting](#troubleshooting) ## Prerequisites @@ -231,11 +231,17 @@ starter template files on first startup, and agents grow it from there. See [Memory](../../README.md#memory) in the main README for how the layer works, and [templates/memory](../../templates/memory/README.md) for the file format. +## File Tools + +File tools (`vault_read_file`, `vault_list_files`) are enabled by default. Set +`FILE_TOOLS_ENABLED=false` in your `.env` to hide them — useful when Obsidian +Sync has attachment syncing disabled and no files exist on disk. + ## Configuration Only `MCP_AUTH_TOKEN` and `VAULT_PATH` are required. For optional settings -(memory folder, protected paths, orphan exclusions, timezone), see the -[Configuration](../../README.md#configuration) section in the main README. +(memory folder, protected paths, orphan exclusions, file tools, timezone), see +the [Configuration](../../README.md#configuration) section in the main README. ## Troubleshooting diff --git a/deploy/local/docker-compose.yml b/deploy/local/docker-compose.yml index 9599961d..f0bbc322 100644 --- a/deploy/local/docker-compose.yml +++ b/deploy/local/docker-compose.yml @@ -28,6 +28,7 @@ services: EMBEDDING_ENABLED: ${EMBEDDING_ENABLED:-true} RERANK_MODE: ${RERANK_MODE:-blended} MEMORY_ENABLED: ${MEMORY_ENABLED:-true} + FILE_TOOLS_ENABLED: ${FILE_TOOLS_ENABLED:-true} MEMORY_DIR: ${MEMORY_DIR:-About Me} LOG_LEVEL: ${LOG_LEVEL:-info} LOG_DIR: ${LOG_DIR:-} diff --git a/deploy/remote/.env.example b/deploy/remote/.env.example index 7bcdc616..7e277453 100644 --- a/deploy/remote/.env.example +++ b/deploy/remote/.env.example @@ -67,6 +67,9 @@ MAX_PDF_RENDER_PAGES=5 # Enable or disable the memory layer (default: true). # Set to false to hide memory tools and skip About Me/ creation. MEMORY_ENABLED=true +# Enable or disable file tools — vault_read_file and vault_list_files (default: true). +# Set to false when Obsidian Sync has attachment syncing disabled. +FILE_TOOLS_ENABLED=true # Memory folder name in your vault (default: About Me). MEMORY_DIR=About Me diff --git a/deploy/remote/README.md b/deploy/remote/README.md index c109946a..2d095219 100644 --- a/deploy/remote/README.md +++ b/deploy/remote/README.md @@ -15,7 +15,7 @@ configuration. Both produce an identical container — restart policy, log rotation, and health check included — and Podman or any OCI-compatible container runtime works in place of Docker. -**Contents** — [Prerequisites](#prerequisites) · [Setup](#setup) · [HTTPS access](#https-access) · [Connect](#connect-your-mcp-client) · [Verify](#verify) · [Monitoring](#monitoring) · [Updating](#updating) · [Restart](#restart) · [Stop](#stop) · [Memory](#memory) · [Config](#configuration) · [Hardening](#hardening-recommended) · [Troubleshooting](#troubleshooting) +**Contents** — [Prerequisites](#prerequisites) · [Setup](#setup) · [HTTPS access](#https-access) · [Connect](#connect-your-mcp-client) · [Verify](#verify) · [Monitoring](#monitoring) · [Updating](#updating) · [Restart](#restart) · [Stop](#stop) · [Memory](#memory) · [File Tools](#file-tools) · [Config](#configuration) · [Hardening](#hardening-recommended) · [Troubleshooting](#troubleshooting) ## Prerequisites @@ -366,18 +366,25 @@ starter template files on first startup, and agents grow it from there. See [Memory](../../README.md#memory) in the main README for how the layer works, and [templates/memory](../../templates/memory/README.md) for the file format. +## File Tools + +File tools (`vault_read_file`, `vault_list_files`) are enabled by default. Set +`FILE_TOOLS_ENABLED=false` in your `.env` to hide them — useful when Obsidian +Sync has attachment syncing disabled and no files exist on disk. + ## Configuration Only `MCP_AUTH_TOKEN`, `PUBLIC_URL`, `OBSIDIAN_AUTH_TOKEN`, and `VAULT_NAME` are required. These optional settings are worth knowing about: -| Setting | Default | What it does | -| ------------------- | --------- | ----------------------------------------------------------------------------------------------------- | -| `TZ` | `UTC` | Your IANA timezone (e.g. `America/New_York`) — affects daily note dates and timestamps | -| `VAULT_PASSWORD` | — | Set this if your vault has end-to-end encryption enabled | -| `EMBEDDING_ENABLED` | `true` | Set `false` to skip AI models (~45MB) and use keyword search only — saves memory on smaller instances | -| `RERANK_MODE` | `blended` | Set `none` to skip reranking for lower latency | -| `MEMORY_ENABLED` | `true` | Set `false` to disable the structured memory layer | +| Setting | Default | What it does | +| -------------------- | --------- | ----------------------------------------------------------------------------------------------------- | +| `TZ` | `UTC` | Your IANA timezone (e.g. `America/New_York`) — affects daily note dates and timestamps | +| `VAULT_PASSWORD` | — | Set this if your vault has end-to-end encryption enabled | +| `EMBEDDING_ENABLED` | `true` | Set `false` to skip AI models (~45MB) and use keyword search only — saves memory on smaller instances | +| `RERANK_MODE` | `blended` | Set `none` to skip reranking for lower latency | +| `MEMORY_ENABLED` | `true` | Set `false` to disable the structured memory layer | +| `FILE_TOOLS_ENABLED` | `true` | Set `false` to hide file tools when Obsidian Sync has attachment syncing disabled | All settings are documented in `.env.example` and in the [Configuration](../../README.md#configuration) section of the main README. diff --git a/deploy/remote/docker-compose.yml b/deploy/remote/docker-compose.yml index 525d8815..d73616c9 100644 --- a/deploy/remote/docker-compose.yml +++ b/deploy/remote/docker-compose.yml @@ -41,6 +41,7 @@ services: EMBEDDING_ENABLED: ${EMBEDDING_ENABLED:-true} RERANK_MODE: ${RERANK_MODE:-blended} MEMORY_ENABLED: ${MEMORY_ENABLED:-true} + FILE_TOOLS_ENABLED: ${FILE_TOOLS_ENABLED:-true} MEMORY_DIR: ${MEMORY_DIR:-About Me} LOG_LEVEL: ${LOG_LEVEL:-info} LOG_DIR: ${LOG_DIR:-/data/logs} diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 8224f4f5..6cc9a9e1 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -27,6 +27,7 @@ services: EMBEDDING_ENABLED: ${EMBEDDING_ENABLED:-true} RERANK_MODE: ${RERANK_MODE:-blended} MEMORY_ENABLED: ${MEMORY_ENABLED:-true} + FILE_TOOLS_ENABLED: ${FILE_TOOLS_ENABLED:-true} MEMORY_DIR: ${MEMORY_DIR:-About Me} TZ: ${TZ:-UTC} # Windows: set WINDOWS_MODE=true when developing against a vault on a C: drive diff --git a/docker-compose.yml b/docker-compose.yml index ecdd2674..14eb89da 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,6 +44,7 @@ services: EMBEDDING_ENABLED: ${EMBEDDING_ENABLED:-true} RERANK_MODE: ${RERANK_MODE:-blended} MEMORY_ENABLED: ${MEMORY_ENABLED:-true} + FILE_TOOLS_ENABLED: ${FILE_TOOLS_ENABLED:-true} WINDOWS_MODE: ${WINDOWS_MODE:-false} MAX_FILE_BYTES: ${MAX_FILE_BYTES:-52428800} MAX_IMAGE_OUTPUT_BYTES: ${MAX_IMAGE_OUTPUT_BYTES:-49152} diff --git a/server.json b/server.json index 2a26ef3d..5f8c874d 100644 --- a/server.json +++ b/server.json @@ -96,6 +96,11 @@ "description": "Enable or disable the structured memory layer. When false, memory tools are hidden, bootstrap is skipped, and server metadata omits memory references.", "default": "true" }, + { + "name": "FILE_TOOLS_ENABLED", + "description": "Enable or disable file tools (vault_read_file, vault_list_files). When false, file tools are hidden and server metadata omits file tool references.", + "default": "true" + }, { "name": "MEMORY_DIR", "description": "Vault folder for structured memory files (About Me-style notes). Memory tools are hidden when MEMORY_ENABLED is false, but this value still feeds the defaults for PROTECTED_PATHS and ORPHAN_EXCLUDE_FOLDERS.", diff --git a/src/vault-mcp/__tests__/config.test.ts b/src/vault-mcp/__tests__/config.test.ts index e172ec58..37cbfb15 100644 --- a/src/vault-mcp/__tests__/config.test.ts +++ b/src/vault-mcp/__tests__/config.test.ts @@ -301,6 +301,47 @@ describe("loadConfig", () => { }) }) + describe("FILE_TOOLS_ENABLED", () => { + it("defaults to true when unset", () => { + const config = loadConfig(EMPTY_ENV) + expect(config.fileToolsEnabled).toBe(true) + }) + + it("is true when set to 'true'", () => { + const config = loadConfig({ FILE_TOOLS_ENABLED: "true" }) + expect(config.fileToolsEnabled).toBe(true) + }) + + it("is false when set to 'false'", () => { + const config = loadConfig({ FILE_TOOLS_ENABLED: "false" }) + expect(config.fileToolsEnabled).toBe(false) + }) + + it("rejects a non-boolean value", () => { + expect(() => loadConfig({ FILE_TOOLS_ENABLED: "yes" })).toThrow( + /FILE_TOOLS_ENABLED/, + ) + }) + + it("still parses MAX_FILE_BYTES when disabled", () => { + const config = loadConfig({ + FILE_TOOLS_ENABLED: "false", + MAX_FILE_BYTES: "10485760", + }) + expect(config.fileToolsEnabled).toBe(false) + expect(config.maxFileBytes).toBe(10_485_760) + }) + + it("still parses MAX_IMAGE_OUTPUT_BYTES when disabled", () => { + const config = loadConfig({ + FILE_TOOLS_ENABLED: "false", + MAX_IMAGE_OUTPUT_BYTES: "65536", + }) + expect(config.fileToolsEnabled).toBe(false) + expect(config.maxImageOutputBytes).toBe(65_536) + }) + }) + describe("MAX_FILE_BYTES", () => { it("defaults to 50 MiB (52428800) when unset", () => { const config = loadConfig(EMPTY_ENV) diff --git a/src/vault-mcp/config.ts b/src/vault-mcp/config.ts index a8a4b4a7..14c7d82d 100644 --- a/src/vault-mcp/config.ts +++ b/src/vault-mcp/config.ts @@ -37,6 +37,10 @@ export type VaultConfig = Readonly<{ /** When false, the memory layer is fully disabled — bootstrap is skipped, * memory tools are hidden, and server metadata omits memory references. */ memoryEnabled: boolean + /** When false, file tools (vault_read_file, vault_list_files) are hidden — + * tool registration is skipped and server metadata omits file tool + * references. File config vars are still parsed when disabled. */ + fileToolsEnabled: boolean memoryDir: string protectedPaths: readonly string[] orphanExcludeFolders: readonly string[] @@ -107,6 +111,12 @@ export const loadConfig = ( .default("true") .asBool() + const fileToolsEnabled = envVar + .from(env) + .get("FILE_TOOLS_ENABLED") + .default("true") + .asBool() + const embeddingEnabled = envVar .from(env) .get("EMBEDDING_ENABLED") @@ -156,6 +166,7 @@ export const loadConfig = ( return Object.freeze({ memoryEnabled, + fileToolsEnabled, memoryDir, protectedPaths, orphanExcludeFolders, diff --git a/src/vault-mcp/mcp-core/__tests__/mcp-router.test.ts b/src/vault-mcp/mcp-core/__tests__/mcp-router.test.ts index d1531a42..017f0b15 100644 --- a/src/vault-mcp/mcp-core/__tests__/mcp-router.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/mcp-router.test.ts @@ -285,6 +285,44 @@ describe("createMcpRouter — POST /mcp", () => { expect(options.instructions).not.toContain("About Me/") }) + it("instructions omit vault_read_file when FILE_TOOLS_ENABLED is false", async () => { + const disabledConfig = loadConfig({ FILE_TOOLS_ENABLED: "false" }) + const harness = await setupHarness({ config: disabledConfig }) + vi.mocked(isInitializeRequest).mockReturnValue(true) + await fetch(harness.url(), { + method: "POST", + headers: { ...baseHeaders }, + body: JSON.stringify(initializeBody), + }) + const constructorCalls = vi.mocked(McpServer).mock.calls + expect(constructorCalls).toHaveLength(1) + const options = constructorCalls[0]?.[1] as + { instructions?: string } | undefined + expect(options?.instructions).not.toContain("vault_read_file") + expect(options?.instructions).toContain("vault_search") + }) + + it("instructions omit both vault_read_file and vault_get_memory when both disabled", async () => { + const bothDisabledConfig = loadConfig({ + MEMORY_ENABLED: "false", + FILE_TOOLS_ENABLED: "false", + }) + const harness = await setupHarness({ config: bothDisabledConfig }) + vi.mocked(isInitializeRequest).mockReturnValue(true) + await fetch(harness.url(), { + method: "POST", + headers: { ...baseHeaders }, + body: JSON.stringify(initializeBody), + }) + const constructorCalls = vi.mocked(McpServer).mock.calls + expect(constructorCalls).toHaveLength(1) + const options = constructorCalls[0]?.[1] as + { instructions?: string } | undefined + expect(options?.instructions).not.toContain("vault_read_file") + expect(options?.instructions).not.toContain("vault_get_memory") + expect(options?.instructions).toContain("vault_write_note") + }) + it("connects the new server to the new transport", async () => { const { harness, transport } = await setupInitializedSession() expect(harness.serverInstances[0]!.connect).toHaveBeenCalledWith( diff --git a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts index 3e38c3bd..aa2b350e 100644 --- a/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/tool-definitions.test.ts @@ -724,6 +724,55 @@ describe("MEMORY_ENABLED=false", () => { }) }) +describe("FILE_TOOLS_ENABLED=false", () => { + const FILE_TOOLS = [ + TOOL_NAMES.VAULT_READ_FILE, + TOOL_NAMES.VAULT_LIST_FILES, + ] as const + + const FILE_TOOL_SET = new Set(FILE_TOOLS) + const EXPECTED_NON_FILE_TOOLS = ALL_TOOL_NAMES.filter( + (toolName) => !FILE_TOOL_SET.has(toolName), + ) + + const registerWithDisabledFileTools = (): RegisterToolCall[] => { + const server = { registerTool: vi.fn() } + registerTools({ + server: server as unknown as McpServer, + vaultPath: "/test-vault", + search: {} as SearchIndex, + logger, + config: loadConfig({ FILE_TOOLS_ENABLED: "false" }), + }) + return server.registerTool.mock.calls as RegisterToolCall[] + } + + it("does not register file tools", () => { + const disabledCalls = registerWithDisabledFileTools() + const registeredNames = disabledCalls.map(([toolName]) => toolName) + for (const fileTool of FILE_TOOLS) { + expect(registeredNames).not.toContain(fileTool) + } + }) + + it("registers exactly the non-file tools", () => { + const disabledCalls = registerWithDisabledFileTools() + const registeredNames = disabledCalls.map(([toolName]) => toolName) + expect(new Set(registeredNames)).toEqual(new Set(EXPECTED_NON_FILE_TOOLS)) + expect(registeredNames).toHaveLength(EXPECTED_NON_FILE_TOOLS.length) + }) + + it("non-file tool descriptions do not reference file tools", () => { + const disabledCalls = registerWithDisabledFileTools() + for (const [, toolConfig] of disabledCalls) { + expect(toolConfig.description).toBeDefined() + for (const fileToolName of FILE_TOOLS) { + expect(toolConfig.description).not.toContain(fileToolName) + } + } + }) +}) + describe("vault_memory_recall handler", () => { const mockExtra = { requestId: "test-1", sessionId: "session-1" } diff --git a/src/vault-mcp/mcp-core/__tests__/vault-orientation-prompt.test.ts b/src/vault-mcp/mcp-core/__tests__/vault-orientation-prompt.test.ts index 2f09d7c2..6a2dffa5 100644 --- a/src/vault-mcp/mcp-core/__tests__/vault-orientation-prompt.test.ts +++ b/src/vault-mcp/mcp-core/__tests__/vault-orientation-prompt.test.ts @@ -373,6 +373,21 @@ describe("vault-orientation with MEMORY_ENABLED=false", () => { }) }) +// ── FILE_TOOLS_ENABLED=false ────────────────────────────────── + +describe("vault-orientation with FILE_TOOLS_ENABLED=false", () => { + const disabledConfig = loadConfig({ FILE_TOOLS_ENABLED: "false" }) + + it("omits vault_list_files from the go-deeper tools", async () => { + const { calls } = await setupVault({ config: disabledConfig }) + const handler = findCall(calls, PROMPT_NAMES.VAULT_ORIENTATION)[2] + const text = textOf(await handler(fakeExtra)) + + expect(text).not.toContain("vault_list_files") + expect(text).toContain("vault_read_note") + }) +}) + // ── Genericness ───────────────────────────────────────────────── describe("vault-orientation genericness", () => { diff --git a/src/vault-mcp/mcp-core/mcp-router.ts b/src/vault-mcp/mcp-core/mcp-router.ts index ef00a483..421d2379 100644 --- a/src/vault-mcp/mcp-core/mcp-router.ts +++ b/src/vault-mcp/mcp-core/mcp-router.ts @@ -82,6 +82,15 @@ export const createMcpRouter = ({ const searchDescription = config.embeddingEnabled ? "hybrid search" : "full-text search" + const fileToolsClause = config.fileToolsEnabled + ? "; vault_read_file for images, canvases, and other non-markdown files" + : "" + const memoryClause = config.memoryEnabled + ? `. Use vault_get_memory to retrieve user preferences and context from ${config.memoryDir}/ files. Use vault_write_note and vault_update_memory for writes.` + : ". Use vault_write_note for writes." + const instructions = `Read, write, and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes${fileToolsClause}${memoryClause} + +Vault content is Obsidian Flavored Markdown. Write tools pass content through without escaping — be intentional about Obsidian syntax (#, [[, %%, etc.) in inputs.` const server = new McpServer( { name: "vault-cortex", @@ -93,15 +102,7 @@ export const createMcpRouter = ({ icons: SERVER_ICONS, websiteUrl: SERVER_WEBSITE_URL, }, - { - instructions: config.memoryEnabled - ? `Read, write, and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes; vault_read_file for images, canvases, and other non-markdown files. Use vault_get_memory to retrieve user preferences and context from ${config.memoryDir}/ files. Use vault_write_note and vault_update_memory for writes. - -Vault content is Obsidian Flavored Markdown. Write tools pass content through without escaping — be intentional about Obsidian syntax (#, [[, %%, etc.) in inputs.` - : `Read, write, and search an Obsidian vault. Use vault_search and vault_read_note to find and read notes; vault_read_file for images, canvases, and other non-markdown files. Use vault_write_note for writes. - -Vault content is Obsidian Flavored Markdown. Write tools pass content through without escaping — be intentional about Obsidian syntax (#, [[, %%, etc.) in inputs.`, - }, + { instructions }, ) const sessionLogger = logger.child({ diff --git a/src/vault-mcp/mcp-core/prompts/vault-orientation-prompt.ts b/src/vault-mcp/mcp-core/prompts/vault-orientation-prompt.ts index bfa9fe1b..a8b1bc1e 100644 --- a/src/vault-mcp/mcp-core/prompts/vault-orientation-prompt.ts +++ b/src/vault-mcp/mcp-core/prompts/vault-orientation-prompt.ts @@ -240,6 +240,9 @@ export const registerVaultOrientationPrompt = ({ const memoryTools = config.memoryEnabled ? "- `vault_get_memory` — read memory files in detail" : "" + const fileTools = config.fileToolsEnabled + ? "- `vault_list_files` — browse non-markdown files (images, canvases, data files)" + : "" const goDeeper = [ `- \`vault_search\` — ${config.embeddingEnabled ? "hybrid" : "full-text"} search across all notes`, "- `vault_search_by_tag` — explore notes by tag", @@ -247,7 +250,7 @@ export const registerVaultOrientationPrompt = ({ orphanTools, memoryTools, "- `vault_read_note` — read any note's full content", - "- `vault_list_files` — browse non-markdown files (images, canvases, data files)", + fileTools, ] .filter(Boolean) .join("\n") diff --git a/src/vault-mcp/mcp-core/tool-definitions.ts b/src/vault-mcp/mcp-core/tool-definitions.ts index 55178327..82c7d0c2 100644 --- a/src/vault-mcp/mcp-core/tool-definitions.ts +++ b/src/vault-mcp/mcp-core/tool-definitions.ts @@ -40,7 +40,9 @@ export const registerTools = (params: { } registerDailyNoteTools(params) registerTaskTools(params) - registerAssetTools(params) + if (params.config.fileToolsEnabled) { + registerAssetTools(params) + } const registeredCount = Object.keys(VAULT_CRUD_TOOL_NAMES).length + @@ -48,6 +50,6 @@ export const registerTools = (params: { (params.config.memoryEnabled ? Object.keys(MEMORY_TOOL_NAMES).length : 0) + Object.keys(DAILY_NOTE_TOOL_NAMES).length + Object.keys(TASK_TOOL_NAMES).length + - Object.keys(FILE_TOOL_NAMES).length + (params.config.fileToolsEnabled ? Object.keys(FILE_TOOL_NAMES).length : 0) params.logger.info("registered tools", { count: registeredCount }) } diff --git a/src/vault-mcp/mcp-core/tools/search-tools.ts b/src/vault-mcp/mcp-core/tools/search-tools.ts index b93e3d8d..43e68228 100644 --- a/src/vault-mcp/mcp-core/tools/search-tools.ts +++ b/src/vault-mcp/mcp-core/tools/search-tools.ts @@ -604,6 +604,12 @@ Returns: JSON with path (the queried note), backlinks (array of { path, title, b }, ) + const fileReadableClause = config.fileToolsEnabled + ? " readable via vault_read_file" + : "" + const fileBytesClause = config.fileToolsEnabled + ? " — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image file is still cheap to read" + : "" server.registerTool( TOOL_NAMES.VAULT_GET_OUTGOING_LINKS, { @@ -618,7 +624,7 @@ For incoming links (what links TO a note), use vault_get_backlinks. Parameters: - path is matched against the search index, so the note must be indexed (file watcher processes new/moved files within seconds). A path not in the index returns an empty result (count 0), not an error — indistinguishable from a note with no outbound links. -Returns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes } sorted by target path), and count. Each link carries exists (boolean) and kind ("note"|"file"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF) readable via vault_read_file; !exists+note = broken link. bytes is the on-disk file size for notes and files alike (null for broken links) — not the delivery cost: vault_read_file downscales images to fit response limits, so a large image file is still cheap to read.`, +Returns: JSON with path, outgoing_links (array of { path, title, exists, kind, bytes } sorted by target path), and count. Each link carries exists (boolean) and kind ("note"|"file"): exists+note = readable via vault_read_note; exists+file = non-markdown file (.canvas, image, PDF)${fileReadableClause}; !exists+note = broken link. bytes is the on-disk file size for notes and files alike (null for broken links)${fileBytesClause}.`, inputSchema: { path: z .string() diff --git a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts index 2bfd650e..c3e66994 100644 --- a/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts +++ b/src/vault-mcp/mcp-core/tools/vault-crud-tools.ts @@ -52,7 +52,7 @@ Errors: - "heading not found" — no heading matches the text; error lists available headings - "ambiguous heading" — multiple headings match; use heading_level to disambiguate - "outline, heading, and properties_only are mutually exclusive" — only one mode per call -- 'path must end in ".md"' — the path names a non-markdown file; read files (images, .canvas, data files) with vault_read_file instead +- 'path must end in ".md"' — the path names a non-markdown file${config.fileToolsEnabled ? "; read files (images, .canvas, data files) with vault_read_file instead" : ""} Returns: Raw markdown string (default); JSON object of properties (properties_only); JSON outline object (outline); raw markdown of the section, heading line included (heading). diff --git a/src/vault-mcp/server.ts b/src/vault-mcp/server.ts index 9878665a..544f5164 100644 --- a/src/vault-mcp/server.ts +++ b/src/vault-mcp/server.ts @@ -76,6 +76,7 @@ const startServer = async (): Promise => { logger.info("config loaded", { memoryEnabled: config.memoryEnabled, + fileToolsEnabled: config.fileToolsEnabled, memoryDir: config.memoryDir, embeddingEnabled: config.embeddingEnabled, rerankMode: config.rerankMode,