Recall Select
Provides long-term memory for AI agents via MCP tools to store, recall, and delete memories, with per-user scoping and usage limits.
README
recall.select
A minimal agentic memory system - feed one URL to any agent and it gains long-term memory with near-zero setup. Built on Qdrant + FastMCP + FastAPI/Bootstrap.
See docs/specs/initial_specification.md
for the full design and the incremental build plan, and
docs/specs/changelog.md for a running record of
notable changes.
How it works
Memory is stored as vectors. Each memory store is a Qdrant collection, mapped
one-to-one to a (user, project) pair. Metadata around those vectors - users,
API keys, projects, and per-collection usage/limit stats - lives in MongoDB.
agent ──▶ FastAPI (web) ──▶ Qdrant (vectors: one collection per user+project)
└▶ MongoDB (users, API keys, projects, stats/limits)
└▶ embedding API (remote, text → vector)
Qdrant collections are created lazily: nothing touches Qdrant until the
first memory is stored into a (user, project) pair.
Architecture
app/main.py- FastAPI app. Serves the Bootstrap landing page and, on startup, ensures the Mongo indexes exist (tolerant of a cold/remote DB).app/mcp_server.py- the MCP server behind the memory link. An agent's MCP client points at{PUBLIC_BASE_URL}/m/{key}(Streamable HTTP, stateless, JSON responses); the API key in the path is the whole credential and scopes thestore_memory/recall_memory/delete_memorytools to the key owner's default project. The same key can instead be sent asAuthorization: Beareragainst the key-less/mcpendpoint, to keep the secret out of the URL/logs.{...}/m/{key}.md(inapp/api/connect.py) serves the matching setup instructions (both forms).app/dependencies.py- the core DI container (injector). Constructs the shared singletons (Qdrant client, Mongo client/db, the remote embedder). FastAPI deps (app/api/deps.py) and startup resolve fromapp_containerrather than building clients themselves.app/services/- the service layer (no HTTP/route code, just I/O):qdrant_store.py- Qdrant client +ensure_collection/upsert_memory/search/delete_memory.mongo.py- Mongo client,get_db(), andensure_indexes()(enforces the one-to-one(user, project)rule with a unique compound index).users.py-add_user,get_user,get_user_by_email,update_user.api_keys.py- user-bounded keys, stored as a SHA-256 hash (the plaintext is returned once, fromadd_api_key, and never persisted):add_api_key,delete_api_key,delete_user_keys,list_api_keys,get_by_key(hashes the presented token and matches on the digest).projects.py-add_project,get_project,list_projects,update_project,delete_project.collections.py- the(user, project) ↔ Qdrant collectionregistry.collection_name(user_id, project_id)is the internal naming standard (rs_{user}_{project}); trackspoints_count/calls_countfor limits & stats.collection_provisioning.py- the two-sidedcreate_collection/destroy_collectionstep. A collection only exists once both its Mongo registry row and its backing Qdrant collection do; this composes thecollectionsregistry withqdrant_storeinto one atomic, idempotent operation so the two stores never fall out of step. Creation is lazy, so its only creating caller is the first memory write (memory.store_memory); the collection API's delete usesdestroy_collection.embeddings.py- theEmbedderabstraction;embeddings_remote.py- the concrete text→vector backend (remote embedding API, e.g. DeepInfra).monobank.py- minimal Monobank acquiring client (create_invoice) plus webhook auth (fetch_pubkey/verify_signature, ECDSA-SHA256 over the raw body). Reuses the mcp-api.net merchant token; recall.select owns its own invoice/redirect/webhook.billing.py- the plan catalogue and the payment record keyed by Monobank'sinvoiceId.record_pendingon checkout;apply_webhookflips the buyer'stieronce onsuccess(idempotent against retries/duplicates). Also the single source of truth for per-tier allowances:call_allowance(tier)/project_allowance(tier)(None= unlimited; unknown tiers fall back to free).usage.py- the monthly call meter and the price-model gate. Every accepted store/recall/delete is tallied into a per-(user, calendar-month)usagerow;check_call_allowedrejects a call once the tier's monthlycall_allowanceis spent, raisingQuotaExceeded. Enforced inmemory.py(so both the MCP tools and the HTTP memory API are covered) and mapped to HTTP 429 byapp/main.py; the MCP transport surfaces it as a tool error. Separate from the all-timecollections.calls_count.account.py- the read-only snapshot the signed-in/accountpage shows (plan, monthly usage, per-project stored counts), composed frombilling/usage/projects/collections.docs.py- content for the public/docsintegration guides. Builds the MCP client config in one place (mcp_config/mcp_config_json), reused by both the docs pages andapp/api/connect.py's per-key.md, so the two never drift.INTEGRATIONSis the guide registry (add a page by adding an entry).
Public pages (served from app/main.py, Bootstrap + Jinja, i18n via
app/translations/*.yml): / landing, /plans, /account (signed-in), and the
/docs/integrations guides. FastAPI's built-in API docs are moved off /docs to
/api/docs (/api/redoc, /api/openapi.json) so the public site owns /docs.
Payments ride the HTTP layer in app/api/payments.py: POST /api/me/checkout
(signed-in) creates the invoice and returns the Monobank pay_url; the verified
POST /webhooks/monobank grants the tier; GET /payment/success|fail are the
cosmetic browser return pages (entitlement is webhook-driven, never these).
Every CRUD function takes an optional db=/client= argument so it can be driven
in tests without a live backend.
Configuration
Set via environment (a local .env is auto-loaded; never commit it - see
.env.example):
| Variable | Default | Purpose |
|---|---|---|
MONGODB_URI |
(required) | Remote, managed MongoDB connection string. |
MONGODB_DB |
recall_select |
Database name. |
QDRANT_URL |
http://qdrant:6333 |
Qdrant endpoint (internal compose network). |
QDRANT_API_KEY |
(none locally; required in prod) | Shared secret between the app and Qdrant. Compose sets Qdrant's QDRANT__SERVICE__API_KEY from it, and the app sends it on every request. It's the only gate on the qdrant.recall.select dashboard, which has no auth of its own. |
VECTOR_SIZE |
768 |
Vector dimension for every collection. The remote embedder is asked (via the API dimensions param) to return vectors of exactly this size, so the two stay in sync. |
EMBEDDING_API_KEY |
(required) | API key for the remote embedding API. |
EMBEDDING_BASE_URL |
https://api.deepinfra.com/v1 |
OpenAI-compatible embeddings API base URL. |
GOOGLE_CLIENT_ID |
(required for sign-in) | Google OAuth 2.0 Web client id. |
GOOGLE_CLIENT_SECRET |
(required for sign-in) | Google OAuth 2.0 client secret. |
SESSION_SECRET |
(dev fallback) | Signs the session cookie. Set a stable value in prod. |
PUBLIC_BASE_URL |
http://localhost:8000 |
Public origin; builds the memory link + OAuth redirect URI. |
MONOBANK_API_KEY |
(required for payments) | Monobank acquiring merchant token. Shared with the mcp-api.net platform - same merchant, one account; invoices are told apart by reference. |
MONOBANK_REDIRECT_URL |
{PUBLIC_BASE_URL}/payment/success |
Where the shopper's browser returns after paying. |
MONOBANK_WEBHOOK_URL |
{PUBLIC_BASE_URL}/webhooks/monobank |
Server-to-server callback that grants the tier. Must be publicly reachable. |
MONOBANK_WEBHOOK_VERIFY |
1 |
Verify the webhook's X-Sign against the merchant pubkey. Keep on wherever money moves; 0 only for local dev. |
Auth (Google sign-in)
Sign-in gates the memory link: a user signs in with Google, then clicks Generate my memory link to provision their default project + collection + API key and get the URL to feed an agent. To set up the Google credentials:
- Google Cloud Console → APIs & Services → OAuth consent screen - configure it (External; add your email as a test user while unverified).
- Credentials → Create credentials → OAuth client ID → Web application.
- Add an Authorized redirect URI:
{PUBLIC_BASE_URL}/auth/callback- e.g.http://localhost:8000/auth/callbackfor local dev andhttps://recall.select/auth/callbackin prod (add both if you test locally). - Copy the Client ID and Client secret into
.env(GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET), and set a stableSESSION_SECRET(python -c "import secrets; print(secrets.token_urlsafe(48))").
Run locally
The full stack (web + Qdrant) via Docker Compose:
cp .env.example .env # then fill in MONGODB_URI
docker compose up --build
# open http://localhost:8000
Or just the app, against your own Qdrant/Mongo:
pip install -e ".[dev]"
uvicorn app.main:app --reload
Tests
pip install -e ".[dev]"
pytest
CRUD tests run against an in-memory Mongo (mongomock) and Qdrant/embedding
clients are faked - no live backends required.
Deploy
./deploy/deploy.sh
The same command works from two places - it detects where it's run:
- From a dev machine (or the agent's box): pushes local commits, then runs the
deploy on the server over the
recall-serverSSH alias. - On the server itself (
setti@setti-server:~/recall_select$ ./deploy/deploy.sh): deploys in place, no SSH hop.
Both paths run the same worker - deploy/_server_deploy.sh:
git sync of master, rebuild the Compose stack (FastAPI web + Qdrant), reload
the shared Caddy proxy (automatic HTTPS for recall.select), prune old images.
MongoDB is remote/managed, so the auth/MONGODB_URI env (see .env) must be present
on the server.
Whoever runs it on the server needs GitHub pull access to the repo (an
authorised SSH key in their ~/.ssh) and membership of the docker group - both
true for claude-agent and setti. The worker auto-registers the repo as a git
safe.directory so a deployer who isn't the repo's owner isn't blocked by
"dubious ownership".
Automated deploys (CI)
Every push to master auto-deploys via GitHub Actions
(.github/workflows/deploy.yml) - the same flow as
above, just triggered by CI instead of a person. The job SSHes into the server and
pipes deploy/_server_deploy.sh over stdin, so it runs
the pushed commit's own deploy logic. Deploys are serialized (concurrency), and a
Run workflow button (workflow_dispatch) lets you deploy on demand.
One-time setup - add under Settings → Secrets and variables → Actions:
| Secret | Required | Purpose |
|---|---|---|
DEPLOY_SSH_KEY |
yes | Private key whose public half is in the deploy user's ~/.ssh/authorized_keys. |
DEPLOY_HOST / DEPLOY_USER |
yes | Server address and the SSH user to deploy as. |
DEPLOY_PORT |
no | SSH port (default 22). |
DEPLOY_KNOWN_HOSTS |
no | Pin the server host key; if unset, CI trusts it on first use via ssh-keyscan. |
App secrets (MONGODB_URI, OAuth, etc.) stay in the server's .env - CI never sees
them.
License
Licensed under the GNU Affero General Public License v3.0. If you run a modified version as a network service, the AGPL requires you to offer its source to your users. Copyright © 2026 Sergii Setti.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。