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. Sixteen 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
16tools, 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 memory 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= 1, ≤ 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.

Seven learning tools

Structured study for Japanese and Vietnamese.

Seven tools turn every answer into data: one track per language, items with their own mastery and review schedule per skill, timed sessions and the full answer history. FSRS spaces long-term reviews; a short-term queue brings every miss back three times, spaced apart, before it counts as learned. Japanese is ja with JLPT N5–N1 levels, Vietnamese is vi.

learning_start
write

The one fetch at the start of a language session. Creates the track on first use (Japanese and Vietnamese start from preset preferences, and earlier lesson logs keep their numbers), opens or resumes today's session, and returns the teaching protocol, today's lesson, weak items, due reviews, new items and your long-form notes.

language_codeja · vistart_session= truelimit= 20include_memories= truemax_characters= 40,000current_leveltarget_leveldaily_goal_minutestimezonepreferences

Settings passed here update the track. A session left open for 12 hours is closed as abandoned at its last answer; nothing is lost.

learning_record_attempts
write

Records a batch of answers in order and updates mastery, the practice queue and the FSRS review schedule in one transaction. A wrong, prompted or hesitant answer puts the item back in the queue.

language_codeattempts≤ 500batch_idsession_id

Retrying the same batch records nothing twice. Answers are never edited or deleted.

learning_plan
write

Creates or updates the next lesson from current mastery: weak and overdue items first, then a few new ones (fewer while many are weak). Or pass your own items.

language_codeitemstitleobjectiveslesson_datanew_item_limit= 5review_limit= 15lesson_number

Numbering continues from earlier lessons; completed lessons are never rewritten.

learning_finish_session
write

Closes the session with its actual study time and a summary, and marks the lesson complete when every item was taught.

language_codeactive_minuteslesson_completed= falsesummarysession_id

Finishing twice changes nothing.

learning_manage_items
write

Adds or updates vocabulary, kana, kanji, grammar patterns and phrases with readings, meanings, JLPT levels and examples.

language_codeitems≤ 200

Keyed by <item_type>:<written form>. Optional progress seeds a starting state for what you learned before tracking; it never overwrites real history.

learning_get_reviews
read

What to practise now: weak items that still owe spaced successes first, then items overdue on the review schedule, most overdue first.

language_codeskilllimit= 20, ≤ 100include_new= 0

Each skill has its own schedule: you can read a kanji well and still be due to write it.

learning_get_progress
read

Mastery by item type and skill, lessons, JLPT N5–N1 coverage for Japanese, and minutes, answers and accuracy for the last 7 days and 4 weeks.

language_code= every track

JLPT targets are commonly cited estimates; JLPT publishes no official lists.

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
learning_startlesson loop

“Start my Japanese session.”

the only fetch: lesson, weak items, reviews, notes
learning_planlesson loop

“Plan my next Vietnamese lesson.”

weak and overdue first, then a few new words
no toollesson loop

“Give me the session JSON.”

no tool: every answer, new item and the session close in one block
memory_importlesson loop

“Import this session JSON: { … }”

records the answers, asks before any deletion
learning_get_reviews

“What should I review in Vietnamese right now?”

weak items first, then overdue
learning_get_progress

“How did my Japanese go this week? Am I on track for N5?”

minutes, accuracy, JLPT coverage
learning_manage_items

“Add 日, 月 and 火 as N5 kanji with their readings.”

readings, meanings, level N5
Two layers of spacing
Each item is scheduled per skill. Long term, FSRS decides: a remembered item's interval grows, a forgotten one comes back the next day. Short term, any wrong, prompted or hesitant answer puts the item in a practice queue that clears only after three successful retrievals with other items in between; echoing the tutor never counts, and a new miss restarts the count. The next session opens with that queue, then overdue reviews, then a few new items.
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 session per conversation, in four prompts.

A session 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. Vietnamese is the worked example; for Japanese, swap `vi` for `ja`.

Step 1
Plan the next lesson
In a chat that can call tools, when no lesson is pending. Optional: without one, the session runs free practice.
Call learning_plan with language_code "vi" and show me the lesson's items and their roles. If it has fewer than five new items, suggest new words for my level, add them with learning_manage_items, and plan again.
Step 2
Open a session
First message; this is the only fetch.
Start my Vietnamese session: call learning_start with language_code "vi". That is the only tool call in this conversation; follow its instructions field throughout.

Teach today's lesson, or free practice if there is none: weak items first, then due reviews, then a few new words. One word -> meaning -> pronunciation -> sentence -> drill -> recombination, Northern/Hanoi accent. Keep a private log of every answer: item, skill, grade, correct, prompted, hesitant. When every lesson item is done, say "Lesson N is complete".
Step 3
Close the session
Voice or text, no tools needed.
Give me the session JSON: one code block, version 1, with a "learning" block for language_code "vi" holding every new item, every answer in order (item_key, skill, direction, grade 1-4, is_correct, was_prompted, was_hesitant, my response when it was wrong) and "finish" with active_minutes and lesson_completed. Add a "store" of type "session_log" for today and an "update" of my baseline memory with only the big-picture changes. Nothing else inside the block.
Step 4
Apply it
Next conversation that can call tools.
Import this session JSON with memory_import (no deletions unless I confirm), then show my progress with learning_get_progress:
<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.

Language study lives in its own tables: one track per language with items, per-skill progress, lessons, timed sessions and an append-only answer history. The database itself keeps every row inside its track, so Japanese work never touches Vietnamese progress, and each review state stores its FSRS version so mastery can be recalculated from the raw answers.

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
Answers per batch500
Items per call200
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 sixteen 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 the Prisma 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
7Change the database schema

Edit apps/connector/prisma/schema.prisma, print the SQL, review it (qualify public.vector; write HNSW indexes and CHECK constraints by hand) and save it as prisma/migrations/<timestamp>_<name>/migration.sql. The deploy applies it; npm run db:check -w @mcp/connector reports drift.

cd apps/connector && npx prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma --script
8Read 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
9Later: 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.