Skip to content

Repository files navigation

MetaCortex

MetaCortex is a serverless MCP memory service backed by Firestore vector search and deployed through Firebase Cloud Functions 2nd Gen.

Status and roadmap

MetaCortex is already released (repository tag v0.3.0). This checkout contains a reconciled local baseline; publication and production deployment are separate operations. See the baseline record, unified roadmap, and immediate next steps.

The browser setup wizard, owner login/OAuth, management dashboard, and ChatGPT extension are planned, not available in this baseline. Software is MIT-licensed; owners pay their own Firebase/model usage and agent subscriptions. One installation holds one owner's shared corpus; profiles do not provide tenant isolation.

Why Metacortex?

Persistent memory shared by compatible MCP clients, with Firebase-managed infrastructure. Operators still configure, deploy, monitor, and back up their own project.

Tip

Memory Layer: MetaCortex provides the hosted memory service, while autonomous agents such as OpenClaw use it as a shared remote memory backend. See the full Architecture & Use Cases for details.

The practical target is a remote MCP server that chat clients such as ChatGPT web or Claude web can use for:

  • searching what the project already knows
  • saving new durable memories from chat
  • fetching the full stored memory behind a search result

How It Works

1. Chat clients use a narrow memory contract

MetaCortex gives browser clients a three-tool memory contract. The first write comes through save_context, which stores canonical text and lifecycle metadata on the server side.

2. Retrieval stays on the same remote backend

The same scoped MCP contract makes retrieval available later through search_context and fetch_context. ChatGPT and Claude both follow the same server-side retrieval flow, each with its own token and origin allowlist.

3. Firestore stores durable memory records plus vectors

Every saved memory lands in Firestore with searchable metadata and the corresponding vector embedding. That keeps the retrieval layer serverless while still exposing durable semantic search.

4. Scoped client profiles keep browser access safe

Browser clients never connect to the admin surface directly. Each client profile gets its own endpoint, token, origin allowlist, and allowed tool list so public chat surfaces only see the read/write tools they actually need.

Scoped client profile routing

Important constraint

As of March 10, 2026, Cloud Functions production deployment requires the Firebase Blaze plan. The original Spark-only production target from the initial spec is not compatible with current Firebase Functions deployment rules, though low-traffic usage can still remain close to zero cost within Blaze no-cost quotas.

Primary use cases

This project is set up for these workflows:

  1. A chat client asks, "What do we already know about auth/session handling?" The model calls search_context.
  2. The search results include stable id values and external artifact refs when available. The model can call fetch_context with that same id for the one result it wants in full.
  3. A user says, "Remember that we use Ktor for shared Android and iOS networking." The model calls save_context.
  4. A user shares a screenshot and says to save it for later retrieval. The model calls save_context with image input plus artifact_refs if the real asset lives in storage.

Tool strategy

The current MCP surface is intentionally split between:

  • a client-facing contract for browser-hosted chat clients
  • a smaller admin-only maintenance surface documented later in this README

That means the server currently exposes 6 MCP tools total, but normal browser clients should only see 3 of them by default.

Ordinary-agent tools

These are the recommended ordinary-agent tools:

  • save_context The single write tool for normal chat use. The client supplies the memory text, optional topic, optional draft=true for rough notes, optional image input, and optional artifact_refs. The server fills in sensible defaults.
  • search_context Vector search over stored memories. Results include stable id values and artifact refs when available.
  • fetch_context Fetch one memory by id after save_context or search_context.

Optional listing permission

list_context enumerates stored memories with cursor pagination and metadata/creation-time/provenance filtering. It returns summaries and IDs. Grant it explicitly; it is not part of the three-tool ordinary-agent default.

Why save_context Is The Write Tool

save_context keeps the public write surface simple:

  • topic is the public label and maps to the stored module_name internally
  • normal writes store canonical memory as active
  • draft=true stores draft material as wip

Explicit lifecycle overrides and cleanup flows are part of the admin maintenance surface described later in this README.

Metadata model

The main public metadata field is topic. Stored records also carry lifecycle metadata such as branch_state, but that is primarily for admin cleanup and filtering and is documented later in Admin Cleanup And Consolidation.

topic

Public topic or subsystem label for MCP clients. Internally this is stored as module_name.

Examples:

  • auth
  • billing
  • kmp-networking
  • ui-settings

If omitted, the server defaults it to general.

Images

This project supports image-backed memories, but it does not store raw image bytes for later download.

What happens today:

  • the image is normalized into retrieval text by Gemini
  • that text is embedded and stored
  • optional artifact_refs can point to the real asset, for example gs://bucket/path.png
  • search results and fetched records return those artifact refs when they exist

That means the practical image flow is:

  1. save a screenshot with save_context
  2. store the real asset elsewhere
  3. include its artifact_refs
  4. let semantic search find the memory
  5. let the client follow the returned artifact ref to the actual screenshot

Endpoints

  • Default Streamable HTTP MCP endpoint: /metaCortexMcp/mcp
  • Client-scoped Streamable HTTP MCP endpoint: /metaCortexMcp/clients/<clientId>/mcp

Security model:

  • the default /mcp endpoint is the admin endpoint
  • clients/<clientId> endpoints let you expose smaller toolsets to specific consumers
  • MCP_ALLOWED_ORIGINS applies only to the default admin endpoint
  • browser CORS should be configured per client profile through MCP_CLIENT_PROFILES_JSON[].allowedOrigins
  • leave MCP_ALLOWED_ORIGINS empty unless you intentionally want browser access to the admin endpoint

Recommended browser read/write toolset:

  • save_context
  • search_context
  • fetch_context

This is the default ordinary-agent profile; the server offers six tools overall.

Client setup and authentication status

Use a dedicated scoped endpoint <FUNCTION_BASE_URL>/clients/<clientId>/mcp and its matching bearer credential. Do not register the admin endpoint with an ordinary client. Configure exact origins per profile; clients without an Origin header do not need browser origins.

This baseline has static credentials, not OAuth or owner login. Current code also accepts ?auth_token=<SCOPED_TOKEN> for legacy URL-only clients. That credential is part of a URL and may enter infrastructure logs; do not describe it as secure secret transport. Existing connections are not removed by this baseline. The roadmap disables URL tokens for fresh installations after the auth work, migrates existing clients, then removes support in a documented breaking release.

Use the capabilities of the specific client/version being connected; do not assume a particular ChatGPT or Claude settings screen supports bearer headers. The future core compatibility matrix covers ChatGPT, Claude, Codex, and generic MCP. No fresh compatibility verification is implied by these examples. See deployment and the client recipe.

Tool Contract

The client-facing tools return one TextContent block whose text is a single JSON object.

save_context

Minimal text memory:

{
  "content": "We use Ktor for shared Android and iOS networking.",
  "topic": "kmp-networking"
}

Typical result:

{
  "item": {
    "id": "abc123",
    "content": "We use Ktor for shared Android and iOS networking.",
    "metadata": {
      "topic": "kmp-networking",
      "branch_state": "active",
      "modality": "text",
      "created_at": "2026-03-14T12:00:00.000Z",
      "updated_at": "2026-03-14T12:00:00.000Z"
    }
  },
  "write_status": "created"
}

Use item.id directly with fetch_context.

Image-backed memory with an external asset reference:

{
  "content": "Settings screen screenshot for the Compose UI.",
  "topic": "ui-settings",
  "artifact_refs": ["gs://your-bucket/settings-screen.png"],
  "image_base64": "<base64 image bytes>",
  "image_mime_type": "image/png"
}

search_context

Example input:

{
  "query": "shared networking for android and ios",
  "filter_topic": "kmp-networking",
  "filter_state": "active"
}

Typical result:

{
  "matches": [
    {
      "id": "abc123",
      "summary": "We use Ktor for shared Android and iOS networking.",
      "score": 0.92,
      "metadata": {
        "topic": "kmp-networking",
        "branch_state": "active",
        "modality": "text",
        "created_at": "2026-03-14T12:00:00.000Z",
        "updated_at": "2026-03-14T12:00:00.000Z"
      }
    }
  ],
  "applied_filters": {
    "filter_topic": "kmp-networking",
    "filter_state": "active"
  }
}

If an item has external refs, they appear in metadata.artifact_refs.

If nothing matches, the result is:

{
  "matches": [],
  "applied_filters": {
    "filter_topic": null,
    "filter_state": "active"
  }
}

fetch_context

Preferred input: pass the same id returned by save_context or search_context. document_id is accepted as a compatibility alias for older connector wrappers.

Example input:

{
  "id": "abc123"
}

Compatibility alias:

{
  "document_id": "abc123"
}

Typical result:

{
  "item": {
    "id": "abc123",
    "content": "We use Ktor for shared Android and iOS networking.",
    "metadata": {
      "topic": "kmp-networking",
      "branch_state": "active",
      "modality": "text",
      "created_at": "2026-03-14T12:00:00.000Z",
      "updated_at": "2026-03-14T12:00:00.000Z"
    }
  }
}

Search Behavior

search_context does one exact metadata filter step and one vector step:

  • filter_state is always applied before nearest-neighbor search
  • filter_topic, when present, is an exact match on the stored topic label
  • vector search then runs Firestore findNearest() with cosine distance
  • the result count is limit when provided, otherwise SEARCH_RESULT_LIMIT
  • the default state is active unless the client profile allows and requests another state

fetch_context can still fail with a neutral 404 if the document exists but its branch_state is outside that client profile's allowedFilterStates.

Write Constraints

Write behavior that matters in production:

  • request bodies are limited to 1mb, including base64 image data
  • content or image_base64 is required
  • image_mime_type is required whenever image_base64 is provided
  • images are normalized into retrieval text and embedded as text; raw image bytes are not stored for download
  • if you want the real asset later, store it elsewhere and include artifact_refs
  • exact duplicate writes within the current idempotency window are replay-safe and reuse the existing memory id
  • duplicate suppression is intentionally light and based on the normalized write fingerprint, not semantic similarity

save_context defaults:

  • omitted topic becomes general
  • omitted draft and omitted lifecycle overrides store branch_state=active
  • draft=true stores branch_state=wip

Explicit lifecycle overrides are part of the admin maintenance surface described later in this README.

Admin Cleanup And Consolidation

Maintenance may run automatically only after owner opt-in, in a separate trusted session with a configured small batch and review for uncertain changes. User corrections require owner authorization. These policies are not fully enforced by the current static-token service: prompts and caller-supplied initiator metadata are not proof of human action. See security limitations.

There is no permanent deletion feature. Deprecation retains memory history. Omit superseding_id to retire a memory with no replacement. A supplied one must exist, differ from the memory, and not lead back to it. Repeating the same deprecation changes nothing, and a different one on an already deprecated memory is rejected.

This section is for operators using the admin endpoint. Browser-hosted clients can usually ignore it.

Admin-only maintenance surface:

  • deprecate_context Soft-delete obsolete memories by setting branch_state=deprecated and recording superseded_by.
  • consolidate_context Merge multiple related memories into one canonical active memory via LLM. By default merges all WIP memories for a topic. Pass source_ids to consolidate specific memories regardless of their current state. All source memories are deprecated and linked to the merged result.

Lifecycle states:

  • active Canonical memory that normal search should return.
  • wip Draft memory awaiting consolidation.
  • merged Incorporated memory that is no longer the main active record.
  • deprecated Obsolete memory kept only for history and audit.

Recommended usage:

  1. Browser clients save durable memories with save_context.
  2. Agent clients such as OpenClaw should use a dedicated scoped client profile with save_context, search_context, and fetch_context only.
  3. Use draft=true only for provisional notes that should not appear in normal active search.
  4. Use consolidate_context to merge a batch of WIP drafts or fragmented active memories into one canonical record.
  5. Admin flows can set explicit branch_state when they need non-default lifecycle control.
  6. After writing the canonical replacement, admins can mark obsolete records with the admin-only deprecate_context tool.

Current lifecycle behavior:

  • save_context defaults to active, supports draft=true for wip, and also accepts explicit branch_state for advanced writes
  • draft and branch_state are mutually exclusive
  • deprecate_context does not delete data; it sets branch_state=deprecated and records superseded_by
  • consolidate_context calls Gemini to merge N source memories into one, stores the result as active, and deprecates all sources with superseded_by pointing to the merged id
  • merged exists as a searchable historical state for explicit admin writes
  • client profiles can restrict visible lifecycle states through allowedFilterStates

Observability

After deployment, there are three places to look:

  • memory_vectors in Firestore shows the current memory corpus
  • memory_events in Firestore shows client-attributed tool usage over time
  • Cloud Logging shows request failures and structured tool-event logs

When RETRIEVAL_EVENT_LOGGING_ENABLED=true, retrieval_query_events also stores full search queries, filters, limits, ranked result ids/scores, and fetch ids. This collection is separate because the full queries may contain sensitive user text; it uses the same 90-day TTL target as memory_events.

memory_events records one document per tool call and one document per ingress rejection. Events include:

  • client_id
  • event_type
  • status
  • timestamp
  • expires_at
  • latency_ms
  • a compact request summary
  • either a compact response summary, an error, or a request rejection reason

Examples:

  • public tool payloads use id for fetchable memory identifiers

  • save_context events record the written id, topic, branch_state, and modality

  • search_context events record the requested filters, result_count, and returned result_ids

  • fetch_context events record which id was read

  • deprecate_context events record id, superseding_id, and previous_state

  • rejected browser/admin requests record reason=origin_not_allowed or reason=unauthorized Traceability is by client profile id, so:

  • admin endpoint traffic is attributed to client_id=default

  • ChatGPT web traffic is attributed to client_id=chatgpt-web

  • Claude web traffic is attributed to client_id=claude-web

What is intentionally not stored in observability events:

  • full memory bodies
  • full image bytes
  • raw image downloads

Search events do include a short query_preview, but the observability collection is designed to track behavior, not duplicate the corpus.

Retention is handled with Firestore TTL policies:

  • memory_events.expires_at targets 90-day audit retention
  • retrieval_query_events.expires_at targets 90-day retrieval telemetry retention
  • memory_vectors_write_fingerprints.expires_at targets 30-day fingerprint retention
  • fingerprint documents keep numeric dedupe_expires_at for the short duplicate-write window

Retrieval evaluation

The evaluation corpus is stored in retrieval_eval_cases; isolated synthetic memories are stored separately in memory_vectors_eval. Cases have no lifecycle field: regeneration replaces the selected source partition and removes obsolete cases.

# Seed a deterministic isolated corpus and replace its eval cases.
npm --prefix functions run eval:generate

# Measure hit@k, recall@k, MRR, empty results, and p50/p95 latency.
npm --prefix functions run eval:run -- --mode isolated

# Explicitly convert recent production search-to-fetch evidence into eval cases.
npm --prefix functions run eval:import -- --lookback-hours 168

# Run imported production cases through the deployed MCP endpoint.
npm --prefix functions run eval:run -- --mode production --url "$MCP_BASE_URL"

Production retrieval events are evidence only. They do not become benchmark cases until eval:import is run, and a successful fetch is treated as a positive label; the harness does not infer negative relevance judgments.

Backup and recovery

Existing operator commands are integrated from the archive worktree. Portable memory archives omit embeddings and re-embed on restore. Full top-level Firestore backups preserve vectors and discover collection inventory. Their current limits and destructive operator restore semantics are explicit in those guides; future recovery hardening remains on the roadmap.

Quick start

  1. Install dependencies:

    npm --prefix functions install
  2. Create local env vars:

    cp functions/.env.example functions/.env

    For local development, use placeholder/local values only. For deployment, place credentials and profiles in Secret Manager, not production dotenv. Example profile value:

    MCP_CLIENT_PROFILES_JSON=[{"id":"chatgpt-web","token":"replace-chatgpt-token","allowedTools":["save_context","search_context","fetch_context"],"allowedFilterStates":["active"],"allowedOrigins":["https://chatgpt.com"]},{"id":"claude-web","token":"replace-claude-token","allowedTools":["save_context","search_context","fetch_context"],"allowedFilterStates":["active"],"allowedOrigins":["https://claude.ai"]}]
  3. Run verification:

    npm --prefix functions test
    npm --prefix functions run build
  4. Start emulators:

    npm --prefix functions run serve
  5. Optional MCP smoke test:

    cd functions
    MCP_BASE_URL="http://127.0.0.1:5001/demo-open-brain/us-central1/metaCortexMcp/mcp" \
    MCP_ADMIN_TOKEN="replace-me" \
    MCP_SMOKE_MODE="admin-read-write" \
    npm run smoke

    Browser-client flow:

    cd functions
    MCP_BASE_URL="http://127.0.0.1:5001/demo-open-brain/us-central1/metaCortexMcp/clients/chatgpt-web/mcp" \
    MCP_ADMIN_TOKEN="replace-chatgpt-token" \
    MCP_SMOKE_MODE="browser-read-write" \
    npm run smoke

    Repeat with /clients/claude-web/mcp and the Claude token to verify Claude separately.

Deployment

Deployment playbook: docs/DEPLOYMENT.md

For the maintainer’s existing production project, the preflight below checks local configuration and production secrets. New owners should follow the explicit-target deployment playbook instead:

cd <your-metacortex-checkout>
./scripts/deploy-session-preflight.sh