Skip to content

cpinto/estratos

Repository files navigation

Estratos

A very simple/basic context layer system for LLM agents. Estratos is an MCP server that stores markdown memories in a filesystem-backed namespace tree and resolves context by walking ancestry from root to target, interleaving layer-wide and value-targeted crosscutting memories along the way.

Imagine you're managing a content marketing project (blog) in eg. Claude Cowork. As you iterate on content, you'll end up telling Claude about eg. your writing style — "conversational, second-person, short paragraphs, always open with a hook question" — and it'll be stored as a root memory that follows you across every project.

Inside your Cowork project called "Content", as you keep iterating on a blog post, you'll collect and save project-level context: warm and practical tone, target audience of early-stage founders, never mention competitors by name.

Then for each blog post you layer on blog-specific details: the angle, the target reader, relevant links/instructions. Three different layers of memory.

Two days later you start a fresh chat for a blog post and say "let's write a new blog post about hiring — observe the memories for root.content.blog" Claude calls Estratos, and the ancestry walk does the rest: your personal style, the brand voice, the audience personas, and the positioning rules, all load automatically without you repeating a word.

The following week, you start a help documentation project. You'll store the memories in root.content.helpdocs. When you start a new help document and load helpdocs, it'll also load your personal style, the brand voice, the audience personas, etc. alongisde the helpdocs memories.

Or if you're managing different brands, you could structure the memory layers as root.brand-a.content.blog and root.brand-b.content.blog.

And as these are text files, you can edit them any way you want (if you want to), or connect the MCP server to other agents so that memories are now shared.

Install

The easiest path for Claude Desktop is the one-click extension bundle; every other client uses the npm package.

Claude Desktop: one-click extension (recommended)

  1. Download the latest estratos.mcpb from the releases page.
  2. Double-click the file, or drag it onto a running Claude Desktop window.
  3. When prompted, pick a data directory (defaults to ~/.estratos/data) and leave "Multi-tenant mode" off unless you know you need it.
  4. Claude Desktop restarts the extension automatically; the estratos tools appear in the tool picker.

No JSON editing, no manual npx wiring. Upgrades are a matter of dropping in a newer .mcpb.

Everywhere else: npm

npm install -g estratos

Or run on demand:

npx estratos

Connecting an MCP client

Estratos speaks MCP over stdio, so any client that can launch a stdio MCP server can use it. The snippets below assume estratos is on your PATH (e.g. via npm install -g estratos); if not, substitute the absolute path or use npx -y estratos.

Claude Desktop

Edit your claude_desktop_config.json and add an entry under mcpServers. The file lives at:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "estratos": {
      "command": "npx",
      "args": ["-y", "estratos"],
      "env": {
        "ESTRATOS_DATA_DIR": "~/.estratos/data"
      }
    }
  }
}

Restart Claude Desktop. The estratos tools (store, retrieve, …) should appear in the tool picker.

To run estratos in multi-tenant mode, pass --multi-tenant in args and point ESTRATOS_DATA_DIR at a base directory:

{
  "mcpServers": {
    "estratos": {
      "command": "npx",
      "args": ["-y", "estratos", "--multi-tenant"],
      "env": {
        "ESTRATOS_DATA_DIR": "~/.estratos/tenants"
      }
    }
  }
}

Claude Code (CLI)

Register the server with the claude mcp add command:

claude mcp add estratos -- npx -y estratos

To set environment variables or scope the server to your user config, use the flags:

claude mcp add estratos \
  --scope user \
  --env ESTRATOS_DATA_DIR=~/.estratos/data \
  -- npx -y estratos

You can verify the server is registered with claude mcp list and inspect its tools from inside a Claude Code session.

Cursor

Cursor reads MCP server definitions from ~/.cursor/mcp.json (global) or .cursor/mcp.json inside a project (per-project). Add:

{
  "mcpServers": {
    "estratos": {
      "command": "npx",
      "args": ["-y", "estratos"],
      "env": {
        "ESTRATOS_DATA_DIR": "~/.estratos/data"
      }
    }
  }
}

Open Settings → MCP in Cursor to confirm the server is connected and the tools are listed.

ChatGPT Desktop

ChatGPT exposes MCP servers as custom connectors in Developer Mode (currently a beta feature available to ChatGPT Plus, Pro, and Business workspaces). One important constraint: ChatGPT only talks to MCP servers reachable over HTTP/SSE — it does not spawn local stdio processes — so estratos has to be fronted by an stdio→HTTP bridge such as supergateway or mcp-proxy.

  1. Run estratos behind a stdio→HTTP bridge:

    npx -y supergateway --stdio "npx -y estratos" --port 8765
  2. If ChatGPT needs to reach the bridge from outside your machine, expose it over a public URL — for example with ngrok http 8765.

  3. In ChatGPT, enable Developer Mode under Settings → Connectors → Advanced → Developer Mode. Workspace admins may need to allow it under Permissions & Roles → Connected Data → Developer mode / Create custom MCP connectors.

  4. Open Settings → Connectors → Create, give the connector a name (e.g. Estratos), point its URL at the bridge endpoint (e.g. https://<your-tunnel>.ngrok.app/sse), and save.

The estratos tools then appear in any ChatGPT conversation where the connector is enabled.

OpenClaw

OpenClaw discovers MCP servers through its plugin system. Add an entry under mcpServers in ~/.openclaw/openclaw.json:

{
  "mcpServers": {
    "estratos": {
      "command": "npx",
      "args": ["-y", "estratos"],
      "transport": "stdio",
      "env": {
        "ESTRATOS_DATA_DIR": "~/.estratos/data"
      }
    }
  }
}

Or register it through the CLI, which writes the same entry for you:

openclaw mcp add --transport stdio estratos -- npx -y estratos

Restart your OpenClaw session; the estratos tools are picked up automatically. The exact location of the mcpServers block has moved across OpenClaw releases (some versions place it under plugins.mcp), so check openclaw mcp --help against the version you're running if the snippet above isn't picked up.

Other clients

Most other MCP clients accept the same command / args / env shape used above. If your client expects a single command line, use npx -y estratos (or the absolute path to a globally installed estratos binary). The server has no required CLI arguments — defaults are taken from ESTRATOS_DATA_DIR and the optional config.json in that directory.

Configuration

Command-line options

estratos [options]

  -d, --data-dir <path>   Path to the data root. Overrides ESTRATOS_DATA_DIR.
                          A leading '~' is expanded to the user's home dir.
                          In multi-tenant mode this is the BASE directory
                          that contains per-tenant subdirectories.
      --multi-tenant      Enable multi-tenant mode. See "Multi-tenancy" below.
  -h, --help              Show help and exit.

Environment variables

  • ESTRATOS_DATA_DIR — default data root. Defaults to ~/.estratos/data. A leading ~ is expanded to the user's home directory. The --data-dir CLI flag takes precedence when both are set.
  • ESTRATOS_MULTI_TENANT — set to 1/true/yes/on to enable multi-tenant mode when --multi-tenant is not given. The CLI flag takes precedence when both are set.

Hierarchy config

Estratos uses a named hierarchy of layers to interpret namespace paths and to address crosscutting memories. Declare it in <data-dir>/config.json:

{
  "hierarchy": ["root", "clients", "services", "jobs"]
}

Layer names must be unique and may contain only letters, digits, -, and _. The array position is the layer depth: root is layer 1, clients is layer 2, and so on.

If config.json is absent, Estratos defaults to ["root"] (single layer) so you can start using it immediately.

Concepts

Scoped memories

A scoped memory lives at a single namespace node, e.g. root.acme-corp.audit.q3-review. Retrieving context for a path walks from the root down to that node, concatenating every scoped memory from general to specific.

Layer-wide crosscutting memories

A crosscutting memory is attached to a layer rather than a specific node. Any retrieval whose path reaches that layer includes the memory, regardless of which node value appears there. For hierarchy [root, clients, services, jobs], a memory stored at the services layer is applied to every request with at least three path segments — it sits between the scoped memories at depth 2 and depth 3.

You can also target a specific value at that layer. For the same hierarchy, a crosscutting memory stored at services with value = "content" applies only when the third segment is exactly content. That lets you say "save this for all content projects" while keeping paths like root.client-acme.content and root.client-cola.content.

Retrieval order

Given a path with segments [s1, s2, …, sN] and hierarchy [h1, h2, …, hM], retrieval produces, in order:

scoped at s1
crosscutting at h1
crosscutting at h1=s1   (only if a value-targeted match exists)
scoped at s1.s2
crosscutting at h2
crosscutting at h2=s2   (only if a value-targeted match exists)
…
scoped at s1.s2…sN
crosscutting at hN    (only if N ≤ M)
crosscutting at hN=sN   (only if N ≤ M and a value-targeted match exists)

Each memory block is prefixed with a header identifying its source:

<!-- scoped: root.acme-corp | key: onboarding -->
<memory content here>

<!-- crosscutting: clients | key: contact-preferences -->
<memory content here>

<!-- crosscutting: services=content | key: writing-style -->
<memory content here>

MCP tools

Tool Parameters Description
store path, key, content Write a scoped memory. Overwrites if the key already exists at that path.
store_crosscutting layer, key, content, value?, tenant? Write a crosscutting memory. Omit value to make it layer-wide; provide value to target one exact segment value at that layer. layer must exist in the configured hierarchy. tenant is required in multi-tenant mode, forbidden in single mode.
retrieve path Return the concatenated context for path, ordered from general to specific.
delete path, key Delete a scoped memory.
delete_crosscutting layer, key, value?, tenant? Delete a crosscutting memory. Omit value to delete the layer-wide one; provide value to delete a targeted one. tenant is required in multi-tenant mode.
list path List the keys stored at a specific scoped node (not ancestors or descendants).
list_tree path? Return the namespace tree rooted at path (defaults to the first configured layer in single mode; path is required in multi-tenant mode).
get_hierarchy tenant? Return the effective hierarchy as a JSON array. tenant is required in multi-tenant mode.
set_hierarchy hierarchy, tenant? Set the hierarchy layers (e.g. ["root", "clients", "projects"]). Overwrites config.json and takes effect immediately. tenant is required in multi-tenant mode.

In multi-tenant mode, tools that take path interpret the first segment as the tenant identifier (e.g. alice.root.acme.audit). Tools that take layer instead accept an explicit tenant field.

Directory layout

Single-tenant mode

<data-dir>/
  config.json
  scoped/
    root/
      memory-a.md
      acme-corp/
        memory-b.md
        audit/
          memory-c.md
  crosscutting/
    root/
      __all__/
        memory-e.md
    clients/
      __all__/
        memory-f.md
    services/
      __all__/
        memory-g.md
      __match__/
        content/
          memory-h.md

Multi-tenant mode

<base-dir>/
  config.json           (optional: default hierarchy for tenants without their own)
  alice/
    config.json         (optional: alice's own hierarchy, overrides the base default)
    scoped/
      root/
        memory-a.md
    crosscutting/
      services/
        __all__/
          memory-g.md
        __match__/
          content/
            memory-h.md
  bob/
    scoped/
    crosscutting/

Each tenant subdirectory is a self-contained Estratos data root: its own optional config.json, its own scoped/ tree, its own crosscutting/ tree. There is zero data sharing across tenants.

Multi-tenancy

Estratos can serve many logical tenants from a single process, the way a Postgres server hosts many schemas. Enable it with --multi-tenant (or ESTRATOS_MULTI_TENANT=1) and point --data-dir at the base directory. Each request then identifies its tenant:

  • Path-based tools (store, retrieve, delete, list, list_tree): the first segment of path is the tenant. alice.root.acme.audit means "tenant alice, scoped path root.acme.audit".
  • Crosscutting tools (store_crosscutting, delete_crosscutting): take an explicit tenant parameter alongside layer, plus an optional value when you want to target one exact segment value at that layer.

Each tenant can supply its own <base-dir>/<tenant>/config.json with its own hierarchy. When a tenant has none, Estratos falls back to the hierarchy from <base-dir>/config.json, and ultimately to ["root"]. Tenant hierarchies are loaded on first access and cached for the lifetime of the process — editing a tenant's config file requires a server restart.

Trust model

Estratos has no authentication or authorization. The embedding application is trusted to pass the correct tenant on every call — the same way an application is trusted to pick the correct database or schema. Multi-tenant mode defends against accidental cross-pollution (filesystem bugs, path traversal, logic errors); it is not a defense against a malicious MCP client that lies about its identity. If your deployment requires that, you need a different product: bearer tokens, HTTP transport, and a real auth layer.

Isolation guarantees

  1. Scoped ancestry walks stay within a tenant. Retrieving alice.root.acme.service-a only reads files under <base-dir>/alice/scoped/root/acme/service-a/ and its ancestors up to <base-dir>/alice/scoped/root/. It cannot surface data from bob.* or from sibling paths like alice.root.example.*.
  2. Crosscutting memories are tenant-local. They live inside each tenant's crosscutting/ directory, so alice's crosscutting at the services layer never appears in bob's retrieves.
  3. Per-tenant hierarchies are independent. A layer name valid for alice but absent from bob's hierarchy is rejected when bob tries to use it — each tenant's store_crosscutting validates against its own hierarchy.
  4. Path-traversal hardening. Tenant identifiers pass the same character-class regex as every other segment (^[a-zA-Z0-9_-]+$), so .., /, and friends are rejected before any disk I/O.

Mode is decided at startup

Estratos does not mix modes against the same directory. If you enable multi-tenant mode, point it at a directory structured for multi-tenant use, not one that already contains single-tenant scoped//crosscutting/ data. Automatic migration between modes is not supported.

Validation rules

  • Path segments and memory keys must match ^[a-zA-Z0-9_-]+$.
  • Paths are dot-separated, may not start or end with a dot, and may not contain empty segments.
  • layer arguments to crosscutting tools must appear in the configured hierarchy.

Violations produce clear MCP error responses (isError: true) with actionable messages.

Development

npm install
npm run build
npm test

All tests run against real tmp directories (no mocked filesystem) via vitest.

Building the Claude Desktop extension locally

npm run validate:mcpb      # sanity-check manifest.json
npm run pack:mcpb          # builds dist/ and writes estratos.mcpb

Releases are fully automated

Releases are managed by release-please. There is no manual version bumping, tagging, or asset uploading.

  1. Write commits in Conventional Commits format. feat: → minor bump, fix: → patch bump, feat!: or BREAKING CHANGE: → minor bump (while pre-1.0) / major bump (post-1.0). docs:, chore:, refactor:, test: don't move the version on their own but do appear in the changelog.
  2. Merge PRs to main normally. On every push to main, the Release GitHub Actions workflow runs release-please, which opens (or updates) a single "Release v0.X.Y" PR summarising every Conventional Commit since the last tag. The Release PR contains the package.json + manifest.json version bump and a regenerated CHANGELOG.md — review it like any other PR.
  3. Merge the Release PR when you're ready to cut a release. Merging it makes release-please create the git tag vX.Y.Z and a GitHub Release with auto-generated notes. In the same workflow run, the build-mcpb job then builds dist/, prunes dev dependencies, packs estratos.mcpb via @anthropic-ai/mcpb, and attaches it as an asset on the freshly created release.

Config lives in release-please-config.json (bump-minor-pre-major: true keeps us in 0.x until we explicitly cut a 1.0.0) and .release-please-manifest.json (tracks the last released version). The workflow lives at .github/workflows/release.yml.

License

MIT © Celso Pinto

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors