Persistence ConnectorGitHub
Memory for ChatGPT, Claude and any MCP client

A memory your assistants keep between conversations.

Persistence Connector stores what you ask ChatGPT or Claude to remember, finds it again by meaning, and keeps every record private to your account. Nine tools, one endpoint, one sign-in.
Example memory record
Text is stored once in PostgreSQL; the vector is derived and can be rebuilt at any time.
work:infra
id
9d3a1f0e-7c2b-4e5a-9a8f-2b6c1d4e7f10
title
Postgres pool size for the shared PgBouncer
content
Keep DB_POOL_MAX at 2. The shared PgBouncer allows two backend connections for this service; raising it drops connections under load.
type
decision
tags
postgrespgbouncerdecision
importance
80 / 100
created
2026-09-10 14:02 UTC
index
1 chunk · 1,536-dim vector
Later, from a different chatmemory_search “how many db connections can we use?” → this record, score 0.81
9tools, one endpoint
2OAuth scopes
100 KBof text per memory
0.35minimum search score
Live statusChecked against mcp.datahashira.com, at most once a minute.
Service · onlineDatabase · connectedSign-in · mcp.datahashira.com
Connect

Three ways in, one URL.

Every client gets the same endpoint. The first use opens a sign-in page; after that, the assistant calls the tools on its own.

ChatGPT
Web and desktop, on plans with Developer mode.
  1. 1Open Settings → Apps & Connectors → Advanced settings and turn on Developer mode.
  2. 2Back in Apps & Connectors choose Create. Name it Memory, paste the endpoint as the MCP server URL, and pick OAuth for authentication.
  3. 3In a chat open the + menu, then More, and switch Memory on. ChatGPT sends you to the sign-in page the first time.
  4. 4Say something worth keeping: “Remember that…”. ChatGPT calls memory_store; later questions call memory_search.
Claude
claude.ai and Claude Desktop, on Pro, Max, Team and Enterprise.
  1. 1Open Settings → Connectors and choose Add custom connector. On Team and Enterprise an owner adds it for everyone.
  2. 2Name it Memory, paste the endpoint as the remote MCP server URL, and add it.
  3. 3Choose Connect and finish the sign-in. In a chat, open the search-and-tools menu and turn Memory on.
  4. 4Ask Claude to remember or recall things. It picks the right tool and asks you before deleting anything.
Claude Code and other clients
Anything that speaks MCP over Streamable HTTP with OAuth.
claude mcp add --transport http memory \
  https://mcp.datahashira.com/mcp
claude   # then run /mcp and pick Memory to sign in

Clients that read a JSON config, such as Cursor or Windsurf, take the same URL:

{ "mcpServers": {
    "memory": { "url": "https://mcp.datahashira.com/mcp" }
} }

The connector publishes its OAuth metadata at /.well-known/oauth-protected-resource/mcp, so a compliant client discovers the sign-in server on its own.

Sign in

Once per client, with your username and password.

Your client registers itself, sends you to the connector's own sign-in page, and receives a short-lived token. Google or per-user accounts can be added later without any client changes.

Step 1
Add the connector
Your client reads the connector's metadata and registers itself automatically. There is no callback URL to configure.
Step 2
Sign in
Username and password on the connector's sign-in page. PKCE protects the exchange end to end.
Step 3
Approve two scopes
memory:read lets the assistant search and fetch. memory:write lets it store, update and delete.
Step 4
Use it
Every call carries the token. The connector checks its signature, issuer, audience, expiry and scopes each time.
What the connector keeps about you
The username you signed in with and the scopes you approved, both carried in the token. Every memory is filed under that username, and your assistant can only reach your own.
Where your text goes
The canonical copy lives only in the connector's PostgreSQL database. Text is sent to OpenAI's embeddings API to compute search vectors and nowhere else. Logs record IDs and timings, never memory content or tokens.
Nine tools

What your assistant can do with it.

Read tools need memory:read; write tools need memory:write. Green parameters are required; the rest show their defaults.

memory_search
read

Find memories by meaning and by keywords, narrowed by namespace, tag, type, source or date. Returns nothing rather than a guess when confidence is low.

querynamespacestagscontent_typessourcecreated_aftercreated_beforetop_k= 8, ≤ 25include_content= true

Hybrid ranking: vector similarity plus trigram keyword match, one result per memory.

memory_store
write

Save a fact, decision, preference, note, snippet or document. Long content is split and indexed automatically; identical content returns the existing ID instead of a duplicate.

contentnamespace= defaulttitlecontent_type= notetags≤ 50importance0–100, = 50metadata≤ 20 KBsource

Up to 100 KB of text per memory. Returns the stable memory ID.

memory_get
read

Fetch one complete memory by its stable ID, usually after a search or list has identified it.

idUUID

Returns the full canonical text, metadata, tags and timestamps.

memory_list
read

Browse the newest memories with the same filters as search, without a query and without touching the embedding model.

namespacestagscontent_typessourcecreated_aftercreated_beforelimit= 25, ≤ 100

Newest first.

memory_update
write

Change the title, content, type, tags, importance, source or metadata while keeping the same ID. Changed text is re-indexed automatically.

idtitlecontentcontent_typetagsimportancemetadatasourceexpected_updated_at

expected_updated_at refuses the write if someone else changed the memory first.

memory_briefing
read

The one fetch at the start of a conversation. Returns the operating instructions, counts per namespace, and the full text of the most important, topic-related and recent memories within a character budget; with lesson it adds exactly one pending lesson and leaves the others out.

namespacestopiclesson"next" or a numberlimit= 15, ≤ 40include_content= truemax_characters= 80,000include_instructions= true

Whole memories that don't fit the budget are listed under omitted. Compact excerpts are always included as an index.

memory_delete
write · destructive

Remove a memory so it disappears from every read and search immediately. Runs only with confirm=true, which the assistant sets after you say yes.

idconfirm= false

Soft delete: an administrator can still recover the record from the database.

memory_curriculum
write

Turns a namespace's material into ordered, self-contained lessons using the backend planning model: weak items first, plus a few new items in each lesson. Every lesson is stored as a memory with a number and a status. From then on memory_briefing with lesson: "next" serves one lesson per conversation.

namespacesource_idslesson_count= 8, ≤ 30goalreplace_existing= false

Pending lessons block a new plan unless replace_existing is set. Once every lesson is done, run it again: the finished lessons are archived and the next set starts from the updated material.

memory_import
write · destructive

Applies a session export JSON written at the end of a chat that had no tool access: stores new memories, updates existing ones by id (a lesson by namespace and lesson_number), and deletes listed ids only with confirm_deletes=true after you say yes.

sessionversion 1 JSONconfirm_deletes= false

Up to 100 stores, 100 updates and 50 deletes per call; every item reports stored, duplicate, updated, deleted, not found, skipped or error.

What to say

Plain requests, the right tool.

You never name a tool. Copy a prompt, paste it into a chat with Memory switched on, and the assistant picks the tool for you.

memory_store

Remember that our deploy window is Tuesdays at 10:00 IST.

content, tags: deploy, schedule
memory_search

What did we decide about the Postgres pool size?

query, top_k 8
memory_list

Show my latest notes tagged onboarding.

tags: onboarding, limit 25
memory_update

Make that memory importance 90 and add the tag infra.

id, importance, tags
memory_searchmemory_delete

Forget the note about the old API key.

asks you to confirm first
memory_curriculumlesson loop

Split my Vietnamese material into eight lessons.

backend planning model, stored as lessons
memory_briefinglesson loop

Load my briefing with the next lesson.

lesson "next": full text, one lesson, instructions
no toollesson loop

Give me the session JSON.

no tool: the assistant writes the export block for you to copy
memory_importlesson loop

Import this session JSON: { … }

asks before applying deletions
One fetch, one lesson, one JSON
Every conversation follows the same shape whether or not it can call tools mid-way. First, the briefing: one call that returns the full text of what matters plus today's lesson, so nothing needs fetching later. Second, the session: the assistant teaches only that lesson, keeps a private list of what went right and wrong, and announces “Lesson N is complete”. Third, the close: “give me the session JSON” produces one block that marks the lesson done, updates the baseline and logs the day; paste it into a tool-enabled chat and say “import this session JSON”. When no lesson is pending, run memory_curriculum again: it archives the finished lessons and plans the next set, weak items first plus a few new ones.
How a memory is graded
Importance is a number from 0 to 100 that the assistant assigns when it stores: 90–100 for standing decisions and identity-level facts, 70–89 for decisions, preferences and recurring project or people context, 40–69 for useful details and task state, 10–39 for minor details. It counts for a tenth of the search score, so an important memory outranks a trivial one at equal relevance, and it is the sort key of the briefing's pinned list.
Session prompts

One lesson per conversation, in four prompts.

The memory is fetched once at the start of a conversation and written once at the end; nothing is called in between, so a voice chat works exactly like a text chat. Material is split into lessons on the backend, and each conversation gets one lesson. The Vietnamese lessons are the worked example; swap the namespace, topic and goal for any subject.

Step 1
Create the curriculum
Once, in a chat that can call tools, and again after the last lesson is done.
Call memory_curriculum with namespace "vietnamese_learning", lesson_count 8 and goal "conversational Vietnamese with a Hanoi accent, fast pace". Then list the lesson titles it created.
Step 2
Open a conversation
First message; this is the only fetch.
Start by calling memory_briefing with namespaces ["vietnamese_learning"], lesson "next", topic "what to review next" and limit 20. That is the only tool call in this conversation: everything you need is in its memories field, and the lesson field is today's lesson. Follow the instructions field for the whole conversation.

Teach only that lesson: begin with a short unprompted diagnostic on its weak and new items, then one word -> meaning -> pronunciation -> sentence -> drill -> recombination, Northern/Hanoi accent. Tell me which lesson number this is out of how many.

Keep a private running list of what I get right unprompted, what I get wrong or needed a prompt for, and anything new we cover. When every item of the lesson is done, say "Lesson N is complete" and tell me to start a new chat for the next one.
Step 3
Close the conversation
Voice or text, no tools needed.
Give me the session JSON: exactly one JSON code block in the Session Export format (version 1). Include an "update" for today's lesson by its "namespace" and "lesson_number" (no id) with metadata {"status": "done", "completed_at": ..., "result": ...}, an "update" for the mastery baseline with its complete new text, and a "store" entry of type "lesson_log" for today with date, items covered and my scores. Nothing else inside the block.
Step 4
Apply it
Next conversation that can call tools, or the same one if it can.
Import this session JSON with memory_import (no deletions unless I confirm):
<paste the block>
Inside

How a memory is kept.

Stored once, found by meaning

Canonical text is stored once in PostgreSQL under your account and a namespace, so work can stay apart from personal, or one project from another.

Long entries are split by headings and paragraphs. Each piece gets a 1,536-dimension embedding in pgvector. Search combines vector similarity with trigram keyword matching, applies your filters, then collapses the results to one per memory. Anything under a confidence score of 0.35 is dropped rather than returned as a guess.

Identical content is caught by hash and returns the existing ID. Deleting is soft: the record leaves every read and search at once, but nothing is destroyed without an administrator. Vectors are derived data and can be rebuilt from the text at any time.

The service is stateless between calls, runs behind HTTPS only, and filters every query by both tenant and user. No tool accepts a user or tenant ID as an argument; both come from the verified token.

Limits
Text per memory100 KB
Metadata per memory20 KB JSON
Tags per memory50
Search results≤ 25
List page≤ 100
Minimum search score0.35
Requests per client address120 / min
Request body750 KB
Embedding modeltext-embedding-3-small
Developers

Endpoints and the wire contract.

A stateless MCP Streamable HTTP server. Every POST carries its own bearer token; there are no session IDs to keep.

Routes
RoutePurposeAuth
POST /mcpMCP JSON-RPC over Streamable HTTP. GET and DELETE return 405.Bearer token
GET /.well-known/oauth-protected-resource/mcpProtected-resource metadata: resource URL, authorization server, scopes.None
GET /health/liveProcess liveness.None
GET /health/readyReadiness; checks the database connection.None
/authorize, /token, /register, /revokeBuilt-in OAuth 2.1 authorization server: dynamic client registration, PKCE S256, refresh-token rotation. Metadata at /.well-known/oauth-authorization-server, keys at /.well-known/jwks.json.Per endpoint
GET /docsThe connector's built-in docs page.None
Without a token
An unauthenticated call gets a challenge that points the client at the metadata document, which is how discovery starts.
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
  resource_metadata="https://mcp.datahashira.com/.well-known/oauth-protected-resource/mcp",
  scope="memory:read memory:write"
Token requirements

ES256, issued by the connector's own authorization server, with aud equal to https://mcp.datahashira.com/mcp, a stable sub, and the scopes above in scope or permissions. A call with a valid token but a missing scope returns an insufficient_scope challenge inside the tool result, so the client can ask for more access rather than fail silently.

Source, deployment notes and the full tool schemas live in the GitHub repository.

Operate

Everything the owner needs to do.

The service runs on ECS in ap-south-1 behind the shared nginx. Secrets live in SSM under /mcp-memory/; the deploy key in .env.production can read and write them.

1Connect a client

Follow the ChatGPT or Claude steps above with https://mcp.datahashira.com/mcp. Sign in with the account stored in SSM (MCP_LOGIN_USERNAME and MCP_LOGIN_PASSWORD). Nothing else needs configuring on the client side.

2Prove a login from the terminal

Registers a throwaway client, opens the sign-in page, exchanges the code with PKCE and calls the live endpoint expecting nine tools.

npm run check:oauth
3Change the password (or username)

Write the new value, then restart the task so it is read again; about a minute of downtime.

aws ssm put-parameter --name /mcp-memory/MCP_LOGIN_PASSWORD --type SecureString --overwrite --value 'NEW-PASSWORD'
aws ecs update-service --cluster production-cluster --service mcp-memory --force-new-deployment
4Rotate the token signing key

Every access token stops working within an hour; clients refresh on their own.

openssl ecparam -name prime256v1 -genkey -noout | openssl pkcs8 -topk8 -nocrypt > key.pem
aws ssm put-parameter --name /mcp-memory/OAUTH_SIGNING_KEY --type SecureString --overwrite --value "$(cat key.pem)" && rm key.pem
aws ecs update-service --cluster production-cluster --service mcp-memory --force-new-deployment
5Rotate the OpenAI key

Still pending: the current one was shared in chat.

aws ssm put-parameter --name /mcp-memory/OPENAI_API_KEY --type SecureString --overwrite --value 'sk-proj-...'
aws ecs update-service --cluster production-cluster --service mcp-memory --force-new-deployment
6Deploy a code change

Merge to main. GitHub Actions runs the checks, builds, pushes, runs migrations, rolls the service and streams the CloudWatch log in the run until the rollout is stable. Redeploy without a change from the Actions tab (Run workflow). apps/connector/deploy/deploy.sh is the local fallback.

gh run watch   # or open the Actions tab
7Read logs and check health

Tail the service log, or ask the readiness probe directly.

aws logs tail /ecs/mcp-memory --follow
curl https://mcp.datahashira.com/health/ready
8Later: more users or Google sign-in

Replace the single-account check and the login page in apps/connector/src/modules/oauth/provider.ts; clients and the protocol layer stay as they are.