Reviewed-on: #29
Avalon Memory Crystal Server (amcs)
A Go MCP server for capturing and retrieving thoughts, memory, and project context. Exposes tools over Streamable HTTP, backed by Postgres with pgvector for semantic search.
What it does
- Capture thoughts with automatic embedding and metadata extraction
- Search thoughts semantically via vector similarity
- Organise thoughts into projects and retrieve full project context
- Summarise and recall memory across topics and time windows
- Link related thoughts and traverse relationships
Stack
- Go — MCP server over Streamable HTTP
- Postgres + pgvector — storage and vector search
- LiteLLM — primary hosted AI provider (embeddings + metadata extraction)
- OpenRouter — default upstream behind LiteLLM
- Ollama — supported local or self-hosted OpenAI-compatible provider
Tools
| Tool | Purpose |
|---|---|
capture_thought |
Store a thought with embedding and metadata |
search_thoughts |
Semantic similarity search |
list_thoughts |
Filter thoughts by type, topic, person, date |
thought_stats |
Counts and top topics/people |
get_thought |
Retrieve a thought by ID |
update_thought |
Patch content or metadata |
delete_thought |
Hard delete |
archive_thought |
Soft delete |
create_project |
Register a named project |
list_projects |
List projects with thought counts |
get_project_context |
Recent + semantic context for a project; uses explicit project or the active session project |
set_active_project |
Set session project scope; requires a stateful MCP session |
get_active_project |
Get current session project |
summarize_thoughts |
LLM prose summary over a filtered set |
recall_context |
Semantic + recency context block for injection |
link_thoughts |
Create a typed relationship between thoughts |
related_thoughts |
Explicit links + semantic neighbours |
upload_file |
Stage a file from a server-side path or base64 and get an amcs://files/{id} resource URI |
save_file |
Store a file (base64 or resource URI) and optionally link it to a thought |
load_file |
Retrieve a stored file by ID; returns metadata, base64 content, and an embedded MCP binary resource |
list_files |
Browse stored files by thought, project, or kind |
backfill_embeddings |
Generate missing embeddings for stored thoughts |
reparse_thought_metadata |
Re-extract metadata from thought content |
retry_failed_metadata |
Retry pending/failed metadata extraction |
add_maintenance_task |
Create a recurring or one-time home maintenance task |
log_maintenance |
Log completed maintenance; updates next due date |
get_upcoming_maintenance |
List maintenance tasks due within the next N days |
search_maintenance_history |
Search the maintenance log by task name, category, or date range |
save_chat_history |
Save chat messages with optional title, summary, channel, agent, and project |
get_chat_history |
Fetch chat history by UUID or session_id |
list_chat_histories |
List chat histories; filter by project, channel, agent_id, session_id, or days |
delete_chat_history |
Delete a chat history by id |
add_skill |
Store an agent skill (instruction or capability prompt) |
remove_skill |
Delete an agent skill by id |
list_skills |
List all agent skills, optionally filtered by tag |
add_guardrail |
Store an agent guardrail (constraint or safety rule) |
remove_guardrail |
Delete an agent guardrail by id |
list_guardrails |
List all agent guardrails, optionally filtered by tag or severity |
add_project_skill |
Link a skill to a project; pass project if client is stateless |
remove_project_skill |
Unlink a skill from a project; pass project if client is stateless |
list_project_skills |
Skills for a project; pass project if client is stateless |
add_project_guardrail |
Link a guardrail to a project; pass project if client is stateless |
remove_project_guardrail |
Unlink a guardrail from a project; pass project if client is stateless |
list_project_guardrails |
Guardrails for a project; pass project if client is stateless |
get_version_info |
Build version, commit, and date |
describe_tools |
List all available MCP tools with names, descriptions, categories, and model-authored usage notes; call this at the start of a session to orient yourself |
annotate_tool |
Persist your own usage notes for a specific tool; notes are returned by describe_tools in future sessions |
Self-Documenting Tools
AMCS includes a built-in tool directory that models can read and annotate.
describe_tools returns every registered tool with its name, description, category, and any model-written notes. Call it with no arguments to get the full list, or filter by category:
{ "category": "thoughts" }
Available categories: system, thoughts, projects, files, admin, maintenance, skills, chat, meta.
annotate_tool lets a model write persistent usage notes against a tool name. Notes survive across sessions and are returned by describe_tools:
{ "tool_name": "capture_thought", "notes": "Always pass project explicitly — session state is not reliable in this client." }
Pass an empty string to clear notes. The intended workflow is:
- At the start of a session, call
describe_toolsto discover tools and read accumulated notes. - As you learn something non-obvious about a tool — a gotcha, a workflow pattern, a required field ordering — call
annotate_toolto record it. - Future sessions receive the annotation automatically via
describe_tools.
MCP Error Contract
AMCS returns structured JSON-RPC errors for common MCP failures. Clients should branch on both error.code and error.data.type instead of parsing the human-readable message.
Stable error codes
| Code | data.type |
Meaning |
|---|---|---|
-32602 |
invalid_arguments |
MCP argument/schema validation failed before the tool handler ran |
-32602 |
invalid_input |
Tool-level input validation failed inside the handler |
-32050 |
session_required |
Tool requires a stateful MCP session |
-32051 |
project_required |
No explicit project was provided and no active session project was available |
-32052 |
project_not_found |
The referenced project does not exist |
-32053 |
invalid_id |
A UUID-like identifier was malformed |
-32054 |
entity_not_found |
A referenced entity such as a thought or contact does not exist |
Error data shape
AMCS may include these fields in error.data:
type— stable machine-readable error typefield— single argument name such asname,project, orthought_idfields— multiple argument names for one-of or mutually-exclusive validationvalue— offending value when safe to exposedetail— validation detail such asrequired,invalid,one_of_required,mutually_exclusive, or a schema validation messagehint— remediation guidanceentity— entity name for generic not-found errors
Example schema-level error:
{
"code": -32602,
"message": "invalid tool arguments",
"data": {
"type": "invalid_arguments",
"field": "name",
"detail": "validating root: required: missing properties: [\"name\"]",
"hint": "check the name argument"
}
}
Example tool-level error:
{
"code": -32051,
"message": "project is required; pass project explicitly or call set_active_project in this MCP session first",
"data": {
"type": "project_required",
"field": "project",
"hint": "pass project explicitly or call set_active_project in this MCP session first"
}
}
Client example
Go client example handling AMCS MCP errors:
result, err := session.CallTool(ctx, &mcp.CallToolParams{
Name: "get_project_context",
Arguments: map[string]any{},
})
if err != nil {
var rpcErr *jsonrpc.Error
if errors.As(err, &rpcErr) {
var data struct {
Type string `json:"type"`
Field string `json:"field"`
Hint string `json:"hint"`
}
_ = json.Unmarshal(rpcErr.Data, &data)
switch {
case rpcErr.Code == -32051 && data.Type == "project_required":
// Retry with an explicit project, or call set_active_project first.
case rpcErr.Code == -32602 && data.Type == "invalid_arguments":
// Ask the caller to fix the malformed arguments.
}
}
}
_ = result
Build Versioning
AMCS embeds build metadata into the binary at build time.
versionis generated from the current git tag when building from a tagged committag_nameis the repo tag name, for examplev1.0.1build_dateis the UTC build timestamp in RFC3339 formatcommitis the short git commit SHA
For untagged builds, version and tag_name fall back to dev.
Use get_version_info to retrieve the runtime build metadata:
{
"server_name": "amcs",
"version": "v1.0.1",
"tag_name": "v1.0.1",
"commit": "abc1234",
"build_date": "2026-03-31T14:22:10Z"
}
Agent Skills and Guardrails
Skills and guardrails are reusable agent behaviour instructions and constraints that can be attached to projects.
At the start of every project session, always call list_project_skills and list_project_guardrails first. Use the returned skills and guardrails to guide agent behaviour for that project. Only generate or create new skills/guardrails if none are returned. If your MCP client does not preserve sessions across calls, pass project explicitly instead of relying on set_active_project.
Skills
A skill is a reusable behavioural instruction or capability prompt — for example, "always respond in structured markdown" or "break complex tasks into numbered steps before starting".
{ "name": "structured-output", "description": "Enforce markdown output format", "content": "Always structure responses using markdown headers and bullet points.", "tags": ["formatting"] }
Guardrails
A guardrail is a constraint or safety rule — for example, "never delete files without explicit confirmation" or "do not expose secrets in output".
{ "name": "no-silent-deletes", "description": "Require confirmation before deletes", "content": "Never delete, drop, or truncate data without first confirming with the user.", "severity": "high", "tags": ["safety"] }
Severity levels: low, medium, high, critical.
Project linking
Link existing skills and guardrails to a project so they are automatically available when that project is active:
{ "project": "my-project", "skill_id": "<uuid>" }
{ "project": "my-project", "guardrail_id": "<uuid>" }
Configuration
Config is YAML-driven. Copy configs/config.example.yaml and set:
database.url— Postgres connection stringauth.mode—api_keysoroauth_client_credentialsauth.keys— API keys for MCP access viax-brain-keyorAuthorization: Bearer <key>whenauth.mode=api_keysauth.oauth.clients— client registry whenauth.mode=oauth_client_credentialsmcp.versionis build-generated and should not be set in config
OAuth Client Credentials flow (auth.mode=oauth_client_credentials):
-
Obtain a token —
POST /oauth/token(public, no auth required):POST /oauth/token Content-Type: application/x-www-form-urlencoded Authorization: Basic base64(client_id:client_secret) grant_type=client_credentialsReturns:
{"access_token": "...", "token_type": "bearer", "expires_in": 3600} -
Use the token on the MCP endpoint:
Authorization: Bearer <access_token>
Alternatively, pass client_id and client_secret as body parameters instead of Authorization: Basic. Direct Authorization: Basic credential validation on the MCP endpoint is also supported as a fallback (no token required).
ai.litellm.base_urlandai.litellm.api_key— LiteLLM proxyai.ollama.base_urlandai.ollama.api_key— Ollama local or remote server
See llm/plan.md for an audited high-level status summary of the original implementation plan, and llm/todo.md for the audited backfill/fallback follow-up status.
Backfill
Run backfill_embeddings after switching embedding models or importing thoughts without vectors.
{
"project": "optional-project-name",
"limit": 100,
"include_archived": false,
"older_than_days": 0,
"dry_run": false
}
dry_run: true— report counts without calling the embedding providerlimit— max thoughts per call (default 100)- Embeddings are generated in parallel (4 workers) and upserted; one failure does not abort the run
Metadata Reparse
Run reparse_thought_metadata to fix stale or inconsistent metadata by re-extracting it from thought content.
{
"project": "optional-project-name",
"limit": 100,
"include_archived": false,
"older_than_days": 0,
"dry_run": false
}
dry_run: truescans only and does not call metadata extraction or write updates- If extraction fails for a thought, existing metadata is normalized and written only if it changes
- Metadata reparse runs in parallel (4 workers); one failure does not abort the run
Failed Metadata Retry
capture_thought now stores the thought even when metadata extraction times out or fails. Those thoughts are marked with metadata_status: "pending" and retried in the background. Use retry_failed_metadata to sweep any thoughts still marked pending or failed.
{
"project": "optional-project-name",
"limit": 100,
"include_archived": false,
"older_than_days": 1,
"dry_run": false
}
dry_run: truescans only and does not call metadata extraction or write updates- successful retries mark the thought metadata as
completeand clear the last error - failed retries update the retry markers so the daily sweep can pick them up again later
File Storage
Files can optionally be linked to a thought by passing thought_id, which also adds an attachment reference to that thought's metadata. AI clients should prefer save_file when the goal is to retain the artifact itself, rather than reading or summarizing the file first. Stored files and attachment metadata are not forwarded to the metadata extraction client.
MCP tools
Stage a file and get a URI (upload_file) — preferred for large or binary files:
{
"name": "diagram.png",
"content_path": "/absolute/path/to/diagram.png"
}
Or with base64 for small files (≤10 MB):
{
"name": "diagram.png",
"content_base64": "<base64-payload>"
}
Returns {"file": {...}, "uri": "amcs://files/<id>"}. Pass thought_id/project to link immediately, or omit them and use the URI in a later save_file call.
Link a staged file to a thought (save_file with content_uri):
{
"name": "meeting-notes.pdf",
"thought_id": "optional-thought-uuid",
"content_uri": "amcs://files/<id-from-upload_file>"
}
Save small files inline (save_file with content_base64, ≤10 MB):
{
"name": "meeting-notes.pdf",
"media_type": "application/pdf",
"kind": "document",
"thought_id": "optional-thought-uuid",
"content_base64": "<base64-payload>"
}
content_base64 and content_uri are mutually exclusive in both tools.
Load a file — returns metadata, base64 content, and an embedded MCP binary resource (amcs://files/{id}). The id field accepts either the bare stored file UUID or the full amcs://files/{id} URI:
{ "id": "stored-file-uuid" }
List files for a thought or project:
{
"thought_id": "optional-thought-uuid",
"project": "optional-project-name",
"kind": "optional-image-document-audio-file",
"limit": 20
}
MCP resources
Stored files are also exposed as MCP resources at amcs://files/{id}. MCP clients can read raw binary content directly via resources/read without going through load_file.
HTTP upload and download
Direct HTTP access avoids base64 encoding entirely. The Go server caps /files uploads at 100 MB per request. Large uploads are also subject to available memory, Postgres limits, and any reverse proxy or load balancer in front of AMCS.
Multipart upload:
curl -X POST http://localhost:8080/files \
-H "x-brain-key: <key>" \
-F "file=@./diagram.png" \
-F "project=amcs" \
-F "kind=image"
Raw body upload:
curl -X POST "http://localhost:8080/files?project=amcs&name=meeting-notes.pdf" \
-H "x-brain-key: <key>" \
-H "Content-Type: application/pdf" \
--data-binary @./meeting-notes.pdf
Binary download:
curl http://localhost:8080/files/<id> \
-H "x-brain-key: <key>" \
-o meeting-notes.pdf
Automatic backfill (optional, config-gated):
backfill:
enabled: true
run_on_startup: true # run once on server start
interval: "15m" # repeat every 15 minutes
batch_size: 20
max_per_run: 100
include_archived: false
metadata_retry:
enabled: true
run_on_startup: true # retry failed metadata once on server start
interval: "24h" # retry pending/failed metadata daily
max_per_run: 100
include_archived: false
Search fallback: when no embeddings exist for the active model in scope, search_thoughts, recall_context, get_project_context, summarize_thoughts, and related_thoughts automatically fall back to Postgres full-text search so results are never silently empty.
Client Setup
Claude Code
# API key auth
claude mcp add --transport http amcs http://localhost:8080/mcp --header "x-brain-key: <key>"
# Bearer token auth
claude mcp add --transport http amcs http://localhost:8080/mcp --header "Authorization: Bearer <token>"
OpenAI Codex
Add to ~/.codex/config.toml:
[[mcp_servers]]
name = "amcs"
url = "http://localhost:8080/mcp"
[mcp_servers.headers]
x-brain-key = "<key>"
OpenCode
# API key auth
opencode mcp add --name amcs --type remote --url http://localhost:8080/mcp --header "x-brain-key=<key>"
# Bearer token auth
opencode mcp add --name amcs --type remote --url http://localhost:8080/mcp --header "Authorization=Bearer <token>"
Or add directly to opencode.json / ~/.config/opencode/config.json:
{
"mcp": {
"amcs": {
"type": "remote",
"url": "http://localhost:8080/mcp",
"headers": {
"x-brain-key": "<key>"
}
}
}
}
Apache Proxy
If AMCS is deployed behind Apache HTTP Server, configure the proxy explicitly for larger uploads and longer-running requests.
Example virtual host settings for the current AMCS defaults:
<VirtualHost *:443>
ServerName amcs.example.com
ProxyPreserveHost On
LimitRequestBody 104857600
RequestReadTimeout handshake=0 header=20-40,MinRate=500 body=600,MinRate=500
Timeout 600
ProxyTimeout 600
ProxyPass /mcp http://127.0.0.1:8080/mcp connectiontimeout=30 timeout=600
ProxyPassReverse /mcp http://127.0.0.1:8080/mcp
ProxyPass /files http://127.0.0.1:8080/files connectiontimeout=30 timeout=600
ProxyPassReverse /files http://127.0.0.1:8080/files
</VirtualHost>
Recommended Apache settings:
LimitRequestBody 104857600matches AMCS's 100 MB/filesupload cap.RequestReadTimeout ... body=600gives clients up to 10 minutes to send larger request bodies.ProxyTimeout 600andProxyPass ... timeout=600give Apache enough time to wait for the Go backend.- If another proxy or load balancer sits in front of Apache, align its size and timeout settings too.
CLI
amcs-cli is a pre-built CLI client for the AMCS MCP server. Download it from https://git.warky.dev/wdevs/amcs/releases
The primary purpose is to give agents and MCP clients a ready-made bridge to the AMCS server so they do not need to implement their own HTTP MCP client. Configure it once and any stdio-based MCP client can use AMCS immediately.
Commands
| Command | Purpose |
|---|---|
amcs-cli tools |
List all tools available on the remote server |
amcs-cli call <tool> |
Call a tool by name with --arg key=value flags |
amcs-cli stdio |
Start a stdio MCP bridge backed by the remote server |
stdio is the main integration point. It connects to the remote HTTP MCP server, discovers all its tools, and re-exposes them over stdio. Register it as a stdio MCP server in your agent config and it proxies every tool call through to AMCS.
Configuration
Config file: ~/.config/amcs/config.yaml
server: https://your-amcs-server
token: your-bearer-token
Env vars override the config file: AMCS_URL, AMCS_TOKEN. Flags --server and --token override env vars.
stdio MCP client setup
Claude Code
claude mcp add --transport stdio amcs amcs-cli stdio
With inline credentials (no config file):
claude mcp add --transport stdio amcs amcs-cli stdio \
--env AMCS_URL=https://your-amcs-server \
--env AMCS_TOKEN=your-bearer-token
Output format
call outputs JSON by default. Pass --output yaml for YAML.
Development
Run the SQL migrations against a local database with:
DATABASE_URL=postgres://... make migrate
Backend + embedded UI build
The web UI now lives in the top-level ui/ module and is embedded into the Go binary at build time with go:embed.
Use pnpm for all UI work in this repo.
make build— runs the real UI build first, then compiles the Go servermake test— runssvelte-checkfor the frontend andgo test ./...for the backendmake ui-install— installs frontend dependencies withpnpm install --frozen-lockfilemake ui-build— builds only the frontend bundlemake ui-dev— starts the Vite dev server with hot reload onhttp://localhost:5173make ui-check— runs the frontend type and Svelte checks
Local UI workflow
For the normal production-style local flow:
- Start the backend:
./scripts/run-local.sh configs/dev.yaml - Open
http://localhost:8080
For frontend iteration with hot reload and no Go rebuilds:
- Start the backend once:
go run ./cmd/amcs-server --config configs/dev.yaml - In another shell start the UI dev server:
make ui-dev - Open
http://localhost:5173
The Vite dev server proxies backend routes such as /api/status, /llm, /healthz, /readyz, /files, /mcp, and the OAuth endpoints back to the Go server on http://127.0.0.1:8080 by default. Override that target with AMCS_UI_BACKEND if needed.
The root page (/) is now the Svelte frontend. It preserves the existing landing-page content and status information by fetching data from GET /api/status.
LLM integration instructions are still served at /llm.
Containers
The repo now includes a Dockerfile and Compose files for running the app with Postgres + pgvector.
- Set a real LiteLLM key in your shell:
export AMCS_LITELLM_API_KEY=your-key - Start the stack with your runtime:
docker compose -f docker-compose.yml -f docker-compose.docker.yml up --buildpodman compose -f docker-compose.yml up --build - Call the service on
http://localhost:8080
Notes:
- The app uses
configs/docker.yamlinside the container. - The local
./configsdirectory is mounted into/app/configs, so config edits apply without rebuilding the image. AMCS_LITELLM_BASE_URLoverrides the LiteLLM endpoint, so you can retarget it without editing YAML.AMCS_OLLAMA_BASE_URLoverrides the Ollama endpoint for local or remote servers.- The Compose stack uses a default bridge network named
amcs. - The base Compose file uses
host.containers.internal, which is Podman-friendly. - The Docker override file adds
host-gatewayaliases so Docker can resolve the same host endpoint. - Database migrations
001through005run automatically when the Postgres volume is created for the first time. migrations/006_rls_and_grants.sqlis intentionally skipped during container bootstrap because it contains deployment-specific grants for a role namedamcs_user.
Ollama
Set ai.provider: "ollama" to use a local or self-hosted Ollama server through its OpenAI-compatible API.
Example:
ai:
provider: "ollama"
embeddings:
model: "nomic-embed-text"
dimensions: 768
metadata:
model: "llama3.2"
temperature: 0.1
ollama:
base_url: "http://localhost:11434/v1"
api_key: "ollama"
request_headers: {}
Notes:
- For remote Ollama servers, point
ai.ollama.base_urlat the remote/v1endpoint. - The client always sends Bearer auth; Ollama ignores it locally, so
api_key: "ollama"is a safe default. ai.embeddings.dimensionsmust match the embedding model you actually use, or startup will fail the database vector-dimension check.
