Model Context Protocol (MCP) Server: tech.tresbien/drupal-code-query
The tech.tresbien/drupal-code-query MCP server provides access to information related to Drupal code. Its scope covers Drupal core API changes, contrib usage, upgrade readiness, patch verdicts, code search, and SQL. The server is available under the slug tech-tresbien-drupal-code-query and includes 16 tools.
🛠️ Key Features
Drupal core API changes
Contrib usage
Upgrade readiness
Patch verdicts
Code search
SQL
🚀 Use Cases
Verify how Drupal core API changes impact existing code
Assess contrib usage for upgrade readiness
Review patch verdicts and search relevant code
Query and analyze data with SQL
⚡ Developer Benefits
Structured access to code-related Drupal information
Tool coverage spanning core changes, contrib usage, and code search
⚠️ Limitations
No additional details provided about authentication, supported environments, or specific tool behaviors
Captured live from the server via tools/list.
check_patches
Does each composer patch still apply to the release the site installed, and is its fix already in that release? Send composer_json + composer_lock (plus patch_files for local patches) after composer update and every extra.patches entry is checked at its installed version in one call; do not download releases or run patch --dry-run yourself.
- Send a package's patches in the order composer.json declares them, and keep them together: they are judged in that order because a patch manager applies them in it, so a later patch is applied to what the earlier ones left. Reordering them changes the verdicts.
- items: up to 50 of {project, version, patch, title}, when you would rather name them yourself. project is the drupal.org machine name (webform) or composer name (drupal/webform), drupal or core for Drupal core; version is the composer version (1.13.0, 2.0.0-beta4, 11.4.5); patch is the diff text or its URL on www.drupal.org/files/issues or a git.drupalcode.org merge request .patch/.diff.
- Per item: tag (the release tag matched, or unknown_version), applies_at (the -p level at which git apply passes, null when none), fuzzy (applied only with reduced context and whitespace ignored, as GNU patch fuzz does: review it), hunks_failed (file, line, reason), reverse_applies (the patched lines are already in the tag), mr (merge-request state when the patch is an MR URL), merged_in_version, suggested.
- suggested is the verdict: merged (fix is in the release: drop the patch), applies (applies cleanly: keep it), conflicts (neither: call reroll_patch with that item, it returns the re-rolled diff), unknown (tag or patch unresolved; see error).
- next_step names the follow-up calls and the row count the report owes; counts is the verdict tally to check that table against. An item that could not be judged comes back suggested unknown with the reason in error: report it as unclear, never leave the row out.
- files_shipped on a result names the patch's files the release already carries when only some of them do: the patch is partly upstream and what is left is a re-roll, not a drop. A failed hunk carries moved_to when the release keeps that file under another path.
- next_step names the follow-up calls and the row count the report owes; counts is the verdict tally to check that table against. An item that could not be judged comes back suggested unknown with the reason in error: report it as unclear, never leave the row out.
- hunks_shipped and files_shipped name the parts of a patch the release already carries when only some of it does: a partly-merged patch, so what is left is a re-roll rather than a drop. A failed hunk carries moved_to when the release keeps that file under another path.
- next_step names the follow-up call when any item needs a re-roll. Each verdict is the result of running git apply against the release the site installed, so it replaces a hand dry-run rather than pointing at one.
- core_references: what the code the patch adds references in core, checked at target_core (defaults to the lock's core): flagged lists removed or moved classes and calls whose argument count no longer fits the target signature (parent::__construct is checked against the constructor the core parent class actually declares, walking core's extends chain), each with file, line, change_record and replacement; deprecated is a short side list; checked counts references the catalog knows. Scope: added lines, direct references only (extends, implements, trait use, new, static calls, parent::__construct). A patch that does not apply carries a note instead.
- Verdicts come from git apply --check against the tag's tree; an applies patch can still be wrong at runtime.
Parameters5
items
null | array
optional
Patches to check, up to 50: project (drupal.org machine name webform or composer name drupal/webform; drupal or core for Drupal core), version (the composer version installed, e.g. 1.13.0 or 11.4.5), patch (unified diff text, or a drupal.org / git.drupalcode.org patch URL), title (optional, echoed back). Leave empty to derive the items from composer_json + composer_lock.
composer_json
string
optional
composer.json contents as text (not a path): extra.patches becomes the item list, one per patch.
composer_lock
string
optional
composer.lock contents as text, not a path (or the slim {"packages":[{"name","version"}]} form): the installed version of each patched package.
patch_files
object
optional
Text of each local patch file named in extra.patches, keyed by the path as written (patchs/x.patch) or its base name. URL patches need nothing.
target_core
string
optional
Core version the code each patch adds is checked against (11.4.5). Defaults to composer.lock's drupal/core, else the version of a drupal item.
Raw schema
{
"type": "object",
"properties": {
"items": {
"type": [
"null",
"array"
],
"items": {
"type": "object",
"properties": {
"project": {
"type": "string"
},
"version": {
"type": "string"
},
"patch": {
"type": "string"
},
"title": {
"type": "string"
},
"resolutions": {
"type": [
"null",
"array"
],
"items": {
"type": "object",
"properties": {
"file": {
"type": "string"
},
"region": {
"type": "integer"
},
"choice": {
"type": "string"
},
"text": {
"type": "string"
},
"delete": {
"type": "boolean"
}
},
"required": [
"file",
"region"
],
"additionalProperties": false
}
}
},
"required": [
"project",
"version",
"patch"
],
"additionalProperties": false
},
"description": "Patches to check, up to 50: project (drupal.org machine name webform or composer name drupal/webform; drupal or core for Drupal core), version (the composer version installed, e.g. 1.13.0 or 11.4.5), patch (unified diff text, or a drupal.org / git.drupalcode.org patch URL), title (optional, echoed back). Leave empty to derive the items from composer_json + composer_lock."
},
"composer_json": {
"type": "string",
"description": "composer.json contents as text (not a path): extra.patches becomes the item list, one per patch."
},
"composer_lock": {
"type": "string",
"description": "composer.lock contents as text, not a path (or the slim {\"packages\":[{\"name\",\"version\"}]} form): the installed version of each patched package."
},
"patch_files": {
"type": "object",
"description": "Text of each local patch file named in extra.patches, keyed by the path as written (patchs/x.patch) or its base name. URL patches need nothing.",
"additionalProperties": {
"type": "string"
}
},
"target_core": {
"type": "string",
"description": "Core version the code each patch adds is checked against (11.4.5). Defaults to composer.lock's drupal/core, else the version of a drupal item."
}
},
"additionalProperties": false
}
describe_dataset
Learn the dataset before writing SQL for query_dataset; call with no arguments first. Returns text. Prefer a typed tool when one answers the question.
- No arguments: every view with one line, the join map, the four invariants (dev-branch isolation, no SUM(usage), *_seq compares, adoption polarity), the gotchas behind empty results (fqn forms, placeholders, machine names), the recipe index.
- view=<name>, or views=[…] for several in one call: columns with types and descriptions, an example filter that returns rows, the joins that reach the view.
- recipe=<id>: a ready-to-run statement for a common question (who uses a symbol, deprecated symbols per project, a change record's adoption, symbols deprecated between minors, one project's deprecated uses, change records between minors); substitute the placeholders and run it with query_dataset.
Parameters3
view
string
optional
One view name: its columns with types and descriptions, an example filter, and the joins that reach it. Leave empty for the overview.
views
null | array
optional
Several view names at once (up to 8), documented one after the other in one call.
recipe
string
optional
One recipe id from the overview's index: a ready-to-run query_dataset statement with its placeholders explained.
Raw schema
{
"type": "object",
"properties": {
"view": {
"type": "string",
"description": "One view name: its columns with types and descriptions, an example filter, and the joins that reach it. Leave empty for the overview."
},
"views": {
"type": [
"null",
"array"
],
"items": {
"type": "string"
},
"description": "Several view names at once (up to 8), documented one after the other in one call."
},
"recipe": {
"type": "string",
"description": "One recipe id from the overview's index: a ready-to-run query_dataset statement with its placeholders explained."
}
},
"additionalProperties": false
}
get_change_record
Return one core change record in full: title, flavour, target version, machine-checkable tracks, linked issues, linked core symbols, and contrib adoption counts. To find records use list_change_records or what_changed's change_record_nids; for one symbol's detail use lookup_core_symbol.
- Flavour: coming-break (API going away), coming-new-API (API added), landed-but-still-relevant (shipped), policy-only (no code target, no adoption).
- Adoption counts contrib development branches only. legacy = still calls the going-away side (not adopted); migrated = moved.
- Returns counts across every affected project plus a ranked head, outstanding first. For one project use project_upgrade_report rather than raising top_projects.
- symbols: the core API the record is about, role from (going away) or to (replacement); use the fqn with lookup_core_symbol. Empty means the catalog links no symbol, which is common.
Parameters2
nid
integer
required
The change record's drupal.org node id, for example 3581981.
top_projects
integer
optional
How many impacted projects to return in the ranked head. Defaults to 10, capped at 40.
Raw schema
{
"type": "object",
"properties": {
"nid": {
"type": "integer",
"description": "The change record's drupal.org node id, for example 3581981."
},
"top_projects": {
"type": "integer",
"description": "How many impacted projects to return in the ranked head. Defaults to 10, capped at 40."
}
},
"required": [
"nid"
],
"additionalProperties": false
}
list_change_records
List the core change records (the human write-ups) targeting a range of core versions, tagged by flavour and ranked, with counts. For the symbol-level diff use what_changed; for one record in full use get_change_record.
- Bounds inclusive; forms 11.2, 11.2.x, 11.2.0, 11, 11.x; a bare major covers every minor of it.
- Flavours: coming-break (an API going away), coming-new-API (one being added), landed-but-still-relevant (already shipped). policy-only records target no version and never appear here.
- project narrows to one maintainer's view: each record then carries how many of that project's development branches are still on the legacy side.
- Returns the count of every record in range plus a ranked head; each entry carries its nid for get_change_record.
Parameters4
from
string
required
Lower core version bound, inclusive. Write it the way people say it: 11.2, 11.2.x, 11.2.0, 11, or 11.x. A bare major (11) covers every minor of it.
to
string
required
Upper core version bound, inclusive. Same forms as from.
project
string
optional
Optional contrib project machine name. Narrows the list to records a development branch of that project still matches.
limit
integer
optional
How many records to return in the head, 1 to 40 (default 15); above the cap the call is refused.
Raw schema
{
"type": "object",
"properties": {
"from": {
"type": "string",
"description": "Lower core version bound, inclusive. Write it the way people say it: 11.2, 11.2.x, 11.2.0, 11, or 11.x. A bare major (11) covers every minor of it."
},
"to": {
"type": "string",
"description": "Upper core version bound, inclusive. Same forms as from."
},
"project": {
"type": "string",
"description": "Optional contrib project machine name. Narrows the list to records a development branch of that project still matches."
},
"limit": {
"type": "integer",
"description": "How many records to return in the head, 1 to 40 (default 15); above the cap the call is refused."
}
},
"required": [
"from",
"to"
],
"additionalProperties": false
}
list_class_relations
List the contrib classes that extend, implement or use a core class, interface or trait, one row per edge with file and line, paged: "who subclasses FormBase". Call-site counts: list_symbol_users; the symbol's status: lookup_core_symbol.
- target with or without the leading backslash; unknown → refused, pointing at lookup_core_symbol (a fragment lists candidates). rel_kind narrows to one of extends, implements, uses_trait; project to one machine name.
- Development branches only. edge_count = every edge, project_count = projects, count = edges after the per_project cap (default 3 per project per page, so "top projects" reads off one page; project_edges on each row is the full count; per_project=50 with project= lists one project in full).
- Rows by project installs, project, source_fqn: project, branch, installs, source_fqn, rel_kind, file, line, project_edges. limit ≤ 100; next_offset absent on the last page.
Parameters6
target
string
required
The core class, interface or trait, with or without the leading backslash: Drupal\Core\Form\FormBase.
rel_kind
string
optional
extends, implements, or uses_trait; any when absent.
project
string
optional
Restrict to one project machine name.
per_project
integer
optional
Edges shown per project on a page, 1 to 50 (default 3), so one project with many subclasses does not fill the page; each row carries project_edges, the project's full count.
limit
integer
optional
Rows per page, 1 to 100 (default 50).
offset
integer
optional
Skip this many rows in the same ranked order; pass the previous page's next_offset.
Raw schema
{
"type": "object",
"properties": {
"target": {
"type": "string",
"description": "The core class, interface or trait, with or without the leading backslash: Drupal\\Core\\Form\\FormBase."
},
"rel_kind": {
"type": "string",
"description": "extends, implements, or uses_trait; any when absent."
},
"project": {
"type": "string",
"description": "Restrict to one project machine name."
},
"per_project": {
"type": "integer",
"description": "Edges shown per project on a page, 1 to 50 (default 3), so one project with many subclasses does not fill the page; each row carries project_edges, the project's full count."
},
"limit": {
"type": "integer",
"description": "Rows per page, 1 to 100 (default 50)."
},
"offset": {
"type": "integer",
"description": "Skip this many rows in the same ranked order; pass the previous page's next_offset."
}
},
"required": [
"target"
],
"additionalProperties": false
}
list_projects
List drupal.org projects after filters, largest install base first, paged: "top modules without an 11.4 branch", "themes over 10k installs without security coverage". One project: project_profile; its pending records: project_upgrade_report.
- Filters combine: type, min_installs, security (covered, not-covered, revoked), core_minor (+ missing), era (d8plus, pre_d8); all apply to count and rows.
- core_minor reads the composer constraint of the NEWEST RELEASE on each development branch (drupal.org metadata, composer/semver; a minor counts when any patch of it satisfies); an unreleased tip change is not seen. missing=true keeps projects with no branch declaring it — the upgrade gap list.
- Row: machine_name, title, type, installs, security, status, dev_branches (branch, installs, core_minors, era), latest_release. count = projects; next_offset absent on the last page.
Filter: a core minor in 11.4.x form that the newest release on some development branch declares support for (composer constraint from drupal.org); with missing=true, projects with NO such branch.
missing
boolean
optional
Invert core_minor: keep projects whose development branches do not declare it.
era
string
optional
Filter: d8plus (Drupal 8+ code) or pre_d8, on any development branch.
limit
integer
optional
Projects per page, 1 to 100 (default 50).
offset
integer
optional
Skip this many projects in the same ranked order; pass the previous page's next_offset.
Raw schema
{
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Filter: project_module, project_theme, project_distribution, project_general, project_theme_engine, project_translation."
},
"min_installs": {
"type": "integer",
"description": "Filter: reported install base at or above this."
},
"security": {
"type": "string",
"description": "Filter: security advisory coverage — covered, not-covered, revoked."
},
"core_minor": {
"type": "string",
"description": "Filter: a core minor in 11.4.x form that the newest release on some development branch declares support for (composer constraint from drupal.org); with missing=true, projects with NO such branch."
},
"missing": {
"type": "boolean",
"description": "Invert core_minor: keep projects whose development branches do not declare it."
},
"era": {
"type": "string",
"description": "Filter: d8plus (Drupal 8+ code) or pre_d8, on any development branch."
},
"limit": {
"type": "integer",
"description": "Projects per page, 1 to 100 (default 50)."
},
"offset": {
"type": "integer",
"description": "Skip this many projects in the same ranked order; pass the previous page's next_offset."
}
},
"additionalProperties": false
}
list_symbol_users
List every contrib project using one core symbol or any symbol in a class of symbols, one row per project, paged — and the matched symbols themselves. lookup_core_symbol answers one symbol with a head of 30.
- Pass fqn, or filters: kind, status (deprecated, scheduled_removal, removed), subsystem, removal_in (13.0), deprecated_in (11.4). "Top modules still calling a method going away in 13.0" = kind=method, status=scheduled_removal, removal_in=13.0. Filters take public symbols only.
- Development branches only, evidence rollup as of evidence_built_at. count = projects; symbol_count = matched symbols, listed in symbols_matched (≤ matched, most-used first, with stamps, projects_using, change_record_nids).
- Rows by installs, occurrences, name: project, title, installs, branch, branches, symbol_count, occurrences, symbols (heaviest first, each fqn, kind, removal_in, occurrences, files, change_record_nids → get_change_record). next_offset absent on the last page.
Parameters10
fqn
string
optional
One core symbol, as lookup_core_symbol takes it. Pass this or a filter.
kind
string
optional
Filter: one symbol kind (function, method, class, library, hook, service …).
Filter: a core subsystem name (Entity API, Views), case-insensitive; subsystem_coupling with no arguments lists them.
removal_in
string
optional
Filter: removal stamp in this core minor (13.0); with status=scheduled_removal = still present, going away then.
deprecated_in
string
optional
Filter: symbols deprecated in this core minor (11.4).
limit
integer
optional
Projects per page, 1 to 100 (default 50).
offset
integer
optional
Skip this many projects; pass the previous next_offset.
symbols
integer
optional
Symbols per project row, 1 to 30 (default 10), heaviest first.
matched
integer
optional
Matched symbols listed on the envelope, 1 to 50 (default 20).
Raw schema
{
"type": "object",
"properties": {
"fqn": {
"type": "string",
"description": "One core symbol, as lookup_core_symbol takes it. Pass this or a filter."
},
"kind": {
"type": "string",
"description": "Filter: one symbol kind (function, method, class, library, hook, service …)."
},
"status": {
"type": "string",
"description": "Filter: deprecated | scheduled_removal (still present) | removed (gone)."
},
"subsystem": {
"type": "string",
"description": "Filter: a core subsystem name (Entity API, Views), case-insensitive; subsystem_coupling with no arguments lists them."
},
"removal_in": {
"type": "string",
"description": "Filter: removal stamp in this core minor (13.0); with status=scheduled_removal = still present, going away then."
},
"deprecated_in": {
"type": "string",
"description": "Filter: symbols deprecated in this core minor (11.4)."
},
"limit": {
"type": "integer",
"description": "Projects per page, 1 to 100 (default 50)."
},
"offset": {
"type": "integer",
"description": "Skip this many projects; pass the previous next_offset."
},
"symbols": {
"type": "integer",
"description": "Symbols per project row, 1 to 30 (default 10), heaviest first."
},
"matched": {
"type": "integer",
"description": "Matched symbols listed on the envelope, 1 to 50 (default 20)."
}
},
"additionalProperties": false
}
lookup_core_symbol
Answer "is this core symbol safe to use, and who still uses it" for one symbol; a name that resolves to nothing returns the catalog names containing it instead. Every user in pages: list_symbol_users. Changed between versions: what_changed. Code text: search_code.
- fqn forms: namespaced with or without the leading backslash, a bare function or constant name, a prefixed pseudo-symbol (fn:check_markup, core:hook:preprocess_page, core:library:claro/drupal.shortcut), or a fragment (fromRoute, EntityInterface).
- Resolved: symbol with status flags: removed (gone), scheduled_removal (@deprecated names the version, still present), deprecated, internal, placeholder (catalog row no scanner located); audience contrib | test_support | test | theme. usage: contrib development branches only, evidence rollup as of evidence_built_at; projects and branches counts plus top_projects (≤ 30) by install base. change_records: records that touched the symbol, with nid for get_change_record.
- Not resolved (found false): count = catalog names containing the fragment (case-insensitive, public symbols; kind narrows), candidates a head of ≤ candidates with fqn, kind, subsystem, stamps, projects_using, change_record_nids; names starting with the fragment first, then by projects_using. Call again with one fqn.
Parameters4
fqn
string
required
Core symbol name, or part of one. Namespaced names with or without the leading backslash (Drupal\Core\Entity\EntityInterface, Drupal\Core\Recipe\RecipeRunner::installModule); a bare function or constant name (check_markup, DRUPAL_DISABLED); a prefixed pseudo-symbol as what_changed reports it (fn:check_markup, core:hook:preprocess_page, core:library:claro/drupal.shortcut); or a fragment of 3+ characters (fromRoute), which returns candidates when no symbol has that exact name.
top_projects
integer
optional
How many using projects to return in the ranked head. Defaults to 10, capped at 30.
candidates
integer
optional
When the name resolves to no symbol: how many catalog names containing it to return, 1 to 50 (default 10).
kind
string
optional
Restrict candidates to one symbol kind (class, method, function, hook, library, service …).
Raw schema
{
"type": "object",
"properties": {
"fqn": {
"type": "string",
"description": "Core symbol name, or part of one. Namespaced names with or without the leading backslash (Drupal\\Core\\Entity\\EntityInterface, Drupal\\Core\\Recipe\\RecipeRunner::installModule); a bare function or constant name (check_markup, DRUPAL_DISABLED); a prefixed pseudo-symbol as what_changed reports it (fn:check_markup, core:hook:preprocess_page, core:library:claro/drupal.shortcut); or a fragment of 3+ characters (fromRoute), which returns candidates when no symbol has that exact name."
},
"top_projects": {
"type": "integer",
"description": "How many using projects to return in the ranked head. Defaults to 10, capped at 30."
},
"candidates": {
"type": "integer",
"description": "When the name resolves to no symbol: how many catalog names containing it to return, 1 to 50 (default 10)."
},
"kind": {
"type": "string",
"description": "Restrict candidates to one symbol kind (class, method, function, hook, library, service …)."
}
},
"required": [
"fqn"
],
"additionalProperties": false
}
project_profile
Answer "what is this contrib project, how big is it, which core versions does it support, what moves with it" for one drupal.org project. Pending change records: project_upgrade_report; core symbols it calls: list_symbol_users.
- project: title, type, status, security, installs. installs_by_version: install base per contrib version.
- dev_branches: each scanned development branch with installs, core_constraint (composer constraint of the newest release on that line, from drupal.org — not the unreleased tip), core_minors it admits (a minor counts when any patch satisfies), latest commit date, era. latest_core_minor = newest admitted.
- releases (newest first, ≤ releases, with core_compatibility and security), release_count, supported_branches. missing_sections names what the bundle could not fill.
- related (summary): requires / required_by counts + top 3, co_installed top 3 or a note, delta_12m per release line ("*" = total).
- include=["related"]: related becomes the full report: requires (every dependency, via info_yml or composer, core modules left out), required_by paged by installs (count, next_offset; dependents / dependents_offset), co_installed (rho, rank, method residual_spearman) or a note.
- include=["trend"]: trend = months (oldest first), series per release_line aligned with months (null = no count, "*" = total), lines with installs_now, share, delta_3m / delta_12m / delta_36m, peak, first_seen / last_seen. drupal.org month-end installs.
- Unknown project: error. An included section refuses when the bundle predates its view.
Parameters6
project
string
required
drupal.org project machine name, e.g. webform; the composer name drupal/webform is accepted.
releases
integer
optional
Releases to list, newest first, 1 to 30 (default 10). release_count says how many exist.
include
null | array
optional
Optional sections: trend (monthly install series per release line) and related (full requires / required_by / co_installed lists, replacing the related summary).
months
integer
optional
With include trend: months of history, 1 to 120 (default 36), ending at the latest month in the data.
dependents
integer
optional
With include related: dependents per page in required_by, 1 to 100 (default 20).
dependents_offset
integer
optional
With include related: dependents to skip; pass required_by.next_offset from the previous call.
Raw schema
{
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "drupal.org project machine name, e.g. webform; the composer name drupal/webform is accepted."
},
"releases": {
"type": "integer",
"description": "Releases to list, newest first, 1 to 30 (default 10). release_count says how many exist."
},
"include": {
"type": [
"null",
"array"
],
"items": {
"type": "string"
},
"description": "Optional sections: trend (monthly install series per release line) and related (full requires / required_by / co_installed lists, replacing the related summary)."
},
"months": {
"type": "integer",
"description": "With include trend: months of history, 1 to 120 (default 36), ending at the latest month in the data."
},
"dependents": {
"type": "integer",
"description": "With include related: dependents per page in required_by, 1 to 100 (default 20)."
},
"dependents_offset": {
"type": "integer",
"description": "With include related: dependents to skip; pass required_by.next_offset from the previous call."
}
},
"required": [
"project"
],
"additionalProperties": false
}
project_upgrade_report
Answer "is this contrib project ready for a target core version, and what work is left". For one record's detail use get_change_record; for a symbol's users use list_symbol_users.
- A record applies when it targets the target version or earlier and a development branch of the project matched one of its tracks.
- Verdict per record: outstanding (a branch still calls the going-away side), in_progress (some branches moved), adopted (all moved).
- Returns counts by verdict, then a ranked head of outstanding records with file and line evidence from the project's own code.
- Each entry carries how many other projects are outstanding on the same record (wait for upstream or not) and its nid for get_change_record.
- removed_symbols: core symbols removed by the target, or scheduled for removal at or before it, that the project's development branches still reference, with occurrences, files and the replacement when the catalog names one. These need no change record, so they hold the breakage a record-only view misses: a class moved to another module, a service dropped. removed_symbol_count is the whole set; the head is newest removal first, then by occurrences; test and test-support symbols are left out.
Parameters3
project
string
required
Contrib project machine name as it appears on drupal.org, for example webform; the composer name drupal/webform is accepted. Packages from other vendors (acquia/cohesion) are not in the dataset.
target_version
string
required
Target core version. Write it the way people say it: 11.2, 11.2.x, 11.2.0, 11, or 11.x. A bare major (11) covers every minor of it.
limit
integer
optional
How many outstanding records to return with evidence, 1 to 15 (default 8); above the cap the call is refused.
Raw schema
{
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "Contrib project machine name as it appears on drupal.org, for example webform; the composer name drupal/webform is accepted. Packages from other vendors (acquia/cohesion) are not in the dataset."
},
"target_version": {
"type": "string",
"description": "Target core version. Write it the way people say it: 11.2, 11.2.x, 11.2.0, 11, or 11.x. A bare major (11) covers every minor of it."
},
"limit": {
"type": "integer",
"description": "How many outstanding records to return with evidence, 1 to 15 (default 8); above the cap the call is refused."
}
},
"required": [
"project",
"target_version"
],
"additionalProperties": false
}
query_dataset
Run read-only DuckDB SELECTs over the dataset behind the other tools, for a question none of them asks. Call describe_dataset first (it lists the 29 views, their columns, joins and recipes); prefer a typed tool when one fits.
- statements=[…] runs up to 5 in one call, one result or error each — explore in one round trip.
- Result: columns, rows as arrays, ≤ max_rows (≤ 500, default 100) and ≤ 16 KB; truncated → aggregate, filter, LIMIT/OFFSET. Single SELECT (or SHOW, DESCRIBE, FROM-first), no semicolon, 15 s limit, nothing outside the bundle.
- Dev-branch isolation: JOIN contrib_branch AND filter kind = 'dev_branch' AND project <> 'drupal' before counting projects (change_record_adoption, symbol_usage, core_symbol_evidence carry release tags too; core_symbol_evidence is the full rollup, symbol_usage its string-scan subset).
- Adoption polarity: legacy = still on the old API (NOT adopted); migrated = adopted. Versions are text: compare *_seq integers (major*1000+minor). Never SUM(usage) across branch rows.
- Errors name the views, the columns of the views you used, or the join map; an empty result over a backslash-less fqn carries a hint.
- The same views are downloadable as parquet under https://api.tresbien.tech/data/docs; its cookbook targets api.duckdb plus prelude views this mirror does not carry, so take recipes from describe_dataset.
Parameters3
sql
string
optional
One DuckDB SELECT over the views listed in the description. SHOW TABLES, DESCRIBE <view> and FROM-first syntax also work. No semicolon; one statement. Use statements for several at once.
statements
null | array
optional
Up to 5 statements run in one call (explore in one round trip): results come back in order under results, each with its own rows or error; the byte budget is shared. Use instead of sql when you have more than one.
max_rows
integer
optional
Rows to return per statement, 1 to 500 (default 100). The result byte budget may cut a wide result sooner; truncated says so either way.
Raw schema
{
"type": "object",
"properties": {
"sql": {
"type": "string",
"description": "One DuckDB SELECT over the views listed in the description. SHOW TABLES, DESCRIBE <view> and FROM-first syntax also work. No semicolon; one statement. Use statements for several at once."
},
"statements": {
"type": [
"null",
"array"
],
"items": {
"type": "string"
},
"description": "Up to 5 statements run in one call (explore in one round trip): results come back in order under results, each with its own rows or error; the byte budget is shared. Use instead of sql when you have more than one."
},
"max_rows": {
"type": "integer",
"description": "Rows to return per statement, 1 to 500 (default 100). The result byte budget may cut a wide result sooner; truncated says so either way."
}
},
"additionalProperties": false
}
reroll_patch
Re-roll one composer patch that check_patches reported conflicts: a 3-way merge of the patch onto the installed release, returned as a new diff. Call check_patches first for the whole list; call this per patch that came back conflicts.
- Input: project, version, patch (text or URL), title. One patch per call. A hand-made patch without index lines is merged from the newest tag it applies to (reroll.base).
- reroll.verified true means the service already ran the check a caller would run by hand; reroll.verified_by names it (the command, the -p level, the tag). Write reroll.patch to the patch file and move on: no git apply --check, no patch --dry-run, no pristine copy of the release to diff against. composer install is the test.
- A conflicts result is not the end: send the same project, version and patch again with resolutions, one per region of reroll.conflicts[].hunks ({file, region, choice: release|patch}, {file, region, text}, or {file, region, delete: true}), and the service re-merges with your decisions and apply-checks the diff. What comes back is the finished patch file: write it and run composer install. Deciding regions this way replaces reading the release files, editing the patch by hand and dry-running it.
- reroll.status: clean (every file merged and the diff apply-checked against the release, reroll.verified true: write reroll.patch as the new patch file, no re-test needed; an empty reroll.patch with reroll.note means the release already carries the change and suggested is merged), conflicts (reroll.patch holds the hunks that merged cleanly, apply-checked when reroll.verified is true; each file in reroll.conflicts carries hunks with the three sides of every region: release = what the release has, base = what the patch was written against, patch = what the patch wants, plus release_line and release_context, the release file's numbered lines around the region. Write the missing hunks from those and append them to reroll.patch; no download, file read or dry-run needed), unavailable (no index lines and no recent tag takes the patch; re-roll by hand from hunks_failed).
- reroll.patch paths are relative to the repository root: contrib patches apply at -p1, core patches carry core/ and apply at -p2 from web/core. patch_truncated means the diff was cut to the result budget; POST /v1/patch/check on api.tresbien.tech with reroll: true returns it whole.
- A patch that still applies (applies_at set) or is already in the release (merged) returns its verdict with reroll null.
- core_references: as on check_patches, read from the re-rolled diff when the merge was clean: removed or moved core classes and calls whose argument count no longer fits the target signature, at target_core.
Parameters6
project
string
required
drupal.org machine name (webform) or composer name (drupal/webform); drupal or core for Drupal core.
version
string
required
The composer version installed, e.g. 1.13.0 or 11.4.5.
patch
string
required
Unified diff text, or a drupal.org / git.drupalcode.org patch URL. With the index lines git diff writes it is merged onto the release; without them (hand-made) it is merged from the newest tag it applies to, reported as reroll.base.
title
string
optional
Optional, echoed back.
target_core
string
optional
Core version the code the patch adds is checked against (11.4.5); defaults to version for a core patch.
resolutions
null | array
optional
Decisions for the conflicted regions of a previous call on the same project, version and patch: one entry per region, {file, region, choice} where region is the index in that file's conflicts[].hunks and choice is release or patch, {file, region, text} to put your own text there instead, or {file, region, delete: true} to empty the region. The service re-runs the merge with them and apply-checks the result. Regions you leave out keep the release side and come back in reroll.resolutions_missing.
Raw schema
{
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "drupal.org machine name (webform) or composer name (drupal/webform); drupal or core for Drupal core."
},
"version": {
"type": "string",
"description": "The composer version installed, e.g. 1.13.0 or 11.4.5."
},
"patch": {
"type": "string",
"description": "Unified diff text, or a drupal.org / git.drupalcode.org patch URL. With the index lines git diff writes it is merged onto the release; without them (hand-made) it is merged from the newest tag it applies to, reported as reroll.base."
},
"title": {
"type": "string",
"description": "Optional, echoed back."
},
"target_core": {
"type": "string",
"description": "Core version the code the patch adds is checked against (11.4.5); defaults to version for a core patch."
},
"resolutions": {
"type": [
"null",
"array"
],
"items": {
"type": "object",
"properties": {
"file": {
"type": "string"
},
"region": {
"type": "integer"
},
"choice": {
"type": "string"
},
"text": {
"type": "string"
},
"delete": {
"type": "boolean"
}
},
"required": [
"file",
"region"
],
"additionalProperties": false
},
"description": "Decisions for the conflicted regions of a previous call on the same project, version and patch: one entry per region, {file, region, choice} where region is the index in that file's conflicts[].hunks and choice is release or patch, {file, region, text} to put your own text there instead, or {file, region, delete: true} to empty the region. The service re-runs the merge with them and apply-checks the result. Regions you leave out keep the release side and come back in reroll.resolutions_missing."
}
},
"required": [
"project",
"version",
"patch"
],
"additionalProperties": false
}
scan_composer
Which of a site's drupal/* packages need a new release for a target core, and which have none? Send composer.lock (and composer.json) once instead of one query per package; the answer is the release scan for every package plus extra.patches as check_patches items. Constraints are read by composer's own semver library.
- composer_lock: the file, or the slim form {"packages":[{"name","version"}]} for drupal/* only. composer_json adds constraints and extra.patches. target_core: 11.4 or 11.4.5; empty scans against the installed core (target_is_installed), i.e. what can be updated without a core upgrade.
- A row whose installed version the release data does not carry says installed_unknown and offers no older release; installed_from_tag says the project's git tag answered instead. Neither is a finding about the site.
- A sub-module has no releases of its own: drupal.org packages it as a metapackage built from its project's release. Its row says submodule_of and carries that project's answer. Send composer_lock with each entry's type, and require plus extra.drupal.datestamp for a metapackage, and the pairing is made for you; the slim form without them reports the sub-module as an unknown project.
- rows, ranked problems first: no_release (no published release supports the target; latest_any and dev_branch say what exists), update (a compatible release the site does not have: latest, latest_core), unknown (not a drupal.org project in the bundle; the note says where its releases are), current (installed_supports and nothing newer needed; such a row carries package, installed and installed_supports only). counts covers every package even when rows are cut.
- candidates on every row that is not current: the newest three releases with version, core constraint, date and supports_target. That is what choosing a version needs; project_profile adds installs and branches, not more releases.
- patches: one item per extra.patches entry with the installed version filled in; pass them to check_patches after composer update (local paths need the file text). A patch declared on a package outside drupal/ gets no item and no row; outside_drupal names it.
- patches:true answers with plan instead: every patch judged against the release its package would install for target_core, so the join needs no second call. plan.counts is the verdict tally, plan.package_counts the scan tallies, plan.no_release the packages that block the target, plan.patches one row per patch needing a decision (conflicts, unknown, merged), and plan.applying the number that still apply. The scan rows themselves are not in a plan; call again without patches for them. A patch whose package has no release for the target is judged against the branch when the lock installs a dev version, and unknown otherwise with the reason in note; plan.warnings says when a package is in no_release because of something the site controls, naming the requirement to change: its own constraint forbids the release that would work, or its minimum stability does; a row's project for reroll_patch is its package without the drupal/ prefix.
- A plan never carries a re-rolled diff: plan.next_step names reroll_patch, one call per patch, when any patch no longer applies.
- bundle_date is when the release data was published; a release after it is invisible here, so confirm a no_release with composer show or drupal.org before acting.
Parameters6
composer_json
string
optional
The composer.json contents as text, not a path (require, require-dev, extra.patches). Optional: without it there are no constraints and no patches.
composer_lock
string
required
The composer.lock contents as text (not a path), or the slim form {"packages":[{"name":…,"version":…}]} with only drupal/* packages (a few KB instead of hundreds).
target_core
string
optional
Core version to move to, e.g. 11.4 or 11.4.5. Leave empty to scan against the core the site already runs: which packages have a newer release for it.
patches
boolean
optional
Judge the site's patches in the same call: each one against the release its package would install for target_core. Returns a verdict per patch instead of items to check yourself.
patch_files
object
optional
Text of each local patch file named in extra.patches, keyed by the path as written (patchs/x.patch) or its base name. Only needed with patches:true; URL patches need nothing.
installed_core
object
optional
What each installed release requires of core, keyed by composer name, read from the site's own vendor directory (composer show -f json <pkg>, or the drupal/core entry of its composer.json). The release data here can be months behind a project; a site cannot. Send it and the installed release is judged as it actually is.
Raw schema
{
"type": "object",
"properties": {
"composer_json": {
"type": "string",
"description": "The composer.json contents as text, not a path (require, require-dev, extra.patches). Optional: without it there are no constraints and no patches."
},
"composer_lock": {
"type": "string",
"description": "The composer.lock contents as text (not a path), or the slim form {\"packages\":[{\"name\":…,\"version\":…}]} with only drupal/* packages (a few KB instead of hundreds)."
},
"target_core": {
"type": "string",
"description": "Core version to move to, e.g. 11.4 or 11.4.5. Leave empty to scan against the core the site already runs: which packages have a newer release for it."
},
"patches": {
"type": "boolean",
"description": "Judge the site's patches in the same call: each one against the release its package would install for target_core. Returns a verdict per patch instead of items to check yourself."
},
"patch_files": {
"type": "object",
"description": "Text of each local patch file named in extra.patches, keyed by the path as written (patchs/x.patch) or its base name. Only needed with patches:true; URL patches need nothing.",
"additionalProperties": {
"type": "string"
}
},
"installed_core": {
"type": "object",
"description": "What each installed release requires of core, keyed by composer name, read from the site's own vendor directory (composer show -f json <pkg>, or the drupal/core entry of its composer.json). The release data here can be months behind a project; a site cannot. Send it and the installed release is judged as it actually is.",
"additionalProperties": {
"type": "string"
}
}
},
"required": [
"composer_lock"
],
"additionalProperties": false
}
search_code
Search the source of every indexed contrib project, plus core, for a code pattern: which files, or with by_repo which projects. Who-uses-a-core-symbol counts: list_symbol_users or lookup_core_symbol.
- query is a regex; set literal for exact text with ( [ ] . $ : or a space, and put r: f: lang: case: sym: b: terms in filters. repos takes machine names. The index runs RE2: no lookaround, no backreferences.
- .module, .install, .theme, .engine, .profile, .inc count as PHP (lang:php reaches them; sym: resolves inside them); no language filter is applied for you.
- Returns total_matches and total_files plus a head of files (repo, path, matching lines), limit ≤ 50. by_repo: (repo, file_count) rows from a pull of ≤ 1000 files, total_files the ceiling, truncated when cut. A parse error returns the index's own message.
Parameters6
query
string
required
Code to find; a regex unless literal is true. Filters may be inline when literal is false, else in filters. Example: hook_form_alter f:\.module$
literal
boolean
optional
true = query is exact text, not a regex: use it for text with ( [ ] $ . such as #[Hook( ; with literal, filters go in filters.
Restrict to these project machine names (one query).
limit
integer
optional
Files to return, default 20, cap 50; ignored with by_repo.
by_repo
boolean
optional
Return repos as (repo, file_count) instead of files; pulls up to 1000 files, total_files is the ceiling.
Raw schema
{
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Code to find; a regex unless literal is true. Filters may be inline when literal is false, else in filters. Example: hook_form_alter f:\\.module$"
},
"literal": {
"type": "boolean",
"description": "true = query is exact text, not a regex: use it for text with ( [ ] $ . such as #[Hook( ; with literal, filters go in filters."
},
"filters": {
"type": "string",
"description": "Zoekt filters appended as written: r:<repo regex> f:<path regex> lang:<language> b:<branch> sym:<symbol> case:yes."
},
"repos": {
"type": [
"null",
"array"
],
"items": {
"type": "string"
},
"description": "Restrict to these project machine names (one query)."
},
"limit": {
"type": "integer",
"description": "Files to return, default 20, cap 50; ignored with by_repo."
},
"by_repo": {
"type": "boolean",
"description": "Return repos as (repo, file_count) instead of files; pulls up to 1000 files, total_files is the ceiling."
}
},
"required": [
"query"
],
"additionalProperties": false
}
subsystem_coupling
Answer "what else tends to fail when a merge request touches this core subsystem" from CI history over ~9,000 core MRs; with issue_nid, "which subsystems does this issue touch and what co-fails with them". No arguments: the directory of subsystem names (shared by core_symbol.subsystem and list_symbol_users subsystem=).
- subsystem resolves a name (Views, case-insensitive) or a bucket (module:views, core_lib:Entity); unknown → refused with the nearest names.
- Edge: touched_bucket → failing_bucket (+ subsystem), lift, co_fail_count, touched_count, failed_count, total_mrs; lift = P(fail | touched) − P(fail | any MR). Self edges left out.
- Floors min_lift (0.05) and min_co_fail (3) drop noise; count = edges above them, edges ≤ limit (≤ 50) by lift. When nothing clears the floors the strongest edges are listed with a note.
- issue_nid: touched_subsystems first; a note explains an issue with no MR file data.
Parameters5
subsystem
string
optional
A core subsystem name (Views, Entity API) or a path bucket (module:views, core_lib:Entity). Leave both subsystem and issue_nid empty to get the directory of subsystem names.
issue_nid
integer
optional
A drupal.org core issue nid: reports the subsystems its merge request touches, then the coupling edges from each.
min_lift
number
optional
Keep edges with lift at or above this (default 0.05, the survey report's floor).
min_co_fail
integer
optional
Keep edges seen in at least this many merge requests (default 3).
limit
integer
optional
Edges to return, 1 to 50 (default 20). count says how many pass the floors.
Raw schema
{
"type": "object",
"properties": {
"subsystem": {
"type": "string",
"description": "A core subsystem name (Views, Entity API) or a path bucket (module:views, core_lib:Entity). Leave both subsystem and issue_nid empty to get the directory of subsystem names."
},
"issue_nid": {
"type": "integer",
"description": "A drupal.org core issue nid: reports the subsystems its merge request touches, then the coupling edges from each."
},
"min_lift": {
"type": "number",
"description": "Keep edges with lift at or above this (default 0.05, the survey report's floor)."
},
"min_co_fail": {
"type": "integer",
"description": "Keep edges seen in at least this many merge requests (default 3)."
},
"limit": {
"type": "integer",
"description": "Edges to return, 1 to 50 (default 20). count says how many pass the floors."
}
},
"additionalProperties": false
}
what_changed
Answer "what changed in the core API between two core versions": symbols added, deprecated, removed, as ranked groups with counts. One symbol: lookup_core_symbol. Human write-ups: list_change_records. Every row of one bucket: flat=true with bucket, paged; every row of the whole diff: the result's bulk block (attach + sql for the published catalog, rows to disk not context).
- Bounds inclusive; forms 11.2, 11.2.x, 11.2.0, 11, 11.x; same version twice = one minor; upgrading 11.2 → 11.4 means from=11.3. Buckets are independent.
- Grouped (default): a head entry is a symbol group (grouped_by: change_record | name_prefix | singleton); head counts groups (≤ 5), count symbols, group_count groups; members ≤ 3 per group, highest usage first; group fields appear only when every member agrees. Rank: max_projects_using desc, then member_count magnitude, tier, count, key (ranked_by). head_covers_all_callers false → narrow with kind / min_projects_using or page with offset.
- Flat (flat=true, bucket=added, deprecated, removed): rows by projects_using then fqn: fqn, kind, subsystem, stamp, projects_using, removal_in, removal_kind, replacement when the catalog names one, change_record_nids. count = public symbols in the bucket after filters; limit ≤ 100, next_offset absent on the last page.
- Filters apply to every count. internal_excluded and test_theme_excluded are counted apart and left out; unplaceable_symbols have no minor in their stamp.
- Usage = contrib development branches only, evidence rollup as of usage_evidence_built_at; 0 = no caller observed.
- change_record_nids → get_change_record. removal_kind observed = gone, scheduled = promised and still present (say "scheduled for removal in 13.0").
Parameters9
from
string
required
Lower core version bound, inclusive: 11.2, 11.2.x, 11.2.0, 11, 11.x. Upgrading from 11.2 means from=11.3.
to
string
required
Upper bound, inclusive, same forms.
head
integer
optional
Groups per bucket, 1 to 5 (default 5). Ignored when flat is true.
kind
string
optional
Restrict to one symbol kind (method, class, function, hook, service, library …); applies to counts too.
min_projects_using
integer
optional
Drop symbols fewer than this many contrib projects call on a development branch; applies to counts too.
offset
integer
optional
Skip this many groups per bucket (grouped) or rows (flat), in ranked order. Flat: pass the previous page's next_offset.
flat
boolean
optional
true = one bucket as flat symbol rows, paged, instead of three buckets of ranked groups. Needs bucket.
bucket
string
optional
With flat: which side of the diff to list, added, deprecated, or removed; one bucket per call.
limit
integer
optional
With flat: rows per page, 1 to 100 (default 50). A page of very long names may come back shorter; next_offset always says where the next page starts.
Raw schema
{
"type": "object",
"properties": {
"from": {
"type": "string",
"description": "Lower core version bound, inclusive: 11.2, 11.2.x, 11.2.0, 11, 11.x. Upgrading from 11.2 means from=11.3."
},
"to": {
"type": "string",
"description": "Upper bound, inclusive, same forms."
},
"head": {
"type": "integer",
"description": "Groups per bucket, 1 to 5 (default 5). Ignored when flat is true."
},
"kind": {
"type": "string",
"description": "Restrict to one symbol kind (method, class, function, hook, service, library …); applies to counts too."
},
"min_projects_using": {
"type": "integer",
"description": "Drop symbols fewer than this many contrib projects call on a development branch; applies to counts too."
},
"offset": {
"type": "integer",
"description": "Skip this many groups per bucket (grouped) or rows (flat), in ranked order. Flat: pass the previous page's next_offset."
},
"flat": {
"type": "boolean",
"description": "true = one bucket as flat symbol rows, paged, instead of three buckets of ranked groups. Needs bucket."
},
"bucket": {
"type": "string",
"description": "With flat: which side of the diff to list, added, deprecated, or removed; one bucket per call."
},
"limit": {
"type": "integer",
"description": "With flat: rows per page, 1 to 100 (default 50). A page of very long names may come back shorter; next_offset always says where the next page starts."
}
},
"required": [
"from",
"to"
],
"additionalProperties": false
}