renovate-mcp 0.5.0 → 0.7.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 +62 -11
- package/dist/data/managers.generated.d.ts +12 -0
- package/dist/data/managers.generated.js +132 -0
- package/dist/index.js +7 -2
- package/dist/lib/configLinter.d.ts +1 -1
- package/dist/lib/configLinter.js +76 -0
- package/dist/lib/credentialResolver.d.ts +13 -0
- package/dist/lib/credentialResolver.js +15 -0
- package/dist/lib/dryRunDiff.d.ts +65 -0
- package/dist/lib/dryRunDiff.js +0 -0
- package/dist/lib/externalPresetFetcher.js +1 -8
- package/dist/lib/hostRulesConfig.d.ts +4 -2
- package/dist/lib/hostRulesConfig.js +4 -2
- package/dist/lib/presetResolver.js +1 -1
- package/dist/lib/setupCheck.d.ts +36 -0
- package/dist/lib/setupCheck.js +81 -0
- package/dist/lib/version.d.ts +9 -0
- package/dist/lib/version.js +21 -0
- package/dist/tools/dryRun.js +218 -8
- package/dist/tools/dryRunDiff.d.ts +2 -0
- package/dist/tools/dryRunDiff.js +25 -0
- package/dist/tools/getVersion.d.ts +2 -0
- package/dist/tools/getVersion.js +18 -0
- package/dist/tools/lintConfig.js +1 -1
- package/package.json +2 -1
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,19 @@ An MCP server for designing [Renovate](https://github.com/renovatebot/renovate)
|
|
|
21
22
|
|
|
22
23
|
## What it does
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
Ten 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`. |
|
|
31
33
|
| `preview_custom_manager` | Preview a `customManagers` (regex) entry against a local repo — shows file/line hits and extracted dep info. Offline. |
|
|
32
34
|
| `validate_config` | Run `renovate-config-validator` against a file or inline object. |
|
|
33
|
-
| `lint_config` | Semantic lint for Renovate-specific footguns schema validation misses
|
|
34
|
-
| `dry_run` | Run Renovate with `--
|
|
35
|
+
| `lint_config` | Semantic lint for Renovate-specific footguns schema validation misses — malformed `/…/` regex patterns plus unknown manager names in `matchManagers` / `excludeManagers`. Offline. |
|
|
36
|
+
| `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`. |
|
|
37
|
+
| `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
38
|
| `write_config` | Validate, then atomically write a config to disk. Refuses to save invalid configs unless `force: true`. |
|
|
36
39
|
| `renovate://presets` (resource) | Markdown index of all 1000+ built-in presets grouped by namespace. |
|
|
37
40
|
| `renovate://presets/{namespace}` (resource template) | Markdown listing for a single namespace (e.g. `renovate://presets/config`) — cheaper than the full index. |
|
|
@@ -57,7 +60,7 @@ See [Design notes](#design-notes) for implementation details (timeouts, safety c
|
|
|
57
60
|
|
|
58
61
|
- `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN` — override binary locations.
|
|
59
62
|
- `RENOVATE_MCP_REQUIRE_CLI=false` — suppress the startup "partial availability" notice when you only intend to use the offline tools.
|
|
60
|
-
- `
|
|
63
|
+
- 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
64
|
- 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
65
|
|
|
63
66
|
> **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 +109,51 @@ Restart your client and prompt it:
|
|
|
106
109
|
|
|
107
110
|
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
111
|
|
|
112
|
+
## Platform setup
|
|
113
|
+
|
|
114
|
+
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.
|
|
115
|
+
|
|
116
|
+
| Setup | `RENOVATE_PLATFORM` | `RENOVATE_ENDPOINT` | Token env var |
|
|
117
|
+
| --- | --- | --- | --- |
|
|
118
|
+
| github.com | `github` | (omit — defaults to `https://api.github.com`) | `RENOVATE_TOKEN` *or* `GITHUB_TOKEN` |
|
|
119
|
+
| GitHub Enterprise | `github` | `https://github.example.com/api/v3/` | `RENOVATE_TOKEN` *or* `GITHUB_TOKEN` |
|
|
120
|
+
| gitlab.com | `gitlab` | (omit — defaults to `https://gitlab.com/api/v4`) | `RENOVATE_TOKEN` *or* `GITLAB_TOKEN` |
|
|
121
|
+
| Self-hosted GitLab | `gitlab` | `https://gitlab.example.com/api/v4/` | `RENOVATE_TOKEN` *or* `GITLAB_TOKEN` |
|
|
122
|
+
|
|
123
|
+
Notes that apply to every row:
|
|
124
|
+
|
|
125
|
+
- 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.
|
|
126
|
+
- `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).
|
|
127
|
+
- For repository identifiers, GitLab accepts nested-group paths like `group/subgroup/project`, not just `group/project`.
|
|
128
|
+
- 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.
|
|
129
|
+
|
|
130
|
+
### Worked example — self-hosted GitLab
|
|
131
|
+
|
|
132
|
+
```jsonc
|
|
133
|
+
{
|
|
134
|
+
"mcpServers": {
|
|
135
|
+
"renovate": {
|
|
136
|
+
"command": "npx",
|
|
137
|
+
"args": ["-y", "renovate-mcp"],
|
|
138
|
+
"env": {
|
|
139
|
+
"RENOVATE_PLATFORM": "gitlab",
|
|
140
|
+
"RENOVATE_ENDPOINT": "https://gitlab.example.com/api/v4/",
|
|
141
|
+
"GITLAB_TOKEN": "<your token>"
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
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.
|
|
149
|
+
|
|
150
|
+
### `local>` presets
|
|
151
|
+
|
|
152
|
+
A config that extends `local>owner/repo:preset` only resolves when there's a platform context to expand it against:
|
|
153
|
+
|
|
154
|
+
- **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.
|
|
155
|
+
- **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.
|
|
156
|
+
|
|
109
157
|
## Example prompts
|
|
110
158
|
|
|
111
159
|
Once the server is wired up, try prompts like these. They're written for Claude but work with any MCP-capable client.
|
|
@@ -121,12 +169,11 @@ Once the server is wired up, try prompts like these. They're written for Claude
|
|
|
121
169
|
- "What does `config:recommended` actually enable? Show me its expanded JSON."
|
|
122
170
|
- "Find a built-in preset that pins GitHub Actions digests."
|
|
123
171
|
|
|
124
|
-
**Self-hosted GitLab / GitHub Enterprise**
|
|
172
|
+
**Self-hosted GitLab / GitHub Enterprise** (env set per [Platform setup](#platform-setup))
|
|
125
173
|
|
|
126
174
|
- "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
175
|
- "Expand `github>acme/renovate-config//base` from our GitHub Enterprise at `https://github.acme.corp/api/v3`."
|
|
128
|
-
|
|
129
|
-
(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.)
|
|
176
|
+
- "Dry-run `infrastructure/kubernetes/our-platform` (a nested-group GitLab project) so Renovate can fetch the `local>` presets the config extends."
|
|
130
177
|
|
|
131
178
|
**Authoring a custom manager (regex)**
|
|
132
179
|
|
|
@@ -176,9 +223,10 @@ npm test # vitest run (builds first)
|
|
|
176
223
|
npm run test:watch # vitest watch mode
|
|
177
224
|
npm run test:coverage # vitest run --coverage (writes coverage/ report)
|
|
178
225
|
npm run generate:presets # regenerate src/data/presets.generated.ts from the renovate devDep
|
|
226
|
+
npm run generate:managers # regenerate src/data/managers.generated.ts from the renovate devDep
|
|
179
227
|
```
|
|
180
228
|
|
|
181
|
-
The preset catalogue at `src/data/presets.generated.ts`
|
|
229
|
+
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.
|
|
182
230
|
|
|
183
231
|
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.
|
|
184
232
|
|
|
@@ -201,7 +249,7 @@ Scope and non-goals are summarized in [What this is NOT](#what-this-is-not); thi
|
|
|
201
249
|
|
|
202
250
|
- `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).
|
|
203
251
|
- `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.
|
|
204
|
-
- `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 (
|
|
252
|
+
- `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.
|
|
205
253
|
- `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.
|
|
206
254
|
- `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.
|
|
207
255
|
- `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.
|
|
@@ -209,6 +257,9 @@ Scope and non-goals are summarized in [What this is NOT](#what-this-is-not); thi
|
|
|
209
257
|
- `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.
|
|
210
258
|
- `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.
|
|
211
259
|
- 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.
|
|
212
|
-
- `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 `--config-file
|
|
260
|
+
- `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.
|
|
261
|
+
- `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.
|
|
262
|
+
- `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.
|
|
213
263
|
- `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.
|
|
264
|
+
- `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.
|
|
214
265
|
- `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
|
+
]);
|
package/dist/index.js
CHANGED
|
@@ -8,9 +8,12 @@ import { registerLintConfig } from "./tools/lintConfig.js";
|
|
|
8
8
|
import { registerResolveConfig } from "./tools/resolveConfig.js";
|
|
9
9
|
import { registerPreviewCustomManager } from "./tools/previewCustomManager.js";
|
|
10
10
|
import { registerDryRun } from "./tools/dryRun.js";
|
|
11
|
+
import { registerDryRunDiff } from "./tools/dryRunDiff.js";
|
|
11
12
|
import { registerWriteConfig } from "./tools/writeConfig.js";
|
|
13
|
+
import { registerGetVersion } from "./tools/getVersion.js";
|
|
12
14
|
import { registerPresetResources } from "./resources/presets.js";
|
|
13
15
|
import { checkSetup, startupBanner } from "./lib/setupCheck.js";
|
|
16
|
+
import { SERVER_VERSION } from "./lib/version.js";
|
|
14
17
|
const BASE_INSTRUCTIONS = [
|
|
15
18
|
"Design and debug Renovate configurations interactively.",
|
|
16
19
|
"",
|
|
@@ -21,7 +24,8 @@ const BASE_INSTRUCTIONS = [
|
|
|
21
24
|
" 4. validate_config — check a proposed config against Renovate's schema",
|
|
22
25
|
" 5. lint_config — semantic lint pass: catches Renovate-specific footguns schema validation misses (e.g. malformed /…/ regex patterns)",
|
|
23
26
|
" 6. dry_run — preview what Renovate would actually do (no PRs)",
|
|
24
|
-
" 7.
|
|
27
|
+
" 7. dry_run_diff — semantic diff between two dry_run reports (added/removed/changed updates)",
|
|
28
|
+
" 8. write_config — save the agreed-upon config (validates first)",
|
|
25
29
|
"",
|
|
26
30
|
"If any tool fails unexpectedly, call check_setup to diagnose CLI availability.",
|
|
27
31
|
"Built-in preset reference: renovate://presets (namespace index), renovate://presets/{namespace} (one namespace), renovate://preset/{name} (one preset's expanded JSON).",
|
|
@@ -33,7 +37,6 @@ const requireCliRaw = process.env.RENOVATE_MCP_REQUIRE_CLI?.trim().toLowerCase()
|
|
|
33
37
|
const suppressBanner = requireCliRaw === "false" || requireCliRaw === "0";
|
|
34
38
|
const banner = suppressBanner ? null : startupBanner(setup);
|
|
35
39
|
const instructions = banner ? [BASE_INSTRUCTIONS, "", banner].join("\n") : BASE_INSTRUCTIONS;
|
|
36
|
-
const SERVER_VERSION = "0.5.0"; // x-release-please-version
|
|
37
40
|
const server = new McpServer({ name: "renovate-mcp", version: SERVER_VERSION }, { instructions });
|
|
38
41
|
registerCheckSetup(server);
|
|
39
42
|
registerReadConfig(server);
|
|
@@ -42,7 +45,9 @@ registerPreviewCustomManager(server);
|
|
|
42
45
|
registerValidateConfig(server);
|
|
43
46
|
registerLintConfig(server);
|
|
44
47
|
registerDryRun(server);
|
|
48
|
+
registerDryRunDiff(server);
|
|
45
49
|
registerWriteConfig(server);
|
|
50
|
+
registerGetVersion(server);
|
|
46
51
|
registerPresetResources(server);
|
|
47
52
|
const transport = new StdioServerTransport();
|
|
48
53
|
await server.connect(transport);
|
package/dist/lib/configLinter.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import { ALL_MANAGERS, CUSTOM_MANAGERS } from "../data/managers.generated.js";
|
|
1
2
|
const REGEX_AWARE_FIELDS = new Set([
|
|
2
3
|
"matchPackageNames",
|
|
3
4
|
"matchDepNames",
|
|
4
5
|
"matchSourceUrls",
|
|
5
6
|
"matchCurrentVersion",
|
|
6
7
|
]);
|
|
8
|
+
const MANAGER_FIELDS = new Set(["matchManagers", "excludeManagers"]);
|
|
9
|
+
const VALID_MANAGER_NAMES = new Set([
|
|
10
|
+
...ALL_MANAGERS,
|
|
11
|
+
...CUSTOM_MANAGERS.map((m) => `custom.${m}`),
|
|
12
|
+
]);
|
|
7
13
|
export function lintConfig(config) {
|
|
8
14
|
const findings = [];
|
|
9
15
|
walk(config, "", findings);
|
|
@@ -29,6 +35,18 @@ function walk(node, pathStr, findings) {
|
|
|
29
35
|
checkPattern(value, childPath, findings);
|
|
30
36
|
}
|
|
31
37
|
}
|
|
38
|
+
else if (MANAGER_FIELDS.has(key)) {
|
|
39
|
+
if (Array.isArray(value)) {
|
|
40
|
+
value.forEach((entry, i) => {
|
|
41
|
+
if (typeof entry === "string") {
|
|
42
|
+
checkManager(entry, `${childPath}[${i}]`, findings);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
else if (typeof value === "string") {
|
|
47
|
+
checkManager(value, childPath, findings);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
32
50
|
else {
|
|
33
51
|
walk(value, childPath, findings);
|
|
34
52
|
}
|
|
@@ -65,6 +83,64 @@ function checkPattern(raw, path, findings) {
|
|
|
65
83
|
});
|
|
66
84
|
}
|
|
67
85
|
}
|
|
86
|
+
function checkManager(raw, path, findings) {
|
|
87
|
+
if (VALID_MANAGER_NAMES.has(raw))
|
|
88
|
+
return;
|
|
89
|
+
const suggestion = nearestManager(raw);
|
|
90
|
+
const hint = suggestion ? ` Did you mean '${suggestion}'?` : "";
|
|
91
|
+
findings.push({
|
|
92
|
+
ruleId: "matchManagers-unknown-name",
|
|
93
|
+
path,
|
|
94
|
+
value: raw,
|
|
95
|
+
message: `'${raw}' is not a known Renovate manager. Renovate will silently apply this rule to zero packages.${hint}`,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function nearestManager(name) {
|
|
99
|
+
let best = null;
|
|
100
|
+
let bestDistance = Infinity;
|
|
101
|
+
for (const candidate of VALID_MANAGER_NAMES) {
|
|
102
|
+
const d = damerauLevenshtein(name, candidate);
|
|
103
|
+
if (d < bestDistance) {
|
|
104
|
+
bestDistance = d;
|
|
105
|
+
best = candidate;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Tolerate up to ~30 % of the typed length (min 2) — keeps "nmp" → "npm"
|
|
109
|
+
// but avoids suggesting an unrelated name for something wildly off.
|
|
110
|
+
const threshold = Math.max(2, Math.floor(name.length * 0.3));
|
|
111
|
+
return bestDistance <= threshold ? best : null;
|
|
112
|
+
}
|
|
113
|
+
// Optimal String Alignment distance: like Levenshtein but counts a single
|
|
114
|
+
// adjacent-character transposition (e.g. "nmp" ↔ "npm") as one edit instead
|
|
115
|
+
// of two — important for catching common keystroke typos.
|
|
116
|
+
function damerauLevenshtein(a, b) {
|
|
117
|
+
if (a === b)
|
|
118
|
+
return 0;
|
|
119
|
+
if (a.length === 0)
|
|
120
|
+
return b.length;
|
|
121
|
+
if (b.length === 0)
|
|
122
|
+
return a.length;
|
|
123
|
+
const m = a.length;
|
|
124
|
+
const n = b.length;
|
|
125
|
+
const d = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
|
126
|
+
for (let i = 0; i <= m; i++)
|
|
127
|
+
d[i][0] = i;
|
|
128
|
+
for (let j = 0; j <= n; j++)
|
|
129
|
+
d[0][j] = j;
|
|
130
|
+
for (let i = 1; i <= m; i++) {
|
|
131
|
+
for (let j = 1; j <= n; j++) {
|
|
132
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
133
|
+
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
|
|
134
|
+
if (i > 1 &&
|
|
135
|
+
j > 1 &&
|
|
136
|
+
a.charCodeAt(i - 1) === b.charCodeAt(j - 2) &&
|
|
137
|
+
a.charCodeAt(i - 2) === b.charCodeAt(j - 1)) {
|
|
138
|
+
d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return d[m][n];
|
|
143
|
+
}
|
|
68
144
|
function hasStrongRegexSignal(s) {
|
|
69
145
|
return (/\\[dwsbDWSB]/.test(s) ||
|
|
70
146
|
/\.[+*?]/.test(s) ||
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface Credential {
|
|
2
|
+
envVar: string | null;
|
|
3
|
+
token: string | undefined;
|
|
4
|
+
triedVars: string[];
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a credential by checking each env var name in order and returning
|
|
8
|
+
* the first one that's set. Used by both `resolve_config` (for `github>` /
|
|
9
|
+
* `gitlab>` preset fetches) and `dry_run` (when picking which token to export
|
|
10
|
+
* as `RENOVATE_TOKEN` to the spawned Renovate CLI). Keeping the precedence in
|
|
11
|
+
* one place ensures the two tools cannot drift out of sync.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveCredential(vars: string[]): Credential;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a credential by checking each env var name in order and returning
|
|
3
|
+
* the first one that's set. Used by both `resolve_config` (for `github>` /
|
|
4
|
+
* `gitlab>` preset fetches) and `dry_run` (when picking which token to export
|
|
5
|
+
* as `RENOVATE_TOKEN` to the spawned Renovate CLI). Keeping the precedence in
|
|
6
|
+
* one place ensures the two tools cannot drift out of sync.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveCredential(vars) {
|
|
9
|
+
for (const envVar of vars) {
|
|
10
|
+
const value = process.env[envVar];
|
|
11
|
+
if (value)
|
|
12
|
+
return { envVar, token: value, triedVars: vars };
|
|
13
|
+
}
|
|
14
|
+
return { envVar: null, token: undefined, triedVars: vars };
|
|
15
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure semantic diff over Renovate dry-run reports.
|
|
3
|
+
*
|
|
4
|
+
* The Renovate report shape is not a stable API, so we walk it defensively:
|
|
5
|
+
* we extract a flat list of "proposed updates" — the entries that would
|
|
6
|
+
* become PRs — keyed by `(manager, packageFile, depName)` and compare on the
|
|
7
|
+
* fields that meaningfully describe what each PR would do.
|
|
8
|
+
*
|
|
9
|
+
* Identity: a coarse key (manager, packageFile, depName) groups version
|
|
10
|
+
* bumps as `changed` rather than `removed + added` — the common case when a
|
|
11
|
+
* user is iterating on config.
|
|
12
|
+
*/
|
|
13
|
+
declare const COMPARED_FIELDS: readonly ["newValue", "newVersion", "updateType", "branchName", "groupName", "schedule"];
|
|
14
|
+
type ComparedField = (typeof COMPARED_FIELDS)[number];
|
|
15
|
+
export interface ProposedUpdate {
|
|
16
|
+
manager: string;
|
|
17
|
+
packageFile: string;
|
|
18
|
+
depName: string;
|
|
19
|
+
currentValue?: string;
|
|
20
|
+
currentVersion?: string;
|
|
21
|
+
newValue?: string;
|
|
22
|
+
newVersion?: string;
|
|
23
|
+
updateType?: string;
|
|
24
|
+
branchName?: string;
|
|
25
|
+
groupName?: string;
|
|
26
|
+
schedule?: unknown;
|
|
27
|
+
}
|
|
28
|
+
export interface AddedUpdate extends ProposedUpdate {
|
|
29
|
+
}
|
|
30
|
+
export interface RemovedUpdate extends ProposedUpdate {
|
|
31
|
+
}
|
|
32
|
+
export interface FieldChange {
|
|
33
|
+
field: ComparedField;
|
|
34
|
+
before: unknown;
|
|
35
|
+
after: unknown;
|
|
36
|
+
}
|
|
37
|
+
export interface ChangedUpdate {
|
|
38
|
+
manager: string;
|
|
39
|
+
packageFile: string;
|
|
40
|
+
depName: string;
|
|
41
|
+
before: ProposedUpdate;
|
|
42
|
+
after: ProposedUpdate;
|
|
43
|
+
changes: FieldChange[];
|
|
44
|
+
}
|
|
45
|
+
export interface DryRunDiff {
|
|
46
|
+
summary: {
|
|
47
|
+
added: number;
|
|
48
|
+
removed: number;
|
|
49
|
+
changed: number;
|
|
50
|
+
unchanged: number;
|
|
51
|
+
};
|
|
52
|
+
added: AddedUpdate[];
|
|
53
|
+
removed: RemovedUpdate[];
|
|
54
|
+
changed: ChangedUpdate[];
|
|
55
|
+
text: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Walk the report and collect every proposed update found under
|
|
59
|
+
* `repositories[*].branches[*].upgrades[]`. Renovate emits the same dep info
|
|
60
|
+
* in `packageFiles` too, but only `branches[].upgrades[]` reflects what
|
|
61
|
+
* would actually become a PR — which is what the diff is about.
|
|
62
|
+
*/
|
|
63
|
+
export declare function collectProposedUpdates(report: unknown): ProposedUpdate[];
|
|
64
|
+
export declare function diffDryRunReports(beforeInput: unknown, afterInput: unknown): DryRunDiff;
|
|
65
|
+
export {};
|
|
Binary file
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveCredential } from "./credentialResolver.js";
|
|
1
2
|
import { classifyExternalSource } from "./presetResolver.js";
|
|
2
3
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
3
4
|
const DEFAULT_GITHUB_API_BASE = "https://api.github.com";
|
|
@@ -69,14 +70,6 @@ async function fetchGitLab(parsed, timeoutMs, fetchImpl, endpoint) {
|
|
|
69
70
|
headers["PRIVATE-TOKEN"] = credential.token;
|
|
70
71
|
return fetchJson(url, headers, timeoutMs, parsed.original, fetchImpl, "gitlab", credential);
|
|
71
72
|
}
|
|
72
|
-
function resolveCredential(vars) {
|
|
73
|
-
for (const envVar of vars) {
|
|
74
|
-
const value = process.env[envVar];
|
|
75
|
-
if (value)
|
|
76
|
-
return { envVar, token: value, triedVars: vars };
|
|
77
|
-
}
|
|
78
|
-
return { envVar: null, token: undefined, triedVars: vars };
|
|
79
|
-
}
|
|
80
73
|
function presetFileName(parsed) {
|
|
81
74
|
if (parsed.subpath)
|
|
82
75
|
return ensureJson(parsed.subpath);
|
|
@@ -5,8 +5,10 @@ export type HostRule = Record<string, unknown>;
|
|
|
5
5
|
* responsible for unlinking the file (in a `finally` block) once Renovate has
|
|
6
6
|
* consumed it — we do not keep a reference here.
|
|
7
7
|
*
|
|
8
|
-
* Renovate
|
|
9
|
-
* (
|
|
8
|
+
* The caller points Renovate at this file via the `RENOVATE_CONFIG_FILE` env
|
|
9
|
+
* var (not a CLI flag — `--config-file` is not a real Renovate option).
|
|
10
|
+
* Renovate merges its rules with any repo-level `hostRules` via array
|
|
11
|
+
* concatenation (most-specific match wins), so callers can combine
|
|
10
12
|
* per-invocation credentials with whatever the repo itself already declares.
|
|
11
13
|
*/
|
|
12
14
|
export declare function writeHostRulesConfig(hostRules: HostRule[]): Promise<string>;
|
|
@@ -8,8 +8,10 @@ import { randomUUID } from "node:crypto";
|
|
|
8
8
|
* responsible for unlinking the file (in a `finally` block) once Renovate has
|
|
9
9
|
* consumed it — we do not keep a reference here.
|
|
10
10
|
*
|
|
11
|
-
* Renovate
|
|
12
|
-
* (
|
|
11
|
+
* The caller points Renovate at this file via the `RENOVATE_CONFIG_FILE` env
|
|
12
|
+
* var (not a CLI flag — `--config-file` is not a real Renovate option).
|
|
13
|
+
* Renovate merges its rules with any repo-level `hostRules` via array
|
|
14
|
+
* concatenation (most-specific match wins), so callers can combine
|
|
13
15
|
* per-invocation credentials with whatever the repo itself already declares.
|
|
14
16
|
*/
|
|
15
17
|
export async function writeHostRulesConfig(hostRules) {
|
|
@@ -20,7 +20,7 @@ export function classifyExternalSource(source) {
|
|
|
20
20
|
case "local":
|
|
21
21
|
return {
|
|
22
22
|
fetchable: false,
|
|
23
|
-
reason: "local> presets
|
|
23
|
+
reason: "local> presets need a platform context. Pass `platform` (`github` or `gitlab`) plus `endpoint` to resolve_config so they're routed through your platform's API. For full-fidelity merging (hostRules, regexManagers, etc.), run `dry_run` with the same params instead.",
|
|
24
24
|
};
|
|
25
25
|
case "bitbucket":
|
|
26
26
|
case "gitea":
|
package/dist/lib/setupCheck.d.ts
CHANGED
|
@@ -6,15 +6,51 @@ export interface BinaryStatus {
|
|
|
6
6
|
version?: string;
|
|
7
7
|
error?: string;
|
|
8
8
|
}
|
|
9
|
+
export type DryRunPlatform = "local" | "github" | "gitlab";
|
|
10
|
+
export interface PlatformContext {
|
|
11
|
+
/** Raw `RENOVATE_PLATFORM` value from the MCP server's env, or null. */
|
|
12
|
+
renovatePlatform: string | null;
|
|
13
|
+
/** Raw `RENOVATE_ENDPOINT` value from the MCP server's env, or null. */
|
|
14
|
+
renovateEndpoint: string | null;
|
|
15
|
+
/** Presence-only — never echo the value, since these are secrets. */
|
|
16
|
+
tokensPresent: {
|
|
17
|
+
RENOVATE_TOKEN: boolean;
|
|
18
|
+
GITHUB_TOKEN: boolean;
|
|
19
|
+
GITLAB_TOKEN: boolean;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* What `dry_run` would pick for `--platform=` when its `platform` input is
|
|
23
|
+
* unset. Mirrors the env fallback in `src/tools/dryRun.ts`: only values
|
|
24
|
+
* inside the dry_run schema enum (`local`/`github`/`gitlab`) are honored,
|
|
25
|
+
* anything else silently degrades to `local`.
|
|
26
|
+
*/
|
|
27
|
+
effectiveDryRunPlatform: DryRunPlatform;
|
|
28
|
+
/** Cross-checks that are mechanical to compute but easy for callers to miss. */
|
|
29
|
+
notes: string[];
|
|
30
|
+
}
|
|
9
31
|
export interface SetupStatus {
|
|
10
32
|
node: string;
|
|
11
33
|
renovate: BinaryStatus;
|
|
12
34
|
renovateConfigValidator: BinaryStatus;
|
|
13
35
|
envOverrides: Record<string, string>;
|
|
36
|
+
platformContext: PlatformContext;
|
|
14
37
|
ok: boolean;
|
|
15
38
|
hints: string[];
|
|
16
39
|
}
|
|
17
40
|
export declare function checkSetup(): Promise<SetupStatus>;
|
|
41
|
+
/**
|
|
42
|
+
* Reads platform-related env (RENOVATE_PLATFORM/_ENDPOINT/_TOKEN, plus
|
|
43
|
+
* GITHUB_TOKEN / GITLAB_TOKEN) and reports what the `dry_run` tool would
|
|
44
|
+
* actually do when its inputs are unset. Tokens are surfaced as presence
|
|
45
|
+
* booleans only — values are never echoed.
|
|
46
|
+
*
|
|
47
|
+
* The `notes` array carries cross-checks mechanical enough to compute here
|
|
48
|
+
* but easy to miss when staring at a `dry_run` failure: missing token for the
|
|
49
|
+
* selected platform, an endpoint that looks like a UI URL instead of an API
|
|
50
|
+
* URL, and an unsupported `RENOVATE_PLATFORM` value (which silently degrades
|
|
51
|
+
* to `local` because of dry_run's enum whitelist).
|
|
52
|
+
*/
|
|
53
|
+
export declare function inspectPlatformContext(env: NodeJS.ProcessEnv): PlatformContext;
|
|
18
54
|
export declare const OFFLINE_TOOLS: readonly ["read_config", "resolve_config", "preview_custom_manager"];
|
|
19
55
|
export declare function unavailableTools(status: SetupStatus): string[];
|
|
20
56
|
/**
|
package/dist/lib/setupCheck.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { run, resolveRenovateTool } from "./renovateCli.js";
|
|
2
|
+
const DRY_RUN_PLATFORMS = ["local", "github", "gitlab"];
|
|
2
3
|
const VERSION_TIMEOUT_MS = 10_000;
|
|
3
4
|
const INSTALL_HINT = "Install Renovate globally with `npm i -g renovate`, or set RENOVATE_BIN / RENOVATE_CONFIG_VALIDATOR_BIN to existing binaries.";
|
|
4
5
|
async function checkBinary(tool) {
|
|
@@ -52,10 +53,75 @@ export async function checkSetup() {
|
|
|
52
53
|
renovate,
|
|
53
54
|
renovateConfigValidator,
|
|
54
55
|
envOverrides,
|
|
56
|
+
platformContext: inspectPlatformContext(process.env),
|
|
55
57
|
ok: renovate.found && renovateConfigValidator.found,
|
|
56
58
|
hints,
|
|
57
59
|
};
|
|
58
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Reads platform-related env (RENOVATE_PLATFORM/_ENDPOINT/_TOKEN, plus
|
|
63
|
+
* GITHUB_TOKEN / GITLAB_TOKEN) and reports what the `dry_run` tool would
|
|
64
|
+
* actually do when its inputs are unset. Tokens are surfaced as presence
|
|
65
|
+
* booleans only — values are never echoed.
|
|
66
|
+
*
|
|
67
|
+
* The `notes` array carries cross-checks mechanical enough to compute here
|
|
68
|
+
* but easy to miss when staring at a `dry_run` failure: missing token for the
|
|
69
|
+
* selected platform, an endpoint that looks like a UI URL instead of an API
|
|
70
|
+
* URL, and an unsupported `RENOVATE_PLATFORM` value (which silently degrades
|
|
71
|
+
* to `local` because of dry_run's enum whitelist).
|
|
72
|
+
*/
|
|
73
|
+
export function inspectPlatformContext(env) {
|
|
74
|
+
const renovatePlatformRaw = env.RENOVATE_PLATFORM ?? null;
|
|
75
|
+
const renovateEndpoint = env.RENOVATE_ENDPOINT ?? null;
|
|
76
|
+
const tokensPresent = {
|
|
77
|
+
RENOVATE_TOKEN: Boolean(env.RENOVATE_TOKEN),
|
|
78
|
+
GITHUB_TOKEN: Boolean(env.GITHUB_TOKEN),
|
|
79
|
+
GITLAB_TOKEN: Boolean(env.GITLAB_TOKEN),
|
|
80
|
+
};
|
|
81
|
+
const allowedPlatform = DRY_RUN_PLATFORMS.find((p) => p === renovatePlatformRaw);
|
|
82
|
+
const effectiveDryRunPlatform = allowedPlatform ?? "local";
|
|
83
|
+
const notes = [];
|
|
84
|
+
if (renovatePlatformRaw && !allowedPlatform) {
|
|
85
|
+
notes.push(`\`RENOVATE_PLATFORM=${renovatePlatformRaw}\` is outside the \`dry_run\` schema enum (\`local\`/\`github\`/\`gitlab\`). The env fallback ignores it, so \`dry_run\` will silently use \`local\` — pass \`platform\` explicitly when calling \`dry_run\` if you need a different value.`);
|
|
86
|
+
}
|
|
87
|
+
if (allowedPlatform === "gitlab" && !tokensPresent.GITLAB_TOKEN && !tokensPresent.RENOVATE_TOKEN) {
|
|
88
|
+
notes.push("`RENOVATE_PLATFORM=gitlab` is set but neither `GITLAB_TOKEN` nor `RENOVATE_TOKEN` is present in the MCP server's env — `gitlab>` presets and private-repo lookups will likely fail to authenticate.");
|
|
89
|
+
}
|
|
90
|
+
else if (allowedPlatform === "gitlab"
|
|
91
|
+
&& tokensPresent.GITLAB_TOKEN
|
|
92
|
+
&& !tokensPresent.RENOVATE_TOKEN) {
|
|
93
|
+
notes.push("Info: `GITLAB_TOKEN` is set without `RENOVATE_TOKEN`. `dry_run` will export `GITLAB_TOKEN` as `RENOVATE_TOKEN` to the spawned Renovate CLI when `platform=gitlab` (Renovate itself only reads `RENOVATE_TOKEN`). `resolve_config` already accepts this fallback directly.");
|
|
94
|
+
}
|
|
95
|
+
if (allowedPlatform === "github" && !tokensPresent.GITHUB_TOKEN && !tokensPresent.RENOVATE_TOKEN) {
|
|
96
|
+
notes.push("`RENOVATE_PLATFORM=github` is set but neither `GITHUB_TOKEN` nor `RENOVATE_TOKEN` is present in the MCP server's env — `github>` presets and private-repo lookups will likely fail to authenticate.");
|
|
97
|
+
}
|
|
98
|
+
else if (allowedPlatform === "github"
|
|
99
|
+
&& tokensPresent.GITHUB_TOKEN
|
|
100
|
+
&& !tokensPresent.RENOVATE_TOKEN) {
|
|
101
|
+
notes.push("Info: `GITHUB_TOKEN` is set without `RENOVATE_TOKEN`. `dry_run` will export `GITHUB_TOKEN` as `RENOVATE_TOKEN` to the spawned Renovate CLI when `platform=github` (Renovate itself only reads `RENOVATE_TOKEN`). `resolve_config` already accepts this fallback directly.");
|
|
102
|
+
}
|
|
103
|
+
if (renovateEndpoint && looksLikeUiUrl(renovateEndpoint)) {
|
|
104
|
+
notes.push(`\`RENOVATE_ENDPOINT=${renovateEndpoint}\` looks like a UI URL. Renovate expects an API base URL — typically \`/api/v4/\` for GitLab or \`/api/v3/\` for GitHub Enterprise.`);
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
renovatePlatform: renovatePlatformRaw,
|
|
108
|
+
renovateEndpoint,
|
|
109
|
+
tokensPresent,
|
|
110
|
+
effectiveDryRunPlatform,
|
|
111
|
+
notes,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Heuristic: a GitLab/GitHub endpoint is meant to be the API base, but users
|
|
116
|
+
* routinely paste the web UI URL. Trigger the note when the URL is missing
|
|
117
|
+
* any `/api/` segment. We avoid hard-coding `/api/v4/` vs `/api/v3/` since
|
|
118
|
+
* Renovate accepts either depending on platform. Applies regardless of the
|
|
119
|
+
* effective platform — `dry_run` forwards `--endpoint` even in local mode to
|
|
120
|
+
* redirect `gitlab>` / `github>` preset shortcuts at a self-hosted host.
|
|
121
|
+
*/
|
|
122
|
+
function looksLikeUiUrl(endpoint) {
|
|
123
|
+
return !/\/api\//.test(endpoint);
|
|
124
|
+
}
|
|
59
125
|
// Tools that do not depend on the Renovate CLI and are always callable.
|
|
60
126
|
export const OFFLINE_TOOLS = ["read_config", "resolve_config", "preview_custom_manager"];
|
|
61
127
|
// Returns the tool names that cannot currently run because their backing
|
|
@@ -100,6 +166,21 @@ export function describeSetup(status) {
|
|
|
100
166
|
for (const key of overrideKeys)
|
|
101
167
|
lines.push(` ${key}=${status.envOverrides[key]}`);
|
|
102
168
|
}
|
|
169
|
+
lines.push("");
|
|
170
|
+
lines.push("Platform context:");
|
|
171
|
+
const ctx = status.platformContext;
|
|
172
|
+
lines.push(` RENOVATE_PLATFORM: ${ctx.renovatePlatform ?? "(unset)"}`);
|
|
173
|
+
lines.push(` RENOVATE_ENDPOINT: ${ctx.renovateEndpoint ?? "(unset)"}`);
|
|
174
|
+
const tokens = Object.entries(ctx.tokensPresent)
|
|
175
|
+
.map(([k, v]) => `${k}=${v ? "set" : "unset"}`)
|
|
176
|
+
.join(", ");
|
|
177
|
+
lines.push(` Tokens: ${tokens}`);
|
|
178
|
+
lines.push(` Effective dry_run platform (when input unset): ${ctx.effectiveDryRunPlatform}`);
|
|
179
|
+
if (ctx.notes.length > 0) {
|
|
180
|
+
lines.push(" Notes:");
|
|
181
|
+
for (const n of ctx.notes)
|
|
182
|
+
lines.push(` - ${n}`);
|
|
183
|
+
}
|
|
103
184
|
if (status.hints.length > 0) {
|
|
104
185
|
lines.push("");
|
|
105
186
|
lines.push("Hints:");
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const SERVER_VERSION = "0.7.0";
|
|
2
|
+
export type BuildMode = "local" | "released";
|
|
3
|
+
export interface VersionInfo {
|
|
4
|
+
version: string;
|
|
5
|
+
mode: BuildMode;
|
|
6
|
+
scriptPath: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function getVersionInfo(entryUrl?: string): VersionInfo;
|
|
9
|
+
export declare function describeVersion(info: VersionInfo): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
export const SERVER_VERSION = "0.7.0"; // x-release-please-version
|
|
3
|
+
// "released" = the server is running from a `node_modules/` install (global
|
|
4
|
+
// `npm i -g`, project-local `npm i`, or `npx renovate-mcp`). Anything else —
|
|
5
|
+
// most commonly an MCP config of `{"command": "node", "args": [".../dist/index.js"]}`
|
|
6
|
+
// pointed at a checkout — is treated as a local/dev build.
|
|
7
|
+
function detectMode(scriptPath) {
|
|
8
|
+
return /[\\/]node_modules[\\/]/.test(scriptPath) ? "released" : "local";
|
|
9
|
+
}
|
|
10
|
+
export function getVersionInfo(entryUrl = import.meta.url) {
|
|
11
|
+
const scriptPath = fileURLToPath(entryUrl);
|
|
12
|
+
return {
|
|
13
|
+
version: SERVER_VERSION,
|
|
14
|
+
mode: detectMode(scriptPath),
|
|
15
|
+
scriptPath,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function describeVersion(info) {
|
|
19
|
+
const tag = info.mode === "local" ? " (local/dev build)" : "";
|
|
20
|
+
return `renovate-mcp ${info.version}${tag}\nRunning from: ${info.scriptPath}`;
|
|
21
|
+
}
|
package/dist/tools/dryRun.js
CHANGED
|
@@ -4,18 +4,95 @@ import { promises as fs } from "node:fs";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { run, resolveRenovateTool, formatMissingBinaryError } from "../lib/renovateCli.js";
|
|
7
|
+
import { resolveCredential } from "../lib/credentialResolver.js";
|
|
8
|
+
import { locateConfig } from "../lib/configLocations.js";
|
|
7
9
|
import { detectLookupProblems } from "../lib/lookupProblems.js";
|
|
8
10
|
import { collectSecrets, scrubSecrets, writeHostRulesConfig, } from "../lib/hostRulesConfig.js";
|
|
9
11
|
import { extractRenovateLogMsg, buildDryRunHeartbeatMessage, } from "../lib/dryRunProgress.js";
|
|
10
12
|
const hostRuleSchema = z
|
|
11
13
|
.record(z.string(), z.unknown())
|
|
12
14
|
.describe("A single Renovate hostRule (matchHost, hostType, username, password, token, …). Structure is not validated here — Renovate's own config loader checks it when the temp file is read.");
|
|
15
|
+
/**
|
|
16
|
+
* Walk Renovate's JSON report looking for `problems` arrays and collect the
|
|
17
|
+
* ones that represent failed runs — not informational notices. Criteria:
|
|
18
|
+
* - `level >= 40` (Renovate's bunyan-derived ERROR/FATAL bands), or
|
|
19
|
+
* - `message === "config-validation"` (Renovate's canonical config-failure
|
|
20
|
+
* marker — it can appear with lower levels but still means the run was
|
|
21
|
+
* effectively a no-op), or
|
|
22
|
+
* - the problem object has a `validationError` field (preset/extends
|
|
23
|
+
* resolution failures carry their explanation here).
|
|
24
|
+
*
|
|
25
|
+
* The report shape isn't a stable Renovate API, so we walk defensively: any
|
|
26
|
+
* nested `problems` array anywhere in the tree is inspected.
|
|
27
|
+
*/
|
|
28
|
+
function collectReportErrors(report) {
|
|
29
|
+
const out = [];
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
const visit = (node) => {
|
|
32
|
+
if (node === null || typeof node !== "object")
|
|
33
|
+
return;
|
|
34
|
+
if (seen.has(node))
|
|
35
|
+
return;
|
|
36
|
+
seen.add(node);
|
|
37
|
+
if (Array.isArray(node)) {
|
|
38
|
+
for (const entry of node)
|
|
39
|
+
visit(entry);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const obj = node;
|
|
43
|
+
const problems = obj.problems;
|
|
44
|
+
if (Array.isArray(problems)) {
|
|
45
|
+
for (const raw of problems) {
|
|
46
|
+
if (!raw || typeof raw !== "object")
|
|
47
|
+
continue;
|
|
48
|
+
const p = raw;
|
|
49
|
+
const level = typeof p.level === "number" ? p.level : 0;
|
|
50
|
+
const isErrorLevel = level >= 40;
|
|
51
|
+
const isConfigValidation = p.message === "config-validation";
|
|
52
|
+
const hasValidationError = typeof p.validationError === "string" && p.validationError.length > 0;
|
|
53
|
+
if (isErrorLevel || isConfigValidation || hasValidationError) {
|
|
54
|
+
out.push(p);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const value of Object.values(obj))
|
|
59
|
+
visit(value);
|
|
60
|
+
};
|
|
61
|
+
visit(report);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Scan the repo's Renovate config for `extends` entries of the form
|
|
66
|
+
* `local>owner/repo[:preset]` and return them along with the config-file path
|
|
67
|
+
* they were found in. Returns `null` if no config is found (Renovate will
|
|
68
|
+
* handle that case itself with its usual error). Silently returns `null` on
|
|
69
|
+
* parse errors too — we don't want preflight to be stricter than Renovate's
|
|
70
|
+
* own config loader; any real problems will surface when Renovate runs.
|
|
71
|
+
*/
|
|
72
|
+
async function detectUnresolvableLocalPresets(repoPath) {
|
|
73
|
+
let located;
|
|
74
|
+
try {
|
|
75
|
+
located = await locateConfig(repoPath);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
if (!located)
|
|
81
|
+
return null;
|
|
82
|
+
const extendsRaw = located.config.extends;
|
|
83
|
+
if (!Array.isArray(extendsRaw))
|
|
84
|
+
return null;
|
|
85
|
+
const presets = extendsRaw.filter((entry) => typeof entry === "string" && entry.startsWith("local>"));
|
|
86
|
+
return { relPath: located.relPath, presets };
|
|
87
|
+
}
|
|
13
88
|
export function registerDryRun(server) {
|
|
14
89
|
server.registerTool("dry_run", {
|
|
15
90
|
title: "Dry-run Renovate",
|
|
16
|
-
description: "Run Renovate in dry-run mode
|
|
91
|
+
description: "Run Renovate in dry-run mode to preview what it would do — no PRs opened, no git pushes. Returns a structured JSON report plus a top-level `ok` boolean (false when the CLI failed OR the report records a validation/error-level problem, even if the exit code was 0).\n\nDefault mode runs `--platform=local` against the filesystem at `repoPath`. If your config extends `local>…` presets, pass `platform` (`github` or `gitlab`), `endpoint` (API base URL), `token`, and `repository` to run as a real platform client that can actually fetch those presets — Renovate still runs with `--dry-run`, so no PRs are opened. If you only need `gitlab>…` / `github>…` presets resolved against a self-hosted host (not a full remote run), pass just `endpoint` (and `token` if needed) while leaving `platform` unset — both flow through to Renovate in local mode too, which is enough to redirect those preset shortcuts away from the public defaults. The tool preflight-checks for `local>` presets under `--platform=local` (in `lookup` and `full` modes) and fails fast with remediation guidance rather than spawning a Renovate run that would fail opaquely with `config-validation`. The preflight is skipped for `dryRunMode=extract` so manifest-only extraction can be attempted regardless.\n\nWhen the `token` input is omitted, the tool falls back to `RENOVATE_TOKEN` from the MCP server's env, then to `GITLAB_TOKEN` (when `platform=gitlab`) or `GITHUB_TOKEN` (when `platform=github`) — whichever is auto-translated to `RENOVATE_TOKEN` for the spawned Renovate CLI. This matches the precedence `resolve_config` already uses, so a single `GITLAB_TOKEN` in `.mcp.json` works for both tools. For a remote-platform run, an actionable preflight error is returned before spawning Renovate when no token can be resolved at all.\n\nCredentials for private registries (e.g. `COMPOSER_AUTH` for Packagist/Satis proxies, `NPM_TOKEN` / `.npmrc` for npm, Docker registry creds, `RENOVATE_HOST_RULES` for anything else) 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 Claude and does not inherit shell env. Alternatively, encode credentials as `hostRules` in the Renovate config, or pass them per-call via the `hostRules` input on this tool (written to a mode-0600 temp file that is cleaned up after the run; token/password values — including the platform `token` input — are scrubbed from `logTail` and `problems`). Per-call `hostRules` are appended to any the repo's own config already declares. If a lookup can't auth to a registry, Renovate often reports 0 updates without a loud error; when that happens this tool surfaces detected auth failures under `problems` in the output so callers can distinguish a genuine \"no updates\" from a silent registry-auth failure.",
|
|
17
92
|
inputSchema: {
|
|
18
|
-
repoPath: z
|
|
93
|
+
repoPath: z
|
|
94
|
+
.string()
|
|
95
|
+
.describe("Absolute path to the repository root. Required for the default `platform=local` mode. When `platform` is overridden (e.g. `gitlab` / `github`), Renovate runs against the remote `repository` instead — `repoPath` is still used as the child's working directory but its manifest files are ignored."),
|
|
19
96
|
dryRunMode: z
|
|
20
97
|
.enum(["extract", "lookup", "full"])
|
|
21
98
|
.default("full")
|
|
@@ -28,17 +105,120 @@ export function registerDryRun(server) {
|
|
|
28
105
|
.max(15 * 60_000)
|
|
29
106
|
.default(5 * 60_000)
|
|
30
107
|
.describe("Max wait time in ms (default 5 minutes, capped at 15)"),
|
|
108
|
+
platform: z
|
|
109
|
+
.enum(["local", "github", "gitlab"])
|
|
110
|
+
.optional()
|
|
111
|
+
.describe("Renovate platform to run as. When unset, falls back to `RENOVATE_PLATFORM` from the MCP server's env (if it's one of `local`/`github`/`gitlab`), then to `local`. Default `local` runs against the filesystem at `repoPath`. Set `github` or `gitlab` (with `endpoint` + `token` + `repository`) to run a full remote dry-run — this is what you need when your config extends `local>` presets that live on a private GitHub Enterprise / self-hosted GitLab. Still no PRs opened because `--dry-run` is always set."),
|
|
112
|
+
endpoint: z
|
|
113
|
+
.string()
|
|
114
|
+
.optional()
|
|
115
|
+
.describe("Custom API base URL (e.g. `https://gitlab.example.com/api/v4/` or `https://ghe.example.com/api/v3/`). Forwarded as `--endpoint` regardless of `platform`: required for non-default GitHub/GitLab hosts when `platform=github`/`gitlab`, and also useful in the default `platform=local` mode to point `gitlab>…` / `github>…` preset shortcuts at a self-hosted host instead of the public defaults."),
|
|
116
|
+
token: z
|
|
117
|
+
.string()
|
|
118
|
+
.optional()
|
|
119
|
+
.describe("Auth token, exported as `RENOVATE_TOKEN` to the child. Scrubbed from any log output this tool returns. Used both for the platform connection (when `platform` is `github`/`gitlab`) and for preset resolution against private repos (e.g. when fetching a `gitlab>…` preset from a private project while in local mode). When omitted, falls back to `RENOVATE_TOKEN` from MCP env, then to `GITLAB_TOKEN` (when `platform=gitlab`) or `GITHUB_TOKEN` (when `platform=github`) — same precedence as `resolve_config`. Platform-specific fallbacks only kick in when the matching remote platform is selected; in `platform=local` they're ignored."),
|
|
120
|
+
repository: z
|
|
121
|
+
.string()
|
|
122
|
+
.optional()
|
|
123
|
+
.describe("Identifier of the repository Renovate should operate on, passed as a positional argument (Renovate has no `--repository` flag). For GitHub: `owner/repo`. For GitLab: `group/project` or a nested-group path like `group/subgroup/project` — both are accepted. Required when `platform` is `github`/`gitlab`. Ignored when `platform=local`."),
|
|
31
124
|
hostRules: z
|
|
32
125
|
.array(hostRuleSchema)
|
|
33
126
|
.optional()
|
|
34
|
-
.describe("Optional per-invocation Renovate hostRules for private registry auth. Written to a mode-0600 temp file that is passed via
|
|
127
|
+
.describe("Optional per-invocation Renovate hostRules for private registry auth. Written to a mode-0600 temp file that is passed via the RENOVATE_CONFIG_FILE env var and deleted after the run. Token/password values are scrubbed from any log output this tool returns. Appended to (not replacing) any hostRules declared in the repo's own config."),
|
|
35
128
|
},
|
|
36
|
-
}, async ({ repoPath, dryRunMode, logLevel, timeoutMs, hostRules }, extra) => {
|
|
129
|
+
}, async ({ repoPath, dryRunMode, logLevel, timeoutMs, platform, endpoint, token, repository, hostRules }, extra) => {
|
|
37
130
|
const reportPath = path.join(tmpdir(), `renovate-mcp-report-${randomUUID()}.json`);
|
|
38
131
|
const bin = resolveRenovateTool("renovate");
|
|
39
132
|
const ruleList = hostRules ?? [];
|
|
133
|
+
// When the caller doesn't pass `platform`, fall back to RENOVATE_PLATFORM
|
|
134
|
+
// from the MCP server's env before defaulting to `local`. Without this,
|
|
135
|
+
// the wrapper unconditionally appended `--platform=local` and that won
|
|
136
|
+
// over a `RENOVATE_PLATFORM=gitlab` env var the user had set in their
|
|
137
|
+
// mcp.json, silently downgrading correctly-configured self-hosted setups
|
|
138
|
+
// to local mode. Only the platforms in this tool's schema are honored
|
|
139
|
+
// from env; anything else (e.g. `bitbucket`) must be passed explicitly.
|
|
140
|
+
const envPlatform = process.env.RENOVATE_PLATFORM;
|
|
141
|
+
const envPlatformAllowed = envPlatform === "local" || envPlatform === "github" || envPlatform === "gitlab"
|
|
142
|
+
? envPlatform
|
|
143
|
+
: undefined;
|
|
144
|
+
const effectivePlatform = platform ?? envPlatformAllowed ?? "local";
|
|
145
|
+
const isRemotePlatform = effectivePlatform !== "local";
|
|
146
|
+
// Renovate only reads `RENOVATE_TOKEN` (plus `GITHUB_COM_TOKEN`) — it
|
|
147
|
+
// doesn't know about `GITLAB_TOKEN`/`GITHUB_TOKEN`. When the caller
|
|
148
|
+
// doesn't pass `token`, fall back to MCP env in the same precedence
|
|
149
|
+
// order `resolve_config` uses, then export the resolved value as
|
|
150
|
+
// `RENOVATE_TOKEN` to the spawned child. The platform-specific
|
|
151
|
+
// fallback only kicks in when the matching remote platform is selected
|
|
152
|
+
// (in `local` mode we don't want a stray `GITLAB_TOKEN` in the user's
|
|
153
|
+
// env to suddenly be promoted to a Renovate auth token).
|
|
154
|
+
const platformFallbackVar = effectivePlatform === "gitlab"
|
|
155
|
+
? "GITLAB_TOKEN"
|
|
156
|
+
: effectivePlatform === "github"
|
|
157
|
+
? "GITHUB_TOKEN"
|
|
158
|
+
: null;
|
|
159
|
+
const credentialEnvVars = platformFallbackVar
|
|
160
|
+
? ["RENOVATE_TOKEN", platformFallbackVar]
|
|
161
|
+
: ["RENOVATE_TOKEN"];
|
|
162
|
+
const envCredential = resolveCredential(credentialEnvVars);
|
|
163
|
+
const resolvedToken = token ?? envCredential.token;
|
|
40
164
|
const secrets = collectSecrets(ruleList);
|
|
165
|
+
if (resolvedToken)
|
|
166
|
+
secrets.push(resolvedToken);
|
|
41
167
|
let hostRulesConfigPath;
|
|
168
|
+
if (isRemotePlatform && !repository) {
|
|
169
|
+
return {
|
|
170
|
+
isError: true,
|
|
171
|
+
content: [
|
|
172
|
+
{
|
|
173
|
+
type: "text",
|
|
174
|
+
text: `\`repository\` is required when \`platform\` is \`${effectivePlatform}\`. Pass the target repo as \`owner/repo\` (GitHub) or \`group/project\` / \`group/subgroup/project\` (GitLab — nested groups are accepted), or unset \`platform\` to run against the local filesystem at \`repoPath\`.`,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (isRemotePlatform && !resolvedToken) {
|
|
180
|
+
const fallbackHint = platformFallbackVar
|
|
181
|
+
? `\`RENOVATE_TOKEN\` (preferred) or \`${platformFallbackVar}\``
|
|
182
|
+
: "`RENOVATE_TOKEN`";
|
|
183
|
+
return {
|
|
184
|
+
isError: true,
|
|
185
|
+
content: [
|
|
186
|
+
{
|
|
187
|
+
type: "text",
|
|
188
|
+
text: `No auth token found for \`platform=${effectivePlatform}\`. Set ${fallbackHint} in the MCP server's \`env\` ` +
|
|
189
|
+
"(in `.mcp.json` / `claude_desktop_config.json` — not your shell, since the MCP server runs as a child of Claude and does not inherit shell env), " +
|
|
190
|
+
"or pass `token` as a tool input. Run `check_setup` to see which env vars the server currently sees.",
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// Preflight: if the repo's config extends any `local>…` preset and we're
|
|
196
|
+
// about to run with `--platform=local`, Renovate will fail opaquely with
|
|
197
|
+
// a generic "config-validation" error, because `local>` has no platform
|
|
198
|
+
// context to resolve against in local mode. Detect this up front and
|
|
199
|
+
// return an actionable message instead of spawning Renovate.
|
|
200
|
+
// Skipped for `dryRunMode=extract` so manifest-only extraction can be
|
|
201
|
+
// attempted regardless — Renovate may still fail config resolution, but
|
|
202
|
+
// letting the spawn happen surfaces its real error rather than ours.
|
|
203
|
+
if (!isRemotePlatform && dryRunMode !== "extract") {
|
|
204
|
+
const unresolvable = await detectUnresolvableLocalPresets(repoPath);
|
|
205
|
+
if (unresolvable && unresolvable.presets.length > 0) {
|
|
206
|
+
const sample = unresolvable.presets.slice(0, 3).join(", ");
|
|
207
|
+
const more = unresolvable.presets.length > 3
|
|
208
|
+
? ` (+${unresolvable.presets.length - 3} more)`
|
|
209
|
+
: "";
|
|
210
|
+
return {
|
|
211
|
+
isError: true,
|
|
212
|
+
content: [
|
|
213
|
+
{
|
|
214
|
+
type: "text",
|
|
215
|
+
text: `\`${unresolvable.relPath}\` extends \`local>\` presets (${sample}${more}) that cannot be resolved under \`--platform=local\` — Renovate has no platform context to expand them against, so the run would fail with an opaque \`config-validation\` error.\n\n` +
|
|
216
|
+
`To fix: pass \`platform\` (e.g. \`gitlab\` or \`github\`), \`endpoint\`, \`token\`, and \`repository\` so Renovate runs as a real platform client and can fetch the preset. Alternatively, rewrite the \`extends\` entries from \`local>…\` to \`gitlab>…\` / \`github>…\` in your config.`,
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
42
222
|
// Hybrid progress reporter: a heartbeat tick every HEARTBEAT_MS keeps
|
|
43
223
|
// MCP clients informed during long runs, and each tick enriches its
|
|
44
224
|
// message with the latest Renovate JSON-log `msg` we saw (best-effort —
|
|
@@ -67,14 +247,33 @@ export function registerDryRun(server) {
|
|
|
67
247
|
};
|
|
68
248
|
try {
|
|
69
249
|
const args = [
|
|
70
|
-
|
|
250
|
+
`--platform=${effectivePlatform}`,
|
|
71
251
|
`--dry-run=${dryRunMode}`,
|
|
72
252
|
"--report-type=file",
|
|
73
253
|
`--report-path=${reportPath}`,
|
|
74
254
|
];
|
|
255
|
+
const childEnv = {
|
|
256
|
+
LOG_LEVEL: logLevel,
|
|
257
|
+
LOG_FORMAT: "json",
|
|
258
|
+
};
|
|
259
|
+
// `endpoint` and the resolved token flow through regardless of
|
|
260
|
+
// platform: in local mode they redirect `gitlab>…`/`github>…` preset
|
|
261
|
+
// shortcuts to a self-hosted host (Renovate's preset shortcut hosts
|
|
262
|
+
// are otherwise hardcoded to gitlab.com/github.com).
|
|
263
|
+
if (endpoint)
|
|
264
|
+
args.push(`--endpoint=${endpoint}`);
|
|
265
|
+
if (resolvedToken)
|
|
266
|
+
childEnv.RENOVATE_TOKEN = resolvedToken;
|
|
267
|
+
// Renovate has no `--repository` flag — repos are positional args.
|
|
268
|
+
if (isRemotePlatform && repository) {
|
|
269
|
+
args.push(repository);
|
|
270
|
+
}
|
|
75
271
|
if (ruleList.length > 0) {
|
|
76
272
|
hostRulesConfigPath = await writeHostRulesConfig(ruleList);
|
|
77
|
-
|
|
273
|
+
// Renovate reads this config file via the RENOVATE_CONFIG_FILE env
|
|
274
|
+
// var, not a CLI flag — `--config-file` is not a real Renovate flag
|
|
275
|
+
// and passing it crashes the CLI with "unknown option".
|
|
276
|
+
childEnv.RENOVATE_CONFIG_FILE = hostRulesConfigPath;
|
|
78
277
|
}
|
|
79
278
|
if (progressEnabled) {
|
|
80
279
|
emit(`Starting Renovate dry-run (${dryRunMode})`);
|
|
@@ -82,7 +281,7 @@ export function registerDryRun(server) {
|
|
|
82
281
|
}
|
|
83
282
|
const result = await run(bin, args, {
|
|
84
283
|
cwd: repoPath,
|
|
85
|
-
env:
|
|
284
|
+
env: childEnv,
|
|
86
285
|
timeoutMs,
|
|
87
286
|
onStdoutLine: progressEnabled
|
|
88
287
|
? (line) => {
|
|
@@ -104,7 +303,15 @@ export function registerDryRun(server) {
|
|
|
104
303
|
catch {
|
|
105
304
|
// no report produced (e.g., renovate errored before writing)
|
|
106
305
|
}
|
|
306
|
+
const reportErrors = collectReportErrors(report);
|
|
107
307
|
const summary = {
|
|
308
|
+
// `ok` collapses the three failure modes (spawn error, non-zero
|
|
309
|
+
// exit, in-report validation/error-level problems) into a single
|
|
310
|
+
// field so callers don't need to know which one fired. Renovate
|
|
311
|
+
// frequently writes a report with exitCode=0 while its `problems`
|
|
312
|
+
// array records a validation/config failure — judging only by
|
|
313
|
+
// exitCode hides those runs.
|
|
314
|
+
ok: result.exitCode === 0 && reportErrors.length === 0,
|
|
108
315
|
exitCode: result.exitCode,
|
|
109
316
|
hasReport: report !== null,
|
|
110
317
|
report,
|
|
@@ -115,6 +322,9 @@ export function registerDryRun(server) {
|
|
|
115
322
|
if (problems.length > 0) {
|
|
116
323
|
summary.problems = problems;
|
|
117
324
|
}
|
|
325
|
+
if (reportErrors.length > 0) {
|
|
326
|
+
summary.reportErrors = reportErrors;
|
|
327
|
+
}
|
|
118
328
|
// If no structured report, surface the last bit of stderr so Claude can
|
|
119
329
|
// debug without blowing up the context with Renovate's verbose logs.
|
|
120
330
|
if (!report) {
|
|
@@ -137,7 +347,7 @@ export function registerDryRun(server) {
|
|
|
137
347
|
text: JSON.stringify(summary, null, 2),
|
|
138
348
|
},
|
|
139
349
|
],
|
|
140
|
-
isError:
|
|
350
|
+
isError: !summary.ok,
|
|
141
351
|
};
|
|
142
352
|
}
|
|
143
353
|
catch (err) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { diffDryRunReports } from "../lib/dryRunDiff.js";
|
|
3
|
+
const reportShape = z
|
|
4
|
+
.record(z.string(), z.unknown())
|
|
5
|
+
.describe("A Renovate dry-run report. Pass either the raw report (an object with a `repositories` key) or the full `dry_run` tool summary (an object with a `report` key); the tool unwraps `report` automatically.");
|
|
6
|
+
export function registerDryRunDiff(server) {
|
|
7
|
+
server.registerTool("dry_run_diff", {
|
|
8
|
+
title: "Diff two Renovate dry-run reports",
|
|
9
|
+
description: "Compute a semantic diff between two `dry_run` reports — the proposed updates that were added, removed, or changed. Stateless: pass both reports as inputs. Updates are keyed by `(manager, packageFile, depName)`, so a version bump on the same dep shows up under `changed` rather than as `removed + added`. Compared fields per identity: `newValue`, `newVersion`, `updateType`, `branchName`, `groupName`, `schedule`. Returns a structured summary object plus a compact human-readable text rendering. Useful when iterating on a Renovate config: capture a `dry_run`, tweak the config, run again, then feed both reports here to see exactly what your tweaks did.",
|
|
10
|
+
inputSchema: {
|
|
11
|
+
before: reportShape,
|
|
12
|
+
after: reportShape,
|
|
13
|
+
},
|
|
14
|
+
}, async ({ before, after }) => {
|
|
15
|
+
const diff = diffDryRunReports(before, after);
|
|
16
|
+
return {
|
|
17
|
+
content: [
|
|
18
|
+
{
|
|
19
|
+
type: "text",
|
|
20
|
+
text: JSON.stringify(diff, null, 2),
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describeVersion, getVersionInfo } from "../lib/version.js";
|
|
2
|
+
export function registerGetVersion(server) {
|
|
3
|
+
server.registerTool("get_version", {
|
|
4
|
+
title: "Get renovate-mcp version",
|
|
5
|
+
description: "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). Useful when the user asks which version is wired up.",
|
|
6
|
+
inputSchema: {},
|
|
7
|
+
}, async () => {
|
|
8
|
+
const info = getVersionInfo();
|
|
9
|
+
return {
|
|
10
|
+
content: [
|
|
11
|
+
{
|
|
12
|
+
type: "text",
|
|
13
|
+
text: `${describeVersion(info)}\n\n${JSON.stringify(info, null, 2)}`,
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
}
|
package/dist/tools/lintConfig.js
CHANGED
|
@@ -6,7 +6,7 @@ import { lintConfig } from "../lib/configLinter.js";
|
|
|
6
6
|
export function registerLintConfig(server) {
|
|
7
7
|
server.registerTool("lint_config", {
|
|
8
8
|
title: "Lint Renovate config for semantic footguns",
|
|
9
|
-
description: "Run a semantic lint pass over a Renovate config. Complements validate_config: schema validation catches structural bugs, this catches Renovate-specific footguns schema validation misses —
|
|
9
|
+
description: "Run a semantic lint pass over a Renovate config. Complements validate_config: schema validation catches structural bugs, this catches Renovate-specific footguns schema validation misses — malformed '/…/' regex patterns in fields like matchPackageNames, matchDepNames, matchSourceUrls, matchCurrentVersion, plus unknown manager names in matchManagers / excludeManagers (typos that Renovate silently ignores). Offline; does not shell out. Pass either configPath (file on disk, JSON or JSON5) or configContent (inline object).",
|
|
10
10
|
inputSchema: {
|
|
11
11
|
configPath: z
|
|
12
12
|
.string()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "renovate-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "MCP server for designing Renovate configurations interactively.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"pretest:coverage": "npm run build",
|
|
24
24
|
"test:coverage": "vitest run --coverage",
|
|
25
25
|
"generate:presets": "node scripts/generate-presets.mjs",
|
|
26
|
+
"generate:managers": "node scripts/generate-managers.mjs",
|
|
26
27
|
"prepack": "npm run build && chmod +x dist/index.js"
|
|
27
28
|
},
|
|
28
29
|
"keywords": [
|