From 97a14fb03abef5d8e474ab0e510a3c04be1844b1 Mon Sep 17 00:00:00 2001 From: Robert Helewka Date: Wed, 29 Apr 2026 06:53:48 -0400 Subject: [PATCH] feat(validator): add bare FastAgent + Pallas validator for Mnemosyne MCP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --name validator`. Co-Authored-By: Claude Opus 4.7 --- validator/.env.example | 9 +++ validator/.gitignore | 6 ++ validator/README.md | 99 ++++++++++++++++++++++++ validator/agents.yaml | 17 ++++ validator/agents/__init__.py | 0 validator/agents/mnemosyne_validator.py | 56 ++++++++++++++ validator/fastagent.config.yaml | 32 ++++++++ validator/fastagent.secrets.yaml.example | 22 ++++++ validator/pyproject.toml | 23 ++++++ 9 files changed, 264 insertions(+) create mode 100644 validator/.env.example create mode 100644 validator/.gitignore create mode 100644 validator/README.md create mode 100644 validator/agents.yaml create mode 100644 validator/agents/__init__.py create mode 100644 validator/agents/mnemosyne_validator.py create mode 100644 validator/fastagent.config.yaml create mode 100644 validator/fastagent.secrets.yaml.example create mode 100644 validator/pyproject.toml diff --git a/validator/.env.example b/validator/.env.example new file mode 100644 index 0000000..ece266c --- /dev/null +++ b/validator/.env.example @@ -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 diff --git a/validator/.gitignore b/validator/.gitignore new file mode 100644 index 0000000..c9e42f2 --- /dev/null +++ b/validator/.gitignore @@ -0,0 +1,6 @@ +fastagent.secrets.yaml +.env +.venv/ +*.egg-info/ +__pycache__/ +*.pyc diff --git a/validator/README.md b/validator/README.md new file mode 100644 index 0000000..4a116a8 --- /dev/null +++ b/validator/README.md @@ -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 --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 ``." | `list_collections` | Returns collections inside the named library. | +| "List items in collection ``." | `list_items` | Returns items with `chunk_count` and `embedding_status`. | +| "Search the technical libraries for ``." | `search` | Returns ranked candidates with `chunk_uid`, `score`, `text_preview`, `library_type`. | +| "Fetch the full text of chunk ``." | `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. diff --git a/validator/agents.yaml b/validator/agents.yaml new file mode 100644 index 0000000..a129199 --- /dev/null +++ b/validator/agents.yaml @@ -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" diff --git a/validator/agents/__init__.py b/validator/agents/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/validator/agents/mnemosyne_validator.py b/validator/agents/mnemosyne_validator.py new file mode 100644 index 0000000..d87c78f --- /dev/null +++ b/validator/agents/mnemosyne_validator.py @@ -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 diff --git a/validator/fastagent.config.yaml b/validator/fastagent.config.yaml new file mode 100644 index 0000000..bd6a6e7 --- /dev/null +++ b/validator/fastagent.config.yaml @@ -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 `). diff --git a/validator/fastagent.secrets.yaml.example b/validator/fastagent.secrets.yaml.example new file mode 100644 index 0000000..8f68481 --- /dev/null +++ b/validator/fastagent.secrets.yaml.example @@ -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 --name validator + # then paste the printed token here (it is shown once and not retrievable). + Authorization: "Bearer paste-mcp-token-here" diff --git a/validator/pyproject.toml b/validator/pyproject.toml new file mode 100644 index 0000000..22c5e13 --- /dev/null +++ b/validator/pyproject.toml @@ -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"]