feat(validator): add bare FastAgent + Pallas validator for Mnemosyne MCP

A self-contained sub-project under validator/ that wraps Mnemosyne's MCP
server in a single FastAgent. Use it to confirm — outside of Daedalus —
that Mnemosyne's MCP transport works, every tool registers, args/responses
round-trip, and an LLM can actually drive the tools.

The validator is its own Pallas-consuming project with its own pyproject
(pallas-mcp + fast-agent-mcp), agents.yaml, and fastagent.config.yaml —
matching the pattern used by Iolaus and other Pallas consumers. It does
not import Mnemosyne Python code; it only speaks MCP over HTTP.

The agent never sets workspace_id, so all calls run against the global
scope (libraries with workspace_id IS NULL). Workspace-scoped validation
will come once Daedalus's chat path is wired (Daedalus injects
workspace_id server-side, force-overwriting whatever the LLM produces).

Default model is openai.Qwen3.5-35B-A3B-UD-Q4_K_XL.gguf served by
llama.cpp at nyx.helu.ca:22079/v1. Token provisioning via
`python manage.py create_mcp_token --user <u> --name validator`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-29 06:53:48 -04:00
parent 2a8a3d75b4
commit 97a14fb03a
9 changed files with 264 additions and 0 deletions

9
validator/.env.example Normal file
View File

@@ -0,0 +1,9 @@
# Mnemosyne Validator — environment variables
#
# Copy to .env and adjust as needed. Loaded by Pallas at startup
# (without overwriting already-set vars).
#
# OPENAI_BASE_URL is also set in fastagent.config.yaml; this is here for
# parity with the standard Pallas-consumer layout.
OPENAI_BASE_URL=http://nyx.helu.ca:22079/v1

6
validator/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
fastagent.secrets.yaml
.env
.venv/
*.egg-info/
__pycache__/
*.pyc

99
validator/README.md Normal file
View File

@@ -0,0 +1,99 @@
# Mnemosyne Validator
A bare [FastAgent](https://github.com/evalstate/fast-agent) + [Pallas](https://git.helu.ca/r/pallas) project whose only purpose is to exercise Mnemosyne's MCP server end-to-end. Use it to confirm the transport works, every MCP tool registers, args/responses round-trip, and the local LLM can actually drive the tools.
This is **not** a production agent. It does not represent the long-term Daedalus integration — when Daedalus ships, it will inject `workspace_id` server-side. The validator never sets `workspace_id`, meaning all calls run against the global scope (libraries with `workspace_id IS NULL`).
## Layout
```
validator/
├── pyproject.toml # pallas-mcp + fast-agent-mcp deps
├── agents.yaml # one-agent Pallas topology
├── fastagent.config.yaml # default_model + mnemosyne MCP server
├── fastagent.secrets.yaml.example # template for the bearer token
├── .env.example # OPENAI_BASE_URL etc.
└── agents/
└── mnemosyne_validator.py # the FastAgent definition
```
## Setup
```bash
cd validator/
uv venv .venv
source .venv/bin/activate
uv pip install -e .
```
Copy and fill the secrets/env templates:
```bash
cp fastagent.secrets.yaml.example fastagent.secrets.yaml
cp .env.example .env
```
## Provision an MCP bearer token
Mnemosyne requires a bearer token when `MCP_REQUIRE_AUTH=True` (the default). Generate one for your user:
```bash
cd ../mnemosyne
python manage.py create_mcp_token --user <username> --name validator
```
The command prints the token **once** — paste it into `validator/fastagent.secrets.yaml` under `mcp.servers.mnemosyne.headers.Authorization` (keep the `Bearer ` prefix).
## Start Mnemosyne's MCP server
The validator hits the Mnemosyne ASGI endpoint, so Mnemosyne's MCP server must be running. From the Mnemosyne project:
```bash
cd mnemosyne/
uvicorn mnemosyne.asgi:app --host 0.0.0.0 --port 22091 --workers 1
```
By default the validator points at `http://localhost:22091/mcp`. If your Mnemosyne is on another host, override `mcp.servers.mnemosyne.url` in `fastagent.secrets.yaml`.
## Run the validator
The `mnemosyne-validator` script is a thin alias for `pallas`:
```bash
# Start with the registry (Pallas mode):
mnemosyne-validator
# Or run the agent directly (no registry):
mnemosyne-validator --agent mnemosyne_validator
```
To chat with the agent directly without spinning up a Pallas registry, use the `fast-agent` CLI (provided by `fast-agent-mcp`):
```bash
fast-agent go --config-path fastagent.config.yaml --url http://localhost:24301/mcp mnemosyne_validator
```
## What to test
These prompts exercise every Mnemosyne MCP tool. After each, the agent should call the named tool and surface the result.
| Prompt | Tool | What to verify |
|--------|------|----------------|
| "Run a health check on Mnemosyne." | `get_health` | Returns `status: ok` if Neo4j + S3 + embedding model are all reachable. `degraded` if one is down. |
| "List all libraries." | `list_libraries` | Returns the libraries seeded by `load_library_types`, each with `library_type` set. |
| "List collections in library `<uid>`." | `list_collections` | Returns collections inside the named library. |
| "List items in collection `<uid>`." | `list_items` | Returns items with `chunk_count` and `embedding_status`. |
| "Search the technical libraries for `<query>`." | `search` | Returns ranked candidates with `chunk_uid`, `score`, `text_preview`, `library_type`. |
| "Fetch the full text of chunk `<chunk_uid>`." | `get_chunk` | Returns the full chunk text from S3. |
If a call errors, the agent surfaces it verbatim — that's the failure mode you want.
## Troubleshooting
**"Invalid MCP token"** — token wasn't provisioned, was provisioned for a different user, or got mangled when pasted. Re-run `create_mcp_token` and paste again. Tokens are SHA-256 hashed at rest and can't be retrieved later.
**"Couldn't connect to Mnemosyne"** — the ASGI server isn't running, or it's bound to a different host/port than `mcp.servers.mnemosyne.url` says. Check `curl http://localhost:22091/mcp/health` returns `{"status":"ok"}`.
**"No system embedding model configured" in `get_health`** — `LLMModel.get_system_embedding_model()` returns nothing. Configure the embedding model via the Mnemosyne admin or `manage.py` before searches will work.
**Search returns zero candidates with no error** — Mnemosyne is reachable but has no embedded content yet. Upload an item and run `embed_item`, or use the Daedalus ingest endpoint, before re-testing search.

17
validator/agents.yaml Normal file
View File

@@ -0,0 +1,17 @@
# Mnemosyne Validator — Pallas deployment topology
#
# A single-agent Pallas project whose only purpose is to validate the
# Mnemosyne MCP server end-to-end. Not a production deployment.
name: mnemosyne-validator
version: "0.1.0"
host: localhost
namespace: ca.helu.mnemosyne-validator
registry_port: 24300
agents:
mnemosyne_validator:
module: agents.mnemosyne_validator
port: 24301
title: Mnemosyne Validator
description: "Exercises Mnemosyne's MCP tools: search, get_chunk, list_*, get_health"

View File

View File

@@ -0,0 +1,56 @@
"""
Mnemosyne Validator Agent
A bare FastAgent that wraps the Mnemosyne MCP server. Exists solely to
exercise the Mnemosyne MCP transport, tool registration, and round-trip
serialization — no production role.
Drive it from the CLI to confirm:
- search works against the running Mnemosyne (vector + fulltext + graph)
- get_chunk fetches full chunk text from S3
- list_libraries / list_collections / list_items return the expected shape
- get_health returns ok/degraded with the right dependency breakdown
When the Daedalus integration ships, the workspace_id parameter will be
injected by Daedalus's chat path (force-overwritten before the call leaves
Daedalus). This validator never sets it — meaning all calls go to the
GLOBAL scope (libraries with workspace_id IS NULL).
"""
from fast_agent import FastAgent
fast = FastAgent("Mnemosyne Validator", parse_cli_args=False)
@fast.agent(
name="mnemosyne_validator",
instruction="""You are a validator for the Mnemosyne knowledge base. Your
job is to exercise its MCP tools when asked, report what you saw, and surface
errors clearly.
You have direct access to Mnemosyne via these tools:
- search(query, library_uid?, library_type?, collection_uid?, limit?, rerank?, include_images?, search_types?)
Hybrid retrieval. Returns ranked chunks with text_preview (~500 chars),
chunk_uid, item_uid, item_title, library_type, score, source.
- get_chunk(chunk_uid)
Fetch the full text of a chunk by uid (typically obtained from search).
- list_libraries(limit?, offset?)
List libraries (uid, name, library_type, description).
- list_collections(library_uid?, limit?, offset?)
List collections, optionally filtered by parent library.
- list_items(collection_uid?, library_uid?, limit?, offset?)
List items (documents) with chunk_count, embedding_status, etc.
- get_health()
Health check: {status: ok|degraded|error, checks: {neo4j, s3, embedding}}.
When the user asks "what libraries exist", call list_libraries and report.
When they ask a research question, call search and surface chunk_uid + score
+ item_title for each candidate. If they want full text, call get_chunk.
Show raw structured output, not flowery prose — this is a validation tool,
not a chat assistant.
If a tool errors, paste the error message verbatim.""",
servers=["mnemosyne"],
)
async def mnemosyne_validator():
pass

View File

@@ -0,0 +1,32 @@
# Mnemosyne Validator — FastAgent + MCP configuration
#
# Secrets (api_key, MCP bearer tokens) live in fastagent.secrets.yaml
# (gitignored) and merge with this file at runtime.
# Local llama.cpp on Nyx (OpenAI-compatible). Override via
# fastagent.secrets.yaml if you want to point at a different model server.
default_model: openai.Qwen3.5-35B-A3B-UD-Q4_K_XL.gguf
# Capabilities for the model — Pallas registers it with fast-agent's
# ModelDatabase using these values. vision: true so we can validate image
# round-trip later (search returns image candidates by default).
model_capabilities:
vision: true
context_window: 192000
max_output_tokens: 16384
# ── LLM Providers ───────────────────────────────────────────────────────────
openai:
base_url: "http://nyx.helu.ca:22079/v1"
# ── MCP Servers ─────────────────────────────────────────────────────────────
mcp:
servers:
# Mnemosyne MCP server — Streamable HTTP at /mcp.
# Default assumes the validator runs on the same host as Mnemosyne;
# override the URL in fastagent.secrets.yaml or via Ansible if remote.
mnemosyne:
transport: http
url: "http://localhost:22091/mcp"
# Bearer token in fastagent.secrets.yaml (provisioned via
# `python manage.py create_mcp_token <user>`).

View File

@@ -0,0 +1,22 @@
# Mnemosyne Validator — secrets template
#
# Copy to fastagent.secrets.yaml and fill in real values. The .yaml is
# gitignored; the .yaml.example is committed.
# ── LLM provider keys ───────────────────────────────────────────────────────
# Local llama.cpp doesn't authenticate, but fast-agent requires the key field
# to be present. "0000" or any non-empty string is fine.
openai:
api_key: "0000"
# ── MCP server bearer tokens ────────────────────────────────────────────────
mcp:
servers:
mnemosyne:
headers:
# Mnemosyne MCP server requires a bearer token when MCP_REQUIRE_AUTH=True.
# Provision one with:
# cd ../mnemosyne
# python manage.py create_mcp_token --user <username> --name validator
# then paste the printed token here (it is shown once and not retrievable).
Authorization: "Bearer paste-mcp-token-here"

23
validator/pyproject.toml Normal file
View File

@@ -0,0 +1,23 @@
[project]
name = "mnemosyne-validator"
version = "0.1.0"
description = "FastAgent + Pallas validator that talks to Mnemosyne's MCP server end-to-end"
requires-python = ">=3.13"
dependencies = [
"pallas-mcp @ git+ssh://git@git.helu.ca:22022/r/pallas.git",
"fast-agent-mcp>=0.6.10",
"pyyaml>=6.0",
]
[project.scripts]
mnemosyne-validator = "pallas.server:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = ["agents"]