renovate-mcp 0.5.0 → 0.6.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
@@ -31,7 +31,7 @@ Eight tools plus a preset reference:
31
31
  | `preview_custom_manager` | Preview a `customManagers` (regex) entry against a local repo — shows file/line hits and extracted dep info. Offline. |
32
32
  | `validate_config` | Run `renovate-config-validator` against a file or inline object. |
33
33
  | `lint_config` | Semantic lint for Renovate-specific footguns schema validation misses (mostly malformed `/…/` regex patterns). Offline. |
34
- | `dry_run` | Run Renovate with `--platform=local --dry-run` and return the structured JSON report. No PRs, no pushes. Emits MCP progress notifications during the run when the caller supplies a `progressToken`. |
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`. |
35
35
  | `write_config` | Validate, then atomically write a config to disk. Refuses to save invalid configs unless `force: true`. |
36
36
  | `renovate://presets` (resource) | Markdown index of all 1000+ built-in presets grouped by namespace. |
37
37
  | `renovate://presets/{namespace}` (resource template) | Markdown listing for a single namespace (e.g. `renovate://presets/config`) — cheaper than the full index. |
@@ -125,6 +125,7 @@ Once the server is wired up, try prompts like these. They're written for Claude
125
125
 
126
126
  - "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
127
  - "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."
128
129
 
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.)
130
131
 
@@ -209,6 +210,8 @@ Scope and non-goals are summarized in [What this is NOT](#what-this-is-not); thi
209
210
  - `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
211
  - `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
212
  - 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=`, and deleted in a `finally` block. Token/password values are scrubbed from both the detected `problems` list and the `logTail` fallback before returning.
213
+ - `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`.
215
+ - `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
216
  - `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.
214
217
  - `write_config` writes to a temp file, validates, then atomically renames — so a failed validation never leaves a broken config on disk.
package/dist/index.js CHANGED
@@ -33,7 +33,7 @@ const requireCliRaw = process.env.RENOVATE_MCP_REQUIRE_CLI?.trim().toLowerCase()
33
33
  const suppressBanner = requireCliRaw === "false" || requireCliRaw === "0";
34
34
  const banner = suppressBanner ? null : startupBanner(setup);
35
35
  const instructions = banner ? [BASE_INSTRUCTIONS, "", banner].join("\n") : BASE_INSTRUCTIONS;
36
- const SERVER_VERSION = "0.5.0"; // x-release-please-version
36
+ const SERVER_VERSION = "0.6.0"; // x-release-please-version
37
37
  const server = new McpServer({ name: "renovate-mcp", version: SERVER_VERSION }, { instructions });
38
38
  registerCheckSetup(server);
39
39
  registerReadConfig(server);
@@ -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 merges CLI `--config-file` rules with any repo-level `hostRules`
9
- * (array concatenation, most-specific match wins), so callers can combine
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 merges CLI `--config-file` rules with any repo-level `hostRules`
12
- * (array concatenation, most-specific match wins), so callers can combine
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) {
@@ -4,18 +4,94 @@ 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 { locateConfig } from "../lib/configLocations.js";
7
8
  import { detectLookupProblems } from "../lib/lookupProblems.js";
8
9
  import { collectSecrets, scrubSecrets, writeHostRulesConfig, } from "../lib/hostRulesConfig.js";
9
10
  import { extractRenovateLogMsg, buildDryRunHeartbeatMessage, } from "../lib/dryRunProgress.js";
10
11
  const hostRuleSchema = z
11
12
  .record(z.string(), z.unknown())
12
13
  .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.");
14
+ /**
15
+ * Walk Renovate's JSON report looking for `problems` arrays and collect the
16
+ * ones that represent failed runs — not informational notices. Criteria:
17
+ * - `level >= 40` (Renovate's bunyan-derived ERROR/FATAL bands), or
18
+ * - `message === "config-validation"` (Renovate's canonical config-failure
19
+ * marker — it can appear with lower levels but still means the run was
20
+ * effectively a no-op), or
21
+ * - the problem object has a `validationError` field (preset/extends
22
+ * resolution failures carry their explanation here).
23
+ *
24
+ * The report shape isn't a stable Renovate API, so we walk defensively: any
25
+ * nested `problems` array anywhere in the tree is inspected.
26
+ */
27
+ function collectReportErrors(report) {
28
+ const out = [];
29
+ const seen = new Set();
30
+ const visit = (node) => {
31
+ if (node === null || typeof node !== "object")
32
+ return;
33
+ if (seen.has(node))
34
+ return;
35
+ seen.add(node);
36
+ if (Array.isArray(node)) {
37
+ for (const entry of node)
38
+ visit(entry);
39
+ return;
40
+ }
41
+ const obj = node;
42
+ const problems = obj.problems;
43
+ if (Array.isArray(problems)) {
44
+ for (const raw of problems) {
45
+ if (!raw || typeof raw !== "object")
46
+ continue;
47
+ const p = raw;
48
+ const level = typeof p.level === "number" ? p.level : 0;
49
+ const isErrorLevel = level >= 40;
50
+ const isConfigValidation = p.message === "config-validation";
51
+ const hasValidationError = typeof p.validationError === "string" && p.validationError.length > 0;
52
+ if (isErrorLevel || isConfigValidation || hasValidationError) {
53
+ out.push(p);
54
+ }
55
+ }
56
+ }
57
+ for (const value of Object.values(obj))
58
+ visit(value);
59
+ };
60
+ visit(report);
61
+ return out;
62
+ }
63
+ /**
64
+ * Scan the repo's Renovate config for `extends` entries of the form
65
+ * `local>owner/repo[:preset]` and return them along with the config-file path
66
+ * they were found in. Returns `null` if no config is found (Renovate will
67
+ * handle that case itself with its usual error). Silently returns `null` on
68
+ * parse errors too — we don't want preflight to be stricter than Renovate's
69
+ * own config loader; any real problems will surface when Renovate runs.
70
+ */
71
+ async function detectUnresolvableLocalPresets(repoPath) {
72
+ let located;
73
+ try {
74
+ located = await locateConfig(repoPath);
75
+ }
76
+ catch {
77
+ return null;
78
+ }
79
+ if (!located)
80
+ return null;
81
+ const extendsRaw = located.config.extends;
82
+ if (!Array.isArray(extendsRaw))
83
+ return null;
84
+ const presets = extendsRaw.filter((entry) => typeof entry === "string" && entry.startsWith("local>"));
85
+ return { relPath: located.relPath, presets };
86
+ }
13
87
  export function registerDryRun(server) {
14
88
  server.registerTool("dry_run", {
15
89
  title: "Dry-run Renovate",
16
- description: "Run Renovate in dry-run mode against a local repository to preview what it would do — no PRs opened, no git pushes. Uses --platform=local so no host token is required. Emits a structured JSON report of the updates Renovate would create.\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 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.",
90
+ 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 (or any preset hosted on a non-default GitHub/GitLab), pass `platform` (`github` or `gitlab`), `endpoint` (API base URL), `token`, and `repository` (`owner/repo`) to run as a real platform client that can actually fetch those presets — Renovate still runs with `--dry-run`, so no PRs are opened. The tool preflight-checks for `local>` presets under `--platform=local` and fails fast with remediation guidance rather than spawning a Renovate run that would fail opaquely with `config-validation`.\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
91
  inputSchema: {
18
- repoPath: z.string().describe("Absolute path to the repository root"),
92
+ repoPath: z
93
+ .string()
94
+ .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
95
  dryRunMode: z
20
96
  .enum(["extract", "lookup", "full"])
21
97
  .default("full")
@@ -28,17 +104,72 @@ export function registerDryRun(server) {
28
104
  .max(15 * 60_000)
29
105
  .default(5 * 60_000)
30
106
  .describe("Max wait time in ms (default 5 minutes, capped at 15)"),
107
+ platform: z
108
+ .enum(["local", "github", "gitlab"])
109
+ .optional()
110
+ .describe("Renovate platform to run as. 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."),
111
+ endpoint: z
112
+ .string()
113
+ .optional()
114
+ .describe("API base URL for the chosen platform (e.g. `https://gitlab.example.com/api/v4/` or `https://ghe.example.com/api/v3/`). Required with non-default GitHub/GitLab hosts. Ignored when `platform=local`."),
115
+ token: z
116
+ .string()
117
+ .optional()
118
+ .describe("Platform auth token. Scrubbed from any log output this tool returns. Ignored when `platform=local`."),
119
+ repository: z
120
+ .string()
121
+ .optional()
122
+ .describe("`owner/repo` identifier of the repository Renovate should operate on. Required when `platform` is not `local`. Ignored when `platform=local`."),
31
123
  hostRules: z
32
124
  .array(hostRuleSchema)
33
125
  .optional()
34
- .describe("Optional per-invocation Renovate hostRules for private registry auth. Written to a mode-0600 temp file that is passed via --config-file 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."),
126
+ .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
127
  },
36
- }, async ({ repoPath, dryRunMode, logLevel, timeoutMs, hostRules }, extra) => {
128
+ }, async ({ repoPath, dryRunMode, logLevel, timeoutMs, platform, endpoint, token, repository, hostRules }, extra) => {
37
129
  const reportPath = path.join(tmpdir(), `renovate-mcp-report-${randomUUID()}.json`);
38
130
  const bin = resolveRenovateTool("renovate");
39
131
  const ruleList = hostRules ?? [];
132
+ const effectivePlatform = platform ?? "local";
133
+ const isRemotePlatform = effectivePlatform !== "local";
40
134
  const secrets = collectSecrets(ruleList);
135
+ if (token)
136
+ secrets.push(token);
41
137
  let hostRulesConfigPath;
138
+ if (isRemotePlatform && !repository) {
139
+ return {
140
+ isError: true,
141
+ content: [
142
+ {
143
+ type: "text",
144
+ text: `\`repository\` is required when \`platform\` is \`${effectivePlatform}\`. Pass the target repo as \`owner/repo\`, or unset \`platform\` to run against the local filesystem at \`repoPath\`.`,
145
+ },
146
+ ],
147
+ };
148
+ }
149
+ // Preflight: if the repo's config extends any `local>…` preset and we're
150
+ // about to run with `--platform=local`, Renovate will fail opaquely with
151
+ // a generic "config-validation" error, because `local>` has no platform
152
+ // context to resolve against in local mode. Detect this up front and
153
+ // return an actionable message instead of spawning Renovate.
154
+ if (!isRemotePlatform) {
155
+ const unresolvable = await detectUnresolvableLocalPresets(repoPath);
156
+ if (unresolvable && unresolvable.presets.length > 0) {
157
+ const sample = unresolvable.presets.slice(0, 3).join(", ");
158
+ const more = unresolvable.presets.length > 3
159
+ ? ` (+${unresolvable.presets.length - 3} more)`
160
+ : "";
161
+ return {
162
+ isError: true,
163
+ content: [
164
+ {
165
+ type: "text",
166
+ 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` +
167
+ `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.`,
168
+ },
169
+ ],
170
+ };
171
+ }
172
+ }
42
173
  // Hybrid progress reporter: a heartbeat tick every HEARTBEAT_MS keeps
43
174
  // MCP clients informed during long runs, and each tick enriches its
44
175
  // message with the latest Renovate JSON-log `msg` we saw (best-effort —
@@ -67,14 +198,28 @@ export function registerDryRun(server) {
67
198
  };
68
199
  try {
69
200
  const args = [
70
- "--platform=local",
201
+ `--platform=${effectivePlatform}`,
71
202
  `--dry-run=${dryRunMode}`,
72
203
  "--report-type=file",
73
204
  `--report-path=${reportPath}`,
74
205
  ];
206
+ const childEnv = {
207
+ LOG_LEVEL: logLevel,
208
+ LOG_FORMAT: "json",
209
+ };
210
+ if (isRemotePlatform) {
211
+ if (endpoint)
212
+ args.push(`--endpoint=${endpoint}`);
213
+ args.push(`--repository=${repository}`);
214
+ if (token)
215
+ childEnv.RENOVATE_TOKEN = token;
216
+ }
75
217
  if (ruleList.length > 0) {
76
218
  hostRulesConfigPath = await writeHostRulesConfig(ruleList);
77
- args.push(`--config-file=${hostRulesConfigPath}`);
219
+ // Renovate reads this config file via the RENOVATE_CONFIG_FILE env
220
+ // var, not a CLI flag — `--config-file` is not a real Renovate flag
221
+ // and passing it crashes the CLI with "unknown option".
222
+ childEnv.RENOVATE_CONFIG_FILE = hostRulesConfigPath;
78
223
  }
79
224
  if (progressEnabled) {
80
225
  emit(`Starting Renovate dry-run (${dryRunMode})`);
@@ -82,7 +227,7 @@ export function registerDryRun(server) {
82
227
  }
83
228
  const result = await run(bin, args, {
84
229
  cwd: repoPath,
85
- env: { LOG_LEVEL: logLevel, LOG_FORMAT: "json" },
230
+ env: childEnv,
86
231
  timeoutMs,
87
232
  onStdoutLine: progressEnabled
88
233
  ? (line) => {
@@ -104,7 +249,15 @@ export function registerDryRun(server) {
104
249
  catch {
105
250
  // no report produced (e.g., renovate errored before writing)
106
251
  }
252
+ const reportErrors = collectReportErrors(report);
107
253
  const summary = {
254
+ // `ok` collapses the three failure modes (spawn error, non-zero
255
+ // exit, in-report validation/error-level problems) into a single
256
+ // field so callers don't need to know which one fired. Renovate
257
+ // frequently writes a report with exitCode=0 while its `problems`
258
+ // array records a validation/config failure — judging only by
259
+ // exitCode hides those runs.
260
+ ok: result.exitCode === 0 && reportErrors.length === 0,
108
261
  exitCode: result.exitCode,
109
262
  hasReport: report !== null,
110
263
  report,
@@ -115,6 +268,9 @@ export function registerDryRun(server) {
115
268
  if (problems.length > 0) {
116
269
  summary.problems = problems;
117
270
  }
271
+ if (reportErrors.length > 0) {
272
+ summary.reportErrors = reportErrors;
273
+ }
118
274
  // If no structured report, surface the last bit of stderr so Claude can
119
275
  // debug without blowing up the context with Renovate's verbose logs.
120
276
  if (!report) {
@@ -137,7 +293,7 @@ export function registerDryRun(server) {
137
293
  text: JSON.stringify(summary, null, 2),
138
294
  },
139
295
  ],
140
- isError: result.exitCode !== 0,
296
+ isError: !summary.ok,
141
297
  };
142
298
  }
143
299
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "renovate-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "MCP server for designing Renovate configurations interactively.",
5
5
  "type": "module",
6
6
  "bin": {