zero-memory
Platform

Contracts and errors

Schema-first contracts with a semver contract version, a fixed error taxonomy, and a written deprecation policy.

Every tool input and output in zero-memory is defined by a zod schema in a single contracts package — one source of truth shared by the MCP server, the HTTP layer, and the dashboard. On top of that sit two commitments to external clients: a semver contract version, and a fixed error taxonomy every error passes through.

The cost of an unstable surface

zero-memory is open source and self-hosted: once installations exist in the wild, a breaking change to a tool schema or an error shape breaks other people's setups, not just one deployment. Errors that arrive as free text are dead ends for both audiences: an LLM client cannot branch on prose, and a human cannot build UX on it. Freezing the contract's shape and the error vocabulary before clients depend on them is what makes the surface safe to evolve.

Everything the contract pins down

One package, one source of schemas

All tool contracts live in the contracts package (packages/contracts) as zod schemas: tool inputs, memory shapes, graph shapes. Servers validate against these schemas, and nothing defines a request or response shape anywhere else. English is the canonical language for everything the contract emits. See Canonical language.

Contract version

The package exports a single semver constant, CONTRACT_VERSION, with fixed semantics:

BumpMeaning
majorBreaking change to a tool's schema or response
minorNew optional field or new tool
patchDescriptions and documentation only

The version is surfaced to MCP clients in the initialize response under the protocol extension slot:

{
  "capabilities": {
    "experimental": {
      "zero-memory/contract": { "version": "2.1.0" }
    }
  }
}

This keeps the server-info object protocol-valid while letting a client detect at connect time which contract it is talking to.

Structured results, annotations, and budgets

The MCP surface itself carries the contract, not just the schemas behind it.

Every success payload travels twice, as the protocol recommends: in structuredContent, validated against the tool's declared output schema, and as a compact JSON text block mirroring it for clients that only read text. Read and write results add a third, human-readable footer block. On remember, recall, and build_context it carries the session-state line (attached project, thread), and read results append the standing reminder to store durable facts. The remember, recall, and build_context results also carry an optional session: { attached_project, thread? } field, the 2.1.0 minor. The thread value is a durable token the agent echoes back on later calls so the project survives a transport reconnect; it is a state selector, not a credential (see Scopes and isolation).

Read results append resource_link blocks. Every surfaced memory is dereferenceable at zm://memory/{id} for its full, untruncated row, and the caller's standing rules are served at zm://rules. Which rows a URI resolves to is decided by the caller's authenticated context, never by the URI itself, so a pasted URI can leak nothing but an opaque id.

Every tool declares MCP ToolAnnotations — read-only, destructive, idempotent, and open-world hints — verified against its actual handler by an automated completeness check, so a client's permission layer can classify a call before approving it. openWorldHint is false across the board: every tool talks to the memory store alone.

Two budgets guard clients that silently truncate. Every registered tool description must fit a 2 KB client-visible budget, enforced by an automated check rather than by review. The connect-time instructions channel has the same problem, so the server composes it under a hard budget for clients known to cap it: when the router text plus the owner-rules announcement would not fit, whole segments are dropped in priority order (owner rules before the router's tail) rather than truncated mid-sentence, and the result records what was dropped. Uncapped clients get the full text, including the project-attachment note.

Error taxonomy

Every tool error is one of seven codes:

CodeMeaningHTTP status
validation_failedInput broke the schema or an argument rule400
unauthorizedNo valid authentication401
forbiddenAuthenticated, but not allowed (e.g. another user's scope)403
not_foundThe referenced id does not exist404
conflictThe operation conflicts with current state409
rate_limitedA rate limit or a metered budget is used up; retry later429
internalUnexpected server error500

Tool-plane errors and non-OAuth HTTP routes share one body shape, defined like everything else as a zod schema in the contracts package:

{
  "error": {
    "code": "forbidden",
    "message": "You do not have access to this scope"
  }
}

details is optional structured context, never a stack trace. Tools construct errors through toolError(code, message, details?). On the MCP tool plane a failure comes back as a tool result flagged isError, with this JSON body as its first text block, which is the same slot clients already parse for success payloads. The HTTP layer maps the same codes to the statuses above.

The class is decided where the failure happens, not guessed at the boundary. Services answer with a failure that carries its own code, and the layer that reports it passes that code through; only a genuinely unclassified failure becomes internal. That is what makes code-based branching reliable. A rejection the caller could fix — a secret in the content, a memory that is not an open loop, an id that does not exist — never arrives labelled as a server fault it can do nothing about.

Two protocol-owned surfaces deliberately keep their own envelopes. MCP transport failures are JSON-RPC errors, and OAuth endpoints use the RFC 6749 flat error / error_description body, whose rate-limit response also keeps Retry-After. The consent endpoints the dashboard calls while an agent is being authorized (GET /oauth/authorizations/:id, POST /oauth/authorizations/:id/consent) answer in that same flat body: 401 invalid_token without a signed-in dashboard session, 404 not_found when nothing is pending for the caller under that id (expired, already answered, or opened first by someone else), 409 invalid_request when two decisions race, and 503 server_error when the agent's session could not be created. Clients should not try to parse those protocol envelopes as ToolErrorSchema.

Deprecation policy

Deprecations are declared, not discovered. The public deprecations ledger records what is deprecated, since which contract version, what replaces it, and when it will be removed, which is never earlier than the next major version, plus every breaking change a client must act on. Evolution is additive by preference: new optional fields and new tools (minor bumps) over breaking changes.

Writing a client against it

An MCP client should read capabilities.experimental["zero-memory/contract"].version from initialize and branch on structured error codes, never on message text. Messages are for humans and may be reworded in any release. The advice holds on every surface: a write rejected for something the caller can fix (a secret in the content, a memory that is not an open loop, a missing id) arrives as validation_failed, conflict or not_found, not as internal, which is reserved for failures no caller can act on. There is one deliberate refinement. A handful of validation_failed messages start with a stable, documented prefix that IS part of the contract: scope_target_required: and project_hint_unresolvable: for refused writes, secret_content_rejected: from the content guard. Match on the prefix, never on the human-readable remainder.

An HTTP client can rely on the status-code mapping above. The response body always carries the same error object, so one parser covers every failure. On rate_limited, retry later; the OAuth endpoints' rate-limit response sets Retry-After, so honor it when present.

Before upgrading a server, check the deprecations ledger and the contract version delta. A minor bump is safe to take blindly. A major bump means reading the table first.

Why a version constant and seven codes

MCP clients are configured with a single endpoint URL, so versioning the path (/v1/...) would force every client to reconfigure on each major. A semver constant plus additive evolution keeps the URL stable and makes version drift observable instead of breaking.

The error vocabulary is closed on purpose. Seven codes are few enough for an LLM client to reason about reliably and for a dashboard to give each a distinct treatment. details carries structured specifics without ever making the code set open-ended.

On this page