3gpp-mcp
# 3gpp-mcp
[](https://pkg.go.dev/github.com/higebu/3gpp-mcp)
[](https://goreportcard.com/report/github.com/higebu/3gpp-mcp)
[](https://github.com/higebu/3gpp-mcp/actions/workflows/ci.yml)
[](https://codecov.io/github/higebu/3gpp-mcp)

An MCP (Model Context Protocol) server that makes 3GPP specifications accessible to LLMs.
## Background
3GPP specifications are essential references for mobile and telecommunications engineering, but they are difficult for LLMs to work with effectively:
- **Too many documents** - Thousands of specifications exist across multiple series, making it hard to find the right one.
- **Individual documents are too large** - Many specs are hundreds of pages long, far exceeding typical context windows.
- **Distributed as Word files** - Specs are published in `.docx` / `.doc` format and require conversion for text processing.
- **Heavy cross-referencing** - Specs frequently reference each other; reading a single document in isolation gives an incomplete picture.
- **Information packed in tables and figures** - Complex tables and flow diagrams carry critical details. This tool converts tables to Markdown and extracts embedded images for LLM viewing.
- **Version complexity** - The same specification exists across multiple 3GPP releases, and identifying the correct version matters.
This tool addresses these challenges by parsing the `.docx` files, structuring the content by section, and storing everything in a SQLite database with full-text search (FTS5). An MCP server then exposes tools for searching, browsing by section, and following cross-references — letting an LLM navigate the specifications the way an engineer would.
### Why not RAG?
Embedding-based RAG is a common way to improve accuracy on document Q&A, and RAG systems specialized for 3GPP documents exist ([Telco-RAG](https://arxiv.org/abs/2404.15939), [TelcoAI](https://arxiv.org/abs/2601.16984)). This tool takes a simpler approach: instead of building a retrieval pipeline in front of the model, it gives the model search and navigation tools and lets it explore the specifications the way an engineer would — full-text search, then following the section hierarchy and cross-references. Since retrieval is plain FTS5 search over structured sections, there is no embedding model or vector database to run, and everything lives in a single SQLite file.
Measured on TeleQnA, this lifts accuracy on 3GPP standards questions by 6.5 to 12.0 percentage points across three model families. Most of that is having the text at all: a single BM25 query over the same database accounts for +7.8 to +9.6pt of it. The tool's own search is what separates them on questions whose answer sits more than one hop from the first retrieved passage — on tasks generated from the specifications themselves (protocol codes, ASN.1 structure, 5G SBI schemas) it answers *and correctly cites* 88-100%, beating that same BM25 baseline by +26 to +88 points on every task type and every model. See [BENCHMARK.md](BENCHMARK.md).
## Getting Started
### 1. Install
```bash
# Homebrew
brew install higebu/tap/3gpp-mcp
# ...or with Go 1.26+
go install github.com/higebu/3gpp-mcp/cmd/3gpp-mcp@latest
```
Prebuilt binaries are also available on the [releases page](https://github.com/higebu/3gpp-mcp/releases). LibreOffice is optional (needed for `.doc` to `.docx` conversion and EMF/WMF image to PNG conversion).
### 2. Build the database
Download and import specifications into the database. Temporary files are deleted after each spec is processed, minimizing disk usage.
```bash
# Download and import the latest version of every spec (all releases)
3gpp-mcp build --latest --db data/3gpp.db --convert-doc --convert-image
# ...or restrict to a single release
3gpp-mcp build --release 19 --db data/3gpp.db --convert-doc --convert-image
```
This will scrape the 3GPP FTP archive, download ZIP files, extract and parse `.docx` files, and insert structured content into the SQLite database.
### 3. Register with your MCP client
#### Claude Code
```bash
claude mcp add --scope user 3gpp -- 3gpp-mcp serve --db /path/to/data/3gpp.db
```
#### VS Code / GitHub Copilot
```bash
code --add-mcp '{"name":"3gpp","command":"3gpp-mcp","args":["serve","--db","/path/to/data/3gpp.db"]}'
```
#### GitHub Copilot CLI
Add to `~/.config/github-copilot/cli-mcp.json` (create if it doesn't exist):
```json
{
"mcpServers": {
"3gpp": {
"command": "3gpp-mcp",
"args": ["serve", "--db", "/path/to/data/3gpp.db"]
}
}
}
```
#### Codex CLI
```bash
codex mcp add --name 3gpp --command 3gpp-mcp --args serve --db /path/to/data/3gpp.db
```
#### Claude Desktop
Add to your configuration file (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows):
```json
{
"mcpServers": {
"3gpp": {
"command": "3gpp-mcp",
"args": ["serve", "--db", "/path/to/data/3gpp.db"]
}
}
}
```
### 4. Web viewer (optional)
Browse specifications in your browser by adding `--web` to the HTTP transport:
```bash
3gpp-mcp serve --db data/3gpp.db --transport http --addr :8080 --web
# MCP endpoint: http://localhost:8080/mcp/
# Web viewer: http://localhost:8080/
```
Features: spec list with filtering, section viewer with TOC sidebar, full-text search with pagination, past-version browsing (versions are listed per spec and downloaded on demand, like the MCP tools), version comparison (structural summary and per-section diffs), embedded images, meeting lists and documents (TDocs) fetched on demand at `/meetings` and `/tdocs`, cross-reference links, OpenAPI definitions with syntax highlighting, KaTeX rendering of the [LaTeX formulas](#formulas) the converter emits, dark mode, responsive design. Code blocks are syntax-highlighted per notation — ASN.1, Diameter, SIP/RTSP, SDP and XML (see [Code blocks](#code-blocks)).
#### WebMCP
When the browser provides the [W3C WebMCP](https://github.com/webmachinelearning/webmcp) API (`document.modelContext`, a Chrome origin trial as of 2026), the viewer registers all of its MCP tools with the browser at page load, so an in-browser agent can query the spec database directly. The registration is a thin same-origin passthrough to the `/mcp/` endpoint — there is nothing to configure server-side, and browsers without the API are unaffected. During the origin trial, enable it locally via Chrome flags (`chrome://flags`), or for a shared deployment serve an `Origin-Trial` header from a fronting proxy.
## Deployment
### Streamable HTTP
The HTTP transport is stateless: it supports MCP protocol version 2026-07-28 (no
initialize handshake, no `Mcp-Session-Id`) while older clients (2024-11-05
through 2025-11-25) keep working through per-request sessions.
Start the server with HTTP transport:
```bash
3gpp-mcp serve --db data/3gpp.db --transport http --addr :8080
```
Optionally enable Bearer token authentication:
```bash
export THREEGPP_MCP_BEARER_TOKEN=$(openssl rand -hex 32)
3gpp-mcp serve --db data/3gpp.db --transport http --addr :8080
```
Then configure your client to connect via HTTP:
```json
{
"mcpServers": {
"3gpp": {
"url": "http://your-server:8080",
"headers": {
"Authorization": "Bearer YOUR_SECRET_TOKEN"
}
}
}
}
```
When using `--web`, the MCP endpoint moves to `/mcp/`.
See [`examples/systemd/`](examples/systemd/) for production deployment with systemd.
### Docker
The `Dockerfile` is multi-stage and builds the database for a release directly,
producing a self-contained image with the SQLite database (sections, OpenAPI
definitions, and embedded images) baked in. No pre-built database is needed in
the build context.
```bash
# Build an image with the latest version of every spec baked in (default)
docker build -t 3gpp-mcp:latest .
# ...or restrict the database to a single release
docker build --build-arg RELEASE=19 -t 3gpp-mcp:rel19 .
# ...or cap the newest release, keeping specs that have no version in it
docker build --build-arg MAX_RELEASE=19 -t 3gpp-mcp:max-rel19 .
# stdio transport (Claude Code / IDE integration)
docker run --rm -i 3gpp-mcp:latest
# HTTP transport
docker run --rm -p 8080:8080 3gpp-mcp:latest serve --db /3gpp.db --transport http --addr :8080
```
`RELEASE` defaults to `latest`, which bakes in the latest version of every spec
across all releases. Set `--build-arg RELEASE=<n>` (e.g. `19`) to restrict the
database to a single release, or `--build-arg MAX_RELEASE=<n>` to cap the newest
release without dropping specs that have no version in it. The two cannot be
combined.
### Cloud Run
To run on Cloud Run, see `cloudbuild.yaml` (build + push + deploy) and
`service.yaml` (Cloud Run service spec).
## Tools
Every tool below also has a CLI twin (`list_specs` → `3gpp-mcp list-specs`, and
so on) for shell use and scripting — see the
[query commands](#query-commands) in the Command Reference.
### Browsing specifications
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `list_specs` | List available specifications (paginated) | `series` (optional): filter by series number, e.g. `"23"`; `query` (optional): spec ID prefix, e.g. `"38.21"`; `limit`, `offset` |
| `list_versions` | List the versions of a spec and where each can be read from | `spec_id` (required): e.g. `"TS 23.501"` |
| `get_toc` | Get table of contents of a spec | `spec_id` (required), `version` |
| `get_section` | Get section content (paginated) | `spec_id`, `section_number` (required), `version`, `include_subsections`, `offset`, `max_lines`, `max_chars` |
| `compare_versions` | Compare two versions of a spec: structural summary, or a section text diff | `spec_id`, `old_version` (required), `new_version`, `section_number`, `include_subsections`, `context_lines`, `offset`, `max_lines`, `max_chars` |
Every `get_toc`, `get_section` and `search` result names the specification and
version it came from, on every page of a paginated response.
### Past versions
The database holds one version per specification. To read another version, pass
`version` to `get_section` or `get_toc`. `version` accepts the dotted form
(`15.8.0`), the archive token (`f80`), a release selector (`Rel-15` or `15`,
picking the newest version in that release), or `latest`. Release selectors and
`latest` are resolved against the 3GPP archive, so they require on-demand
fetching (they do not work under `--no-fetch`). `old_version` and `new_version`
of `compare_versions` accept the same forms; `new_version` defaults to the
version in the database.
A version that is not in the database is downloaded from the 3GPP archive and
converted on first use. This takes up to a few minutes for a large
specification; if it is still running when the call's budget expires, the tool
says so and the same call repeated later returns the content. Results are kept
in a size-bounded cache (see [`serve`](#serve)) that is separate from the main
database, so:
- `search` covers only the version in the database — cross-release full-text
search is not supported
- `get_references` only has data for the version in the database, and a section
read from an archived version says so in its header
- `get_image` and `list_images` accept a `version` too: an archived version's
images are downloaded on their own first use (one extra archive download per
version, with the same retry behavior), and EMF/WMF figures are converted to
PNG when LibreOffice is installed on the server
- section numbers move between releases; check `get_toc` for the older version
before reading a section of it
### Searching
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `search` | Full-text search across all specs | `query` (required), `spec_ids` (optional), `limit`, `offset` |
The `search` tool supports [SQLite FTS5](https://www.sqlite.org/fts5.html) query syntax:
- Phrase search: `"service based interface"`
- Boolean operators: `AMF AND UE`, `AMF OR SMF`, `NOT deprecated`
- Exclusion after a positive term: `handover -conditional`
- Prefix matching: `handov*`
- Column filter: `title:authentication`, `content:handover`
- Proximity: `NEAR(AMF UE, 5)`
Terms containing hyphens or dots (`IMS-AKA`, `38.101`) are quoted automatically,
so they need no manual escaping.
### Cross-references
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `get_references` | Get cross-references between specs and RFCs | `spec_id` (required), `section_number` (required for `"outgoing"`), `direction` (`"outgoing"` or `"incoming"`), `include_subsections`, `offset` |
### OpenAPI definitions
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `list_openapi` | List available OpenAPI definitions | `spec_id` (optional): filter by spec, e.g. `"TS 29.510"` |
| `get_openapi` | Get OpenAPI definition (paginated) | `spec_id`, `api_name` (required), `path`, `schema`, `offset`, `max_lines` |
| `search_openapi` | Full-text search across OpenAPI definitions | `query` (required), `spec_ids`, `api_name`, `kind` (`"schema"` or `"operation"`), `include_body`, `limit`, `offset` |
`search_openapi` uses its own FTS5 index, separate from the one `search` uses:
`search` covers specification clause text and never returns OpenAPI content,
`search_openapi` covers OpenAPI content only. One hit is one definition rather
than one document — a schema from `components.schemas`, or one HTTP method of
one path (named like `PUT /nf-instances/{nfInstanceID}`) — so you can find a
data type or an endpoint without knowing which API document defines it, then
read it in full with `get_openapi`. A query that is a single bare term ranks a
definition of exactly that name first, so `NFProfile` returns the `NFProfile`
schema ahead of the schemas that only reference it.
A schema's indexed text carries one level of `$ref` expansion — through `items`
and `additionalProperties` as well as directly, which is how the 5G SBI
definitions state most of their relationships — so the fields of a referenced
type are searchable from the schema that uses it; a type two hops away is not
in that text. Unlike `search`, this index applies no stemming
— identifiers are matched as written — and `-`, `.` and `_` split tokens, so
`Nnrf_NFManagement` is also found by `NFManagement` and `/nf-instances` by
`instances`. camelCase is not split.
The index is built at the end of `build` and `update`. `import` and `import-dir`
leave it alone: the YAML files ship in the archive zip, so importing a `.docx`
cannot change what there is to index. A database built before this tool existed
has no index; add it in place with
[`build-openapi-index`](#other-commands).
### ASN.1 definitions
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `get_asn1` | Get an ASN.1 assignment by name — in one spec or across all of them — or list a spec's assignment names | `spec_id` (optional; omit to resolve `name` across every spec), `name` (assignment name, e.g. `AMF-UE-NGAP-ID`; required without `spec_id`), `version` (requires `spec_id`), `offset`, `max_lines`, `max_chars` |
The ASN.1-specified protocols (RRC TS 38.331/36.331, NGAP TS 38.413, S1AP
TS 36.413, XnAP, F1AP, ...) write their ASN.1 between `-- ASN1START` /
`-- ASN1STOP` markers, which the converter stores as ` ```asn1 ` fences (see
[Code blocks](#code-blocks)). `get_asn1` extracts every top-level assignment —
types, constants and information objects — from those fences.
With `name` it returns that assignment's full text together with the section
that defines it, so the answer can be cited. This matters for the protocols
that define all their IEs in one clause: NGAP's IE definitions clause is
hundreds of kilobytes, far more than one `get_section` page, while the one
definition that answers "what range does the ASN.1 allow here" is a few lines.
Matching ignores case and separators, so the IE table's `AMF UE NGAP ID` finds
the ASN.1's `AMF-UE-NGAP-ID`; a name that matches nothing gets similar names
suggested. A name defined more than once returns every definition, each under
its own source line.
When you do not know which specification defines a name, omit `spec_id`: the
name is resolved across every specification in the database, from a name
index built at database build time (`build`, `update`, `import` and
`import-dir` all refresh it). A lookup that names the wrong specification
gets told where the name is actually defined. A database built before this
tool existed has no index — add it in place with
[`build-asn1-index`](#other-commands). Cross-spec resolution covers the
database versions only — pass `spec_id` (and optionally `version`) to read
an archived version, with the same on-demand download behavior as
`get_section`.
With a `spec_id` and no `name` it lists every assignment name, grouped by
defining section.
### Embedded images
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `list_images` | List embedded images in a spec | `spec_id` (required), `version` (optional) |
| `get_image` | Get an embedded image as base64 data viewable by LLMs | `spec_id`, `name` (required): image filename, `version` (optional) |
PNG/JPEG/GIF/WebP images are directly viewable by LLMs. EMF/WMF images (most 3GPP figures use this format) are stored as raw data by default; use `--convert-image` to convert them to PNG via LibreOffice at build time.
Figures are referenced from the section text in a single notation, whatever the
image format: `` in body text and
`<img src="image://NAME?w=&h=" ...>` inside table cells. Pass that `NAME` to
`get_image`; both the original filename (`image3.emf`) and the converted one
(`image3.png`) resolve.
### Meeting documents (TDocs)
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `list_meetings` | List a group's meetings, newest first, with codes, dates, TDoc ranges and FTP folders; without a group, the supported groups | `group` (`R1`, `RAN1`, `SA2`, ...), `limit`, `offset` (all optional) |
| `list_tdocs` | List a meeting's documents from its official TDoc list, with type, status, source, agenda item, title and CR/LS details | `meeting` (required): `R1-123`, `RAN1#123` or the folder path; `group`, `agenda_item`, `type`, `status`, `source`, `spec`, `query`, `details`, `limit`, `offset` (all optional) |
| `get_tdoc` | Read a meeting document (TDoc) as Markdown | `tdoc_id` (required): TDoc number (`R1-2509715`) or an FTP path (`tsg_ran/WG1_RL1/TSGR1_123/Report/Final_Minutes_report_RAN1#123_v100.zip`), `meeting`, `section_number`, `offset`, `max_lines`, `max_chars` (all optional) |
| `get_meeting_report` | Read a meeting's report (minutes) or final agenda by meeting name | `meeting` (required); `group`, `kind` (`report`, default, or `agenda`), `section_number`, `offset`, `max_lines`, `max_chars` (all optional) |
TDocs are the working documents of a 3GPP meeting: contributions, change
requests (CRs), liaison statements (LSs), agendas and meeting reports. A TDoc
number is located through the group's DynaReport meeting page
(<https://www.3gpp.org/dynareport?code=Meetings-R1.htm> and its siblings) by
TDoc number range, downloaded from that meeting's `Docs/` folder and converted
on first use. Pass `meeting` (`R1-123`, `RAN1#123`, `TSGR1_123`) when the
number falls in no listed range. A document with no TDoc number — a meeting
report, for instance — is named by its path under
<https://www.3gpp.org/ftp/> instead.
`list_tdocs` lists a meeting's official TDoc list: every document's type,
status (`agreed`, `noted`, `revised`, ...), source, agenda item, revision
chain, and for a CR the spec, CR number and category, for an LS the
addressees. Without filters the output opens with the agenda items and their
document counts; `query` is a full-text search over titles, sources,
abstracts, agenda item descriptions and work items, scoped to that meeting.
The list is refreshed daily while the meeting is recent.
Converted documents and TDoc lists are kept in their own size-bounded cache
(`tdocs.db`, see [`--tdoc-cache`](#serve)), separate from the main database:
TDocs are never imported into it and `search` does not cover them.
`get_meeting_report` reads a meeting's approved report or final agenda by
meeting name alone; the output header names the document it resolved to,
and the sections carry the numbers the document shows, so an agenda item's
section can be read by number.
Notes:
- A CR's cover sheet and an LS's header are in the section named `preamble`,
and summarized at the top of the output: for a CR the spec, CR number,
category, release, source, reason for and summary of the change, the
clauses affected, and the `get_section` call that reads a changed clause
at the version the CR is written against; for an LS the sender,
addressees, the LS it replies to, release, work item and contact. The web
viewer shows the same block and links each changed clause, and each
numbered section of the CR, to the spec.
- A download that holds attachments has only its main Word file converted; the
others are listed in the output header so a reader knows they exist.
- `.doc` documents need LibreOffice (`soffice`) at runtime; `.pptx`, `.xlsx`
and `.pdf` are not converted and the tool reports the file list instead.
- Supported groups: RAN and RAN1-6, SA and SA1-6, CT and CT1, CT3, CT4, CT6,
and GERAN — TDoc prefixes `RP`, `R1`-`R6`, `SP`, `S1`-`S6`, `CP`, `C1`,
`C3`, `C4`, `C6` and `GP`.
### Code blocks
Section text carries tagged code fences, so both LLMs and the web viewer can
tell the notations apart:
| Fence | Content |
|-------|---------|
| ` ```asn1 ` | ASN.1 modules between the `-- ASN1START` / `-- ASN1STOP` markers |
| ` ```diameter ` | Diameter command and grouped-AVP definitions (RFC 6733 CCF) |
| ` ```xml ` | XML schemas, XML body examples and DTDs |
| ` ```sip ` | SIP/RTSP message examples |
| ` ```sdp ` | Standalone SDP session descriptions |
| ` ```latex ` | Standalone equations converted from Word OMML |
| ` ``` ` | Anything else the source document styles as code |
### Formulas
Word formulas (OMML) are converted to LaTeX in three notations, so a formula is
readable whether it stands alone or sits in a sentence:
| Notation | Where |
|----------|-------|
| ` ```latex ` fence | A paragraph whose only content is an equation. Its equation number is kept as `\tag{7.3-1}`, which renders as a right-aligned `(7.3-1)`. |
| `$$...$$` | Display equations that cannot be a fenced block — inside a table cell or a list item. |
| `$...$` | A formula inside a sentence. |
### Indentation
3GPP prose encodes structure in indentation — nested requirement and
condition lists, multi-level definitions. A body paragraph's leading
whitespace is preserved as no-break spaces (U+00A0), one tab of the source
document becoming four: a literal tab or 4+ leading spaces would turn the
line into an indented code block in Markdown (inside which HTML like
`<sub>` is never interpreted), while no-break spaces keep the visual
nesting in any renderer and stay out of the way of full-text search.
## Tips
### Tell the model to use the tools
Attaching the server does not by itself make a model consult it: given the
choice, some models answer 3GPP questions from memory. In
[the benchmark](BENCHMARK.md), Claude Sonnet 5 skipped retrieval on 40% of
TeleQnA questions and GPT 5.6 Luna on 60%, and on those questions the tools
were worth nothing. One sentence in the client's system prompt removes that
discretion. The measured wording:
> Do not answer from memory. Search the specifications first and base your
> answer on the text you retrieve, even when you are confident you already
> know the answer.
That sentence took Luna's skip rate to zero and its gain from +5.9 to +12.0
points, moved nothing on a model that already searched every question, and is
worth nothing without the tools attached — it forces retrieval rather than
smuggling in an answer. Stronger house rules in the same spirit — *base every
answer about 3GPP on clause text retrieved through these tools, and cite the
clause* — are reasonable, but only the sentence above is what the benchmark
measured.
### Separate databases per release
For spot comparisons across releases, `compare_versions` and the `version` parameter need no extra setup. Building a separate database per release still pays off when you work against one release continuously: full-text `search`, `get_references` and OpenAPI definitions only cover the version baked into the database, so a release-specific database gives you all three for that release, with no on-demand downloads.
```bash
# Build databases for different releases
3gpp-mcp build --release 18 --db data/3gpp-rel18.db --convert-doc --convert-image
3gpp-mcp build --release 19 --db data/3gpp-rel19.db --convert-doc --convert-image
```
`--release` keeps only specs that have a version in that exact release, so a
spec frozen in an earlier release (TS 34.108, for example) is missing from the
database entirely. To pin a release without losing those specs, cap the
selection instead — every spec is taken at its newest version at or below the
cap:
```bash
# Everything as of Release 19: specs with no Rel-19 version fall back to their
# newest older version rather than dropping out.
3gpp-mcp build --max-release 19 --db data/3gpp-rel19.db --convert-doc --convert-image
# Keep the cap when refreshing the database later.
3gpp-mcp update --max-release 19 --db data/3gpp-rel19.db --convert-doc
```
Register them as separate MCP servers:
```bash
claude mcp add --scope user 3gpp-rel18 -- 3gpp-mcp serve --db /path/to/data/3gpp-rel18.db
claude mcp add --scope user 3gpp-rel19 -- 3gpp-mcp serve --db /path/to/data/3gpp-rel19.db
```
### Keeping specs up to date
Use the `update` command to check for newer versions of specs already in your database:
```bash
3gpp-mcp update --db data/3gpp.db --convert-doc --convert-image
```
## Command Reference
### `serve`
Start the MCP server.
| Flag | Description | Default |
|------|-------------|---------|
| `--db` | Path to SQLite database | `3gpp.db` |
| `--transport` | Transport type: `stdio` or `http` (env: `THREEGPP_MCP_TRANSPORT`; defaults to `http` when `PORT` is set) | `stdio` |
| `--addr` | HTTP listen address (env: `THREEGPP_MCP_ADDR`, or `PORT` interpreted as `:$PORT`) | `:8080` |
| `--bearer-token` | Bearer token for HTTP auth (env: `THREEGPP_MCP_BEARER_TOKEN`) | |
| `--web` | Enable web viewer alongside MCP server (HTTP transport only) | `false` |
| `--no-fetch` | Disable on-demand fetching of spec versions that are not in the database | `false` |
| `--version-cache` | Path to the on-demand version cache | `$XDG_CACHE_HOME/3gpp-mcp/versions.db` (`~/.cache/3gpp-mcp/versions.db` when unset) |
| `--version-cache-mb` | Size limit of the version cache in MB. `0` keeps only the most recently fetched version, `-1` is unlimited (env: `THREEGPP_VERSION_CACHE_MB`) | `1024` |
| `--tdoc-cache` | Path to the on-demand meeting document (TDoc) cache | `$XDG_CACHE_HOME/3gpp-mcp/tdocs.db` (`~/.cache/3gpp-mcp/tdocs.db` when unset) |
| `--tdoc-cache-mb` | Size limit of the meeting document cache in MB. `0` keeps only the most recently fetched document, `-1` is unlimited (env: `THREEGPP_TDOC_CACHE_MB`) | `512` |
| `--fetch-budget` | How long a tool call waits for an on-demand fetch before asking the caller to retry (env: `THREEGPP_FETCH_BUDGET`) | `60s` |
The version cache is a separate SQLite file, so the main database stays
read-only and is never polluted with extra versions. The meeting document
cache is a third file, bounded and evicted the same way; `--no-fetch` disables
both. When the cache cannot be
created — a read-only or ephemeral filesystem, such as the `scratch`-based
container image — the server logs a warning and runs with on-demand fetching
disabled; everything else keeps working. Cached versions are evicted
least-recently-used once the size limit is exceeded.
HTTP transport also exposes `GET /health`, which returns `200 OK` without authentication. Use this path for platform health checks (Cloud Run, Sakura AppRun, Kubernetes liveness/readiness probes, etc.).
### `build`
Download and import specifications into the database (recommended for initial setup). Alias: `pipeline`.
| Flag | Description | Default |
|------|-------------|---------|
| `--db` | Output SQLite database path | `3gpp.db` |
| `--release` | Process specs for a specific release (e.g. `19`) | |
| `--max-release` | Cap the selection at a release (e.g. `19`): take each spec at its newest version at or below it | |
| `--latest` | Select every spec at its latest version (use when no other selector is given) | `false` |
| `--spec` | Process a specific spec (e.g. `23.501`) | |
| `--series` | Filter by series, comma-separated (e.g. `23,29`) | |
| `--workers` | Number of parallel workers | NumCPU |
| `--convert-doc` | Convert `.doc` files to `.docx` using LibreOffice | `false` |
| `--convert-image` | Convert EMF/WMF images to PNG using LibreOffice | `false` |
| `--spec-list` | Read the spec list from a file instead of scraping the archive (a selector is still required) | |
| `--no-cache` | Disable the spec list cache | `false` |
| `--scrape-workers` | Concurrency for scraping spec listings (`0` = auto) | `0` |
| `--timeout` | HTTP timeout | `30s` |
One of `--release`, `--max-release`, `--latest`, `--series` or `--spec` must be
given, `--spec-list` included: the file supplies the candidate entries and the
selector filters them.
`--release` and `--max-release` differ in what happens to a spec that has no
version in the named release: `--release 19` drops it, `--max-release 19` keeps
it at its newest version below the cap. They cannot be combined.
### Other commands
- `download` — Download specifications without conversion (`--output-dir`, default `specs`). Requires one of `--release`, `--max-release`, `--latest`, `--series` or `--spec`, like `build`.
- `import` — Import a single `.docx` file into the database. Alias: `convert`. Usage: `3gpp-mcp import --db data/3gpp.db path/to/spec.docx`
- `import-dir` — Import all `.docx` files in a directory into the database. Alias: `convert-dir`. Usage: `3gpp-mcp import-dir --db data/3gpp.db ./specs`
- `update` — Update specifications in the database to latest versions, or to a cap with `--max-release`.
- `build-openapi-index` — Rebuild the [OpenAPI search index](#openapi-definitions) of an existing database. `build` and `update` do this themselves, so it is for adding the index to a database built before `search_openapi` existed: `serve` opens the database read-only and cannot create it on the fly.
- `build-asn1-index` — Rebuild the [ASN.1 name index](#asn1-definitions) of an existing database. `build`, `update`, `import` and `import-dir` do this themselves, so it is for adding the index to a database built before `get_asn1` existed.
- `completion` — Print a shell completion script: `3gpp-mcp completion bash` (or `zsh`, `fish`)
The cap is not stored in the database, so a database built with
`--max-release 19` needs the same flag on `update` — otherwise the update
lifts every spec to the newest release on the archive. With a cap the update moves a spec
in either direction, so it also brings an already-built uncapped database down
to the cap; a spec whose every version is above the cap is removed, since no
version of it belongs in a capped database. A spec missing from the archive
listing is left untouched, as a failed listing looks the same as a withdrawn
spec.
### Query commands
The query commands (`list-specs`, `list-versions`, `get-toc`, `get-section`,
`get-asn1`, `compare-versions`, `search`, `list-openapi`, `get-openapi`,
`search-openapi`, `get-references`, `list-images`, `get-image`,
`list-meetings`, `list-tdocs`, `get-tdoc`, `get-meeting-report`) mirror the
MCP read tools 1:1, so
the database can be inspected and scripted from a shell without an MCP client:
```bash
3gpp-mcp search --db data/3gpp.db --limit 3 "AMF AND authentication" | jq '.results[].section_number'
3gpp-mcp get-section --db data/3gpp.db "TS 23.501" 5.15.2 | less
3gpp-mcp list-tdocs --db data/3gpp.db --type CR --status agreed --spec 38.214 R1-123
3gpp-mcp get-tdoc --db data/3gpp.db R1-2509715 | less
3gpp-mcp get-meeting-report --db data/3gpp.db RAN1#123 | less
```
Conventions shared by all of them:
- Flags must come before positional arguments.
- JSON results print to stdout indented and unpaginated — pipe to `jq`,
`head` or `less`. Warnings and progress notes go to stderr, so stdout stays
parseable.
- Commands that accept `--version` (and `compare-versions`) take the same
version forms as the MCP tools (`15.8.0`, `f80`, `Rel-15`, `latest`) and
wait for an on-demand download to finish instead of asking you to retry;
interrupt with Ctrl-C. They share `serve`'s fetch flags: `--no-fetch`,
`--version-cache`, `--version-cache-mb`, `--fetch-budget`. Queries that name
no version never create the version cache (`list-versions` reads an existing
cache to report `cached` availability, but will not create one).
- `get-tdoc` takes a TDoc number or an FTP path and an optional section
([`preamble`](#meeting-documents-tdocs) for a CR cover sheet or an LS
header), plus `--meeting`; `list-meetings` takes a group, `list-tdocs` a
meeting with the same filters as the tool (`--agenda-item`, `--type`,
`--status`, `--source`, `--spec`, `--query`, `--details`) and
`get-meeting-report` a meeting, an optional section and `--kind agenda`
for the agenda. The four share
`serve`'s meeting-document flags `--no-fetch`, `--tdoc-cache`,
`--tdoc-cache-mb` and `--fetch-budget` and read their own cache only, never
the version cache.
- Every command takes `--db` (default `3gpp.db`).
## Environment Variables
| Variable | Description |
|----------|-------------|
| `THREEGPP_MCP_TRANSPORT` | Transport for `serve` (`stdio` or `http`); overridden by `--transport` |
| `THREEGPP_MCP_ADDR` | HTTP listen address for `serve`; overridden by `--addr` |
| `THREEGPP_MCP_BEARER_TOKEN` | Bearer token for HTTP transport auth |
| `PORT` | PaaS convention (Cloud Run / Heroku); `serve` defaults to HTTP transport on `:$PORT` |
| `THREEGPP_VERSION_CACHE_MB` | Size limit of the on-demand version cache in MB (default `1024`) |
| `THREEGPP_TDOC_CACHE_MB` | Size limit of the on-demand meeting document (TDoc) cache in MB (default `512`) |
| `THREEGPP_FETCH_BUDGET` | How long a tool call waits for an on-demand fetch (default `60s`) |
| `THREEGPP_MAX_ZIP_SIZE_MB` | Max ZIP download size (default `512`) |
| `THREEGPP_CACHE_TTL_HOURS` | Spec list cache TTL in hours (default `24`) |
| `THREEGPP_LISTING_RETRY_MS` | Initial backoff between archive listing fetch attempts in ms (default `1000`) |
| `XDG_CACHE_HOME` | Cache directory root, per the XDG Base Directory spec |
TDQS
Scored across 13 tools
Every tool targets a distinct content type or operation: listing vs. retrieval, specification text vs. ASN.1 vs. OpenAPI vs. images vs. cross-references. The two search tools explicitly separate specification clauses from OpenAPI definitions, eliminating ambiguity.
All tools use a consistent lowercase snake_case convention with a verb-noun pattern (list_*, get_*, search_*, compare_versions). No mixed conventions or irregular names, making it easy to infer functionality from the name.
13 tools is a well-scoped set for a specification retrieval system. Each tool adds a distinct capability—discovery, reading, searching, comparison, images, ASN.1, OpenAPI, references—without redundancy or bloat.
The tool surface covers the full lifecycle of read-only specification access: discovery (list_specs, list_versions, list_openapi, list_images), retrieval (get_section, get_image, get_asn1, get_openapi), searching (search, search_openapi), comparison (compare_versions), and cross-references (get_references). No obvious gaps for the stated domain.