Perseus-mcp
About
MCP server for the Perseus Digital Library — giving AI models direct access to ancient Greek texts, CTS passages, and Scaife search.
Details
- Author
- tonyjurg
- Categories
- Other, Search, Knowledge Base
Jump to
Setup
Install Perseus-mcp in your MCP client (Claude Desktop, Cursor, Windsurf, and others).
Repository: https://github.com/tonyjurg/Perseus-mcp
Follow the installation instructions in the repository README, then restart your MCP client.
Give Claude / Cursor / Windsurf direct access to the Perseus Digital Library— ancient Greek and Latin texts, precise CTS navigation, plaintext, search, and more.
A high-quality MCP server for Classical Greek and Latin literature. It runs as a local FastMCP server so MCP-capable applications can attach these Perseus tools to the LLM/model provider of your choice.
This server exposes twenty-three MCP tools. Every tool returns a text payload: some are raw Perseus CTS XML or Scaife JSON, while the discovery and plaintext helpers return locally shaped JSON or readable text.
- get_passage(urn)— fetch a CTS passage by URN.
- get_passage_plus(urn)— fetch passage text plus contextual metadata.
- get_passage_plaintext(urn)— fetch a CTS passage as plain readable text.
- get_valid_references(urn, level=None)— retrieve navigable citation references for a work or edition.
- get_valid_references_json(urn, level=None, limit=100, offset=0)— retrieve paged citation references as JSON (limit: 1–500).
- count_valid_references(urn, level=None)— count valid references without returning the full list.
- get_capabilities()— list available texts/editions from Perseus CTS.
- get_cache_status()— inspect local metadata cache state.
- refresh_metadata_cache()— refresh cached CTS and Scaife library metadata.
- clear_metadata_cache()— clear in-memory and disk metadata cache entries.
- list_text_groups(language=None, query=None, limit=100, offset=0)— list matching authors/textgroups and works with pagination metadata (limit: 1–500).
- get_author_resources(author, language=None)— list works, editions, and translations for a matching author name or CTS textgroup URN.
- find_author_names(query, language=None, limit=100, offset=0)— find author/textgroup names by partial name match with pagination metadata (limit: 1–500).
- get_work_resources(urn_or_title, language=None)— list editions, translations, and resources for a work, optionally filtered by original language.
- get_label(urn)— fetch human-readable metadata labels for a URN.
- get_first_urn(urn)— get the first navigable URN under a work/edition.
- get_prev_next_urn(urn)— get neighboring passage URNs for navigation.
- search_perseus(query, language="greek", query_format="auto", author=None, search_kind="form", preserve_operators=False, page_num=1, text_group=None, work=None, result_format="instances")— search texts via Scaife search API. Greek queries may be entered as Unicode Greek (for exampleμῆνιν) or Beta Code (for examplemh=nin).
- search_within_text(query, text_urn, ..., size=10, offset=0)— search within one Scaife text/edition URN (size: 1–500).
- get_passage_highlights(query, passage_urn, ...)— get Scaife token highlight positions for one passage.
- get_scaife_library_metadata(urn)— get Scaife JSON metadata for a library URN.
- get_scaife_passage_json(urn)— get Scaife JSON for a passage URN.
- get_scaife_passage_text(urn)— get Scaife plaintext for a passage URN.
search_perseusnormalizes search terms before sending them to Scaife's JSON search route. Queries must contain at least one non-whitespace character. The tool returns Scaife's JSON response as text.
search_perseus( '"μῆνιν ἄειδε"', query_format="unicode", preserve_operators=True, ) search_perseus( "μῆνιν -ἄειδε", query_format="unicode", preserve_operators=True, ) search_perseus( "λόγος | ἀνήρ", search_kind="lemma", query_format="unicode", preserve_operators=True, )
For inventory discovery,list_text_groups,find_author_names,get_author_resources, andget_work_resourcestreatlanguageas an actual work-language filter.find_author_namesmerges the legacy CTS inventory with the Scaife library catalog, so Scaife-only authors such as Philo Judaeus remain discoverable. Passage and navigation tools use CTS URNs, whosegreekLit/latinLitnamespace and edition identifier already select the text.
Discovery and navigation tools cache stable CTS metadata locally to avoid repeated multi-megabyteGetCapabilitiesandGetValidReffrequests. The default disk cache lives in.cache/perseus-mcpunder the current working directory and also uses an in-memory cache for the running server process. Configure it with:
- PERSEUS_MCP_CACHE_DIR— override the disk cache directory.
- PERSEUS_MCP_CACHE_TTL_SECONDS— set cache TTL; default is 86400 seconds.
- PERSEUS_MCP_DISABLE_CACHE=1— disable both memory and disk cache reads/writes.
The current working directory is the directory from which the Python process is started. Running the MCP server from the repository root uses.cache/perseus-mcp; running a notebook fromexamples/would otherwise useexamples/.cache/perseus-mcp. That is not a second server instance, only a second cache location for a separate Python process. To keep one cache location across notebooks and MCP clients, setPERSEUS_MCP_CACHE_DIRto an absolute path such as/path/to/Perseus-mcp/.cache/perseus-mcp. Disk entries are written to unique sibling temporary files and atomically replaced, so multiple local processes can safely share that directory without exposing partially written cache files. Disk-cache write failures emit aMetadataCacheWarningbut do not discard a successfully fetched upstream response.
Available edition URNs can differ between Perseus CTS and Scaife search results, and the live inventory can change. Useget_author_resources,get_work_resources, orlist_text_groupsbefore constructing edition-specific CTS passage URNs. The notebooks select advertised CTS editions from discovery results instead of assuming that a Scaife edition URN is valid for Perseus CTS.
The live Perseus CTS implementation may return malformed HTML forGetFirstUrnandGetPrevNextUrn. The MCP tools detect that response and derive valid XML results fromGetValidReff.
Perseus may also return429 Too Many Requestswhen a workflow sends many CTS requests in a short period. Pause before retrying, reduce concurrency, and add delays to passage-processing loops. The server currently exposes the upstream HTTP error instead of retrying automatically.
Once a release is published to PyPI, users can install it without cloning the repository:
The installed console command and module entry point are equivalent:
npx @modelcontextprotocol/inspector uv run perseus-mcp
Perseus MCP uses layered checks rather than relying on one end-to-end test. Most behavior is covered by deterministic pytest tests with local XML/JSON fixtures and mocked asynchronous HTTP calls. GitHub Actions separately verifies the supported Python and operating-system matrix, package artifacts, secrets, release tags, and publication.
The workflow files under.github/workflows/are the executable source of truth.
Pytest is configured inpyproject.tomlto import fromsrc/and discover tests undertests/.
A regression fix should include a focused test that fails for the original problem. Tests should assert observable behavior and cover failure paths and boundary values as well as successful calls.
Routine tests do not depend on live upstream services. HTTP helpers are monkeypatched with asynchronous test doubles, while representative CTS XML and Scaife JSON are stored in test fixtures. This keeps CI deterministic when catalogs change or an upstream service is unavailable, avoids unnecessary traffic to public scholarly infrastructure, and makes malformed-response tests safe.
Live read-only probes may be used during manual review for endpoint compatibility or connection-lifecycle changes, but they supplement rather than replace the automated suite.
Several tests invoke tools withasyncio.run(), which creates a new event loop for each call. The server uses a process-wide sharedhttpx.AsyncClient, so the autouse fixture intests/conftest.pycloses and resets that client after every test. Tests that manipulate shared client state must also leave it reset.
python -m pytest tests/test_disk_cache.py python -m pytest tests/test_disk_cache.py::test_disk_cache_set_writes_readable_content
Show skipped tests, the slowest tests, and local variables on failure:
Disable metadata-cache reads and writes during a test run:
PERSEUS_MCP_DISABLE_CACHE=1 python -m pytest
$env:PERSEUS_MCP_DISABLE_CACHE = "1" python -m pytest
.github/workflows/tests.ymlinstalls the editable project with development dependencies and runspython -m pyteston:
- Ubuntu and Windows;
- Python 3.11, 3.12, and 3.13.
The matrix usesfail-fast: false, so every platform/version job finishes even when one fails. This makes version-specific and Windows-specific regressions visible in one run. Documentation-only changes underdocs/are excluded from the Python test workflow; exact event and branch filters remain defined in the workflow file.
.github/workflows/package.ymlchecks that the repository produces a valid source distribution and universal wheel. It installs Python 3.12, runs:
python -m build python -m twine check dist/
and uploadsdist/as thepython-packageworkflow artifact. The workflow is path-filtered to package-relevant files and supports manual dispatch.
tests/test_packaging.pycomplements this build by checking repository-level expectations such as metadata, dependencies, documentation files, notebook JSON, and workflow configuration. Both layers matter: metadata tests can pass while an isolated build fails, and a package can build while required repository assets are missing.
.github/workflows/secret-scan.ymlrejects tracked OpenRouter keys matching:
The workflow reports affected files without printing the matching secret. A detected key must be removed and rotated; the check should never be bypassed. This focused scan does not replace normal credential hygiene: do not commit.envfiles, tokens, private MCP configuration, or notebook outputs containing credentials.
.github/workflows/codeql.ymlanalyzes both the Python source and GitHub Actions workflows on pushes tomainanddevelopment, on pull requests, and weekly. Python analysis uses CodeQL's security-and-quality query suite, while workflow analysis uses the security-extended suite. The workflow also supports manual dispatch for configuration checks.
.github/workflows/dependency-review.ymlruns on every pull request and fails when a changed dependency introduces a known vulnerability of moderate or higher severity. Dependabot checks both Python and GitHub Actions dependencies weekly. Its default labels are intentionally used so update pull requests do not depend on repository-specific labels being pre-created.
.github/workflows/release.ymlruns forvtags or manual dispatch. For tag runs it verifies that the tag equalsv<project.version>, builds the wheel and source archive, validates both with Twine, attaches them to a generated GitHub release, and dispatches the PyPI workflow using the same tag.
.github/workflows/publish.ymlrequires a tag reference, repeats the tag/version check, rebuilds and revalidates the artifacts, and publishes through PyPI trusted publishing. The protectedpypiGitHub environment uses OIDC (id-token: write), so no long-lived PyPI API token is stored.
Rebuilding during publication avoids trusting an unrelated workflow artifact, while the repeated tag check prevents publishing from a branch or mismatched release tag.
.github/workflows/pages.ymlbuildsdocs/with Jekyll and deploys the generated artifact to GitHub Pages after documentation changes reachmainormaster. This Pages site is intended primarily for end users; development and test-strategy documentation lives in this repository README.
- Failures on every matrix job usually indicate a general regression.
- A single Python-version failure suggests version-specific syntax, dependencies, or standard-library behavior.
- Windows-only failures commonly involve paths, permissions, read-only attributes, or event-loop lifecycle.
- A package failure with green pytest jobs usually concerns metadata, manifests, README rendering, or build isolation.
- A secret-scan failure requires credential removal and rotation.
- A release failure before publication commonly means the tag andproject.versiondo not match.
Understand the failure before rerunning a job, and preserve useful workflow logs or tracebacks in the pull request when the cause is not obvious.
Maintainer-level conventions for extending the suite are also kept beside the tests intests/testing.md.
Theexamples/directory includes Jupyter notebooks that demonstrate both direct endpoint calls and MCP client usage with real Greek and Latin data:
- examples/00_install_and_run_perseus_mcp.ipynb— installation and launch guide covering PyPI, pip, uv, local repository development, MCP client configuration, verification, upgrades, and troubleshooting.
- examples/01_basic_cts_workflow.ipynb— minimal direct CTS requests.
- examples/02_search_and_navigation.ipynb— direct Scaife JSON search and CTS navigation from valid references.
- examples/03_mcp_connection_homer_iliad.ipynb— FastMCP client connection, Homer resource discovery, andIliadGreek passage analysis.
- examples/04_mcp_greek_search_and_navigation.ipynb— MCP Greek search with Unicode/Beta Code, valid references, and passage navigation.
- examples/05_mcp_all_tools.ipynb— complete MCP tool catalog with descriptions and input schemas.
- examples/06_openrouter_llm_mcp_interaction.ipynb— optional OpenRouter LLM tool-calling loop over the local MCP tools, using OpenRouter's Free Models Router by default.
- examples/07_mcp_advanced_search_options.ipynb— MCP form/lemma search, Scaife operator queries, and author-scoped search examples.
- examples/08_mcp_cache_and_search_tools.ipynb— advanced demonstration of cache tools, paged references, scoped search, reader search, highlights, and Scaife metadata/text retrieval.
- examples/09_openrouter_philo_politeia_analysis.ipynb— OpenRouter-assisted, evidence-first analysis ofπολιτείαin Philo of Alexandria using scoped MCP search results and cited passages.
- examples/10_mcp_latin_augustine_workflow.ipynb— Latin-language discovery, CTS navigation, passage retrieval, and a small text analysis using Augustine'sEpistulaeselections.
Run them after installing the project dependencies. The MCP notebooks use FastMCP's in-process client transport and call the same tools exposed to external MCP clients. The optional OpenRouter notebook also requires an OpenRouter API key; the MCP server itself does not. Notebook setup cells install notebook-only helpers such aspython-dotenvdirectly. Those helpers are not core runtime dependencies ofperseus-mcp.
Forexamples/06_openrouter_llm_mcp_interaction.ipynbandexamples/09_openrouter_philo_politeia_analysis.ipynb, copy.env.exampleto.envin the project root and replace the placeholder:
Get your API key atopenrouter.ai. SeeOpenRouter's API key documentationfor authentication details. The.envfile is ignored by Git. You can also setOPENROUTER_API_KEYin your environment or enter it securely when the notebook prompts.
Both OpenRouter notebooks default toopenrouter/free. This router selects among free models currently available on OpenRouter and filters for capabilities required by the request, such as tool calling or structured output. It avoids binding the examples to one free model that may later be removed or temporarily unavailable. The tradeoff is reduced reproducibility: separate runs may use different concrete models, so the notebooks record the resolved model returned by OpenRouter. SetOPENROUTER_MODELto a fixed model slug when exact model selection matters.
Notebook06_can be saved and committed with its LLM and tool-call outputs so they render on GitHub. Python variables and kernel memory are not stored in an.ipynbfile, and the notebook does not print the API key. Before committing a credentialed run, review the visible outputs and scan for a full OpenRouter key:
rg "sk-or-v1-[A-Za-z0-9_-]{20,}" examples/06_openrouter_llm_mcp_interaction.ipynb
The command should produce no output. It does not match the documentedsk-or-v1-...placeholder.
This project does not require a specific LLM. Configure your client to launch the local MCP server with:
uv --directory /full/path/to/Perseus-mcp run perseus-mcp
Most MCP clients need the same pieces: server nameperseus, commanduv, args--directory /full/path/to/Perseus-mcp run perseus-mcp, and an empty environment unless you have local customizations. Seedocs/enduser.mdfor generic client guidance anddocs/architecture.mdfor the architecture choices, including why FastMCP is used.
The server runs with Claude over stdio, with no OpenRouter or API key required (OpenRouter is only needed for the optional demo client).
Claude Desktop*— add toclaude_desktop_config.json:
{ "mcpServers": { "perseus": { "command": "uv", "args": ["--directory", "/full/path/to/Perseus-mcp", "run", "perseus-mcp"] } } }
Restart Claude Desktop; the Perseus tools appear in the tools list.
claude mcp add perseus -- uv --directory /full/path/to/Perseus-mcp run perseus-mcp
Verified against a stdio MCP handshake: all 23 tools register and live calls return (tested withsearch_perseusandlist_text_groups).
Install the development dependencies, then build and validate both distribution formats:
python -m pip install -e ".[dev]" python -m build python -m twine check dist/
The build creates a wheel and source archive underdist/. Test the wheel in a clean virtual environment before publishing. Upload to TestPyPI first:
python -m twine upload --repository testpypi dist/*
After verifying installation from TestPyPI, upload the same artifacts to PyPI:
PyPI does not allow replacing an existing release. Updateproject.versioninpyproject.toml, rebuild from a cleandist/directory, and publish each version only once. The package build workflow also builds and checks artifacts in CI without publishing them.
Automated GitHub release and PyPI publishing
The release automation follows the same trusted-publishing pattern as MorphKit:
- Set the release version inpyproject.toml, for example1.2.3.
- Merge the version change to the commit that should be released.
- Create and push the matching tag, for examplev1.2.3.
- TheBuild release artifactsworkflow verifies the tag/version match, builds and validates both distributions, and attaches them to a generated GitHub release.
- That workflow dispatchesPublish to PyPI, which rebuilds and validates the package before publishing through PyPI trusted publishing.
Configure the repository once before the first automated upload:
- Create a GitHub Actions environment namedpypi.
- In the existing PyPI project settings, or as a pending publisher before the first upload, add a trusted publisher for ownertonyjurg, repositoryPerseus-mcp, workflowpublish.yml, and environmentpypi.
- Do not add a PyPI API token; the workflow uses GitHub OIDC withid-token: write.
The workflows reject a tag such asv1.2.4whenproject.versionis still1.2.3. PyPI versions are immutable, so increment the version before retrying a release that was already uploaded.
Bug reports, documentation fixes, focused feature requests, and pull requests are welcome. Please report problems through the GitHub issue tracker and include the command, Python version, MCP client, tool arguments, traceback, and any relevant CTS URN or Greek search query when possible.
Seedocs/contributing.mdfor contribution guidance.
This project was created with assistance from OpenAI Codex. The human maintainer remains responsible for reviewing, testing, and accepting all code and documentation changes.
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.




