lyra-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@lyra-mcp run the full pipeline for the query 'climate change impacts'"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
lyra-mcp
Typed pipeline-stage abstraction + MCP tool surface for RAG/agent pipelines — lets Claude read/run/modify individual pipeline stages (retrieve, rerank, generate, eval) as structured, typed calls instead of tracing and re-reading raw code each time.
Status: Phase 3 done, plus a wiqas.eval stage. The abstraction is validated against a second, differently-built project (Orion, a FastAPI service vs. WiQAS's script/CLI shape) with zero changes to any shared code — only new adapter files. A third stage type (wiqas.eval, wrapping RAGAS's answer_similarity metric) proves the same abstraction also extends to a stage with a real live-LLM dependency, not just local-inference stages like retrieval/reranking.
Running the MCP server
uv run lyra-mcpRegistered with this session's Claude Code project (.mcp.json, generated via claude mcp add):
claude mcp add lyra -s project -e WIQAS_REPO_ROOT="D:\GitHub\WiQAS" -e WIQAS_VENV_PYTHON="D:\GitHub\WiQAS\.venv\Scripts\python.exe" -- uv run --project D:\GitHub\lyra-mcp lyra-mcpNew MCP servers load at Claude Code session start — restart/reconnect the session for lyra's tools to appear.
Tools
list_stages()— every stage in the loaded graph (graphs/wiqas_query_pipeline.jsonby default, override withLYRA_GRAPH_PATH): id, type, current config, and full config/input/output JSON schema. Enough to call the other tools without opening any adapter source file.get_stage_config(stage_id)/set_stage_config(stage_id, config_overrides)— read/persistently mutate a stage's baseline config (validated before committing).run_stage(stage_id, input, config_override=None)— run one stage.config_overridemerges over the baseline for that call only, without persisting — the way to compare configs on the same input (call repeatedly with a different override each time, nothing to reset).run_pipeline(external_inputs)— run the whole loaded graph end to end viapipelines/executor.py's topological-order executor.
Real bugs found building this (Phase 2)
Two contract mismatches only surfaced through live protocol testing, not unit tests — both are why the project's test-first-workflow rule treats "touching a real external system" as its own verification step:
Client-visible errors were silently generic. The installed MCP SDK (
mcp==2.1.1) only forwards a raised exception's own message to the client when it's the SDK's ownToolError— anything else (including Lyra's ownLyraErrorhierarchy) becomes a bare"Error executing tool <name>", discarding every hand-written error message. Fixed with a_translate_lyra_errorsdecorator inserver.pythat re-raises anyLyraErroras aToolErrorwith the same text, at the MCP boundary only (core/errors.pystays independent of the SDK).Schema and actual data disagreed on field names.
model_json_schema()defaults toby_alias=True;model_dump()defaults toby_alias=False.list_stagesadvertisedSearchResultModel's aliased field as"id", butrun_stage/run_pipelinewere returning it as"document_id"— a real schema/data mismatch an agent trusting the advertised schema would hit immediately. Fixed by addingby_alias=Trueeverywhere aStageOutputgets dumped for an external caller (pipelines/executor.py,server.py).
Also confirmed empirically (not assumed): a bare dict return-type annotation does not produce structured content from this SDK version — only a parameterized dict[str, Any] (or a real pydantic model) does; sync def tool functions are automatically offloaded to a worker thread (anyio.to_thread.run_sync), so no manual async/await was needed anywhere in server.py.
Related MCP server: AgentOps EvalBench MCP
Architecture: the subprocess boundary
WiQAS's internal code (from src.retrieval.retriever import WiQASRetriever, etc.) only imports cleanly inside WiQAS's own venv, because of its heavy ML dependencies (torch, chromadb, sentence-transformers). Rather than running Lyra itself inside WiQAS's venv — which would break down the moment a second, differently-built project (Orion, Phase 3) needs wrapping too — each Lyra stage invokes the target project's own interpreter as a subprocess, running a small bundled shim script that talks JSON over stdin/stdout.
Claude ──calls──> Lyra typed Stage ──subprocess──> target project's own venv
(stable interface, runs a small runner shim script,
same every time) talks JSON over stdin/stdoutLyra itself stays a small, dependency-light project (pydantic only) — the framework (src/lyra/core/) has zero knowledge of WiQAS or Orion specifically, so the same run_subprocess_stage helper is what every adapter reuses unchanged.
A real gap this surfaced during implementation, worth knowing if you're wrapping a new project: don't trust a code investigation's reported function signatures for the exact wire shape — always confirm against the actual runtime output. Two mismatches turned up in WiQAS that weren't visible from reading signatures alone: SearchResult.to_dict() serializes its document_id attribute under the JSON key "id", and RerankerManager.rerank_search_results()'s output dicts omit search_type entirely. Both are handled in SearchResultModel (see its docstring in src/lyra/adapters/wiqas/common.py). Orion (Phase 3) turned out to have the identical mismatch, independently confirmed by reading its own source — not a coincidence worth assuming holds for a third project, but a real, recurring pattern worth checking for every time.
Also found live: WiQAS's own logging (a module-level rich.Console()) prints straight to stdout, which would have polluted the runner scripts' JSON-only stdout contract. Fixed on the Lyra side only (no WiQAS changes) with an OS-file-descriptor-level redirect — see src/lyra/adapters/_shared/shim_io.py. That helper was deliberately kept project-agnostic and outside any single adapter's folder specifically because this pattern was expected to recur — and it did: Orion has the exact same rich.Console()-to-stdout issue, fixed by reusing shim_io.py completely unchanged, zero new code needed.
Phase 3: validated against Orion, zero shared-code changes
Orion (D:\GitHub\Orion) is a FastAPI service with a Tauri desktop shell — a genuinely different shape from WiQAS's script/CLI style — wrapped the same way: adapters/orion/ mirrors adapters/wiqas/ file-for-file (common.py, retrieval.py, reranking.py, runners/), registered as two more STAGE_REGISTRY entries. No changes were needed to core/, adapters/_shared/shim_io.py, registry.py's shape, pipelines/executor.py, or pipelines/state.py — the entire cost of wrapping a second project was the new adapter files, exactly as the architecture was designed to allow. Live integration test (tests/integration/test_orion_live_pipeline.py) passed in ~21s against Orion's real ~232-chunk collection — dramatically faster than WiQAS's several minutes, since Orion's corpus is smaller and it does no query-decomposition/cross-lingual translation.
Eval stage: a stage type with a real live-LLM dependency
adapters/wiqas/eval.py wraps WiQAS's RAGAS answer_similarity metric (src/evaluation/[3] ragas/ragas_fixed.py) as wiqas.eval. Two things found by reading that code directly, not guessed:
The file WiQAS's own docs point to as the RAGAS entrypoint (
ragas_eval.py) is broken — it referencesjson,argparse,evaluate, and theragasmetric objects without ever importing them, so it imports cleanly but raisesNameErrorat call time.ragas_fixed.pyis the working one.A real, hard live dependency, unlike retrieval/reranking. WiQAS's own
setup_ollama_for_ragas()makes an unconditional chat-completion call to Ollama to test connectivity — even inanswer_similarity-only mode, where that chat model is never actually used for scoring (answer_similarityis embeddings-only). Can't be skipped without modifying WiQAS's own code, which this adapter never does. A running Ollama server with the configured chat model (defaultmistral:latest) andnomic-embed-textboth pulled is a hard precondition.
Scoped narrowly and deliberately: answer_similarity only (use_all_metrics=False, WiQAS's own default) — its own code comment calls this "the only one that works reliably," and warns the full 6-metric mode "may timeout." use_all_metrics=True is exposed as a config option but untested here. Live integration test (tests/integration/test_wiqas_eval_live.py) confirmed a real, discriminating score (0.697 similarity between a Filipino ground-truth answer and a differently-phrased English paraphrase) in ~12s wall time — far faster than WiQAS's own "this may take several minutes" comment, which describes the full-metric path this stage doesn't use. Not wired into wiqas_query_pipeline.json: eval needs a generated answer as input, which no existing stage produces (no generate stage exists yet) — registered in STAGE_REGISTRY alone, same precedent as retrieval/reranking being proven directly before Phase 2's MCP layer existed.
Setup
Requires uv and Python 3.12.
uv venv --python 3.12
uv sync.env.example documents the environment variables Lyra reads if your WiQAS/Orion checkouts live somewhere other than their defaults. It's a template, not auto-loaded — nothing in this codebase reads .env — so set these as real environment variables in your shell/session:
WIQAS_REPO_ROOT=D:\GitHub\WiQAS
WIQAS_VENV_PYTHON=D:\GitHub\WiQAS\.venv\Scripts\python.exe
ORION_REPO_ROOT=D:\GitHub\Orion
ORION_VENV_PYTHON=D:\GitHub\Orion\.venv\Scripts\python.exeRunning on a different machine
Everything machine-specific is env vars plus one generated config file — nothing is hand-edited:
uv venv --python 3.12 && uv sync(see Setup above).Set
WIQAS_REPO_ROOT/WIQAS_VENV_PYTHONand/orORION_REPO_ROOT/ORION_VENV_PYTHONas real environment variables for whichever projects you're wrapping on this machine (see.env.example).Regenerate
.mcp.json— it's gitignored on purpose (bakes in this machine's absoluteuv.exepath and repo checkout paths, not portable as-is; see.mcp.json.examplefor the shape). Don't hand-copy the example — run:claude mcp add lyra -s project -e WIQAS_REPO_ROOT="<path>" -e WIQAS_VENV_PYTHON="<path>" -e ORION_REPO_ROOT="<path>" -e ORION_VENV_PYTHON="<path>" -- uv run --project <path to this checkout> lyra-mcpRestart/reconnect the Claude Code session afterward — new MCP servers load at session start.
If a path is wrong, the failure is a clear, actionable SubprocessLaunchError: Interpreter not found: <path> (see core/subprocess_adapter.py) — not a silent hang or a cryptic import error.
Running tests
# Unit tests (default) — fast, subprocess boundary mocked, no WiQAS/Orion needed
uv run pytest
# Integration tests (opt-in) — need a real WiQAS/Orion venv + an already-ingested
# Chroma collection at WIQAS_REPO_ROOT / ORION_REPO_ROOT respectively
uv run pytest -m integration -sLayout
src/lyra/
core/ # StageConfig/StageInput/StageOutput, Stage ABC, the shared subprocess-boundary helper
registry.py # stage_type -> concrete classes lookup, all wrapped projects register here
server.py # the MCP server (list_stages/get_stage_config/set_stage_config/run_stage/run_pipeline)
mcp_models.py # tool-output-only models (StageInfo, RunPipelineResult)
adapters/
_shared/ # project-agnostic runner-script helpers (e.g. the stdout-redirect fix), reused by every adapter
wiqas/ # WiQAS-specific typed stages (retrieval, reranking, eval) + their runner shim scripts
orion/ # Orion-specific typed stages, same shape as wiqas/ — proves the pattern generalizes
pipelines/ # PipelineGraph (load/validate), executor.py (topological-order execution), state.py (mutable loaded-graph wrapper)
graphs/ # Example pipeline-as-data graphs, one per wrapped project
tests/
unit/ # subprocess boundary mocked, run by default
integration/ # opt-in (`pytest -m integration`), needs real WiQAS/OrionAvailable Tools
5 toolsget_stage_configARead-onlyIdempotent
Get a stage's current persistent baseline config.
Inputs: stage_id (required). Returns: object
list_stagesARead-onlyIdempotent
List every stage in the currently loaded pipeline graph: its id, stage_type, current config, and full config/input/output JSON schema. Enough to call run_stage or set_stage_config without opening any adapter source file.
Inputs: no named parameters. Returns: result
run_pipelineA
Run the entire loaded pipeline graph end to end (e.g. retrieve then rerank), using every stage's current persistent baseline config.
Inputs: external_inputs (required). Returns: object
run_stageA
Run one stage from the loaded graph.
If config_override is given, it's merged over the stage's persistent baseline config for this call only (not saved) — this is how to compare 2-3 configs on the same input without mutating shared state: call run_stage repeatedly with a different config_override each time, nothing to reset afterward. Omit config_override to use the current baseline unchanged.
Inputs: input (required), stage_id (required), config_override. Returns: object
set_stage_configAIdempotent
Persistently merge config_overrides into stage_id's baseline config (validated before committing — an invalid override is rejected, not partially applied). Affects every subsequent run_stage call that omits config_override, and every run_pipeline call. For a one-off comparison that shouldn't persist, pass config_override to run_stage instead.
Inputs: stage_id (required), config_overrides (required). Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 5 tools
The tools are mostly distinct: list_stages is for discovery, get_stage_config for targeted reads, set_stage_config for persistent writes, and the two run tools differ by scope. Minor overlap exists because list_stages already includes current config, making get_stage_config somewhat redundant.
All tools follow a clean verb_noun snake_case pattern: list/get/set/run + object. The naming style is perfectly consistent across the set.
Five tools is well-scoped for a pipeline stage configuration and execution server. Each tool covers a distinct operation without bloat or obvious missing essentials.
The surface covers stage discovery, config read/write, single-stage execution, and full-pipeline execution. A minor gap is that run_pipeline does not accept config_overrides, so one-off full-pipeline comparisons require temporarily mutating persistent config.
Maintenance
Related MCP Connectors
Discover, compare, route, and execute machine-accessible capabilities for AI agents.
AI routing, memory, guardrails, and governance. Routes across Claude, GPT, Gemini.
Cloud or self-hosted knowledge for AI agents: hybrid search, reranking, GraphRAG, scoped MCP tools.
Build, test and publish agentic workflows from your AI client: approve steps, read tables, see runs.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables building production-grade RAG systems with agentic reasoning, hybrid retrieval, and MCP protocol integration for use with Claude Desktop.1MIT
- AlicenseNot gradedqualityCmaintenanceEnables LLM evaluation and observability by uploading documents, building test sets, running RAG pipelines, and automatically scoring answers for groundedness, hallucination risk, retrieval quality, latency, and cost, with tools exposed to MCP-compatible clients.1MIT
- AlicenseNot gradedqualityCmaintenanceEnables Claude and other MCP clients to manage Pankow organizations, projects, test cases, and CI/runs, configure AI targets, and run LLM evaluations directly from an agent.MIT
- AlicenseAqualityBmaintenanceEnables users to build and manage a complete retrieval-augmented generation pipeline through conversation, including file ingestion, collection management, hybrid search, reranking, citations, and a guided setup wizard.33Apache 2.0