MCP tools reference¶
Every tool the ubc serve mcp server lists, generated from the server’s own tool registry.
Each tool shows its maturity,
whether it answers without a license,
the annotations a client reads to decide whether to ask before calling it,
and its parameters.
The full description is the text the model is given; Model Context Protocol (MCP) server says how to use the server.
fetch_doc_page¶
- Maturity:
- Without a license:
- Annotations:
readOnlyHint: true,idempotentHint: true,openWorldHint: true
Fetch the full reStructuredText body of one documentation page.
Parameters
path(string, required)The page’s reStructuredText source path, copied from a
search_docshit’spathfield (e.g._sources/configuration.rst.txt). Do not invent one: callsearch_docsfirst.source(string, required)Which documentation source to fetch from:
"sphinx-needs"or"ubcode". Required — copy it from thesourcefield of thesearch_docshit.version(string, optional)The documentation version, for the versioned sphinx-needs docs (e.g.
"latest","7.0.0"). Pass the hit’sversionfield. Defaults to"latest"; ignored for the unversioned ubCode docs.
The full description the model reads
Fetch the full reStructuredText body of one documentation page. Pass `source` and `path`
exactly as they appear on a `search_docs` hit (e.g. source "sphinx-needs", path
`_sources/configuration.rst.txt`), plus the hit's `version` for the versioned
sphinx-needs docs (ignored for ubCode). `source` is required — call `search_docs` first
rather than guessing a path. The page body is the text content: the raw `.rst.txt`
behind a two-line header naming the rendered page's URL and the version, so you can cite
what you read. The structured content carries only where the page came from (`source`,
`path`, `version`, `title`, `htmlUrl`) and whether it was cut (`truncated`) — READ THE
BODY FROM THE TEXT, it is deliberately not duplicated there. Very large pages are
truncated at 100000 bytes with a note naming the URL to read on. This queries public
documentation sites over the network and needs no project or license.
get_graph_schema¶
- Maturity:
- Without a license:
- Annotations:
readOnlyHint: true,idempotentHint: true,openWorldHint: true
Return the schema of the project’s needs graph: node labels (need types) with node counts and their configured titles, relationship types (link options) with relationship counts, and node properties with their data types and nullability.
Parameters
assume_unchanged(boolean, optional)Set
trueto answer from the project as this server last indexed it, WITHOUT re-checking the files — only when you know nothing in the project has changed since your last call to a graph tool without this flag: you have saved no edits, expect none, and are about to run several queries. Such an answer says how old the index is (index.age_seconds). Defaults tofalse: every call then re-checks the files. After ANY edit — yours or anyone’s — or when unsure, omit it. When the project is not in memory yet (or its configuration changed), the call re-checks anyway. A call without this flag on the same project waits for calls with it that are still running.file_path(string, optional)Optional path to any file within the ubcode project directory tree. The server traverses up from this path to locate the nearest ubproject.toml. If not provided, the server uses the default configuration path given at startup.
The full description the model reads
Return the schema of the project's needs graph: node labels (need types) with node
counts and their configured titles, relationship types (link options) with relationship
counts, and node properties with their data types and nullability. Labels cover every
declared need type — a declared-but-unused type appears with count 0. Unless it passes
`assume_unchanged` (below), every call re-scans the project for changed files (by
modification time) and re-indexes what changed, so the schema reflects the saved source
files; the index is usually kept in memory between calls, so a repeated call is cheap
(unless the server keeps no project in memory, the project was unloaded to free memory,
another process rewrote its cache, or a reference-tool call or a configuration edit
forced a reload). To query a project you are NOT editing, pass `assume_unchanged: true`
on the calls after the first: they then skip the re-scan and answer from the index in
memory, and say how old it is (`index.age_seconds`). It is a promise that no file
changed since the last call without it — never pass it on the first call after an edit
(yours, or the user's), and drop it whenever the age matters; the next call without it
re-checks and refreshes the index. The response mirrors Neo4j's
db.schema.nodeTypeProperties() vocabulary (`mandatory`, plural `types`). Call this
before writing openCypher queries with `query_cypher`, so you know which labels,
relationship types, and typed properties exist. It also returns `parameters`: the
`$name` values a query can actually reference in this context — the reserved
`$build_tags`/`$docname`/`$var` when available, plus `variant_data` entries under their
`$var.` spelling (including nested variant leaf paths like `$var.deploy.region.zone`) —
each with a data type and source (names and types only, never values). Only parameters
available in this context are listed, so reference just what appears here. It also
returns `label_properties`: which of those properties the nodes of each label can have,
by name, labels that can have the same ones sharing one entry — a field declared on need
types alone is listed only for the types that declare it or inherit it. They come from
the configuration, so a listed field may hold null on a given node, and `content`,
`docname` and `section_name` may be absent from one. Pass `file_path` (any file inside
the project, to locate its ubproject.toml).
query_cypher¶
- Maturity:
- Without a license:
- Annotations:
readOnlyHint: true,idempotentHint: true,openWorldHint: true
Run a read-only openCypher query over the project’s needs graph.
Parameters
query(string, required)The read-only openCypher query to execute, e.g.
MATCH (n:requirement)-[:links]->(m) WHERE n.status = 'open' RETURN n.id, m.id.assume_unchanged(boolean, optional)Set
trueto answer from the project as this server last indexed it, WITHOUT re-checking the files — only when you know nothing in the project has changed since your last call to a graph tool without this flag: you have saved no edits, expect none, and are about to run several queries (or to page through one result). Such an answer is much faster and says how old the index is (index.ageSeconds, and a closingnote:line). Defaults tofalse: every call then re-checks the files. After ANY edit — yours or anyone’s — or when unsure, omit it. When the project is not in memory yet (or its configuration changed), the call re-checks anyway. A call without this flag on the same project waits for calls with it that are still running.file_path(string, optional)Optional path to any file within the ubcode project directory tree. The server traverses up from this path to locate the nearest ubproject.toml. If not provided, the server uses the default configuration path given at startup.
The full description the model reads
Run a read-only openCypher query over the project's needs graph. The answer carries the
result twice: as a compact, column-aligned text table to read, and as structured content
(`columns`, `rows`, `totalRows`, `truncated`, `notes`) for programmatic use — both
capped identically, so the structured rows are exactly the rows the table shows. Unless
it passes `assume_unchanged` (below), every call re-scans the project for changed files
(by modification time) and re-indexes what changed, so results reflect the saved source
files; the index is usually kept in memory between calls, so a repeated call is cheap
(unless the server keeps no project in memory, the project was unloaded to free memory,
another process rewrote its cache, or a reference-tool call or a configuration edit
forced a reload). To run many queries over a project you are NOT editing, pass
`assume_unchanged: true` on the queries after the first: they then skip the re-scan and
answer from the index in memory, and say how old it is (`index.ageSeconds` and a closing
`note:` line). It is a promise that no file changed since the last call without it —
never pass it on the first call after an edit (yours, or the user's), and drop it
whenever the age matters; the next call without it re-checks and refreshes the index.
Graph mapping: each need is a node labeled with its type (e.g. `(:requirement)`) whose
properties are its fields plus the built-ins `id`, `type`, and `content`; each link is a
directed relationship typed by the link option name (e.g. `-[:links]->`). To learn a
project's schema (node labels, relationship types, node properties and their data
types), call `get_graph_schema` first; for data-shape exploration, run aggregate queries
like `MATCH (n) RETURN n.type AS type, count(*) AS count ORDER BY count DESC` (type
counts) and `MATCH ()-[r]->() RETURN type(r) AS link, count(*) AS count ORDER BY count
DESC` (link counts). Example: `MATCH (n:requirement)-[:links]->(m) WHERE n.status =
'open' RETURN n.id, m.id`. You can reference project context as `$parameters` (e.g.
`WHERE n.status = $var.build_variant`, or a build-tag membership test `WHERE 'html' IN
$build_tags`); the available names are listed by get_graph_schema — variant data is
reached through the reserved `$var` tree (e.g. `$var.deploy.region`), not as a bare
`$name` — and referencing an unavailable one is an error. The engine is read-only —
write clauses (CREATE/MERGE/SET/DELETE) are rejected — and temporal types/functions are
not supported. Results are capped at 200 rows (a truncation note is appended when
exceeded), so for large result sets aggregate (count, collect) or add a WHERE/LIMIT
clause rather than returning every row. Use `count(*)` when you mean how many rows:
`count(x)` counts only the rows where `x` is non-null, so after an `OPTIONAL MATCH` the
two can differ. An unbounded `*` matches every sub-path, not every chain — a 7-hop chain
yields 28 of them. To get whole chains, anchor both ends: `MATCH
p=(start)-[:links*1..7]->(finish) WHERE NOT ()-[:links]->(start) AND NOT
(finish)-[:links]->()`. Bounding the length (`*1..7`, the longest chain you expect) caps
how deep the walk goes on longer or cyclic data but still yields every sub-path up to
that length; anchoring alone selects whole chains but does not shrink the walk — use
both. Results are deterministic (repeatable while the files are unchanged), but only a
bare node scan (`MATCH (n) ...`) is need-id-ordered — traversals and aggregations follow
the match order instead — so to page a large set always add an explicit `ORDER BY n.id
SKIP 200 LIMIT 200` (for a node variable `n`; use whichever variable/key your RETURN
binds) and advance SKIP each call (each call re-scans the project, so pages can shift if
files change between calls; to page over the index in memory while nobody edits the
project, pass `assume_unchanged: true` on the pages after the first). A query that
references a label, relationship type, or property absent from the whole schema appends
a `note:` line (with a did-you-mean suggestion) instead of failing — that token matched
nothing, so check it against get_graph_schema.
resolve_reference¶
- Maturity:
- Without a license:
- Annotations:
readOnlyHint: true,idempotentHint: true,openWorldHint: true
Answer “would this reference resolve, and to what?” for ONE reference, with the build’s own semantics — the resolution oracle to call before or after writing a cross-reference, and the first thing to call when a reference renders as plain text instead of a link.
Parameters
role(string, required)The role name, without its surrounding colons:
ref,term,doc, or an explicit intersphinx form —external:<role>,external:<domain>:<role>,external+<inventory>:<role>,external+<inventory>:<domain>:<role>(e.g.external+python:py:func). A form with no<domain>resolves the role against the standard domain. Surrounding colons are tolerated. Need roles (need,need_incoming,need_outgoing) are NOT accepted — needs are not cross-references; query the needs graph withquery_cypherinstead.target(string, required)The reference target, exactly as it would be written inside the role’s backticks — e.g.
installation,guide/intro,numpy.add.file_path(string, optional)Any file inside the project, used to locate its
ubproject.toml. Omit only if the server was started with a default project.include_usages(boolean, optional)Set
trueto also list the reference SITES in this project that resolve to the same target — the “where is this used?” answer to get BEFORE renaming or removing a label, glossary term or document, since every one of those lines would stop resolving. Defaults tofalse.Each site carries
path, the 1-basedline, and theroleit is written as. Sites are matched through the build’s own resolution of each one, so they are the lines that really do point at this target — including ones spelled differently (a different case, or an explicit:external+<inventory>:form reaching the same URL). For anot_foundtarget they are instead the DANGLING references to the same name, i.e. what creating that label would fix.
The full description the model reads
Answer "would this reference resolve, and to what?" for ONE reference, with the build's
own semantics — the resolution oracle to call before or after writing a cross-reference,
and the first thing to call when a reference renders as plain text instead of a link.
Pass `role` (the role name WITHOUT its surrounding colons: `ref`, `term`, `doc`, or an
explicit intersphinx form such as `external:std:ref`, `external+python:py:func`) and
`target` (exactly what would go inside the backticks). The `result` is one of three
outcomes: `local` — it resolves in this project, with the defining `path`, 1-based
`line`, `title` and the canonical `role_snippet`; `external` — it resolves to an
inventory entry, with the `inventory` qualifier to write in `:external+<inventory>:`,
the entry's `url`, its `reftitle`, the matched `name` (which may differ in CASE from
what you asked for, since Sphinx matches `std:label`/`std:term` case-insensitively) and
the `role_snippet`; or `not_found` — with `disabled` (true when the project's
`disabled_reftypes` blocks the automatic inventory fallback for that object type, so
this target could only ever resolve locally, through an explicit `:external:` role, or
by naming the inventory in the target itself as `<inventory>:<target>` — both of those
spellings ignore the policy), `reason` (Sphinx's own warning text for an `:external:`
miss, including "perhaps you meant one of" when an object-type NAME was written where a
role belongs, e.g. `py:function` instead of `py:func` — or ubcode's own note for a plain
`:doc:` miss, which, when the target names a document under another directory, gives the
project-absolute spelling to write instead), and `suggestions` — the closest names in
the project, each with its own kind, scope and paste-ready snippet. Semantics mirror the
build: `ref`/`term` resolve LOCALLY first and only then fall back to the inventories
(honouring `disabled_reftypes` and `resolve_self`); plain `doc` is local-only (Sphinx
disables `std:doc` in the automatic fallback by default), and its target is read as
project-absolute since there is no referring document; the `external…` forms go through
the explicit path, which ignores `disabled_reftypes` entirely. A role that is not one of
the accepted forms is an argument error naming them (needs are NOT cross-references —
use `query_cypher` for `:need:` targets); a role that is well-formed but cannot resolve
is a `not_found` result, not an error. The response also carries `inventories`, one
status per configured intersphinx project, so a `not_found` caused by an inventory that
failed to load is diagnosable from the response alone. Pass `include_usages: true` to
additionally get `usages`: every reference SITE in the project that the build resolves
to this same target, each with its `path`, 1-based `line` and the `role` it is written
as, plus `usages_total` (larger than the list only when it was capped). That is the
"where is this used?" answer to get BEFORE renaming or removing a label, glossary term
or document — every listed line would stop resolving — and it includes sites spelled
differently (another case, or an explicit `:external+<inventory>:` form reaching the
same URL); for a `not_found` target it lists instead the DANGLING references to that
name, i.e. what creating it would fix. The project is (re)indexed first. Pass
`file_path` (any file inside the project, to locate its ubproject.toml). Call this FIRST
whenever you know the approximate target name — a `not_found` carries `suggestions`, so
one call both checks the reference and offers the alternatives; reach for
`search_references` when you have keywords rather than a name, or want the whole picture
of what CAN be referenced.
search_docs¶
- Maturity:
- Without a license:
- Annotations:
readOnlyHint: true,idempotentHint: true,openWorldHint: true
Search the official product documentation (sphinx-needs and ubCode) and return ranked hits as JSON.
Parameters
query(string, required)A few specific keywords, NOT a sentence — results are AND-matched, so every word must appear. Good:
needs_types,dynamic functions,needimport external. Bad:how do I configure the types of needs in sphinx-needs?. An exact identifier (a configuration value, directive or role name) ranks above everything else, so prefer the identifier when you know it.limit(integer, 1 to 50, optional)Maximum number of hits to return, 1–50. Defaults to 20.
source(string, optional)Restrict the search to one source:
"sphinx-needs"or"ubcode". Omit to search both (recommended — the envelope reports which answered).version(string, optional)The documentation version to search, for the versioned sphinx-needs docs (e.g.
"latest","stable","7.0.0"). Defaults to"latest"; ignored for the unversioned ubCode docs.
The full description the model reads
Search the official product documentation (sphinx-needs and ubCode) and return ranked
hits as JSON. Matching is AND-matched keyword search, so pass a FEW SPECIFIC KEYWORDS
rather than a sentence: `needs_types`, `dynamic functions`, `needimport external` — not
`how do I configure the types of needs?`. An exact identifier (configuration value,
directive, role, label) ranks above every keyword match, so use the identifier when you
know it. Each hit has `source` ("sphinx-needs" or "ubcode"), `kind` ("page", or the
symbol's domain type such as "std:confval" / "rst:directive" / "std:label"), `title`,
`path` (the reStructuredText source path to pass to `fetch_doc_page`, null when the
hit's page is not in the catalog), `version` (null for the unversioned ubCode docs),
`snippet`, and `url` (the rendered HTML page, with an anchor for a symbol). The response
also carries `sources`: one status per documentation source with its `origin`
("ai-index", "objects-inv" or "error"), page and symbol counts, a `degraded` note, and
an `error`. `degraded` is a BENIGN fallback (e.g. sphinx-needs publishes no ai-index, so
its symbols come from objects.inv) — treat it as normal, the hits are fine. Only `error`
means a source could NOT be reached at all; warn the user about that source before
concluding something is undocumented, and rely on the other source's hits. `truncated`
says whether hits were dropped to honour `limit`. Use this to ground product / how-to
answers about sphinx-needs or ubCode configuration, directives and options on the real
docs rather than guessing, then call `fetch_doc_page` on the hits worth reading in full.
This queries public documentation sites over the network and needs no project or
license.
search_references¶
- Maturity:
- Without a license:
- Annotations:
readOnlyHint: true,idempotentHint: true,openWorldHint: true
Search everything the project can cross-reference and return ranked hits as JSON, each with a ready-to-paste role snippet.
Parameters
query(string, required)A few specific keywords, NOT a sentence — results are AND-matched, so every word must appear. An exact name (a label, glossary term, docname, index-entry text or inventory symbol) ranks first, so prefer the exact name when you know it. The top slot agrees with
resolve_reference: where a local target and an inventory entry share a name — even differing only in case — the LOCAL one ranks first and the external namesake directly after; among same-locality spellings that differ only in case, the one you typed ranks first.-wordexcludes;"a phrase"requires all its words. A partial name completes by prefix, sonumpy.adfindsnumpy.add. Keyword matching has a floor: a word shorter than two characters, or a stopword (the,and, …), matches nothing — but an exact name is looked up whole, so a reference actually namedxstill ranks first for the queryx. That whole-name lookup holds only ONE record per name, so below the floor exactly one namesake is reachable:matched: 1with no hits under akindfilter means that one record failed the filter, and dropping the filter shows that record — not the namesake of another object type, which no query reaches. An EMPTY query LISTS the corpus instead of searching it: every reference, ordered by name, under the samekind/scopefilters andlimit— so an empty query withkind: "label"is “list this project’s labels”, andmatchedthen reports how many references exist in total.file_path(string, optional)Any file inside the project, used to locate its
ubproject.toml. Omit only if the server was started with a default project.kind(string, optional)Restrict results to one object type. Either a
domain:objtypeglob (std:label,py:*,*:label) or one of the shorthandslabel,term,doc,index. Matched case-insensitively. Omit to search every type.limit(integer, 1 to 50, optional)Maximum number of hits to return, 1–50. Defaults to 20.
scope(string, optional)Restrict results by where the reference is defined:
"local"(this project),"external"(any loaded intersphinx inventory), or one inventory’s configured project name. Omit to search both. A configured project whose name ISlocalorexternalwins over the keyword, so on such a project read each hit’slocalityfor the unambiguous answer.
The full description the model reads
Search everything the project can cross-reference and return ranked hits as JSON, each
with a ready-to-paste role snippet. Covers BOTH the project's own standard-domain
targets (`std:label` from `.. _label:` targets, `std:term` glossary entries, `std:doc`
documents) AND every entry of every loaded intersphinx inventory (`py:function`,
`std:label`, `rst:directive`, …). It also covers the project's filed INDEX ENTRIES as
`index:entry` hits (from `.. index::`, the `:index:` role and glossary terms) — those
are findable LOCATIONS rather than referenceable targets, so they carry a null
`role_snippet` and answer "is this phrase already indexed, and where": one hit per
distinct entry text, at its first occurrence, with `path` + `line`. Use this before
writing any `:ref:` / `:term:` / `:doc:` / `:external:` reference, instead of guessing a
target name. Matching is AND-matched keyword search, so pass a FEW SPECIFIC KEYWORDS or
the exact name — `installation`, `numpy.add`, `needs_types` — not a sentence; the exact
name ranks first, a partial name completes by prefix (`numpy.ad` finds `numpy.add`),
`-word` excludes and `"a phrase"` requires all its words. When a local target and an
inventory entry share a name, the LOCAL one takes that top slot (the rule the build
resolves by) and the external one ranks among the keyword matches below it — so read
each hit's `scope`/`locality` rather than assuming the first hit is the one you meant.
Each hit carries `name` (what goes inside the backticks), `kind` (the Sphinx object
type), `scope` ("local", or the inventory's configured project name), `locality`
("local"/"external"), `title`, `path` + 1-based `line` for a local target, `url` for an
external one, and `role_snippet` — the exact reStructuredText to paste (`:ref:`label``,
`:term:`word``, `:doc:`/docname``, `:external+project:py:func:`name``). `role_snippet`
is null when the hit cannot be referenced by a role: an `index:entry` hit never can, and
an inventory entry cannot when no role of a built-in Sphinx domain reaches its object
type — ubcode then knows no `:external:` spelling for it (the common case is another
sphinx-needs project's `needs:need` entries: findable here, but write them as `:need:` /
query them with `query_cypher` instead). Filter with `kind` (a `domain:objtype` glob
such as `py:*` or `*:label`, or the shorthands `label`/`term`/`doc`/`index`) and `scope`
(`local`, `external`, or one project name). Pass an EMPTY `query` to LIST rather than
search: every reference, ordered by name, under those same filters and `limit` — an
empty query with `kind` set to `label` is "list this project's labels", which is the
question `corpus` can only answer with a count. The response also carries `corpus` (how
many references of each kind exist, so an empty hit list is distinguishable from an
empty project), `matched` (how many references the answer was drawn from BEFORE the
`kind`/`scope` filters and `limit` — how many the query matched, or how many the whole
corpus holds when the query was empty; so no hits with `matched: 7` means your filter
dropped every match, while `matched: 0` means there was nothing to drop: for a query
that it found nothing, for an empty query that the project has no references at all),
`truncated`, and `inventories` — one status per configured intersphinx project with
`loaded`, `entries` and an `error`; warn the user about a project whose `error` is set
before concluding a symbol does not exist, because that inventory contributed nothing.
The project is (re)indexed first, so results reflect the current source files. Pass
`file_path` (any file inside the project, to locate its ubproject.toml). Use this tool
for keyword EXPLORATION and for the inventory statuses; when you already know the
approximate name you want to write, call `resolve_reference` FIRST instead — it answers
whether that exact reference resolves, and a miss carries the same ranked suggestions
this tool would have given you.