renovate-mcp 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,6 +13,7 @@ An MCP server for designing [Renovate](https://github.com/renovatebot/renovate)
13
13
  - [What this is NOT](#what-this-is-not)
14
14
  - [Requirements](#requirements)
15
15
  - [Install](#install)
16
+ - [Platform setup](#platform-setup)
16
17
  - [Example prompts](#example-prompts)
17
18
  - [Example session](#example-session)
18
19
  - [Development](#development)
@@ -21,17 +22,20 @@ An MCP server for designing [Renovate](https://github.com/renovatebot/renovate)
21
22
 
22
23
  ## What it does
23
24
 
24
- Eight tools plus a preset reference:
25
+ Eleven tools plus a preset reference:
25
26
 
26
27
  | Tool | Purpose |
27
28
  | --- | --- |
28
- | `check_setup` | Report Renovate CLI + validator availability, versions, and install hints. Also runs at startup. |
29
+ | `check_setup` | Report Renovate CLI + validator availability, versions, and install hints. Also surfaces a `platformContext` block (`RENOVATE_PLATFORM` / `RENOVATE_ENDPOINT` values, token-presence booleans, the platform `dry_run` would pick when its input is unset, and notes about likely misconfigurations) so callers can verify env before invoking `dry_run`. Token values are never echoed — only presence booleans. Also runs at startup. |
30
+ | `get_version` | Report the renovate-mcp server version and whether it's a released build (running from `node_modules`) or a local/dev build (typically launched via `command: node` against a checkout). |
29
31
  | `read_config` | Locate and parse a repo's Renovate config (`renovate.json`, `renovate.json5`, `.renovaterc*`, `package.json#renovate`, …) in priority order. |
30
32
  | `resolve_config` | Expand every `extends` preset offline. Opt in to fetching `github>` / `gitlab>` presets over HTTPS with `externalPresets: true`. |
33
+ | `explain_config` | Inverse of `resolve_config`: walk the same preset tree but annotate every leaf field with the chain of presets that touched it. Each leaf is `{ value, setBy }` where `setBy` lists every contribution in merge order — last entry wins for scalars; for arrays each entry adds its own slice. Same offline-by-default behaviour and same `externalPresets` / `endpoint` / `platform` opt-ins as `resolve_config`. |
31
34
  | `preview_custom_manager` | Preview a `customManagers` (regex) entry against a local repo — shows file/line hits and extracted dep info. Offline. |
32
35
  | `validate_config` | Run `renovate-config-validator` against a file or inline object. |
33
- | `lint_config` | Semantic lint for Renovate-specific footguns schema validation misses (mostly malformed `/…/` regex patterns). Offline. |
34
- | `dry_run` | Run Renovate with `--dry-run` and return the structured JSON report. Defaults to `--platform=local` against `repoPath`; pass `platform` + `endpoint` + `token` + `repository` to run as a real GitHub/GitLab client (needed when the config extends `local>` presets on a private host). No PRs, no pushes. Emits MCP progress notifications during the run when the caller supplies a `progressToken`. |
36
+ | `lint_config` | Semantic lint for Renovate-specific footguns schema validation misses — malformed `/…/` regex patterns plus unknown manager names in `matchManagers` / `excludeManagers`. Offline. |
37
+ | `dry_run` | Run Renovate with `--dry-run` and return the structured JSON report. Defaults to `--platform=local` against `repoPath`; pass `platform` + `endpoint` + `token` + `repository` to run as a real GitHub/GitLab client (needed when the config extends `local>` presets on a private host). When `platform` is not passed, the tool reads `RENOVATE_PLATFORM` from the MCP server's env before defaulting to `local`. When `token` is not passed, falls back to `RENOVATE_TOKEN` from MCP env, then to `GITLAB_TOKEN` (when `platform=gitlab`) or `GITHUB_TOKEN` (when `platform=github`) — auto-translated to `RENOVATE_TOKEN` for the spawned Renovate CLI, since Renovate itself only reads that one var. `repository` accepts GitHub-style `owner/repo` and GitLab nested-group paths like `group/subgroup/project`. `endpoint`/`token` also flow through in local mode so `gitlab>…` / `github>…` preset shortcuts can be redirected at a self-hosted host without setting up a full remote run. No PRs, no pushes. Emits MCP progress notifications during the run when the caller supplies a `progressToken`. |
38
+ | `dry_run_diff` | Semantic diff between two `dry_run` reports — `added` / `removed` / `changed` proposed updates plus a compact text rendering. Stateless; takes both reports as inputs. Useful when iterating on a config to see exactly what each tweak did. |
35
39
  | `write_config` | Validate, then atomically write a config to disk. Refuses to save invalid configs unless `force: true`. |
36
40
  | `renovate://presets` (resource) | Markdown index of all 1000+ built-in presets grouped by namespace. |
37
41
  | `renovate://presets/{namespace}` (resource template) | Markdown listing for a single namespace (e.g. `renovate://presets/config`) — cheaper than the full index. |
@@ -43,7 +47,7 @@ See [Design notes](#design-notes) for implementation details (timeouts, safety c
43
47
 
44
48
  - **Not a Renovate replacement.** This server doesn't open PRs, run scheduled updates, or execute in CI — it's a design-time companion for a local `renovate.json`. Use the real Renovate for the actual dependency-update pipeline.
45
49
  - **`resolve_config` is preview-quality.** Preset expansion runs against a committed snapshot, and template substitution implements only positional `{{argN}}` placeholders — non-positional tokens and Handlebars helpers are flagged in `warnings` and pass through verbatim. For authoritative output, run `dry_run`.
46
- - **`preview_custom_manager` is a subset of Renovate's regex manager.** It covers `customType: "regex"`, `matchStringsStrategy: "any"`, and `{{groupName}}` template substitution only — other custom types, other strategies, and full Handlebars (helpers, conditionals) are not implemented. Use it for fast regex iteration; confirm with `dry_run`.
50
+ - **`preview_custom_manager` is a subset of Renovate's regex manager.** It covers `customType: "regex"`, `matchStringsStrategy` of `any` / `combination` / `recursive`, and `{{groupName}}` template substitution only — other custom types and full Handlebars (helpers, conditionals) are not implemented. Use it for fast regex iteration; confirm with `dry_run`.
47
51
  - **`validate_config` / `dry_run` aren't exercised in CI.** CI deliberately doesn't install Renovate, so tools that shell out to it are only covered by unit/integration tests that don't require the binary. Run them locally against the Renovate install you intend to deploy with.
48
52
 
49
53
  ## Requirements
@@ -57,7 +61,7 @@ See [Design notes](#design-notes) for implementation details (timeouts, safety c
57
61
 
58
62
  - `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN` — override binary locations.
59
63
  - `RENOVATE_MCP_REQUIRE_CLI=false` — suppress the startup "partial availability" notice when you only intend to use the offline tools.
60
- - `RENOVATE_TOKEN` (preferred) or `GITHUB_TOKEN` / `GITLAB_TOKEN` as platform-specific fallbacks — only needed by `resolve_config` with `externalPresets: true` for fetching presets from private repos or avoiding rate limits. `RENOVATE_TOKEN` wins when both are set. For GitHub Enterprise / self-hosted GitLab, pass the `endpoint` tool input (and `platform` to route `local>` presets through the same host); `RENOVATE_ENDPOINT` is **not** read.
64
+ - Platform env vars — `RENOVATE_PLATFORM`, `RENOVATE_ENDPOINT`, and a token (`RENOVATE_TOKEN` / `GITHUB_TOKEN` / `GITLAB_TOKEN`). Needed for `dry_run` against a remote platform and for `resolve_config` with `externalPresets: true`. See [Platform setup](#platform-setup) for the matrix and a worked example.
61
65
  - Private-registry credentials for `dry_run` — whatever Renovate itself would need at lookup time (`COMPOSER_AUTH`, `NPM_TOKEN` / `.npmrc`, Docker registry creds, or a `RENOVATE_HOST_RULES` JSON blob). Alternatively encode these as `hostRules` in the Renovate config, or pass them per-call via the `hostRules` input on `dry_run` (no MCP restart needed). Per-call `hostRules` are appended to whatever the repo's own config declares. Values reach Renovate as JSON through the tool-call transport, so the calling LLM sees them in its context — prefer the env route if that matters. Without any of these, Renovate's lookup often returns 0 updates silently; `dry_run` scans its logs for auth failures and surfaces them under `problems`.
62
66
 
63
67
  > **Note:** all env vars (tokens, `COMPOSER_AUTH`, `RENOVATE_HOST_RULES`, binary overrides, …) must be set on the MCP server process itself — via the `env` key in `claude_desktop_config.json` / `.mcp.json`, not your shell — since the MCP server runs as a child of the client and does not inherit shell env.
@@ -106,6 +110,51 @@ Restart your client and prompt it:
106
110
 
107
111
  A response listing namespaces like `config`, `docker`, `npm`, `helpers`, … confirms the server is reachable and the resource is exposed. If the client reports the tool or resource as unavailable, re-check the config file path and command.
108
112
 
113
+ ## Platform setup
114
+
115
+ The four common configurations differ in three settings: `RENOVATE_PLATFORM`, `RENOVATE_ENDPOINT`, and which token env var you set. Pick the row that matches your environment and put the values in your client's `mcpServers.renovate.env` block.
116
+
117
+ | Setup | `RENOVATE_PLATFORM` | `RENOVATE_ENDPOINT` | Token env var |
118
+ | --- | --- | --- | --- |
119
+ | github.com | `github` | (omit — defaults to `https://api.github.com`) | `RENOVATE_TOKEN` *or* `GITHUB_TOKEN` |
120
+ | GitHub Enterprise | `github` | `https://github.example.com/api/v3/` | `RENOVATE_TOKEN` *or* `GITHUB_TOKEN` |
121
+ | gitlab.com | `gitlab` | (omit — defaults to `https://gitlab.com/api/v4`) | `RENOVATE_TOKEN` *or* `GITLAB_TOKEN` |
122
+ | Self-hosted GitLab | `gitlab` | `https://gitlab.example.com/api/v4/` | `RENOVATE_TOKEN` *or* `GITLAB_TOKEN` |
123
+
124
+ Notes that apply to every row:
125
+
126
+ - All env vars must be on the MCP server process — set them via the `env` key in `.mcp.json` / `claude_desktop_config.json`, not your shell, since the MCP server runs as a child of the client and does not inherit shell env.
127
+ - `RENOVATE_TOKEN` wins when both it and the platform-specific var are set. `dry_run` and `resolve_config` honour the same precedence; for `dry_run` the platform-specific var is auto-translated to `RENOVATE_TOKEN` for the spawned Renovate CLI (Renovate itself only reads that one var).
128
+ - For repository identifiers, GitLab accepts nested-group paths like `group/subgroup/project`, not just `group/project`.
129
+ - If you only intend to use the offline tools (`read_config`, `resolve_config` without `externalPresets`, `preview_custom_manager`, `lint_config`), you can skip all of the above.
130
+
131
+ ### Worked example — self-hosted GitLab
132
+
133
+ ```jsonc
134
+ {
135
+ "mcpServers": {
136
+ "renovate": {
137
+ "command": "npx",
138
+ "args": ["-y", "renovate-mcp"],
139
+ "env": {
140
+ "RENOVATE_PLATFORM": "gitlab",
141
+ "RENOVATE_ENDPOINT": "https://gitlab.example.com/api/v4/",
142
+ "GITLAB_TOKEN": "<your token>"
143
+ }
144
+ }
145
+ }
146
+ }
147
+ ```
148
+
149
+ This is enough for both `dry_run` (remote-platform runs) and `resolve_config` (private preset fetches). With this set, `dry_run` against `group/subgroup/project` works without passing `platform` / `endpoint` / `token` per call: `RENOVATE_PLATFORM` and the auto-translated `GITLAB_TOKEN` cover the platform side, and `RENOVATE_ENDPOINT` is inherited naturally by the spawned Renovate CLI. For `resolve_config`, `platform` and `endpoint` are tool *inputs* (not env vars) — pass them when you need `local>` presets routed through your host, since `resolve_config` is in-process and doesn't read `RENOVATE_*` env vars itself.
150
+
151
+ ### `local>` presets
152
+
153
+ A config that extends `local>owner/repo:preset` only resolves when there's a platform context to expand it against:
154
+
155
+ - **For `dry_run`** — pass `platform` + `endpoint` + `repository` (token falls back to env). Use `dryRunMode=extract` if you only need manifest extraction; the preset preflight is skipped in that mode.
156
+ - **For `resolve_config`** — pass `platform` + `endpoint` as inputs and the tool rewrites `local>` into `<platform>>` and fetches over HTTPS. Without these inputs, `local>` stays in `presetsUnresolved` with a pointer to the workaround. Run `dry_run` afterwards for full-fidelity merging.
157
+
109
158
  ## Example prompts
110
159
 
111
160
  Once the server is wired up, try prompts like these. They're written for Claude but work with any MCP-capable client.
@@ -114,6 +163,7 @@ Once the server is wired up, try prompts like these. They're written for Claude
114
163
 
115
164
  - "Read the Renovate config in this repo and summarize what it actually does — expand every preset so I can see the real effective behavior."
116
165
  - "Resolve my config and list anything that landed in `presetsUnresolved`, with the reason for each."
166
+ - "Why is my `prCreation` set to `not-pending`? Use `explain_config` to trace which preset set it."
117
167
 
118
168
  **Browsing presets**
119
169
 
@@ -121,13 +171,11 @@ Once the server is wired up, try prompts like these. They're written for Claude
121
171
  - "What does `config:recommended` actually enable? Show me its expanded JSON."
122
172
  - "Find a built-in preset that pins GitHub Actions digests."
123
173
 
124
- **Self-hosted GitLab / GitHub Enterprise**
174
+ **Self-hosted GitLab / GitHub Enterprise** (env set per [Platform setup](#platform-setup))
125
175
 
126
176
  - "Resolve my config with external presets enabled, fetching `gitlab>platform/renovate-presets` from our self-hosted GitLab at `https://gitlab.example.com/api/v4`. Route `local>` presets through the same host."
127
177
  - "Expand `github>acme/renovate-config//base` from our GitHub Enterprise at `https://github.acme.corp/api/v3`."
128
- - "Dry-run `devops/gitops` against our self-hosted GitLab at `https://gitlab.example.com/api/v4` — use `platform=gitlab` with my token so Renovate can fetch the `local>` presets the config extends."
129
-
130
- (Reminder: auth tokens must be set on the MCP server process — via the `env` key in `.mcp.json` / `claude_desktop_config.json`, not your shell.)
178
+ - "Dry-run `infrastructure/kubernetes/our-platform` (a nested-group GitLab project) so Renovate can fetch the `local>` presets the config extends."
131
179
 
132
180
  **Authoring a custom manager (regex)**
133
181
 
@@ -177,12 +225,15 @@ npm test # vitest run (builds first)
177
225
  npm run test:watch # vitest watch mode
178
226
  npm run test:coverage # vitest run --coverage (writes coverage/ report)
179
227
  npm run generate:presets # regenerate src/data/presets.generated.ts from the renovate devDep
228
+ npm run generate:managers # regenerate src/data/managers.generated.ts from the renovate devDep
180
229
  ```
181
230
 
182
- The preset catalogue at `src/data/presets.generated.ts` is a committed snapshot of Renovate's built-in presets. Runtime code never imports the `renovate` package — only `scripts/generate-presets.mjs` does. Regenerate after bumping the `renovate` devDependency.
231
+ The preset catalogue at `src/data/presets.generated.ts` and the manager-name list at `src/data/managers.generated.ts` are committed snapshots of Renovate's built-in presets and manager registry. Runtime code never imports the `renovate` package — only the `scripts/generate-*.mjs` scripts do. Regenerate both after bumping the `renovate` devDependency.
183
232
 
184
233
  CI runs `typecheck`, `build`, and `test:coverage` on Node 24 for every PR and push to `main` (see `.github/workflows/ci.yml`). Coverage is uploaded as a per-run artifact; no threshold is enforced yet.
185
234
 
235
+ `.github/workflows/claude.yml` is maintainer tooling: it lets the repo owner trigger [`anthropics/claude-code-action`](https://github.com/anthropics/claude-code-action) by mentioning `@claude` in an issue, issue comment, PR review, or PR review comment. It's gated on `sender.login == repository_owner`, so mentions from anyone else are ignored. The workflow needs the `CLAUDE_CODE_OAUTH_TOKEN` secret on the repo; outside contributors and forks do not need any Anthropic credentials to work on this project.
236
+
186
237
  ## Release flow
187
238
 
188
239
  Releases are automated via [release-please](https://github.com/googleapis/release-please) (see `.github/workflows/release.yaml`):
@@ -202,7 +253,7 @@ Scope and non-goals are summarized in [What this is NOT](#what-this-is-not); thi
202
253
 
203
254
  - `validate_config`, `dry_run`, and `write_config` shell out to the Renovate CLI rather than importing Renovate as a library — this decouples our Node version from Renovate's (currently Node 24).
204
255
  - `resolve_config`, `preview_custom_manager`, and `lint_config` are fully in-process and never invoke the Renovate CLI, so they work without a Renovate install.
205
- - `lint_config` is a semantic lint pass that sits alongside `validate_config` rather than replacing it: schema validation catches structural bugs, the linter catches Renovate-specific footguns that schema validation declares valid — most commonly a pattern like `"matchPackageNames": ["/devops\\/pipelines\\/.+"]` where a trailing `/` is missing and Renovate silently degrades the value to an exact-string match that never hits. The ruleset is intentionally small (two rules: `dead-regex-missing-slash`, `unwrapped-regex`), scoped to the four regex-aware fields named in the proposal, and tuned to avoid false positives on benign exact strings containing a `.`. Rule IDs are stable so findings can be suppressed by callers.
256
+ - `lint_config` is a semantic lint pass that sits alongside `validate_config` rather than replacing it: schema validation catches structural bugs, the linter catches Renovate-specific footguns that schema validation declares valid — most commonly a pattern like `"matchPackageNames": ["/devops\\/pipelines\\/.+"]` where a trailing `/` is missing and Renovate silently degrades the value to an exact-string match that never hits, or a typo like `"matchManagers": ["npmm"]` that silently applies the rule to zero packages. The ruleset is intentionally small (three rules: `dead-regex-missing-slash`, `unwrapped-regex`, `matchManagers-unknown-name`), scoped to the four regex-aware fields plus `matchManagers` / `excludeManagers`, and tuned to avoid false positives on benign exact strings containing a `.`. The valid-manager list is snapshotted from the `renovate` devDep at `src/data/managers.generated.ts` (regenerate with `npm run generate:managers`), and unknown names get a Damerau-Levenshtein "did you mean?" suggestion when something close enough exists. Rule IDs are stable so findings can be suppressed by callers.
206
257
  - `preview_custom_manager` honors `.gitignore` (including nested `.gitignore`s and `.git/info/exclude`) when walking the repo, so generated/vendored directories like `dist/`, `.next/`, `target/`, `__pycache__/` don't crowd out real hits against the `maxFilesWalked` cap. `node_modules/` and `.git/` are always skipped as a safety net even when no `.gitignore` is present.
207
258
  - `preview_custom_manager` exposes two separate safety caps so the warning text can name which one tripped: `maxFilesWalked` (default 2000) bounds the directory walk before any `fileMatch` testing, and `maxFilesMatched` (default 500) bounds the result set after `fileMatch` is applied. Previously a single `maxFilesScanned` conflated the two, leaving the user unable to tell whether to narrow `fileMatch` or widen the walk.
208
259
  - `preview_custom_manager` runs every user-supplied regex on a `worker_threads` worker with a wall-clock budget per operation (default 2 s, configurable via `matchTimeoutMs`). Catastrophic backtracking — e.g. `^(a+)+b$` against `aaaa…c`, or `(.*)*=` against a modestly sized file — would otherwise pin the MCP server's event loop indefinitely. On timeout the worker is terminated and a warning is appended identifying which `fileMatch[i]` or `matchStrings[i]` was aborted, so the user can simplify the pattern or raise the budget.
@@ -210,8 +261,10 @@ Scope and non-goals are summarized in [What this is NOT](#what-this-is-not); thi
210
261
  - `resolve_config` expands `extends` against a committed snapshot of Renovate's built-in presets (`src/data/presets.generated.ts`). External `github>` / `gitlab>` fetching is opt-in, uses each platform's contents API with a 10 s timeout, and caches results per call. The `endpoint` input swaps in a custom API base for GHE / self-hosted GitLab; `platform` additionally rewrites `local>` presets to be fetched against that endpoint.
211
262
  - `resolve_config` merges preset bodies with a close approximation of Renovate's own `mergeChildConfig` — arrays concat, objects recursively merge, scalars overwrite — not a bit-identical port. Rule-specific semantics for `hostRules`, `regexManagers` / `customManagers`, and certain boolean flags aren't modeled here. Every response carries `mergeQuality: "preview"` plus a human-readable `disclaimer` so callers can't miss the limitation; run `dry_run` for authoritative output.
212
263
  - When `resolve_config` encounters template tokens outside its supported subset, it records a structured entry in `warnings`: under-argument cases (`{{arg2}}` referenced when only one arg was passed) substitute an empty string, while non-positional tokens (`{{packageRules}}`, Handlebars helpers like `{{#if …}}`) pass through verbatim.
264
+ - `explain_config` reuses the same expansion machinery as `resolve_config` (`parsePreset`, `loadPresetBody`, `applyArgs`, `recordTemplateWarnings`) so the two can't drift — if `resolve_config` resolves a preset, `explain_config` does too. The annotated layer threads each preset body's leaves through a parallel `mergeAnnotated` that preserves contributors instead of dropping them, then pins each contribution with the source name (literal `extends` entry, or `<own>` for the user's input config) and a `via` chain naming every parent preset traversed to reach it. Arrays accumulate every contribution in `setBy` and concat their `value`s; scalars list every contribution in merge order with the last as the winner. `merge_quality: "preview"` carries the same disclaimer as `resolve_config` — Renovate's rule-specific semantics for `hostRules`, `regexManagers`, and certain boolean flags aren't modelled.
213
265
  - `dry_run` uses `--report-type=file` so we get a structured JSON report instead of scraping stdout. When a `hostRules` input is passed it's written to a mode-0600 temp file in `os.tmpdir()`, handed to Renovate via the `RENOVATE_CONFIG_FILE` env var (the CLI has no `--config-file` flag), and deleted in a `finally` block. Token/password values — including the platform `token` input — are scrubbed from the detected `problems` list and the `logTail` fallback before returning.
214
- - `dry_run` defaults to `--platform=local` so no host token is required, but that mode can't resolve `local>` presets (they have no platform context to expand against) and silently hides non-default-host GitHub/GitLab setups. Passing `platform` + `endpoint` + `token` + `repository` switches the run to a real platform client so the preset-fetch path works end-to-end; `--dry-run` is still set, so no PRs are opened. As a guard against the silent-failure mode, `dry_run` preflight-checks the repo's config: if it extends any `local>…` preset while the effective platform is `local`, the tool fails fast with specific remediation rather than spawning a Renovate run that would opaquely report `config-validation`.
266
+ - `dry_run` defaults to `--platform=local` so no host token is required, but that mode can't resolve `local>` presets (they have no platform context to expand against) and silently hides non-default-host GitHub/GitLab setups. When the caller doesn't pass a `platform` input, the tool first falls back to `RENOVATE_PLATFORM` from the MCP server's env (if it's one of `local`/`github`/`gitlab`) before defaulting to `local` — without that fallback, the wrapper would unconditionally pass `--platform=local` and silently override an env var the user had set in their `mcp.json`. Passing `platform` + `endpoint` + `token` + `repository` (or relying on the `RENOVATE_PLATFORM` / `RENOVATE_ENDPOINT` env vars) switches the run to a real platform client so the preset-fetch path works end-to-end; `--dry-run` is still set, so no PRs are opened. The repository is passed to Renovate as a positional argument because the CLI has no `--repository` flag. `endpoint` and `token` are also forwarded in the default `platform=local` mode so `gitlab>…` / `github>…` preset shortcuts (which are otherwise hardcoded to gitlab.com/github.com) can be redirected at a self-hosted host. As a guard against the silent-failure mode, `dry_run` preflight-checks the repo's config: if it extends any `local>…` preset while the effective platform is `local`, the tool fails fast with specific remediation rather than spawning a Renovate run that would opaquely report `config-validation`. The preflight is skipped for `dryRunMode=extract` so manifest-only extraction can still be attempted.
215
267
  - `dry_run` returns a top-level `ok` boolean that is `false` whenever the CLI exited non-zero OR the structured report contains a validation/error-level problem (`level >= 40`, `message === "config-validation"`, or a non-empty `validationError` field). Renovate frequently writes exit-code 0 alongside report-level failures — trusting the exit code alone hides runs that did nothing.
216
268
  - `dry_run` emits MCP progress notifications only when the caller's `tools/call` includes `_meta.progressToken` — no-op otherwise, so legacy clients see zero overhead. A 5-second heartbeat ticks while the child runs; each tick's message is best-effort enriched with the latest Renovate JSON-log `msg` seen on stdout. Notifications are also emitted at start and completion. We deliberately don't couple to Renovate's log schema beyond reading `msg`, since that schema isn't a stable API.
269
+ - `dry_run_diff` is stateless on purpose: both reports are passed as inputs, no per-repo state is kept on the server. Updates are keyed by `(manager, packageFile, depName)` so a version bump on the same dep shows up once under `changed` rather than twice as `removed + added`. Compared per identity: `newValue`, `newVersion`, `updateType`, `branchName`, `groupName`, `schedule`. Either input can be the raw Renovate report or the full `dry_run` summary (with the `report` key) — the tool unwraps `report` automatically.
217
270
  - `write_config` writes to a temp file, validates, then atomically renames — so a failed validation never leaves a broken config on disk.
@@ -0,0 +1,12 @@
1
+ export declare const RENOVATE_VERSION = "43.141.5";
2
+ /**
3
+ * All manager names accepted by Renovate's `matchManagers` /
4
+ * `excludeManagers` fields. Includes both regular managers and custom
5
+ * managers (which may also be referenced with a `custom.` prefix).
6
+ */
7
+ export declare const ALL_MANAGERS: readonly string[];
8
+ /**
9
+ * Custom manager names. Each of these may also be referenced as
10
+ * `custom.<name>` in `matchManagers` / `excludeManagers`.
11
+ */
12
+ export declare const CUSTOM_MANAGERS: readonly string[];
@@ -0,0 +1,132 @@
1
+ // AUTO-GENERATED by scripts/generate-managers.mjs from renovate@43.141.5.
2
+ // Do not edit by hand. Run `npm run generate:managers` to refresh.
3
+ export const RENOVATE_VERSION = "43.141.5";
4
+ /**
5
+ * All manager names accepted by Renovate's `matchManagers` /
6
+ * `excludeManagers` fields. Includes both regular managers and custom
7
+ * managers (which may also be referenced with a `custom.` prefix).
8
+ */
9
+ export const ALL_MANAGERS = Object.freeze([
10
+ "ansible",
11
+ "ansible-galaxy",
12
+ "ant",
13
+ "argocd",
14
+ "asdf",
15
+ "azure-pipelines",
16
+ "batect",
17
+ "batect-wrapper",
18
+ "bazel",
19
+ "bazel-module",
20
+ "bazelisk",
21
+ "bicep",
22
+ "bitbucket-pipelines",
23
+ "bitrise",
24
+ "buildkite",
25
+ "buildpacks",
26
+ "bun",
27
+ "bun-version",
28
+ "bundler",
29
+ "cake",
30
+ "cargo",
31
+ "cdnurl",
32
+ "circleci",
33
+ "cloudbuild",
34
+ "cocoapods",
35
+ "composer",
36
+ "conan",
37
+ "copier",
38
+ "cpanfile",
39
+ "crossplane",
40
+ "crow",
41
+ "deps-edn",
42
+ "devbox",
43
+ "devcontainer",
44
+ "docker-compose",
45
+ "dockerfile",
46
+ "droneci",
47
+ "fleet",
48
+ "flux",
49
+ "fvm",
50
+ "git-submodules",
51
+ "github-actions",
52
+ "gitlabci",
53
+ "gitlabci-include",
54
+ "glasskube",
55
+ "gleam",
56
+ "gomod",
57
+ "gradle",
58
+ "gradle-wrapper",
59
+ "haskell-cabal",
60
+ "helm-requirements",
61
+ "helm-values",
62
+ "helmfile",
63
+ "helmsman",
64
+ "helmv3",
65
+ "hermit",
66
+ "homeassistant-manifest",
67
+ "homebrew",
68
+ "html",
69
+ "jenkins",
70
+ "jsonata",
71
+ "jsonnet-bundler",
72
+ "kotlin-script",
73
+ "kubernetes",
74
+ "kustomize",
75
+ "leiningen",
76
+ "maven",
77
+ "maven-wrapper",
78
+ "meteor",
79
+ "mint",
80
+ "mise",
81
+ "mix",
82
+ "nix",
83
+ "nodenv",
84
+ "npm",
85
+ "nuget",
86
+ "nvm",
87
+ "ocb",
88
+ "osgi",
89
+ "pep621",
90
+ "pep723",
91
+ "pip-compile",
92
+ "pip_requirements",
93
+ "pip_setup",
94
+ "pipenv",
95
+ "pixi",
96
+ "poetry",
97
+ "pre-commit",
98
+ "pub",
99
+ "puppet",
100
+ "pyenv",
101
+ "quadlet",
102
+ "regex",
103
+ "renovate-config-presets",
104
+ "ruby-version",
105
+ "runtime-version",
106
+ "sbt",
107
+ "scalafmt",
108
+ "setup-cfg",
109
+ "sveltos",
110
+ "swift",
111
+ "tekton",
112
+ "terraform",
113
+ "terraform-version",
114
+ "terragrunt",
115
+ "terragrunt-version",
116
+ "tflint-plugin",
117
+ "travis",
118
+ "typst",
119
+ "unity3d",
120
+ "velaci",
121
+ "vendir",
122
+ "woodpecker",
123
+ "xcodegen"
124
+ ]);
125
+ /**
126
+ * Custom manager names. Each of these may also be referenced as
127
+ * `custom.<name>` in `matchManagers` / `excludeManagers`.
128
+ */
129
+ export const CUSTOM_MANAGERS = Object.freeze([
130
+ "jsonata",
131
+ "regex"
132
+ ]);
@@ -3,6 +3,6 @@ export interface GeneratedPreset {
3
3
  description: string | null;
4
4
  body: Record<string, unknown>;
5
5
  }
6
- export declare const RENOVATE_VERSION = "43.139.5";
6
+ export declare const RENOVATE_VERSION = "43.141.5";
7
7
  export declare const PRESETS: Record<string, GeneratedPreset>;
8
8
  export declare const PRESET_NAMES: readonly string[];
@@ -1,6 +1,6 @@
1
1
  // AUTO-GENERATED by scripts/generate-presets.mjs from renovate@43.139.5.
2
2
  // Do not edit by hand. Run `npm run generate:presets` to refresh.
3
- export const RENOVATE_VERSION = "43.139.5";
3
+ export const RENOVATE_VERSION = "43.141.5";
4
4
  export const PRESETS = {
5
5
  "abandonments:recommended": {
6
6
  "namespace": "abandonments",
package/dist/index.js CHANGED
@@ -6,22 +6,28 @@ import { registerReadConfig } from "./tools/readConfig.js";
6
6
  import { registerValidateConfig } from "./tools/validateConfig.js";
7
7
  import { registerLintConfig } from "./tools/lintConfig.js";
8
8
  import { registerResolveConfig } from "./tools/resolveConfig.js";
9
+ import { registerExplainConfig } from "./tools/explainConfig.js";
9
10
  import { registerPreviewCustomManager } from "./tools/previewCustomManager.js";
10
11
  import { registerDryRun } from "./tools/dryRun.js";
12
+ import { registerDryRunDiff } from "./tools/dryRunDiff.js";
11
13
  import { registerWriteConfig } from "./tools/writeConfig.js";
14
+ import { registerGetVersion } from "./tools/getVersion.js";
12
15
  import { registerPresetResources } from "./resources/presets.js";
13
16
  import { checkSetup, startupBanner } from "./lib/setupCheck.js";
17
+ import { SERVER_VERSION } from "./lib/version.js";
14
18
  const BASE_INSTRUCTIONS = [
15
19
  "Design and debug Renovate configurations interactively.",
16
20
  "",
17
21
  "Workflow:",
18
22
  " 1. read_config — inspect the current config in a repo",
19
23
  " 2. resolve_config — expand built-in presets to see what a config actually becomes (offline)",
20
- " 3. preview_custom_manager — iterate on a regex-based customManagers entry; shows file/line hits and extracted deps",
21
- " 4. validate_config — check a proposed config against Renovate's schema",
22
- " 5. lint_config — semantic lint pass: catches Renovate-specific footguns schema validation misses (e.g. malformed /…/ regex patterns)",
23
- " 6. dry_run — preview what Renovate would actually do (no PRs)",
24
- " 7. write_config — save the agreed-upon config (validates first)",
24
+ " 3. explain_config — inverse of resolve_config: trace which preset set each field (offline)",
25
+ " 4. preview_custom_manager — iterate on a regex-based customManagers entry; shows file/line hits and extracted deps",
26
+ " 5. validate_config — check a proposed config against Renovate's schema",
27
+ " 6. lint_config — semantic lint pass: catches Renovate-specific footguns schema validation misses (e.g. malformed /…/ regex patterns)",
28
+ " 7. dry_run — preview what Renovate would actually do (no PRs)",
29
+ " 8. dry_run_diff — semantic diff between two dry_run reports (added/removed/changed updates)",
30
+ " 9. write_config — save the agreed-upon config (validates first)",
25
31
  "",
26
32
  "If any tool fails unexpectedly, call check_setup to diagnose CLI availability.",
27
33
  "Built-in preset reference: renovate://presets (namespace index), renovate://presets/{namespace} (one namespace), renovate://preset/{name} (one preset's expanded JSON).",
@@ -33,16 +39,18 @@ const requireCliRaw = process.env.RENOVATE_MCP_REQUIRE_CLI?.trim().toLowerCase()
33
39
  const suppressBanner = requireCliRaw === "false" || requireCliRaw === "0";
34
40
  const banner = suppressBanner ? null : startupBanner(setup);
35
41
  const instructions = banner ? [BASE_INSTRUCTIONS, "", banner].join("\n") : BASE_INSTRUCTIONS;
36
- const SERVER_VERSION = "0.6.0"; // x-release-please-version
37
42
  const server = new McpServer({ name: "renovate-mcp", version: SERVER_VERSION }, { instructions });
38
43
  registerCheckSetup(server);
39
44
  registerReadConfig(server);
40
45
  registerResolveConfig(server);
46
+ registerExplainConfig(server);
41
47
  registerPreviewCustomManager(server);
42
48
  registerValidateConfig(server);
43
49
  registerLintConfig(server);
44
50
  registerDryRun(server);
51
+ registerDryRunDiff(server);
45
52
  registerWriteConfig(server);
53
+ registerGetVersion(server);
46
54
  registerPresetResources(server);
47
55
  const transport = new StdioServerTransport();
48
56
  await server.connect(transport);
@@ -0,0 +1,64 @@
1
+ import { type PresetWarning, type ResolveOptions, type UnresolvedPreset } from "./presetResolver.js";
2
+ /**
3
+ * Sentinel used as the `source` of contributions that come from the user's own
4
+ * input config (i.e. the keys siblings of `extends`, not from any preset).
5
+ * Distinguishable from any real preset name because angle brackets cannot
6
+ * appear in a Renovate preset reference.
7
+ */
8
+ export declare const OWN_SOURCE = "<own>";
9
+ export interface Contribution {
10
+ /**
11
+ * Where the value came from. `<own>` for the user's input config; otherwise
12
+ * the preset reference exactly as it appeared in `extends` (e.g.
13
+ * `config:recommended`, `:automergeAll`, `github>acme/renovate-config`).
14
+ */
15
+ source: string;
16
+ /**
17
+ * Path of presets that brought this contribution in, outermost first. Empty
18
+ * when the value comes from `<own>` or from a top-level entry in the user's
19
+ * `extends`. For nested presets, lists every parent preset between the
20
+ * user's root and `source`.
21
+ */
22
+ via: string[];
23
+ /**
24
+ * The exact value `source` contributed at the point of the merge — for
25
+ * scalars this is the value `source` set, before any later preset/own
26
+ * override; for arrays this is just the slice `source` added.
27
+ */
28
+ value: unknown;
29
+ }
30
+ export type AnnotatedNode = AnnotatedLeaf | AnnotatedObject;
31
+ export interface AnnotatedLeaf {
32
+ /**
33
+ * Final merged value of this field. Last `setBy` entry is the winner for
34
+ * scalars; for arrays, every contribution's `value` is concatenated into
35
+ * `value`.
36
+ */
37
+ value: unknown;
38
+ /**
39
+ * Contributions in merge order. The last entry is the winner for scalars;
40
+ * for arrays each entry adds its own slice to `value`.
41
+ */
42
+ setBy: Contribution[];
43
+ }
44
+ export type AnnotatedObject = {
45
+ [key: string]: AnnotatedNode;
46
+ };
47
+ export interface ExplainResult {
48
+ explanation: AnnotatedObject;
49
+ presetsResolved: string[];
50
+ presetsUnresolved: UnresolvedPreset[];
51
+ warnings: PresetWarning[];
52
+ }
53
+ /**
54
+ * Inverse of `resolveConfig`: walk the same preset tree but, instead of
55
+ * collapsing everything into a flat resolved object, annotate each leaf field
56
+ * with the chain of presets that touched it. The last entry in `setBy` is the
57
+ * winner; `value` is what `resolveConfig` would have returned.
58
+ *
59
+ * Sharing as much as possible with `presetResolver`'s expansion logic
60
+ * (`parsePreset`, `loadPresetBody`, `applyArgs`, `recordTemplateWarnings`) so
61
+ * the two tools stay consistent — if `resolve_config` resolves a preset,
62
+ * `explain_config` does too, by definition.
63
+ */
64
+ export declare function explainConfig(config: Record<string, unknown>, options?: ResolveOptions): Promise<ExplainResult>;
@@ -0,0 +1,132 @@
1
+ import { applyArgs, loadPresetBody, parsePreset, recordTemplateWarnings, } from "./presetResolver.js";
2
+ /**
3
+ * Sentinel used as the `source` of contributions that come from the user's own
4
+ * input config (i.e. the keys siblings of `extends`, not from any preset).
5
+ * Distinguishable from any real preset name because angle brackets cannot
6
+ * appear in a Renovate preset reference.
7
+ */
8
+ export const OWN_SOURCE = "<own>";
9
+ /**
10
+ * Inverse of `resolveConfig`: walk the same preset tree but, instead of
11
+ * collapsing everything into a flat resolved object, annotate each leaf field
12
+ * with the chain of presets that touched it. The last entry in `setBy` is the
13
+ * winner; `value` is what `resolveConfig` would have returned.
14
+ *
15
+ * Sharing as much as possible with `presetResolver`'s expansion logic
16
+ * (`parsePreset`, `loadPresetBody`, `applyArgs`, `recordTemplateWarnings`) so
17
+ * the two tools stay consistent — if `resolve_config` resolves a preset,
18
+ * `explain_config` does too, by definition.
19
+ */
20
+ export async function explainConfig(config, options = {}) {
21
+ const presetsResolved = [];
22
+ const presetsUnresolved = [];
23
+ const warnings = [];
24
+ const stack = [];
25
+ const ctx = {
26
+ fetchExternal: options.fetchExternal ?? false,
27
+ timeoutMs: options.timeoutMs,
28
+ endpoint: options.endpoint,
29
+ platform: options.platform,
30
+ cache: new Map(),
31
+ };
32
+ const annotated = await expandAnnotated(config, null, [], presetsResolved, presetsUnresolved, warnings, stack, ctx);
33
+ // The user's input is always a JSON object, so the root annotation is always
34
+ // an object, not a leaf. The cast is safe; falling back to `{}` is just
35
+ // defence in depth.
36
+ const explanation = isAnnotatedLeaf(annotated) ? {} : annotated;
37
+ return { explanation, presetsResolved, presetsUnresolved, warnings };
38
+ }
39
+ async function expandAnnotated(input, ownerName, viaChain, resolvedList, unresolvedList, warningsList, stack, ctx) {
40
+ let accumulated = {};
41
+ const rawExtends = input.extends;
42
+ if (Array.isArray(rawExtends) && rawExtends.length > 0) {
43
+ // Children of `input` are reached by going through `ownerName` (if any).
44
+ // The user's root has no name; for it `viaChain` and `childVia` are equal.
45
+ const childVia = ownerName === null ? viaChain : [...viaChain, ownerName];
46
+ for (const entry of rawExtends) {
47
+ if (typeof entry !== "string") {
48
+ unresolvedList.push({
49
+ preset: String(entry),
50
+ reason: "Preset entry must be a string.",
51
+ });
52
+ continue;
53
+ }
54
+ const parsed = parsePreset(entry);
55
+ if (stack.includes(parsed.key)) {
56
+ unresolvedList.push({
57
+ preset: entry,
58
+ reason: `Cycle detected: ${[...stack, parsed.key].join(" → ")}`,
59
+ });
60
+ continue;
61
+ }
62
+ const body = await loadPresetBody(parsed, ctx, unresolvedList);
63
+ if (!body)
64
+ continue;
65
+ const { value, missingArgs, unknownTemplates } = applyArgs(body, parsed.args);
66
+ recordTemplateWarnings(entry, parsed.args.length, missingArgs, unknownTemplates, warningsList);
67
+ stack.push(parsed.key);
68
+ const subNode = await expandAnnotated(value, entry, childVia, resolvedList, unresolvedList, warningsList, stack, ctx);
69
+ stack.pop();
70
+ accumulated = mergeAnnotated(accumulated, subNode);
71
+ resolvedList.push(entry);
72
+ }
73
+ }
74
+ const { extends: _drop, ...ownKeys } = input;
75
+ const source = ownerName ?? OWN_SOURCE;
76
+ const ownAnnotated = annotateValue(ownKeys, source, viaChain);
77
+ return mergeAnnotated(accumulated, ownAnnotated);
78
+ }
79
+ function annotateValue(value, source, via) {
80
+ if (isPlainObject(value)) {
81
+ const out = {};
82
+ for (const [k, v] of Object.entries(value)) {
83
+ out[k] = annotateValue(v, source, via);
84
+ }
85
+ return out;
86
+ }
87
+ return { value, setBy: [{ source, via: [...via], value }] };
88
+ }
89
+ /**
90
+ * Mirrors `mergeConfig` in `presetResolver.ts`: arrays concat, plain objects
91
+ * recurse, everything else `b` wins. The annotated layer additionally keeps
92
+ * each contribution in `setBy` so callers can see the full chain that produced
93
+ * the final value.
94
+ */
95
+ function mergeAnnotated(a, b) {
96
+ const aIsLeaf = isAnnotatedLeaf(a);
97
+ const bIsLeaf = isAnnotatedLeaf(b);
98
+ if (!aIsLeaf && !bIsLeaf) {
99
+ const out = { ...a };
100
+ for (const [k, bChild] of Object.entries(b)) {
101
+ const existing = out[k];
102
+ out[k] = existing ? mergeAnnotated(existing, bChild) : bChild;
103
+ }
104
+ return out;
105
+ }
106
+ if (aIsLeaf && bIsLeaf) {
107
+ if (Array.isArray(a.value) && Array.isArray(b.value)) {
108
+ return {
109
+ value: [...a.value, ...b.value],
110
+ setBy: [...a.setBy, ...b.setBy],
111
+ };
112
+ }
113
+ return {
114
+ value: b.value,
115
+ setBy: [...a.setBy, ...b.setBy],
116
+ };
117
+ }
118
+ // Mixed leaf / object — `b` wins entirely (matches `mergeConfig`'s "b wins
119
+ // unless both plain objects" rule). `a`'s contributions to the discarded
120
+ // shape are dropped, mirroring the resolved config losing them.
121
+ return b;
122
+ }
123
+ function isAnnotatedLeaf(node) {
124
+ if (typeof node !== "object" || node === null)
125
+ return false;
126
+ if (!("value" in node) || !("setBy" in node))
127
+ return false;
128
+ return Array.isArray(node.setBy);
129
+ }
130
+ function isPlainObject(v) {
131
+ return typeof v === "object" && v !== null && !Array.isArray(v);
132
+ }
@@ -1,4 +1,4 @@
1
- export type LintRuleId = "dead-regex-missing-slash" | "unwrapped-regex";
1
+ export type LintRuleId = "dead-regex-missing-slash" | "unwrapped-regex" | "matchManagers-unknown-name";
2
2
  export interface LintFinding {
3
3
  ruleId: LintRuleId;
4
4
  path: string;