glossary-mcp
A small, general-purpose MCP server that lets an AI read, search, and safely edit a glossary file in the conlangkit glossary format — for any glossary, not tied to any one project.
The glossary is a Markdown pipe-table with four columns:
lemma | tags | definition | notes
----- | ---- | ---------- | -----
core | tier | the constitutive tier-1 material of a party | never shared
Writes go through conlangkit, which owns the file: it keeps entries sorted (by byte
order) and re-emits the glossary on save. On an already-sorted file that yields a
minimal diff — only the touched row moves. The file is re-read from disk before
each write (so a long-lived server picks up external hand-edits) and the write is
atomic (temp file + os.replace).
Prerequisites
- Python 3.11+
- uv
- A glossary directory or file, pointed to by
GLOSSARY_PATH
conlangkit (the glossary format library)
is a normal PyPI dependency, pulled in by uv sync.
MCP SDK compatibility
| glossary-mcp | mcp SDK |
|---|---|
0.2.0+ |
2.x |
0.1.1 |
1.x (pinned <2) |
0.1.0 |
broken — do not use |
0.1.0 declared only mcp>=1.0, so once mcp 2.0 was released a fresh resolve
pulled it in — and 2.0 removed mcp.server.fastmcp. The server then died at
import with ModuleNotFoundError, which a client sees only as a server offering
no tools at all (a crashed stdio server cannot report why). If you are pinned
to 0.1.0, upgrade — restarting will not help. 0.1.1 pinned mcp<2 as a
stopgap; 0.2.0 ports to the 2.x API proper.
Installation
uv sync # creates .venv, installs glossary-mcp (editable) + conlangkit + dev deps
Running tests
uv run pytest # runs the suite with branch coverage (gate: 100%)
The smoke tests run without GLOSSARY_PATH. Integration tests run against a
fixture glossary.
Running the server
export GLOSSARY_PATH=/path/to/glossary # a directory containing glossary.md …
export GLOSSARY_PATH=/path/to/glossary.md # … or a direct path to the file
uv run glossary-mcp
GLOSSARY_PATH may be a directory (the server looks for glossary.md inside
it) or a file. The server uses stdio transport. For a local checkout, set
command to a resolvable path, e.g. the project's
/path/to/glossary-mcp/.venv/bin/glossary-mcp:
{
"mcpServers": {
"glossary": {
"command": "glossary-mcp",
"env": {
"GLOSSARY_PATH": "/path/to/glossary"
}
}
}
}
For consuming the published package from another repo without a local
checkout or absolute paths, use uvx — see the next section.
Consuming from another repo (portable .mcp.json)
glossary-mcp is published to PyPI,
so any other repo can wire it into its MCP configuration with no local checkout,
no venv, and no absolute paths. uvx fetches and caches the package (and its
conlangkit / mcp dependencies) on demand:
{
"mcpServers": {
"glossary": {
"type": "stdio",
"command": "uvx",
"args": ["glossary-mcp==0.3.0"],
"env": {
"GLOSSARY_PATH": "${CLAUDE_PROJECT_DIR:-.}/../glossary"
}
}
}
}
command: "uvx",args: ["glossary-mcp"]— the console-script name equals the package name, so no--fromflag is needed.uvxresolves the package from PyPI into an ephemeral, cached environment on first launch.GLOSSARY_PATHis the only required environment variable. It may point at a directory (the server looks forglossary.mdinside) or directly at a file. The${CLAUDE_PROJECT_DIR:-.}/../glossaryvalue assumes the standard sibling layout where the consuming repo and itsglossaryrepo are checked out side by side.${CLAUDE_PROJECT_DIR}resolves the project root and requires Claude Code ≥ v2.1.195; the:-.fallback keeps it working (relative to the current directory) on older versions and other MCP clients.- Pin the version while pre-1.0. Because the tool surface may still change
before 1.0, pin so a new release can't reach every consuming repo at once and
break them in lockstep:
"args": ["glossary-mcp==0.3.0"]. Bump deliberately (repo by repo, or via your propagation tooling). Float the version (bare"glossary-mcp", always latest) only once the surface stabilizes at ≥ 1.0.
Upgrading 0.2.0 → 0.3.0 is a breaking change.
search_by_definitionandsearch_notesare gone, replaced by a singlesearch(see Searching).search("verdict")covers whatsearch_by_definition("verdict")did, andsearch("n:verdict")coverssearch_notes. Before bumping the pin, grep your prompts, skills, and agent docs for the old tool names — a doc that tells a model to callsearch_by_definitionwill send it after a tool that no longer exists.
Tools
| Tool | Description |
|---|---|
lookup_term |
Exact lemma lookup → tags, definition, notes |
search |
Search lemmas, tags, definitions, and notes (see below) |
glossary_stats |
Entry count, tag histogram, unique-tag count |
list_terms |
All lemmas, optionally filtered by tag |
add_term |
Append a new entry (refuses a duplicate lemma) |
update_term |
Edit an existing entry's tags/definition/notes by lemma |
delete_term |
Remove an entry by lemma; returns the deleted entry so it can be restored |
deprecate_term |
Retire a term in place, keeping the entry so older prose stays readable |
Searching
search is a thin pass-through to conlangkit's Glossary.find(), which owns the
query language and the relevance model. A bare query searches lemmas and
definitions together; a field prefix narrows it:
| Query | Finds |
|---|---|
verdict |
any term whose lemma or definition mentions a verdict |
l:core |
lemma only |
t:artifact |
tags only (a tag matches whole, not as a substring) |
d:verdict |
definition only |
n:seed |
notes only |
Wildcards work inside any of these: * any run of characters, ? a single
character, ! a word boundary — so l:wor* finds worker.
Results are ordered by relevance: conlangkit runs a non-fuzzy pass first and
appends fuzzy matches after it, so closer matches lead. Deprecated entries are
included and carry deprecated: true rather than being hidden — a retired term
should stay findable, but a caller should see that it is retired.
A query that carries no search terms ("", "d:") comes back with an error
and no hits instead of matching everything, since a criteria-less expression
would otherwise return an arbitrary page of the glossary that reads like a
genuine result.
Retiring a term: deprecate or delete
Prefer deprecate_term when the term has been used anywhere. It keeps the entry —
lemma and definition intact — and prefixes its notes with a Deprecated marker, so
prose written before the change is still interpretable and every read of that entry
reports deprecated: true. Name the successor with superseded_by whenever one
exists; it is strongly encouraged but never required, since a term can be withdrawn
with nothing to replace it. The successor must already be in the glossary, so a
deprecation cannot leave a dangling reference behind.
Use delete_term only when the entry should leave no trace — a typo, a duplicate,
or a term no prose ever used. It is the one destructive tool, and it returns the
deleted entry so a caller can restore it verbatim with add_term.
Deprecation lives in the notes rather than in a tag or a column of its own, because
the conlangkit format reserves the definition column for glosses — what the term
means — and the notes column for cross-references to other lemmas. The successor is
written as an italic lemma reference (use *charter*), following the notation in
conlangkit's writing-about-language.md.
A note on slashes in definitions
In the conlangkit format, / separates alternative senses in the definition column.
A definition that contains a literal slash (e.g. allow/ask/deny) is stored
escaped on disk as allow\/ask\/deny — which renders as a plain / on GitHub
(CommonMark/GFM). You never type the escape: pass ordinary prose (with plain
slashes) to add_term/update_term, and read ordinary prose back — the server
escapes and unescapes transparently.
Case sensitivity
Lemma lookup, insertion, and sorting are case-sensitive (byte order) — conlangkit
targets writing systems where case-folding is not meaningful, so core and Core
are distinct entries. Use each term's canonical case: lowercase for ordinary terms,
uppercase for acronyms (AID, GCD). This keeps you from creating near-duplicates.
search is the exception: it folds case, because definitions and notes are
ordinary prose where a capitalized query would otherwise miss silently. So when
lookup_term comes back empty and you suspect the term exists under a different
capitalization, search is what will find it.
Methodology
Non-trivial work in this repo runs under the cc craft methodology
(~/code/me/cc); read constitution.md first. Follow strict TDD (see
AGENTS.md).
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file glossary_mcp-0.3.0.tar.gz.
File metadata
- Download URL: glossary_mcp-0.3.0.tar.gz
- Upload date:
- Size: 98.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
03df8bdb196bea8c8358faab7b01e121f11e3bf900b76813000fe652dd68aa9b
|
|
| MD5 |
8d93375fcb926da8833815e7886fb2d4
|
|
| BLAKE2b-256 |
980d61e43a3514c6a104782a3ef7eb55cc613fc1ee1ec7aa346cc58d69cb19bc
|
File details
Details for the file glossary_mcp-0.3.0-py3-none-any.whl.
File metadata
- Download URL: glossary_mcp-0.3.0-py3-none-any.whl
- Upload date:
- Size: 20.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1066456f00d282a8493e7f8e6f919f59293d956f7076180ee2ff82874ff3260a
|
|
| MD5 |
20667e3ae071290921a0bc0cc92cdefe
|
|
| BLAKE2b-256 |
592dbea8bea57a757b7f6925862a4d58d3b898513142240072e21df4a13515ba
|