This is the single deploy guide for MetaCortex.
Use it for:
- local verification before release
- the first production deployment
- the first production smoke test
- the first weeks of rollout after launch
The deploy path in this repo currently assumes:
- Firebase Cloud Functions 2nd Gen in
us-central1 - Firestore in Native mode
- Firebase Blaze plan for production deploys
- Firestore collection
memory_vectors - Firestore collection
memory_eventsfor audit and observability - embedding output dimensions aligned with the Firestore vector indexes
- runtime model choices supplied by the deploy dotenv files, using functions/.env.example as the tracked template
- total MCP surface of 5 tools
- public/browser toolset of 3 tools:
remember_context,search_context,fetch_context - admin-only maintenance tools:
deprecate_context,consolidate_context - WIP consolidation is available only through the admin maintenance surface
For the first production release, if memory_vectors is empty, no embedding migration is required.
Install and verify:
node -v
npm -v
firebase --version
java -versionYou need:
- Firebase CLI authenticated with
firebase login - a Firebase project with Blaze enabled
- Firestore created in Native mode
- access to the correct Firebase project alias
- a valid
GEMINI_API_KEY - a production
MCP_ADMIN_TOKEN
If the repo is not bound to the right Firebase project yet:
cd /Users/nick/git/metacortex
firebase use --addFirebase Functions loads dotenv files from functions/.
Recommended layout:
functions/.env: local development valuesfunctions/.env.dev: development project valuesfunctions/.env.prod: production project values
functions/.env.prod is local deployment config and should stay out of Git.
Start from the template:
cp /Users/nick/git/metacortex/functions/.env.example /Users/nick/git/metacortex/functions/.envMinimum required production values:
GEMINI_API_KEY=...
MCP_ADMIN_TOKEN=...
GEMINI_EMBEDDING_MODEL=...
GEMINI_MULTIMODAL_MODEL=...
GEMINI_MERGE_MODEL=...
GEMINI_GENERATION_VERTEX_LOCATION=...
GEMINI_EMBEDDING_DIMENSIONS=...
MEMORY_COLLECTION=...Use functions/.env.example for the current tracked defaults. Keep exact model ids there and in the deployed dotenv file, not duplicated in this playbook.
Recommended admin endpoint defaults for the first release:
MCP_ALLOWED_TOOLS=remember_context,search_context,fetch_context,deprecate_context
MCP_ALLOWED_ORIGINS=
MCP_ALLOWED_FILTER_STATES=active,merged,deprecated,wip
SEARCH_RESULT_LIMIT=5
DEFAULT_FILTER_STATE=activeOrigin config split:
MCP_ALLOWED_ORIGINSapplies only to the default admin/mcpendpoint- browser-hosted clients should use
MCP_CLIENT_PROFILES_JSON[].allowedOrigins - leave
MCP_ALLOWED_ORIGINSempty unless you intentionally want browser access to the admin endpoint
Important constraints:
GEMINI_EMBEDDING_DIMENSIONSmust match the vector index dimension in firestore.indexes.json- if you change embedding models or dimensions after seeding data, do not mix vector spaces in the same collection
- this codebase embeds text; image-backed memories are normalized into text before embedding
Use the default /mcp endpoint as the admin surface only. For ChatGPT web and Claude web, deploy separate scoped client profiles from day one:
- admin:
<FUNCTION_BASE_URL>/mcp - ChatGPT web:
<FUNCTION_BASE_URL>/clients/chatgpt-web/mcp - Claude web:
<FUNCTION_BASE_URL>/clients/claude-web/mcp
Recommended browser read/write toolset:
remember_contextsearch_contextfetch_context
Do not add deprecate_context to browser-hosted client profiles. Keep it on the admin endpoint only.
Recommended web client profile shape:
MCP_CLIENT_PROFILES_JSON=[{"id":"chatgpt-web","token":"replace-chatgpt-token","allowedTools":["remember_context","search_context","fetch_context"],"allowedFilterStates":["active"],"allowedOrigins":["https://chatgpt.com"]},{"id":"claude-web","token":"replace-claude-token","allowedTools":["remember_context","search_context","fetch_context"],"allowedFilterStates":["active"],"allowedOrigins":["https://claude.ai"]}]For non-browser agent clients such as OpenClaw, add a separate scoped profile instead of reusing the admin token. Recommended shape:
MCP_CLIENT_PROFILES_JSON=[{"id":"chatgpt-web","token":"replace-chatgpt-token","allowedTools":["remember_context","search_context","fetch_context"],"allowedFilterStates":["active"],"allowedOrigins":["https://chatgpt.com"]},{"id":"claude-web","token":"replace-claude-token","allowedTools":["remember_context","search_context","fetch_context"],"allowedFilterStates":["active"],"allowedOrigins":["https://claude.ai"]},{"id":"openclaw","token":"replace-openclaw-token","allowedTools":["remember_context","search_context","fetch_context"],"allowedFilterStates":["active"],"allowedOrigins":[]}]Use allowedOrigins: [] only when the OpenClaw runtime is a headless or non-browser client that does not send an Origin header. If the runtime sends Origin because it runs inside Electron, a WebView, or another browser-like environment, replace the empty list with the exact origin value or values emitted by that client.
Keep each scoped-client token distinct from MCP_ADMIN_TOKEN. The admin token should stay reserved for maintenance and ops-only clients.
Run the preflight first:
cd /Users/nick/git/metacortex
./scripts/deploy-session-preflight.shThat script checks:
- git status
- expected env file presence
- effective production embedding config versus Firestore index dimensions
- current Firebase project selection
- full test suite
- TypeScript build
Validate the live Gemini model configuration before production deploy:
cd /Users/nick/git/metacortex
npm --prefix functions run validate:modelsIf you want a manual local round-trip before production:
cd /Users/nick/git/metacortex
npm --prefix functions run serveThen in another shell:
curl -i "http://127.0.0.1:5001/demo-open-brain/us-central1/metaCortexMcp/healthz"cd /Users/nick/git/metacortex/functions
MCP_BASE_URL="http://127.0.0.1:5001/demo-open-brain/us-central1/metaCortexMcp/mcp" \
MCP_ADMIN_TOKEN="replace-me" \
npm run smoke -- --mode admin-read-writeBrowser-client flow:
cd /Users/nick/git/metacortex/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 smokeRepeat with /clients/claude-web/mcp and the Claude token to validate Claude separately.
The automated tests and build can also be run directly:
cd /Users/nick/git/metacortex
npm --prefix functions test
npm --prefix functions run build
npm --prefix functions run validate:modelscd /Users/nick/git/metacortex
firebase use
firebase projects:listDo not deploy while unsure which alias is active.
Verify that functions/.env.prod or the dotenv file you plan to deploy with includes the intended values, especially:
GEMINI_API_KEYMCP_ADMIN_TOKENMCP_ALLOWED_ORIGINSonly if you intentionally want browser access to the admin endpointMCP_CLIENT_PROFILES_JSONwith bothchatgpt-webandclaude-webprofiles- model and generation settings copied from the current
functions/.env.example:
GEMINI_EMBEDDING_MODEL,GEMINI_MULTIMODAL_MODEL,GEMINI_MERGE_MODEL, andGEMINI_GENERATION_VERTEX_LOCATION GEMINI_EMBEDDING_DIMENSIONSmatching firestore.indexes.jsonMEMORY_COLLECTION
For the first release, an empty production collection means there is no migration work to do.
If you later switch embedding models or dimensions and want to keep old memories, re-embed them or start with a fresh collection.
Also confirm the actual web-client registration values you will use:
- ChatGPT URL:
<FUNCTION_BASE_URL>/clients/chatgpt-web/mcp - Claude URL:
<FUNCTION_BASE_URL>/clients/claude-web/mcp - each bearer token comes from the matching client profile, not
MCP_ADMIN_TOKEN - each web origin must match the profile's
allowedOrigins
The hardening release uses Firestore TTL policies for unbounded operational collections:
memory_vectors_write_fingerprints.expires_at: 30-day retentionmemory_events.expires_at: 90-day retention
Run a dry run first:
cd /Users/nick/git/metacortex
npm --prefix functions run backfill:ttlIf the counts look correct, apply the backfill:
cd /Users/nick/git/metacortex
npm --prefix functions run backfill:ttl -- --write --project my-brain-88870The backfill preserves numeric event timestamp, copies legacy numeric fingerprint expires_at into dedupe_expires_at when needed, and writes Date-valued expires_at fields for Firestore TTL.
cd /Users/nick/git/metacortex
firebase deploy --only firestore:indexesRequired vector indexes:
metadata.module_name ASC + embedding VECTORmetadata.branch_state ASC + embedding VECTORmetadata.branch_state ASC + metadata.module_name ASC + embedding VECTOR
Wait until those indexes are fully built before trusting search results.
cd /Users/nick/git/metacortex
firebase deploy --only functionsOr deploy both together:
cd /Users/nick/git/metacortex
firebase deploy --only firestore:indexes,functionsImportant: always use
firebase deploywith theprodalias active (confirmed in step 1), notfirebase deploy --project my-brain-88870. Firebase loadsfunctions/.env.prodbased on the alias name, not the project ID. Passing the raw project ID silently skipsfunctions/.env.prodand omitsMCP_CLIENT_PROFILES_JSONfrom the deployment, causing all client endpoints to 404. Usefirebase deploy --project prodif you need to pass the project explicitly.
Capture the deployed base URL for metaCortexMcp.
The useful production routes are:
<FUNCTION_BASE_URL>/healthz<FUNCTION_BASE_URL>/mcp<FUNCTION_BASE_URL>/clients/<CLIENT_ID>/mcp
Enable TTL policies after the expires_at fields exist:
cd /Users/nick/git/metacortex
./scripts/deploy-firestore-ttl.sh --project my-brain-88870Verify the policies:
gcloud firestore fields ttls list --project=my-brain-88870curl -i "<FUNCTION_BASE_URL>/healthz"Expected:
- HTTP
200 - response includes
ok: true
curl -i \
-X POST "<FUNCTION_BASE_URL>/mcp" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"ping"}'Expected:
- HTTP
401
Verify that MCP_CLIENT_PROFILES_JSON was bundled into the deployed function by checking
that client endpoints resolve (not 404). A 404 here means the env file was not picked up —
the most common cause is deploying with the raw project ID instead of the prod alias.
curl -s -o /dev/null -w "%{http_code}\n" \
-X OPTIONS "<FUNCTION_BASE_URL>/clients/chatgpt-web/mcp" \
-H "Origin: https://chatgpt.com"
# Expected: 204curl -s -o /dev/null -w "%{http_code}\n" \
-X OPTIONS "<FUNCTION_BASE_URL>/clients/claude-web/mcp" \
-H "Origin: https://claude.ai"
# Expected: 204If either returns 404, the client profiles were not deployed. Fix:
firebase use prod
firebase deploy --only functionsConfirm the correct origin headers are returned:
curl -i \
-X OPTIONS "<FUNCTION_BASE_URL>/clients/chatgpt-web/mcp" \
-H "Origin: https://chatgpt.com"Expected:
- HTTP
204 Access-Control-Allow-Origin: https://chatgpt.com
Repeat with:
curl -i \
-X OPTIONS "<FUNCTION_BASE_URL>/clients/claude-web/mcp" \
-H "Origin: https://claude.ai"cd /Users/nick/git/metacortex/functions
MCP_BASE_URL="<FUNCTION_BASE_URL>/mcp" \
MCP_ADMIN_TOKEN="<ADMIN_MCP_TOKEN>" \
MCP_SMOKE_MODE="admin-read-write" \
npm run smokeExpected:
- tool listing succeeds
remember_contextsucceedssearch_contextreturns the stored sample
This is the first proof that:
- auth works
- the live Gemini call works
- Firestore writes work
- Firestore vector search works
cd /Users/nick/git/metacortex/functions
MCP_BASE_URL="<FUNCTION_BASE_URL>/clients/chatgpt-web/mcp" \
MCP_ADMIN_TOKEN="<CHATGPT_WEB_TOKEN>" \
MCP_SMOKE_MODE="browser-read-write" \
npm run smoke -- --content "Remember that we use Ktor for shared Android and iOS networking." --query "shared networking for android and ios"Expected:
remember_contextsucceedssearch_contextreturns a result withid=...fetch_contextaccepts that sameidand returns the full stored record content and metadata
Repeat the same smoke test against /clients/claude-web/mcp with <CLAUDE_WEB_TOKEN>.
This is the first proof that the 3-tool client-facing browser contract is usable end to end.
Open Firestore and inspect memory_events.
Confirm:
- at least one event exists for each successful smoke-test tool call
- admin calls are recorded with
client_id=default - ChatGPT web calls are recorded with
client_id=chatgpt-web - Claude web calls are recorded with
client_id=claude-web - tool events include
tool_name,status,timestamp,latency_ms, and a compactrequest/responseorerror - request rejections use
event_type=requestwith areasonsuch asunauthorizedororigin_not_allowed
Cloud Logging should also contain structured metaCortexMcp tool event and metaCortexMcp request event entries for the same calls.
Open Firestore and inspect memory_vectors.
Confirm:
- one document was written
metadata.branch_stateisactive- the record stores both canonical
contentand internalretrieval_text metadata.created_atandmetadata.updated_atare present- the stored content is searchable through
search_context
cd /Users/nick/git/metacortex/functions
MCP_BASE_URL="<FUNCTION_BASE_URL>/clients/chatgpt-web/mcp" \
MCP_ADMIN_TOKEN="<CHATGPT_WEB_TOKEN>" \
MCP_SMOKE_MODE="browser-read-write" \
MCP_IMAGE_BASE64="$(base64 < path/to/image.png | tr -d '\n')" \
MCP_IMAGE_MIME_TYPE="image/png" \
MCP_ARTIFACT_REF="gs://your-bucket/path/to/image.png" \
npm run smoke -- --content "Settings screen screenshot for the Compose UI" --query "compose settings screenshot"Repeat with /clients/claude-web/mcp and <CLAUDE_WEB_TOKEN> if Claude web will ingest images.
Expected:
remember_contextaccepts the image-backed memory- returned JSON metadata includes
modality=mixedwhen both text and image are present search_contextreturns a summary-only result with the sameid=...fetch_contextaccepts that sameidand returns the sameartifact_refs
Use separate tokens for separate trust boundaries:
MCP_ADMIN_TOKENis the admin token for/mcpMCP_CLIENT_PROFILES_JSON[].tokenis the scoped token for each client endpoint
Rotation and revocation rules:
- rotate a web client token by changing that profile's
tokenand redeploying functions - revoke a client by removing the profile or replacing its token and redeploying functions
- do not reuse
MCP_ADMIN_TOKENfor browser-hosted clients - if ChatGPT web and Claude web should be revoked independently, give them separate client profiles
- if OpenClaw should be revoked independently from browser clients, give it its own scoped
openclawprofile
After deployment, use these views together:
memory_vectorsfor the current corpusmemory_eventsfor client-attributed usage and audit history- Cloud Logging for request failures and structured tool-event logs
memory_events is populated automatically by successful and failed tool calls plus ingress-level auth/CORS rejections. It is the easiest way to answer:
- which client is writing memories
- which client is searching or fetching most often
- which memory ids are being returned or fetched repeatedly
- how many searches return zero results
- whether a specific client is generating repeated tool errors
- whether a specific client is hitting repeated
401or403failures
The event payload is intentionally compact. It records ids, filters, counts, states, reasons, and latency rather than duplicating full memory bodies.
Do not bulk-seed the corpus before launch.
For the first release:
- deploy the hosted MCP server
- prove the hosted round trip works
- let the first memories come from real work
- watch retrieval quality before expanding automation
Early target:
- 5 to 20 durable memories
Good early memories:
- stable architecture decisions
- durable project constraints
- reusable workflows
- canonical requirements
- meaningful screenshots with lasting retrieval value
Recommended rollout order:
- Admin endpoint reserved for maintenance and smoke tests
- Browser client rollout on
remember_context,search_context, andfetch_context - Controlled writes only for clearly durable events
- Search-only downstream clients such as Nanobot
- Later use of
deprecate_contextplus internal WIP curation workflows
If deploy succeeds but search fails:
- confirm vector indexes finished building
- confirm
GEMINI_EMBEDDING_MODELandGEMINI_EMBEDDING_DIMENSIONSmatch what you deployed - confirm Firestore is in Native mode
- confirm the production collection does not mix vectors from different models or dimensions
If requests return 401:
- verify
Authorization: Bearer <TOKEN> - verify the token belongs to the endpoint you are calling
- verify the deployed dotenv alias loaded the values you expect
If the function deploys but cannot store documents:
- verify the runtime service account has Firestore access
- verify the Firestore API is enabled in the backing Google Cloud project
If browser clients get 403 Origin not allowed:
- verify the request is using a client-scoped endpoint
- verify that client profile has the expected
allowedOrigins - verify the browser token matches the scoped client endpoint
- do not use the admin endpoint for browser-hosted clients
Useful commands:
cd /Users/nick/git/metacortex
firebase functions:listUse Firebase console logs or Cloud Logging for failed production requests.