fix: server.json audit corrections + OCI ownership label#56
Merged
Conversation
Full audit of server.json against the 2025-12-11 MCP server schema, the
actual env vars the code reads, the published Docker image, and GitHub
repo metadata. Eight fixes:
Critical (the existing entry would not work as published):
1. Dockerfile: add `LABEL io.modelcontextprotocol.server.name=...` —
required for `mcp-publisher publish` ownership verification against
the OCI image manifest.
2. packages[0].identifier: ghcr.io/aliasunder/vault-cortex →
ghcr.io/aliasunder/vault-mcp. The published image is vault-mcp
(see .github/workflows/deploy.yml + all docker-compose files);
the vault-cortex image does not exist.
3. repository.id: "aliasunder/vault-cortex" → "1226067541". Schema
requires the numeric GitHub repo ID (from
`gh api repos/aliasunder/vault-cortex --jq '.id'`), not the slug.
High-impact gaps (consumers couldn't auto-configure):
4. packages[0].transport.headers: declare
`Authorization: Bearer {MCP_AUTH_TOKEN}` so clients know what to
send on the wire to /mcp. Previously the server required bearer
auth but no header declaration existed.
5. packages[0].runtimeHint + runtimeArguments: declare `docker` plus
`-p {PORT}:8000` and `-v {HOST_VAULT_PATH}:/vault` so clients can
construct a valid `docker run` from server.json alone.
Env-var inaccuracies:
6. PUBLIC_URL: keep isRequired: false but add `default:
http://localhost:8000`. Server crashes on startup without
PUBLIC_URL (see src/vault-mcp/server.ts:36 — `.required()`);
the default lets the local quickstart work without manual override.
7. Add eight missing env vars the server actually reads — verified
by grepping `env.get(...)` / `env.X` across `src/`: PORT, HOST,
INDEX_DB_PATH, LOG_DIR, LOG_RETENTION_DAYS, PROTECTED_PATHS,
ORPHAN_EXCLUDE_FOLDERS, SERVICE_DOCUMENTATION_URL. All optional
with defaults sourced from the code where they exist.
8. VAULT_PATH description clarified to distinguish the container
path (/vault, declared here) from the host path (declared via
the -v runtime argument's HOST_VAULT_PATH variable).
Out of scope: icons (waits for ^icon-400), remotes (no public
hosted service), URL templating against PORT in transport.url
(done — already templated as {PORT}).
Verification: jq schema-shape check passes (all required fields
present); description 94 chars (under 100 max); identifier matches
the actual published image; repository.id matches GitHub API;
all 14 declared env vars exist in the source. 496/496 tests pass,
lint + tsc clean.
After merge, cut v0.15.4 to publish the labeled image — only then
can `mcp-publisher publish` succeed against the MCP Registry.
The word was ambiguous and misleading for the registry entry:
- Transport sense ("HTTP-based MCP server, not stdio") — true, but
redundant: OAuth-protected already implies HTTP.
- Deployment sense ("hosted somewhere far away") — false for the
package as published. The package describes a `docker run` on the
consumer's own machine; no public hosted URL is offered.
A registry consumer reading "Remote MCP server" reasonably expects a
hosted URL they could plug into Claude Desktop. We don't provide one
(the personal Lightsail deploy is not a public multi-tenant service).
Dropping "Remote" makes the description match what the package
actually delivers.
87 chars (was 94, still under the 100-char schema max).
Mirrors the comment in the repo root's docker-compose.yml so adopters self-hosting from deploy/remote/ also see the upstream tracker link and know the workaround is temporary.
VAULT_PATH was overloaded — appearing as both the user-facing host path in runtimeArguments (as HOST_VAULT_PATH) and the container env var in environmentVariables (default "/vault"). Two knobs for what users should see as one concept. Aligned with the README + deploy/local/.env.example convention: users only see VAULT_PATH meaning "where your vault lives on this machine." - Dockerfile: bake VAULT_PATH=/vault into the runtime ENV, alongside PORT/HOST/NODE_ENV. Container always has it; no need to expose as a configurable knob. - server.json: drop VAULT_PATH from environmentVariables (now a Dockerfile internal). Rename HOST_VAULT_PATH → VAULT_PATH in runtimeArguments so the only VAULT_PATH a user sees means the same thing it means in their .env. - Compose files: untouched. Their explicit `VAULT_PATH: /vault` env lines are now redundant no-ops but harmless.
Both vars are hardcoded in all compose files but had no default in server.json, so a `docker run` constructed from server.json alone would diverge from the compose user experience: - INDEX_DB_PATH: compose sets /data/index.db; without it the code falls back to /data/search.db (different filename). Documented default /data/index.db to match. - LOG_DIR: compose sets /data/logs to enable persistent file logging; without it the file sink is off entirely (stdout only). Documented default /data/logs so registry-spawned containers log to disk like compose deployments do. Noted that an empty string disables it. Traced all 13 env vars against the compose files + src/: every optional var with a static default now declares it. PROTECTED_PATHS and ORPHAN_EXCLUDE_FOLDERS intentionally have no `default` field — their defaults derive from MEMORY_DIR at runtime and can't be a static string, so they stay documented in the description.
PROTECTED_PATHS / ORPHAN_EXCLUDE_FOLDERS / SERVICE_DOCUMENTATION_URL have runtime-derived smart defaults (config.ts) that previously were only discoverable via the README. Stated them inline in all four compose files so operators editing them see the actual defaults: - PROTECTED_PATHS = "<MEMORY_DIR>, Daily Notes" - ORPHAN_EXCLUDE_FOLDERS = "Daily Notes, Templates, <MEMORY_DIR>" - SERVICE_DOCUMENTATION_URL = https://github.com/aliasunder/vault-cortex Root pair (docker-compose.yml, docker-compose.local.yml) keep the active empty-default form; deploy pair (deploy/local, deploy/remote) keep the commented-out form. Comments adapted to each.
Both were exposed as user-tunable container env vars while ALSO being
used as the host side of the port mapping / bind — the same host-vs-
container overload as the earlier VAULT_PATH fix.
PORT appeared in three places: environmentVariables (container listen
port), runtimeArguments `-p {PORT}:8000` (host port), and transport.url
(host port). Overriding it desynced them — e.g. PORT=9000 makes the
server listen on 9000 inside the container while `-p 9000:8000` still
maps host 9000 to container 8000 → connection refused. HOST had the
same latent break (127.0.0.1 would make the server unreachable through
the port mapping).
The container always listens on 0.0.0.0:8000 — already baked into the
Dockerfile ENV. Removing both from environmentVariables leaves PORT
only as the host-side variable in `-p {PORT}:8000` + the transport URL,
so overriding it now works correctly. Every remaining env var is a
value the server actually reads with no runtime-mechanics conflict.
LOG_DIR was hardcoded (LOG_DIR: /data/logs) in the deploy/local and
deploy/remote compose files, so users couldn't override it even though
server.json advertises it as optional with that default. Switched both
to ${LOG_DIR:-/data/logs} and documented it in the matching
.env.example files (set empty to disable file logging).
Root docker-compose.yml left as-is — its .env.example already documents
LOG_DIR as intentionally fixed for the Lightsail deploy.
deploy/local defaulted LOG_DIR to /data/logs, turning on persistent
file logging for every local user. A local user just trying vault-cortex
out doesn't necessarily want log files accumulating — stdout is enough.
Switched deploy/local to ${LOG_DIR:-} (empty default = file logging off;
set a path in .env to opt in). deploy/remote keeps the /data/logs default
since an unattended server benefits from persistent logs. Updated the
deploy/local .env.example wording to match (opt-in, not default-on).
…parity
The registry path spawns containers purely from server.json's
runtimeArguments — the compose files aren't involved — so a
registry-spawned container previously had no /data volume: OAuth tokens,
search index, and logs were all ephemeral, lost on restart.
Brought server.json to parity with the deploy/local compose experience:
- Add `-v vault-cortex-data:/data` named volume so OAuth sessions, the
search index, and any logs persist across restarts (matches
deploy/local's mcp_data:/data).
- Make the vault mount explicitly :rw (matches deploy/local).
- Drop LOG_DIR's default so file logging is opt-in (matches the
deploy/local ${LOG_DIR:-} change — local users don't get log files
unless they set LOG_DIR=/data/logs).
INDEX_DB_PATH is hardcoded (INDEX_DB_PATH: /data/index.db) in all four compose files — never user-overridable — so it's a container-internal implementation detail, not a config knob. Same category as VAULT_PATH, PORT, and HOST. Baked INDEX_DB_PATH=/data/index.db into the Dockerfile ENV (keeps the registry-spawned container on the same filename as the compose flow, rather than the code's /data/search.db fallback) and removed it from server.json environmentVariables. The remaining 10 declared env vars are all genuinely user-tunable.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Follow-up to #55. A thorough audit of
server.jsonagainst the 2025-12-11 MCP server schema, the env vars the code actually reads, the published Docker image, the GitHub repo metadata, and all four compose files.The guiding realization: the MCP Registry path consumes
server.jsononly — the compose files are never involved. A client pulls the OCI image and builds adocker runpurely fromserver.json'sruntimeArguments+environmentVariables+transport. So server.json must be self-sufficient, and container-internal implementation details must live in the image (DockerfileENV), not be exposed as phantom config knobs.Critical fixes (the published entry would not work)
Dockerfile— OCI ownership label.mcp-publisher publishverifies image ownership viaLABEL io.modelcontextprotocol.server.name, which must matchnamein server.json. Was missing.packages[0].identifier—ghcr.io/aliasunder/vault-cortex→ghcr.io/aliasunder/vault-mcp. The published image isvault-mcp(seedeploy.yml:22+ all compose files); thevault-corteximage never existed, so the old entry pointed at a 404.repository.id—"aliasunder/vault-cortex"→"1226067541"(the numeric GitHub repo ID the schema requires).Container internals moved to the Dockerfile (removed from server.json)
These four are hardcoded in every compose file — never user-tunable — so they're implementation details, not knobs. Each was overloaded or latently buggy when exposed in
environmentVariables:VAULT_PATH— was both the user's host path (in-v) and the container path (env var). Now bakedENV VAULT_PATH=/vault; the onlyVAULT_PATHa user sees is the host side of the-v {VAULT_PATH}:/vault:rwbind mount.PORT— was the host port (-p {PORT}:8000) AND the container listen port (env var). Overriding it desynced them (-p 9000:8000while the server moved to 9000 → connection refused). Now bakedENV PORT=8000;PORTsurvives only as the host-side mapping variable.HOST— overriding to127.0.0.1made the server unreachable through the port mapping. Now bakedENV HOST=0.0.0.0.INDEX_DB_PATH— hardcoded/data/index.dbeverywhere. Now bakedENV INDEX_DB_PATH=/data/index.db.Final Dockerfile runtime ENV:
NODE_ENV=production PORT=8000 HOST=0.0.0.0 VAULT_PATH=/vault INDEX_DB_PATH=/data/index.db.server.json now describes the real consumer experience
transport.headers— declaresAuthorization: Bearer {MCP_AUTH_TOKEN}so clients know what to send to/mcp.runtimeHint: "docker"+runtimeArguments—-p {PORT}:8000,-v {VAULT_PATH}:/vault:rw(bind mount, the user's vault), and-v vault-cortex-data:/data(named volume — persists search index, OAuth token DB, and logs across restarts, matchingdeploy/local'smcp_data:/data).PUBLIC_URL—isRequired: falsewithdefault: "http://localhost:8000"(the server.required()s it; the default lets the local quickstart work without manual override).MCP_AUTH_TOKEN,PUBLIC_URL,MEMORY_DIR,TZ,LOG_LEVEL,LOG_DIR,LOG_RETENTION_DAYS,PROTECTED_PATHS,ORPHAN_EXCLUDE_FOLDERS,SERVICE_DOCUMENTATION_URL.LOG_DIR— no default (opt-in), matching thedeploy/localchange below. Logs go to stdout unless the user sets a path.MCP server for Obsidian vaults — search, memory, link graph, 23 tools, OAuth-protected.(87 chars, under the 100 max). "Remote" implied a hosted URL we don't offer; the package describes a localdocker run.Compose + .env.example consistency
PROTECTED_PATHS,ORPHAN_EXCLUDE_FOLDERS,SERVICE_DOCUMENTATION_URL) instead of pointing at the README.LOG_DIRmade user-overridable indeploy/local(${LOG_DIR:-}— opt-in, file logging off by default for a quick local trial) anddeploy/remote(${LOG_DIR:-/data/logs}— on by default for an unattended server). Documented in both.env.examplefiles. Rootdocker-compose.ymlleft hardcoded (its.env.examplealready documents LOG_DIR as intentionally fixed for the Lightsail deploy).deploy/remote'sinit-config-permscomment, mirroring the root file.Validation
jqschema-shape check: all required fields present at every levelenv.get(NAME)/env.NAMEreads insrc/; the 4 removed ones (VAULT_PATH,PORT,HOST,INDEX_DB_PATH) are baked into the DockerfileENVtsc --noEmitclean; prettier cleanserver.jsonon this branch only works against an image built from this branch's Dockerfile. The releasedv0.15.3image bakes onlyPORT+HOST— notVAULT_PATH/INDEX_DB_PATH. SinceVAULT_PATHis.required()(server.ts:35) and is no longer inserver.json, a container spawned from this server.json against the v0.15.3 image would crash on startup.Required order:
packages[0].version→0.15.4mcp-publisher publish— now the registry references the v0.15.4 image that has the bakesSources:
server.json