Skip to content

feat(memory): opt-in write limits on graph size - #5534

Merged
cliffhall merged 5 commits into
v2/mainfrom
v2/feat/5525-memory-write-caps
Nov 12, 2026
Merged

cliffhall merged 5 commits into
v2/mainfrom
v2/feat/5525-memory-write-caps

Conversation

@cliffhall

Copy link
Copy Markdown
Member

Closes #5525

Description

Adds three opt-in write limits to the memory server, and documents the linear per-write cost the issue measured.

  • MEMORY_MAX_ENTITIES, MEMORY_MAX_RELATIONS, MEMORY_MAX_FILE_BYTES: each unset by default (no change in behavior). When one is set, create_entities, create_relations and add_observations refuse a call that would leave the graph over the limit and larger than it was on that dimension, with a tool error naming the variable. The whole call is refused and nothing is saved.
  • Comparing against the size before the call keeps a graph that is already over a limit (one that grew before the limit was set, or a lowered limit) usable: a call that adds nothing is still answered, and the delete tools are never refused, so the graph can always shrink.
  • MEMORY_MAX_FILE_BYTES counts UTF-8 bytes of the file as it would be saved (unreadable lines included), so it bounds each write's cost directly.
  • An invalid value (anything but a positive whole number) stops startup with an error naming the variable, rather than leaving the store silently unbounded.
  • README: the three variables, and a new Limiting how large the graph grows section explaining that every write rewrites the whole file under the lock, so cost and lock time grow linearly with it.

Server Details

  • Server: memory
  • Changes to: configuration (three new environment variables), the three additive write tools' behavior when a limit is set, README. No tool, schema or description changes (interface-diff: no interface changes).

Motivation and Context

#5525: the read side gained opt-in limits in #3953; the write side had no operator-set ceiling while every mutation's cost grows linearly with the file. The issue asked for parity (opt-in caps on entities, relations, bytes) and/or a README note; this does both.

How Has This Been Tested?

Unit and protocol-level tests in src/memory/__tests__/write-limits.test.ts (env parsing, each limit, whole-call refusal with the file left byte-for-byte unchanged, no-op calls at the limit, shrinking a graph over a lowered limit, bytes vs characters, and main() reading the environment). npm run local:gate exits 0.

Inspector CLI 2.12.0, node src/memory/dist/index.js -e MEMORY_MAX_ENTITIES=2, the same sequence in --protocol-era legacy (2025-11-25) and --protocol-era modern (2026-07-28, result _meta carries io.modelcontextprotocol/serverInfo), with identical results:

Step Result Exit
create_entities A, B created both 0
create_entities C isError: true, "Refused: this change would bring the knowledge graph to 3 entities, over the limit of 2 set by MEMORY_MAX_ENTITIES. Nothing was saved. …" 5
delete_entities B "Entities deleted successfully" 0
create_entities C created 0
tools/list with MEMORY_MAX_FILE_BYTES=10MB server refuses to start: MEMORY_MAX_FILE_BYTES must be a positive whole number, got "10MB" 1

LLM client: Claude Code 2.1.322 (claude -p --strict-mcp-config, local build, MEMORY_MAX_ENTITIES=2; the era cannot be selected in this client). Prompt: "Using the memory server, create three entities of type person named Ada, Grace and Linus, in one create_entities call. If the call fails, report the error message verbatim and stop." The model called create_entities, got the refusal, and reported it verbatim; the graph file was never created.

Breaking Changes

None. Every limit is off unless its variable is set.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Protocol Documentation
  • My changes follow MCP security best practices
  • I have updated the server's README accordingly
  • I have added a changeset (npm run changeset) if this changes what a TypeScript server publishes (minor)
  • I have tested this with an LLM client
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have documented all environment variables and configuration options

Additional context

The limits are checked in saveGraph against the size loadGraphWithUnreadable read, after the serialized content is built and before anything is written, so a refusal leaves the file untouched. Only the three additive tools pass a baseline; the delete tools skip the check.

🤖 Generated with Claude Code

Add MEMORY_MAX_ENTITIES, MEMORY_MAX_RELATIONS and MEMORY_MAX_FILE_BYTES.
When set, create_entities, create_relations and add_observations refuse a
call that would leave the graph over a limit and larger than it was, with
a tool error naming the variable; nothing is saved. Delete tools are never
refused, so a graph over a lowered limit can always shrink. An invalid
value stops startup. The README documents the limits and that every write
rewrites the whole memory file under the lock, so its cost grows linearly
with the file.

Closes #5525

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall cliffhall added the v2 label Nov 12, 2026
@changeset-bot

changeset-bot Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fba4d6b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@modelcontextprotocol/server-memory Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

memory: MCP interface diff

Base: c991ded23026 (Merge pull request #5536 from modelcontextprotocol/v2/feat/576-streamable-http-all-servers)
Head: the build in this checkout, at 24327c388da8 (Merge fba4d6b into c991ded)

Diffed because it was named (--server).

✅ No interface changes detected.

Tools Prompts Resources Resource templates
Base 9 0 1 0
Head 9 0 1 0

Updated by this run for fba4d6be541b51a45359eef9eef1d4cfaf1e3472.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Additive no-ops can incorrectly fail the file-size cap when saving normalizes existing content, and blank configured limits silently disable the cap contrary to the documented validation rules.

2 open issues
What changed in this PR

Adds opt-in graph-growth limits to the memory server for #5525, complementing its existing read-side controls and helping operators manage whole-file write costs.

Changes:

  • Introduces environment-configured limits on entity count, relation count, and saved file bytes.
  • Checks additive writes before saving, while leaving delete operations unrestricted.
  • Adds limit-validation and protocol-level tests, operator documentation, and a minor-release changeset.
File Description
src/​memory/​README.md Documents the limits, refusal behavior, and linear write cost.
src/​memory/​index.ts Parses configuration and enforces growth limits before saving.
src/​memory/​__tests__/​write-limits.test.ts Tests parsing, limit enforcement, unchanged files after refusal, and startup wiring.
src/​memory/​__tests__/​helpers.ts Allows test connections to configure graph limits.
.changeset/​memory-write-limits.md Records the feature for a minor package release.

Review effort: Balanced

Comment thread src/memory/index.ts Resolved
Comment thread src/memory/index.ts Resolved
A call that adds nothing is no longer checked against the limits, since
re-saving can grow a file not written in canonical form. An empty or
blank limit variable now stops startup instead of leaving the limit off,
as the README says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 1: two findings, both fixed in c4f8da4.

  • Blank limit values silently disabled a cap, contrary to the README: an empty or blank value now stops startup; only an unset variable leaves a limit off.
  • A no-op additive call could fail MEMORY_MAX_FILE_BYTES when re-saving normalizes the file: the limits now apply only to a call that adds something. Regression test added.

npm run local:gate exits 0. Requesting round 2.

@github-actions

Copy link
Copy Markdown
Contributor

git: MCP interface diff

Base: 9914133c0557 (Merge pull request #5532 from modelcontextprotocol/v2/docs/5524-everything-get-env-masking-wording)
Head: the build in this checkout, at a0239d298480 (Merge c4f8da4 into 1f8badb)

Diffed because it was named (--server).

⚠️ Interface changes detected in 1 part(s) of the interface. Review them below to confirm they are intended.

Tools Prompts Resources Resource templates
Base 14 2 0 0
Head 14 2 0 0

tools

--- base/tools.json
+++ target/tools.json

- tools[git_remote].description: "Lists the repository's remotes with their fetch and push URLs, like `git remote -v`. Credentials emb..."
+ tools[git_remote].description: "Lists the repository's remotes with their fetch and push URLs, like `git remote -v`. Credentials emb..."

Updated by this run for c4f8da452da3008fb435c0c0404aef7b7348e281.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The no-op regression test fixture stays the same byte size after serialization, so it would not catch incorrectly rechecking no-op calls against the byte limit.

1 open issue
2 resolved since last review

Review effort: Balanced

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Copilot round 2: one finding, fixed in a54816d. The no-op byte-limit fixture's extra space offset its missing final newline, so the test could not catch a regression; it now grows on save, and was confirmed to fail without the fix. Round 1's two findings show as resolved. npm run local:gate exits 0. Requesting round 3.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues were identified for the opt-in memory graph limits.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

cliffhall and others added 2 commits November 12, 2026 00:21
…y-write-caps

Signed-off-by: cliffhall <cliff@futurescale.com>

# Conflicts:
#	src/memory/index.ts
…y-write-caps

Resolve the createServer signature against #5536's Streamable HTTP
transport: limits becomes the fourth parameter, after
publishGraphUpdated. run() reads the limits for the HTTP path too, and
startHttpServer passes them to each request's server, with a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
@cliffhall

Copy link
Copy Markdown
Member Author

Round 3 was clean. Since then v2/main gained Streamable HTTP for memory (#5536), which conflicted with this PR's createServer signature. Merged in fba4d6b: limits is now the fourth parameter (after publishGraphUpdated), run() reads the limits for the HTTP path as well, and startHttpServer passes them to each request's server, with a test over real HTTP. npm run local:gate exits 0. Requesting round 4 for the new wiring.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No active review issues remain for the opt-in memory graph limits.

No open issues 🎉

Review effort: Balanced

@cliffhall
cliffhall merged commit e1d154e into v2/main Nov 12, 2026
46 checks passed
@cliffhall
cliffhall deleted the v2/feat/5525-memory-write-caps branch November 12, 2026 05:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

memory: write side has no size caps, and every mutation rewrites the whole graph file — measured linear cost (hardening note)

2 participants