MetaCortex is a serverless MCP memory service backed by Firestore vector search and deployed through Firebase Cloud Functions 2nd Gen.
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.
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
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.
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.
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.
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.
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.
This project is set up for these workflows:
- A chat client asks, "What do we already know about auth/session handling?"
The model calls
search_context. - The search results include stable
idvalues and external artifact refs when available. The model can callfetch_contextwith that sameidfor the one result it wants in full. - A user says, "Remember that we use Ktor for shared Android and iOS networking."
The model calls
save_context. - A user shares a screenshot and says to save it for later retrieval.
The model calls
save_contextwith image input plusartifact_refsif the real asset lives in storage.
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.
These are the recommended ordinary-agent tools:
save_contextThe single write tool for normal chat use. The client supplies the memory text, optional topic, optionaldraft=truefor rough notes, optional image input, and optionalartifact_refs. The server fills in sensible defaults.search_contextVector search over stored memories. Results include stableidvalues and artifact refs when available.fetch_contextFetch one memory byidaftersave_contextorsearch_context.
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.
save_context keeps the public write surface simple:
topicis the public label and maps to the storedmodule_nameinternally- normal writes store canonical memory as
active draft=truestores draft material aswip
Explicit lifecycle overrides and cleanup flows are part of the admin maintenance surface described later in this README.
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.
Public topic or subsystem label for MCP clients. Internally this is stored as module_name.
Examples:
authbillingkmp-networkingui-settings
If omitted, the server defaults it to general.
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_refscan point to the real asset, for examplegs://bucket/path.png - search results and fetched records return those artifact refs when they exist
That means the practical image flow is:
- save a screenshot with
save_context - store the real asset elsewhere
- include its
artifact_refs - let semantic search find the memory
- let the client follow the returned artifact ref to the actual screenshot
- Default Streamable HTTP MCP endpoint:
/metaCortexMcp/mcp - Client-scoped Streamable HTTP MCP endpoint:
/metaCortexMcp/clients/<clientId>/mcp
Security model:
- the default
/mcpendpoint is the admin endpoint clients/<clientId>endpoints let you expose smaller toolsets to specific consumersMCP_ALLOWED_ORIGINSapplies only to the default admin endpoint- browser CORS should be configured per client profile through
MCP_CLIENT_PROFILES_JSON[].allowedOrigins - leave
MCP_ALLOWED_ORIGINSempty unless you intentionally want browser access to the admin endpoint
Recommended browser read/write toolset:
save_contextsearch_contextfetch_context
This is the default ordinary-agent profile; the server offers six tools overall.
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.
The client-facing tools return one TextContent block whose text is a single JSON object.
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"
}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"
}
}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_context does one exact metadata filter step and one vector step:
filter_stateis always applied before nearest-neighbor searchfilter_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
limitwhen provided, otherwiseSEARCH_RESULT_LIMIT - the default state is
activeunless 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 behavior that matters in production:
- request bodies are limited to
1mb, including base64 image data contentorimage_base64is requiredimage_mime_typeis required wheneverimage_base64is 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
topicbecomesgeneral - omitted
draftand omitted lifecycle overrides storebranch_state=active draft=truestoresbranch_state=wip
Explicit lifecycle overrides are part of the admin maintenance surface described later in this README.
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_contextSoft-delete obsolete memories by settingbranch_state=deprecatedand recordingsuperseded_by.consolidate_contextMerge multiple related memories into one canonical active memory via LLM. By default merges all WIP memories for a topic. Passsource_idsto consolidate specific memories regardless of their current state. All source memories are deprecated and linked to the merged result.
Lifecycle states:
activeCanonical memory that normal search should return.wipDraft memory awaiting consolidation.mergedIncorporated memory that is no longer the main active record.deprecatedObsolete memory kept only for history and audit.
Recommended usage:
- Browser clients save durable memories with
save_context. - Agent clients such as OpenClaw should use a dedicated scoped client profile with
save_context,search_context, andfetch_contextonly. - Use
draft=trueonly for provisional notes that should not appear in normal active search. - Use
consolidate_contextto merge a batch of WIP drafts or fragmented active memories into one canonical record. - Admin flows can set explicit
branch_statewhen they need non-default lifecycle control. - After writing the canonical replacement, admins can mark obsolete records with the admin-only
deprecate_contexttool.
Current lifecycle behavior:
save_contextdefaults toactive, supportsdraft=trueforwip, and also accepts explicitbranch_statefor advanced writesdraftandbranch_stateare mutually exclusivedeprecate_contextdoes not delete data; it setsbranch_state=deprecatedand recordssuperseded_byconsolidate_contextcalls Gemini to merge N source memories into one, stores the result asactive, and deprecates all sources withsuperseded_bypointing to the merged idmergedexists as a searchable historical state for explicit admin writes- client profiles can restrict visible lifecycle states through
allowedFilterStates
After deployment, there are three places to look:
memory_vectorsin Firestore shows the current memory corpusmemory_eventsin 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_idevent_typestatustimestampexpires_atlatency_ms- a compact
requestsummary - either a compact
responsesummary, anerror, or a request rejection reason
Examples:
-
public tool payloads use
idfor fetchable memory identifiers -
save_contextevents record the writtenid,topic,branch_state, andmodality -
search_contextevents record the requested filters,result_count, and returnedresult_ids -
fetch_contextevents record whichidwas read -
deprecate_contextevents recordid,superseding_id, andprevious_state -
rejected browser/admin requests record
reason=origin_not_allowedorreason=unauthorizedTraceability 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_attargets 90-day audit retentionretrieval_query_events.expires_attargets 90-day retrieval telemetry retentionmemory_vectors_write_fingerprints.expires_attargets 30-day fingerprint retention- fingerprint documents keep numeric
dedupe_expires_atfor the short duplicate-write window
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.
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.
-
Install dependencies:
npm --prefix functions install
-
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"]}]
-
Run verification:
npm --prefix functions test npm --prefix functions run build -
Start emulators:
npm --prefix functions run serve
-
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/mcpand the Claude token to verify Claude separately.
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