Skip to main content
Glama

img.png

recall.select

최소한의 에이전트 메모리 시스템 - 하나의 URL을 에이전트에 제공하면 거의 설정 없이 장기 기억을 얻을 수 있습니다. Qdrant + FastMCP + FastAPI/Bootstrap 기반.

전체 설계와 점진적 빌드 계획은 docs/specs/initial_specification.md에서, 주요 변경 사항 기록은 docs/specs/changelog.md에서 확인하세요.

작동 방식

메모리는 벡터로 저장됩니다. 각 메모리 저장소는 Qdrant 컬렉션이며, (사용자, 프로젝트) 쌍과 일대일로 매핑됩니다. 해당 벡터 주변의 메타데이터(사용자, API 키, 프로젝트, 컬렉션별 사용량/제한 통계)는 MongoDB에 저장됩니다.

flowchart LR
    agent[Agent] --> web[FastAPI / MCP]
    web <-->qdrant[Qdrant]
    web <--> mongo[MongoDB]
    web <--> embed[Embedding API]

Qdrant 컬렉션은 지연 생성됩니다. 즉, (사용자, 프로젝트) 쌍에 첫 번째 메모리가 저장될 때까지 Qdrant에 접근하지 않습니다.

Related MCP server: LedgerMem MCP Server

아키텍처

  • app/main.py - FastAPI 앱. Bootstrap 랜딩 페이지를 제공하고, 시작 시 Mongo 인덱스가 존재하는지 확인합니다(콜드/원격 DB에 대해 내성적).

  • app/mcp_server.py - 메모리 링크 뒤의 MCP 서버. 에이전트의 MCP 클라이언트는 {PUBLIC_BASE_URL}/m/{key}를 가리킵니다(Streamable HTTP, 상태 비저장, JSON 응답). 경로의 API 키는 전체 자격 증명이며 도구를 키 소유자의 기본 프로젝트로 범위를 지정합니다. 기본 도구: store_memory / recall_memory / delete_memory. 의미 계층 도구(vector_semantics.py 참조): link_memories / unlink_memories / annotate_memory / memory_connections / recall_connected - 연결된 에이전트는 클라이언트 측에서 관계 추론을 수행하며(명시적 요청 시에만), 이러한 도구는 결과를 수집하거나 탐색합니다. 동일한 키는 키가 없는 /mcp 엔드포인트에 대해 Authorization: Bearer로 전송되어 URL/로그에서 비밀을 숨길 수 있습니다. {...}/m/{key}.md(app/api/connect.py에 있음)는 일치하는 설정 지침(두 형식 모두)을 제공합니다.

  • app/dependencies.py - 핵심 DI 컨테이너(injector). 공유 싱글톤(Qdrant 클라이언트, Mongo 클라이언트/DB, 원격 임베더)을 구성합니다. FastAPI 종속성(app/api/deps.py) 및 시작은 app_container에서 해결하며, 자체적으로 클라이언트를 빌드하지 않습니다.

  • app/services/ - 서비스 계층(HTTP/라우트 코드 없음, I/O만):

    • qdrant_store.py - Qdrant 클라이언트 + ensure_collection/upsert_memory/search/delete_memory, 그리고 의미 계층에 필요한 포인트 수준 기본 요소(neighbors, scroll_points, retrieve_points, set_payload).

    • vector_semantics.py - 벡터 메모리 유틸리티 계층: 저장소를 의미 그래프로 취급합니다. 각 포인트의 페이로드에 있는 예약된 _semantics 네임스페이스는 직시 앵커(소유자, 저장 시간, 저장 시 기록됨), 클라이언트 추출 엔터티, 클라이언트 선언 유형 관계(upsert_relations는 이를 검증하고 저장하며, 서버 측 LLM 호출 없음)를 보유합니다. 선언된 관계는 두 가지 품질 헤지를 전달합니다: confidence(0-1], 에지의 탐색 강도를 조정) 및 valid_till(ISO 8601, 만료된 에지는 모든 읽기 경로에서 무시되므로 오래된 구조는 자체적으로 폐기됨). 위생: remove_relations는 잘못된 에지를 삭제하고(upsert_relations의 수정 쌍), memory.delete_memoryprune_relations_to를 호출하여 메모리 삭제 후 매달린 에지가 남지 않도록 합니다. 플러그 가능한 렌즈(topical/temporal/entity/declared)는 유형화된 에지를 파생합니다. 그 위에는 semantic_graph(멀티그래프), spreading_activation(연결에 의한 검색), concept_clusters(발현 온톨로지), infer_relation(선언된 진실 우선, 기하학적 휴리스틱 이후)이 있습니다. 성능 메모: 들어오는 에지 조회(relations_of(include_incoming=True))는 현재 제한된 스크롤 및 스캔입니다. 역방향 탐색이 빈번해지면 해결 방법은 Qdrant 페이로드 인덱스_semantics.relations[].target에 생성하고(create_payload_index, 키워드 스키마) 스캔 대신 필터링된 쿼리를 사용하는 것입니다. 동일한 저장소, 인덱스만 추가되며 스키마는 변경되지 않습니다.

    • mongo.py - Mongo 클라이언트, get_db(), ensure_indexes()(고유 복합 인덱스로 일대일 (사용자, 프로젝트) 규칙을 적용).

    • users.py - add_user, get_user, get_user_by_email, update_user.

    • api_keys.py - 사용자 바운드 키, SHA-256 해시로 저장됨(평문은 add_api_key에서 한 번 반환되며 절대 영구 저장되지 않음): add_api_key, delete_api_key, delete_user_keys, list_api_keys, get_labeled_key, get_by_key(제시된 토큰을 해시하고 다이제스트와 일치시킴, MCP 인증 게이트에서 record_use=Truelast_used_at을 기록). 저장 시 각 키는 비밀이 아닌 표시 힌트(key_prefix + key_last4)도 유지하며, masked()에 의해 rs_ab12…wxyz로 렌더링되므로 비밀을 다시 노출하지 않고 키를 나열하고 구분할 수 있습니다.

    • projects.py - add_project, get_project, list_projects, update_project, delete_project.

    • collections.py - (사용자, 프로젝트) ↔ Qdrant 컬렉션 레지스트리. collection_name(user_id, project_id)는 내부 명명 표준(rs_{user}_{project})이며, 제한 및 통계를 위해 points_count/calls_count를 추적합니다.

    • collection_provisioning.py - 양면 create_collection / destroy_collection 단계. 컬렉션은 Mongo 레지스트리 행 해당 Qdrant 컬렉션이 모두 존재할 때만 존재합니다. 이는 collections 레지스트리와 qdrant_store를 하나의 원자적이고 멱등적인 작업으로 구성하여 두 저장소가 동기화되지 않도록 합니다. 생성은 지연되므로 유일한 호출자는 첫 번째 메모리 쓰기(memory.store_memory)입니다. 컬렉션 API의 삭제는 destroy_collection을 사용합니다.

    • embeddings.py - Embedder 추상화; embeddings_remote.py - 구체적인 텍스트→벡터 백엔드(원격 임베딩 API, 예: DeepInfra).

    • monobank.py - 최소한의 Monobank 수금 클라이언트(create_invoice, fetch_invoice_status) 및 웹훅 인증(fetch_pubkey / verify_signature, 원시 본문에 대한 ECDSA-SHA256). mcp-api.net 판매자 토큰을 재사용합니다. recall.select는 자체 송장/리디렉션/웹훅을 소유합니다.

    • billing.py - 요금제 카탈로그 및 Monobank의 invoiceId로 키가 지정된 결제 기록. 체크아웃 시 record_pending; apply_webhooksuccess 시 구매자의 tier한 번 전환합니다(재시도/중복에 대해 멱등적). reconcile은 웹훅이 놓친 것을 정리합니다(아래 참조). 티어는 시간 제한이 있습니다: grant_tier는 자격이 부여되는 유일한 장소(유료 송장 또는 소유자 호의)이며, tier_expires_attier_grants의 감사 행을 기록합니다. effective_tier(user)는 모든 검사가 읽어야 하는 값입니다. 저장된 paid_2x의 날짜가 지나면 무료 계정이기 때문입니다. 또한 티어별 허용량의 단일 진실 공급원: call_allowance(tier) / project_allowance(tier)(None = 무제한, 알 수 없는 티어는 무료로 대체).

    • usage.py - 월별 호출 측정기 및 가격 모델 게이트. 승인된 모든 저장/검색/삭제는 (사용자, 달력 월)usage 행에 집계됩니다. check_call_allowed는 티어의 월별 call_allowance가 소진되면 호출을 거부하여 QuotaExceeded를 발생시킵니다. memory.py에서 적용되므로(MCP 도구와 HTTP 메모리 API 모두 포함) app/main.py에 의해 HTTP 429로 매핑됩니다. MCP 전송은 이를 도구 오류로 표시합니다. 전체 기간 collections.calls_count와는 별개입니다.

    • account.py - 로그인된 /account 페이지에 표시되는 읽기 전용 스냅샷(요금제, 월별 사용량, 프로젝트별 저장된 개수, 마스킹된 형태의 API 키 목록(생성/마지막 사용 날짜 포함)), billing/usage/projects/collections/api_keys로 구성됩니다.

    • docs.py - 공개 /docs 통합 가이드의 콘텐츠. MCP 클라이언트 구성을 한 곳에서 빌드합니다(mcp_config / mcp_config_json). 문서 페이지 app/api/connect.py의 키별 .md에서 재사용되므로 둘이 어긋나지 않습니다. INTEGRATIONS는 가이드 레지스트리입니다(항목을 추가하여 페이지 추가).

공개 페이지(app/main.py에서 제공, Bootstrap + Jinja, app/translations/*.yml을 통한 i18n): / 랜딩, /plans, /account(로그인됨), /docs/integrations 가이드. FastAPI의 내장 API 문서는 /docs에서 /api/docs로 이동됩니다(/api/redoc, /api/openapi.json). 따라서 공개 사이트가 /docs를 소유합니다.

결제는 **app/api/payments.py**의 HTTP 계층을 통해 처리됩니다: POST /api/me/checkout(로그인됨)은 송장을 생성하고 Monobank pay_url을 반환합니다. 확인된 POST /webhooks/monobank는 티어를 부여합니다. GET /payment/success|fail은 화장용 브라우저 반환 페이지입니다(자격은 웹훅 기반이며, 이 페이지는 아님).

자격은 웹훅에만 의존하지 않습니다. Monobank는 각 상태 변경을 한 번만 보내고 다시 보내지 않으므로, 재시작이나 프록시 결함으로 인해 콜백이 손실되면 지불한 고객이 이전 티어에 남게 되며, 우리 측에서 이를 인지할 방법이 없습니다. 따라서 앱은 또한 가져옵니다: PAYMENT_RECONCILE_MINUTES마다 백그라운드 스윕(billing.reconcile_with_monobank, app/main.py 수명 주기에서 시작)이 5분 후에도 진행 중인 각 결제의 실제 상태를 가져와 동일한 apply_webhook 전환을 통해 푸시합니다. 푸시와 풀은 서로에 대해 멱등적입니다. 먼저 도착하는 쪽이 티어를 부여하고, 다른 쪽은 아무 작업도 하지 않습니다. payments 행은 체크아웃이 시작될 때 기록되므로 created는 "결제 페이지를 열었음"을 의미하며 "지불했음"이 아닙니다. /admin/payments는 이 구분을 명시적으로 보여줍니다.

구매는 한 달(SUBSCRIPTION_DAYS)을 구매하는 것이며, 영원히가 아닙니다. grant_tiertier_expires_at을 기록하고, 동일한 백그라운드 루프는 만료된 계정을 무료로 되돌립니다(downgrade_expired). 계정 페이지에는 요금제가 종료되는 날짜가 표시됩니다. 아직 자동 갱신은 없습니다. 사용자가 다시 구매하며, 크레딧이 남아 있는 동안 구매하면 기간이 다시 시작되지 않고 연장됩니다. 자격은 effective_tier를 통해 읽히므로, 만료된 권한은 스윕이 저장된 필드를 다시 쓰기 전에도 즉시 지급을 중단합니다.

자동 갱신이 없으므로 앱은 묻습니다: billing.renewal_state(user)/account에서 프롬프트를 구동합니다. 요금제 마지막 RENEWAL_WARNING_DAYS(7) 동안 경고와 원클릭 갱신 버튼, 그리고 이후 LAPSED_PROMPT_DAYS(30) 동안 "요금제가 종료되었습니다. 갱신하세요" 프롬프트(다운그레이드 스윕은 lapsed_tier / tier_lapsed_at을 기록하므로 페이지는 여전히 무엇이 종료되었는지 표시할 수 있음). 갱신은 요금제 페이지에서 사용하는 동일한 /api/me/checkout에 게시하며, 보유한 요금제로 미리 설정됩니다. 아직 이메일은 없습니다. 프롬프트는 사이트를 방문하는 사용자에게만 도달합니다.

모든 CRUD 함수는 선택적 db=/client= 인수를 사용하므로 라이브 백엔드 없이 테스트에서 구동할 수 있습니다.

구성

환경 변수로 설정합니다(로컬 .env는 자동 로드되며, 절대 커밋하지 마세요. .env.example 참조):

변수

기본값

목적

MONGODB_URI

(필수)

원격 관리형 MongoDB 연결 문자열.

MONGODB_DB

recall_select

데이터베이스 이름.

QDRANT_URL

http://qdrant:6333

Qdrant 엔드포인트 (내부 Compose 네트워크).

QDRANT_API_KEY

(로컬 없음, 프로덕션 필수)

앱과 Qdrant 간 공유 비밀. Compose는 QDRANT__SERVICE__API_KEY에서 이 값을 설정하며, 앱은 모든 요청에 이 값을 전송합니다. 이는 자체 인증이 없는 qdrant.recall.select 대시보드의 유일한 게이트입니다.

VECTOR_SIZE

768

모든 컬렉션의 벡터 차원. 원격 임베더는 (API dimensions 파라미터를 통해) 정확히 이 크기의 벡터를 반환하도록 요청받으므로, 둘은 동기화 상태를 유지합니다.

EMBEDDING_API_KEY

(필수)

원격 임베딩 API의 API 키.

EMBEDDING_BASE_URL

https://api.deepinfra.com/v1

OpenAI 호환 임베딩 API 기본 URL.

GOOGLE_CLIENT_ID

(로그인 필수)

Google OAuth 2.0 웹 클라이언트 ID.

GOOGLE_CLIENT_SECRET

(로그인 필수)

Google OAuth 2.0 클라이언트 시크릿.

SESSION_SECRET

(개발 폴백)

세션 쿠키에 서명합니다. 프로덕션에서는 안정적인 값을 설정하세요.

PUBLIC_BASE_URL

http://localhost:8000

공개 출처; 메모리 링크 + OAuth 리디렉션 URI를 구성합니다.

FORWARDED_ALLOW_IPS

172.25.0.0/16 (compose) / 127.0.0.1 (uvicorn)

uvicorn이 신뢰하는 X-Forwarded-Proto/-For 피어. Compose는 이를 caddy_net 서브넷으로 기본 설정하여 리디렉션이 https 체계를 유지하고 로그가 실제 클라이언트 IP를 볼 수 있도록 합니다. 해당 네트워크가 재생성된 경우 docker network inspect caddy_net으로 확인하세요.

MONOBANK_API_KEY

(결제 필수)

Monobank 인수 머천트 토큰. mcp-api.net 플랫폼과 공유 - 동일한 머천트, 하나의 계정; 청구서는 reference로 구분됩니다.

MONOBANK_REDIRECT_URL

{PUBLIC_BASE_URL}/payment/success

구매자가 결제 후 브라우저가 이동하는 URL.

MONOBANK_WEBHOOK_URL

{PUBLIC_BASE_URL}/webhooks/monobank

등급을 부여하는 서버 간 콜백. 공개적으로 접근 가능해야 합니다.

MONOBANK_WEBHOOK_VERIFY

1

머천트 공개 키에 대한 웹훅의 X-Sign을 확인합니다. 돈이 오가는 곳에서는 항상 켜두세요. 로컬 개발에서만 0으로 설정합니다.

PAYMENT_RECONCILE_MINUTES

15

Monobank에서 진행 중인 결제의 실제 상태를 가져오는 빈도입니다. 웹훅이 손실되어도 결제 고객이 피해를 보지 않도록 합니다. 0은 스윕을 비활성화합니다.

ADMIN_SECRET

(설정 안 함 - 영역 비활성화)

/admin에서 소유자 관리자 영역을 잠금 해제합니다. 설정하지 않으면 모든 /admin 경로가 404를 반환합니다.

ADMIN_SESSION_HOURS

12

잠금 해제된 관리자 세션이 다시 잠기기까지 지속되는 시간입니다.

소유자 관리자 영역 (/admin)

지원 및 사용자 화면 확인을 위해 모든 사용자의 개인 영역을 읽기 전용으로 볼 수 있는 창입니다. ADMIN_SECRET을 설정하고 (python -c "import secrets; print(secrets.token_urlsafe(32))"로 생성), 웹 컨테이너를 다시 만든 다음 {PUBLIC_BASE_URL}/admin을 열고 세션당 한 번 키를 입력하세요. /admin/users는 모든 계정을 나열하며 (이메일, 이름 또는 사용자 ID로 검색 가능), 각 행은 해당 사용자의 요금제, 이 기간 사용량, 메모리 카운트가 있는 프로젝트, 마스킹된 형태의 메모리 링크를 엽니다.

경계는 의도적입니다. 키는 POST로 제출되며 (URL 파라미터가 아니므로 기록 및 액세스 로그에 남지 않음), 반복된 잘못된 추측은 클라이언트를 5분 동안 잠그고, 세션은 ADMIN_SESSION_HOURS 후에 자동으로 다시 잠깁니다. 이 경로는 아무것도 쓰거나 메모리 텍스트 또는 키 비밀을 공개하지 않습니다 - 소유자는 계정의 형태만 볼 수 있고 내용은 볼 수 없습니다. ADMIN_SECRET이 설정되지 않으면 이 영역은 아예 존재하지 않습니다.

인증 (Google 로그인)

로그인은 메모리 링크를 게이트합니다. 사용자가 Google로 로그인한 다음 메모리 링크 복사를 클릭하여 기본 프로젝트 + 컬렉션 + API 키를 프로비저닝하고 에이전트에 제공할 URL을 얻습니다. 비밀은 정확히 한 번만 표시됩니다 (해시만 저장됨): 이후 랜딩 페이지는 링크를 마스킹하여 표시하고 (GET /api/me/link를 통해), 버튼은 명시적이고 확인된 "새 링크 받기"로 바뀝니다. 재생성은 이전 링크를 자동으로 무효화합니다. 키는 /account에서 관리됩니다: 마스킹된 목록, 생성/마지막 사용 날짜, 레이블로 생성 (한 번 공개), 취소. Google 자격 증명을 설정하려면:

  1. Google Cloud Console → APIs & Services → OAuth 동의 화면 - 구성합니다 (외부; 미검증 상태에서 테스트 사용자로 이메일 추가).

  2. 사용자 인증 정보 → 사용자 인증 정보 만들기 → OAuth 클라이언트 ID → 웹 애플리케이션.

  3. 승인된 리디렉션 URI 추가: {PUBLIC_BASE_URL}/auth/callback - 예: 로컬 개발의 경우 http://localhost:8000/auth/callback, 프로덕션의 경우 https://recall.select/auth/callback (로컬에서 테스트하는 경우 둘 다 추가).

  4. 클라이언트 ID클라이언트 시크릿.env에 복사합니다 (GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET). 안정적인 SESSION_SECRET을 설정합니다 (python -c "import secrets; print(secrets.token_urlsafe(48))").

로컬 실행

Docker Compose를 통한 전체 스택 (웹 + Qdrant):

cp .env.example .env   # then fill in MONGODB_URI
docker compose up --build
# open http://localhost:8000

또는 자체 Qdrant/Mongo를 대상으로 앱만 실행:

pip install -e ".[dev]"
uvicorn app.main:app --reload

테스트

pip install -e ".[dev]"
pytest

CRUD 테스트는 인메모리 Mongo (mongomock)에 대해 실행되며 Qdrant/임베딩 클라이언트는 가짜입니다 - 라이브 백엔드가 필요하지 않습니다.

배포

./deploy/deploy.sh

동일한 명령이 두 곳에서 작동합니다. 실행 위치를 감지합니다:

  • 개발 머신 (또는 에이전트 상자)에서: 로컬 커밋을 푸시한 다음 recall-server SSH 별칭을 통해 서버에서 배포를 실행합니다.

  • 서버 자체 (setti@setti-server:~/recall_select$ ./deploy/deploy.sh)에서: 제자리에서 배포하며 SSH 홉이 없습니다.

두 경로 모두 동일한 작업자 deploy/_server_deploy.sh를 실행합니다: mastergit 동기화, Compose 스택 재구축 (FastAPI web + Qdrant), 공유 Caddy 프록시 재로드 (recall.select에 대한 자동 HTTPS), 오래된 이미지 정리. MongoDB는 원격/관리형이므로 서버에 인증/MONGODB_URI 환경 변수 (.env 참조)가 있어야 합니다.

서버에서 실행하는 사람은 저장소에 대한 GitHub 풀 액세스 (자신의 ~/.ssh에 승인된 SSH 키)와 docker 그룹의 구성원 자격이 필요합니다. claude-agentsetti 모두 해당됩니다. 작업자는 저장소를 git safe.directory로 자동 등록하므로 저장소 소유자가 아닌 배포자가 "의심스러운 소유권"으로 인해 차단되지 않습니다.

자동 배포 (CI)

master에 대한 모든 푸시는 GitHub Actions를 통해 자동 배포됩니다 (.github/workflows/deploy.yml). 위와 동일한 흐름이며, 사람 대신 CI에 의해 트리거됩니다. 작업은 서버에 SSH로 접속하여 deploy/_server_deploy.sh를 stdin으로 파이프하므로 푸시된 커밋 자체의 배포 로직을 실행합니다. 배포는 직렬화되며 (concurrency), Run workflow 버튼 (workflow_dispatch)을 사용하여 필요 시 배포할 수 있습니다.

일회성 설정 - Settings → Secrets and variables → Actions 아래에 추가:

비밀

필수 여부

목적

DEPLOY_SSH_KEY

공개 키가 배포 사용자의 ~/.ssh/authorized_keys에 있는 개인 키.

DEPLOY_HOST / DEPLOY_USER

서버 주소 및 배포할 SSH 사용자.

DEPLOY_PORT

아니오

SSH 포트 (기본값 22).

DEPLOY_KNOWN_HOSTS

아니오

서버 호스트 키를 고정합니다. 설정하지 않으면 CI가 ssh-keyscan을 통해 처음 사용 시 신뢰합니다.

앱 비밀 (MONGODB_URI, OAuth 등)은 서버의 .env에 남아 있습니다. CI는 이를 절대 보지 않습니다.

라이선스

GNU Affero General Public License v3.0에 따라 라이선스가 부여됩니다. 수정된 버전을 네트워크 서비스로 실행하는 경우, AGPL은 사용자에게 해당 소스 코드를 제공할 것을 요구합니다. Copyright © 2026 Sergii Setti.

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Provides persistent, local-first AI memory across sessions via MCP tools for storing, searching, and retrieving context from past interactions.
    1
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    Provides persistent memory for AI agents via 10 MCP tools that map to the AgentRAM REST API, enabling store, retrieve, search, and share memories across personal and shared namespaces.
    10
    191
    MIT

View all related MCP servers

Related MCP Connectors

  • Shared, governed long-term memory for AI agents across tools and sessions via MCP and REST.

  • Shared long-term memory vault for AI agents with 20 MCP tools.

  • Your memory, everywhere AI goes. Build knowledge once, access it via MCP anywhere.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/SergeySetti/recall_select'

If you have feedback or need assistance with the MCP directory API, please join our Discord server