renovate-mcp 1.0.4 → 1.1.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
@@ -38,7 +38,7 @@ Restart your client and try the prompt: *"List the namespaces available under `r
38
38
 
39
39
  ## Tools & resources
40
40
 
41
- Twelve tools and three resource templates. Each tool name below links to its full reference in [`docs/tools.md`](docs/tools.md).
41
+ Thirteen tools and three resource templates. Each tool name below links to its full reference in [`docs/tools.md`](docs/tools.md).
42
42
 
43
43
  | Tool | Purpose |
44
44
  | --- | --- |
@@ -47,6 +47,7 @@ Twelve tools and three resource templates. Each tool name below links to its ful
47
47
  | [`read_config`](docs/tools.md#read_config) | Locate and parse a repo's Renovate config in Renovate's own discovery order. |
48
48
  | [`resolve_config`](docs/tools.md#resolve_config) | Expand every `extends` preset offline. Opt in to fetching `github>` / `gitlab>` presets over HTTPS. |
49
49
  | [`explain_config`](docs/tools.md#explain_config) | Inverse of `resolve_config`: annotate every leaf field with the chain of presets that set it. |
50
+ | [`resolve_config_diff`](docs/tools.md#resolve_config_diff) | Offline structural diff of two fully-resolved configs — changed fields plus an order-insensitive set diff of array keys (`packageRules`, `customManagers`, …). The refactor-friendly counterpart to `dry_run_diff`. |
50
51
  | [`preview_custom_manager`](docs/tools.md#preview_custom_manager) | Preview a `customManagers` entry (regex or JSONata) against a local repo. Offline. |
51
52
  | [`validate_config`](docs/tools.md#validate_config) | Run `renovate-config-validator` against a file or inline object. |
52
53
  | [`lint_config`](docs/tools.md#lint_config) | Semantic lint pass for Renovate-specific footguns the schema validator declares valid. Offline. |
@@ -63,13 +64,14 @@ Twelve tools and three resource templates. Each tool name below links to its ful
63
64
  - **Linux or macOS.** Windows is not supported — `package.json` declares `"os": ["darwin", "linux"]`, so `npm i` surfaces an `EBADPLATFORM` warning on Windows and the server exits with a clear stderr message at startup. Use WSL2 or a Linux/macOS host instead.
64
65
  - **Node.js ≥ 24** (aligns with Renovate's own engine requirement).
65
66
 
66
- Renovate ships bundled — the `renovate` package is a runtime dependency, so `validate_config`, `dry_run`, and `write_config` work out of the box with no separate install. The offline tools (`read_config`, `resolve_config`, `explain_config`, `preview_custom_manager`, `lint_config`) never spawn Renovate at all.
67
+ Renovate ships bundled — the `renovate` package is a runtime dependency, so `validate_config`, `dry_run`, and `write_config` work out of the box with no separate install. The offline tools (`read_config`, `resolve_config`, `explain_config`, `resolve_config_diff`, `preview_custom_manager`, `lint_config`) never spawn Renovate at all.
67
68
 
68
69
  **Optional env vars:**
69
70
 
70
71
  - `RENOVATE_BIN` / `RENOVATE_CONFIG_VALIDATOR_BIN` — override the bundled binaries. When set, the override always wins.
71
72
  - `RENOVATE_MCP_REQUIRE_CLI=false` — suppress the startup "partial availability" notice when you only intend to use the offline tools.
72
73
  - Platform + token env vars — see [`docs/platform-setup.md`](docs/platform-setup.md) for the per-platform matrix.
74
+ - `GITHUB_COM_TOKEN` — authenticates github.com *datasource* lookups (release notes, `github-tags` / `github-releases` / `github-actions`). Distinct from the platform token and never auto-derived from it; set it when github.com-hosted deps are resolved under the default `dry_run` `platform: local`. See [`docs/security.md`](docs/security.md#github_com_token--a-separate-role).
73
75
 
74
76
  > Heads up: MCP servers do **not** inherit your shell env. Set every env var via the `env` key in `.mcp.json` / `claude_desktop_config.json`. See [`docs/security.md`](docs/security.md#where-env-vars-must-live).
75
77
 
@@ -1,4 +1,4 @@
1
- export declare const RENOVATE_VERSION = "43.220.0";
1
+ export declare const RENOVATE_VERSION = "43.233.3";
2
2
  /**
3
3
  * All manager names accepted by Renovate's `matchManagers` /
4
4
  * `excludeManagers` fields. Includes both regular managers and custom
@@ -1,6 +1,6 @@
1
- // AUTO-GENERATED by scripts/generate-managers.mjs from renovate@43.220.0.
1
+ // AUTO-GENERATED by scripts/generate-managers.mjs from renovate@43.233.3.
2
2
  // Do not edit by hand. Run `npm run generate:managers` to refresh.
3
- export const RENOVATE_VERSION = "43.220.0";
3
+ export const RENOVATE_VERSION = "43.233.3";
4
4
  /**
5
5
  * All manager names accepted by Renovate's `matchManagers` /
6
6
  * `excludeManagers` fields. Includes both regular managers and custom
@@ -1,4 +1,4 @@
1
- export declare const RENOVATE_VERSION = "43.220.0";
1
+ export declare const RENOVATE_VERSION = "43.233.3";
2
2
  export interface DeprecatedKeyEntry {
3
3
  /** Key as it appeared in old configs. */
4
4
  readonly oldKey: string;
@@ -1,6 +1,6 @@
1
- // AUTO-GENERATED by scripts/generate-migrations.mjs from renovate@43.220.0.
1
+ // AUTO-GENERATED by scripts/generate-migrations.mjs from renovate@43.233.3.
2
2
  // Do not edit by hand. Run `npm run generate:migrations` to refresh.
3
- export const RENOVATE_VERSION = "43.220.0";
3
+ export const RENOVATE_VERSION = "43.233.3";
4
4
  /**
5
5
  * Deprecated config keys with their current replacement. Sourced from
6
6
  * Renovate's `MigrationsService.renamedProperties` — the "RenamePropertyMigration"
@@ -3,6 +3,6 @@ export interface GeneratedPreset {
3
3
  description: string | null;
4
4
  body: Record<string, unknown>;
5
5
  }
6
- export declare const RENOVATE_VERSION = "43.220.0";
6
+ export declare const RENOVATE_VERSION = "43.233.3";
7
7
  export declare const PRESETS: Record<string, GeneratedPreset>;
8
8
  export declare const PRESET_NAMES: readonly string[];
@@ -1,6 +1,6 @@
1
- // AUTO-GENERATED by scripts/generate-presets.mjs from renovate@43.220.0.
1
+ // AUTO-GENERATED by scripts/generate-presets.mjs from renovate@43.233.3.
2
2
  // Do not edit by hand. Run `npm run generate:presets` to refresh.
3
- export const RENOVATE_VERSION = "43.220.0";
3
+ export const RENOVATE_VERSION = "43.233.3";
4
4
  export const PRESETS = {
5
5
  "abandonments:recommended": {
6
6
  "namespace": "abandonments",
@@ -2108,6 +2108,27 @@ export const PRESETS = {
2108
2108
  ]
2109
2109
  }
2110
2110
  },
2111
+ "group:arrow-rsMonorepo": {
2112
+ "namespace": "group",
2113
+ "description": null,
2114
+ "body": {
2115
+ "packageRules": [
2116
+ {
2117
+ "description": "Group packages from arrow-rs monorepo together.",
2118
+ "extends": [
2119
+ "monorepo:arrow-rs"
2120
+ ],
2121
+ "groupName": "arrow-rs monorepo",
2122
+ "matchUpdateTypes": [
2123
+ "digest",
2124
+ "patch",
2125
+ "minor",
2126
+ "major"
2127
+ ]
2128
+ }
2129
+ ]
2130
+ }
2131
+ },
2111
2132
  "group:aspireMonorepo": {
2112
2133
  "namespace": "group",
2113
2134
  "description": null,
@@ -7221,6 +7242,7 @@ export const PRESETS = {
7221
7242
  "group:arcus.securityMonorepo",
7222
7243
  "group:arcus.webapiMonorepo",
7223
7244
  "group:arrow-ktMonorepo",
7245
+ "group:arrow-rsMonorepo",
7224
7246
  "group:aspireMonorepo",
7225
7247
  "group:aspnet aspnetwebstackMonorepo",
7226
7248
  "group:aspnet extensionsMonorepo",
@@ -13384,6 +13406,15 @@ export const PRESETS = {
13384
13406
  ]
13385
13407
  }
13386
13408
  },
13409
+ "monorepo:arrow-rs": {
13410
+ "namespace": "monorepo",
13411
+ "description": "arrow-rs monorepo",
13412
+ "body": {
13413
+ "matchSourceUrls": [
13414
+ "https://github.com/apache/arrow-rs"
13415
+ ]
13416
+ }
13417
+ },
13387
13418
  "monorepo:aspire": {
13388
13419
  "namespace": "monorepo",
13389
13420
  "description": "aspire monorepo",
@@ -20290,6 +20321,7 @@ export const PRESETS = {
20290
20321
  "workarounds:containerbase",
20291
20322
  "workarounds:bitnamiDockerImageVersioning",
20292
20323
  "workarounds:clamavDockerImageVersioning",
20324
+ "workarounds:grafanaDockerImageVersioning",
20293
20325
  "workarounds:k3sKubernetesVersioning",
20294
20326
  "workarounds:rke2KubernetesVersioning",
20295
20327
  "workarounds:libericaJdkDockerVersioning",
@@ -20434,6 +20466,26 @@ export const PRESETS = {
20434
20466
  ]
20435
20467
  }
20436
20468
  },
20469
+ "workarounds:grafanaDockerImageVersioning": {
20470
+ "namespace": "workarounds",
20471
+ "description": "Support security releases on Grafana (product) images",
20472
+ "body": {
20473
+ "packageRules": [
20474
+ {
20475
+ "matchDatasources": [
20476
+ "docker"
20477
+ ],
20478
+ "matchPackageNames": [
20479
+ "grafana/grafana",
20480
+ "docker.io/grafana/grafana",
20481
+ "grafana/grafana-enterprise",
20482
+ "docker.io/grafana/grafana-enterprise"
20483
+ ],
20484
+ "versioning": "regex:^(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)(?:-security-(?<build>\\d+))?(?:-(?<compatibility>.*))?$"
20485
+ }
20486
+ ]
20487
+ }
20488
+ },
20437
20489
  "workarounds:ignoreHttp4sDigestMilestones": {
20438
20490
  "namespace": "workarounds",
20439
20491
  "description": "Ignore `http4s` digest-based `1.x` milestones.",
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import { registerValidateConfig } from "./tools/validateConfig.js";
7
7
  import { registerLintConfig } from "./tools/lintConfig.js";
8
8
  import { registerResolveConfig } from "./tools/resolveConfig.js";
9
9
  import { registerExplainConfig } from "./tools/explainConfig.js";
10
+ import { registerResolveConfigDiff } from "./tools/resolveConfigDiff.js";
10
11
  import { registerPreviewCustomManager } from "./tools/previewCustomManager.js";
11
12
  import { registerDryRun } from "./tools/dryRun.js";
12
13
  import { registerDryRunDiff } from "./tools/dryRunDiff.js";
@@ -33,13 +34,14 @@ const BASE_INSTRUCTIONS = [
33
34
  " 1. read_config — inspect the current config in a repo",
34
35
  " 2. resolve_config — expand built-in presets to see what a config actually becomes (offline)",
35
36
  " 3. explain_config — inverse of resolve_config: trace which preset set each field (offline)",
36
- " 4. preview_custom_manager — iterate on a regex-based customManagers entry; shows file/line hits and extracted deps",
37
- " 5. validate_config — check a proposed config against Renovate's schema",
38
- " 6. lint_config — semantic lint pass: catches Renovate-specific footguns schema validation misses (e.g. malformed /…/ regex patterns)",
39
- " 7. dry_run — preview what Renovate would actually do (no PRs)",
40
- " 8. dry_run_diff — semantic diff between two dry_run reports (added/removed/changed updates)",
41
- " 9. migrate_config — apply Renovate's built-in migrations (deprecated keys → current schema) and return the migrated config",
42
- " 10. write_config — save the agreed-upon config (validates first)",
37
+ " 4. resolve_config_diff — offline structural diff of two resolved configs (changed fields + array/packageRules set diffs); use this over dry_run_diff for config refactors, especially when registries are unreachable",
38
+ " 5. preview_custom_manager — iterate on a regex-based customManagers entry; shows file/line hits and extracted deps",
39
+ " 6. validate_config — check a proposed config against Renovate's schema",
40
+ " 7. lint_config — semantic lint pass: catches Renovate-specific footguns schema validation misses (e.g. malformed /…/ regex patterns)",
41
+ " 8. dry_run — preview what Renovate would actually do (no PRs)",
42
+ " 9. dry_run_diff — semantic diff between two dry_run reports (added/removed/changed updates)",
43
+ " 10. migrate_config — apply Renovate's built-in migrations (deprecated keys → current schema) and return the migrated config",
44
+ " 11. write_config — save the agreed-upon config (validates first)",
43
45
  "",
44
46
  "Before the first repo-touching tool call in a session (read_config, resolve_config, dry_run, write_config, …), call check_setup with the same repoPath. It surfaces token / endpoint / connectivity problems up front (e.g. \"set GITHUB_TOKEN or github-actions deps will be skipped\") instead of waiting for dry_run to fail. Skip if the user has already confirmed setup or is doing offline-only work that doesn't depend on git origin or registries.",
45
47
  "If any tool fails unexpectedly, call check_setup to diagnose CLI availability.",
@@ -61,6 +63,7 @@ registerCheckSetup(server);
61
63
  registerReadConfig(server);
62
64
  registerResolveConfig(server);
63
65
  registerExplainConfig(server);
66
+ registerResolveConfigDiff(server);
64
67
  registerPreviewCustomManager(server);
65
68
  registerValidateConfig(server);
66
69
  registerLintConfig(server);
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Pure structural diff over two *resolved* Renovate configs (the output of
3
+ * `resolve_config` — preset-expanded and merged). This is the resolve-level
4
+ * counterpart to `dryRunDiff.ts`: it answers "did this config refactor change
5
+ * the effective settings only in the ways I intended?" without running
6
+ * Renovate, so it works offline even when datasource lookups would fail.
7
+ *
8
+ * Two diff modes by key shape:
9
+ * - Array-valued top-level keys (`packageRules`, `customManagers`,
10
+ * `matchManagers`, `addLabels`, …) → order-insensitive *set* diff. Members
11
+ * are normalized with `stableStringify` (recursive sort-keys) and compared
12
+ * as a set, so a reordered array reads as no change and a tweaked rule
13
+ * reads as one removed + one added (no pairing). Mirrors the throwaway
14
+ * Python script this tool replaces.
15
+ * - Everything else → deep-compare via `stableStringify` equality; a
16
+ * difference emits the full before/after values.
17
+ *
18
+ * Only TOP-LEVEL keys are walked. Arrays nested inside an object are part of
19
+ * that object's deep-compare and surface as a whole-field change.
20
+ */
21
+ export interface ResolveConfigFieldChange {
22
+ key: string;
23
+ before: unknown;
24
+ after: unknown;
25
+ }
26
+ export interface ResolveConfigArrayChange {
27
+ added: unknown[];
28
+ removed: unknown[];
29
+ }
30
+ export interface ResolveConfigDiffSummary {
31
+ fieldsChanged: number;
32
+ arraysChanged: number;
33
+ arrayItemsAdded: number;
34
+ arrayItemsRemoved: number;
35
+ }
36
+ export interface ResolveConfigDiff {
37
+ summary: ResolveConfigDiffSummary;
38
+ fieldChanges: ResolveConfigFieldChange[];
39
+ arrayChanges: Record<string, ResolveConfigArrayChange>;
40
+ text: string;
41
+ }
42
+ /**
43
+ * Deterministic JSON serialization with object keys sorted recursively, so two
44
+ * structurally-equal values produce byte-identical strings regardless of key
45
+ * insertion order. `JSON.stringify` alone preserves insertion order, which
46
+ * would make `{a,b}` and `{b,a}` compare unequal. Arrays keep their order
47
+ * (callers that want order-insensitivity sort the normalized members).
48
+ */
49
+ export declare function stableStringify(value: unknown): string;
50
+ export declare function diffResolvedConfigs(before: Record<string, unknown>, after: Record<string, unknown>): ResolveConfigDiff;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Pure structural diff over two *resolved* Renovate configs (the output of
3
+ * `resolve_config` — preset-expanded and merged). This is the resolve-level
4
+ * counterpart to `dryRunDiff.ts`: it answers "did this config refactor change
5
+ * the effective settings only in the ways I intended?" without running
6
+ * Renovate, so it works offline even when datasource lookups would fail.
7
+ *
8
+ * Two diff modes by key shape:
9
+ * - Array-valued top-level keys (`packageRules`, `customManagers`,
10
+ * `matchManagers`, `addLabels`, …) → order-insensitive *set* diff. Members
11
+ * are normalized with `stableStringify` (recursive sort-keys) and compared
12
+ * as a set, so a reordered array reads as no change and a tweaked rule
13
+ * reads as one removed + one added (no pairing). Mirrors the throwaway
14
+ * Python script this tool replaces.
15
+ * - Everything else → deep-compare via `stableStringify` equality; a
16
+ * difference emits the full before/after values.
17
+ *
18
+ * Only TOP-LEVEL keys are walked. Arrays nested inside an object are part of
19
+ * that object's deep-compare and surface as a whole-field change.
20
+ */
21
+ /**
22
+ * Deterministic JSON serialization with object keys sorted recursively, so two
23
+ * structurally-equal values produce byte-identical strings regardless of key
24
+ * insertion order. `JSON.stringify` alone preserves insertion order, which
25
+ * would make `{a,b}` and `{b,a}` compare unequal. Arrays keep their order
26
+ * (callers that want order-insensitivity sort the normalized members).
27
+ */
28
+ export function stableStringify(value) {
29
+ return JSON.stringify(sortValue(value));
30
+ }
31
+ function sortValue(value) {
32
+ if (Array.isArray(value)) {
33
+ return value.map(sortValue);
34
+ }
35
+ if (value && typeof value === "object") {
36
+ const obj = value;
37
+ const out = {};
38
+ for (const key of Object.keys(obj).sort()) {
39
+ out[key] = sortValue(obj[key]);
40
+ }
41
+ return out;
42
+ }
43
+ return value;
44
+ }
45
+ function diffArrayKey(before, after) {
46
+ const beforeNorm = new Map();
47
+ for (const item of before)
48
+ beforeNorm.set(stableStringify(item), item);
49
+ const afterNorm = new Map();
50
+ for (const item of after)
51
+ afterNorm.set(stableStringify(item), item);
52
+ const removed = [];
53
+ for (const [norm, item] of beforeNorm) {
54
+ if (!afterNorm.has(norm))
55
+ removed.push(item);
56
+ }
57
+ const added = [];
58
+ for (const [norm, item] of afterNorm) {
59
+ if (!beforeNorm.has(norm))
60
+ added.push(item);
61
+ }
62
+ // Deterministic output regardless of source ordering.
63
+ removed.sort((a, b) => stableStringify(a).localeCompare(stableStringify(b)));
64
+ added.sort((a, b) => stableStringify(a).localeCompare(stableStringify(b)));
65
+ return { added, removed };
66
+ }
67
+ export function diffResolvedConfigs(before, after) {
68
+ const keys = Array.from(new Set([...Object.keys(before), ...Object.keys(after)])).sort();
69
+ const fieldChanges = [];
70
+ const arrayChanges = {};
71
+ for (const key of keys) {
72
+ const hasBefore = Object.prototype.hasOwnProperty.call(before, key);
73
+ const hasAfter = Object.prototype.hasOwnProperty.call(after, key);
74
+ const beforeVal = before[key];
75
+ const afterVal = after[key];
76
+ const beforeIsArray = Array.isArray(beforeVal);
77
+ const afterIsArray = Array.isArray(afterVal);
78
+ // Treat a key as "array-shaped" when it is an array on at least one side
79
+ // and never a non-array object on the other (so we don't set-diff an
80
+ // array against a scalar/object — that's a real type change).
81
+ if ((beforeIsArray || afterIsArray) &&
82
+ (!hasBefore || beforeIsArray) &&
83
+ (!hasAfter || afterIsArray)) {
84
+ const change = diffArrayKey(beforeIsArray ? beforeVal : [], afterIsArray ? afterVal : []);
85
+ if (change.added.length > 0 || change.removed.length > 0) {
86
+ arrayChanges[key] = change;
87
+ }
88
+ continue;
89
+ }
90
+ if (!hasBefore || !hasAfter || stableStringify(beforeVal) !== stableStringify(afterVal)) {
91
+ fieldChanges.push({
92
+ key,
93
+ before: hasBefore ? beforeVal : undefined,
94
+ after: hasAfter ? afterVal : undefined,
95
+ });
96
+ }
97
+ }
98
+ const summary = {
99
+ fieldsChanged: fieldChanges.length,
100
+ arraysChanged: Object.keys(arrayChanges).length,
101
+ arrayItemsAdded: Object.values(arrayChanges).reduce((n, c) => n + c.added.length, 0),
102
+ arrayItemsRemoved: Object.values(arrayChanges).reduce((n, c) => n + c.removed.length, 0),
103
+ };
104
+ return {
105
+ summary,
106
+ fieldChanges,
107
+ arrayChanges,
108
+ text: renderText(summary, fieldChanges, arrayChanges),
109
+ };
110
+ }
111
+ function renderText(summary, fieldChanges, arrayChanges) {
112
+ if (summary.fieldsChanged === 0 && summary.arraysChanged === 0) {
113
+ return "No differences between the two resolved configs.";
114
+ }
115
+ const lines = [];
116
+ lines.push(`${summary.fieldsChanged} field${summary.fieldsChanged === 1 ? "" : "s"} changed · ` +
117
+ `${summary.arraysChanged} array key${summary.arraysChanged === 1 ? "" : "s"} changed ` +
118
+ `(+${summary.arrayItemsAdded} / -${summary.arrayItemsRemoved})`);
119
+ if (fieldChanges.length > 0) {
120
+ lines.push("", "Changed fields:");
121
+ for (const fc of fieldChanges) {
122
+ lines.push(` ${fc.key}: OLD=${formatValue(fc.before)} NEW=${formatValue(fc.after)}`);
123
+ }
124
+ }
125
+ for (const key of Object.keys(arrayChanges).sort()) {
126
+ const change = arrayChanges[key];
127
+ lines.push("", `${key}:`);
128
+ for (const item of change.removed)
129
+ lines.push(` - ${stableStringify(item)}`);
130
+ for (const item of change.added)
131
+ lines.push(` + ${stableStringify(item)}`);
132
+ }
133
+ return lines.join("\n");
134
+ }
135
+ function formatValue(value) {
136
+ if (value === undefined)
137
+ return "(unset)";
138
+ return stableStringify(value);
139
+ }
@@ -32,6 +32,8 @@ export interface PlatformContext {
32
32
  RENOVATE_TOKEN: boolean;
33
33
  GITHUB_TOKEN: boolean;
34
34
  GITLAB_TOKEN: boolean;
35
+ /** github.com *datasource* token (release notes, github-tags/-releases/-actions). Distinct from the platform token; never auto-derived from the others. */
36
+ GITHUB_COM_TOKEN: boolean;
35
37
  };
36
38
  /**
37
39
  * What `dry_run` would pick for `--platform=` when its `platform` input is
@@ -339,6 +339,24 @@ function appendRepoHints(ctx, hints, platformContext) {
339
339
  hints.push("This repo's origin is on GitLab. Set `GITLAB_TOKEN` (or `RENOVATE_TOKEN`) in the MCP server's env, or private-preset / private-registry lookups will fail.");
340
340
  }
341
341
  }
342
+ // github.com *datasource* token gap. `dry_run` defaults to `platform: local`
343
+ // and never reads git origin, so for a github.com-origin repo the platform
344
+ // token (GITHUB_TOKEN/RENOVATE_TOKEN) is NOT applied to github.com datasource
345
+ // lookups under the default local run — Renovate reads GITHUB_COM_TOKEN for
346
+ // that, and it is never auto-derived. The origin-derived `effectivePlatform`
347
+ // can read "github" while the actual dry_run platform is "local"; this hint
348
+ // reconciles that divergence for the case that trips users up.
349
+ if (platformContext.effectiveDryRunPlatform === "local"
350
+ && ctx.remote?.host === "github.com"
351
+ && !platformContext.tokensPresent.GITHUB_COM_TOKEN) {
352
+ const hasPlatformToken = platformContext.tokensPresent.GITHUB_TOKEN || platformContext.tokensPresent.RENOVATE_TOKEN;
353
+ if (hasPlatformToken) {
354
+ hints.push("This repo's origin is github.com, but `dry_run` defaults to `platform: local` and does not read git origin. The `GITHUB_TOKEN` / `RENOVATE_TOKEN` you've set is used for Renovate's *platform* role only — and that role is active only when you run `platform: \"github\"`. Under the default local run it is NOT applied to github.com *datasource* lookups (release notes, `github-tags` / `github-releases` / `github-actions`) at all. Either (a) set `GITHUB_COM_TOKEN` (the same read-only PAT works) so those lookups are authenticated, or (b) call `dry_run` with `platform: \"github\"`, `repository: \"<owner>/<repo>\"`, and the token, which covers both roles. (Moot if your config already has a github.com `hostRules` entry.)");
355
+ }
356
+ else {
357
+ hints.push("This repo's origin is github.com. Under the default `dry_run` `platform: local`, github.com *datasource* lookups (release notes, `github-tags` / `github-releases` / `github-actions`) run anonymously — expect a low rate limit and possible `skipReason`. Set `GITHUB_COM_TOKEN` (a read-only PAT is enough) in the MCP server's env to authenticate them.");
358
+ }
359
+ }
342
360
  if (ctx.remote?.classified === "self-hosted"
343
361
  && !ctx.configEndpoint
344
362
  && !platformContext.renovateEndpoint) {
@@ -378,6 +396,7 @@ export function inspectPlatformContext(env) {
378
396
  RENOVATE_TOKEN: Boolean(env.RENOVATE_TOKEN),
379
397
  GITHUB_TOKEN: Boolean(env.GITHUB_TOKEN),
380
398
  GITLAB_TOKEN: Boolean(env.GITLAB_TOKEN),
399
+ GITHUB_COM_TOKEN: Boolean(env.GITHUB_COM_TOKEN),
381
400
  };
382
401
  const allowedPlatform = DRY_RUN_PLATFORMS.find((p) => p === renovatePlatformRaw);
383
402
  const effectiveDryRunPlatform = allowedPlatform ?? "local";
@@ -1,4 +1,4 @@
1
- export declare const SERVER_VERSION = "1.0.4";
1
+ export declare const SERVER_VERSION = "1.1.0";
2
2
  export type BuildMode = "local" | "released";
3
3
  export interface VersionInfo {
4
4
  version: string;
@@ -1,5 +1,5 @@
1
1
  import { fileURLToPath } from "node:url";
2
- export const SERVER_VERSION = "1.0.4"; // x-release-please-version
2
+ export const SERVER_VERSION = "1.1.0"; // x-release-please-version
3
3
  // "released" = the server is running from a `node_modules/` install (global
4
4
  // `npm i -g`, project-local `npm i`, or `npx renovate-mcp`). Anything else —
5
5
  // most commonly an MCP config of `{"command": "node", "args": [".../dist/index.js"]}`
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerResolveConfigDiff(server: McpServer): void;
@@ -0,0 +1,95 @@
1
+ import { z } from "zod";
2
+ import { locateConfig } from "../lib/configLocations.js";
3
+ import { resolveConfig } from "../lib/presetResolver.js";
4
+ import { diffResolvedConfigs } from "../lib/resolveConfigDiff.js";
5
+ import { configRecord, endpointString, pathString } from "../lib/inputLimits.js";
6
+ const sideSchema = z
7
+ .object({
8
+ repoPath: pathString("Absolute path to the repository root. The tool locates the repo's renovate config automatically.").optional(),
9
+ configContent: configRecord("Inline config object to resolve — use instead of repoPath.").optional(),
10
+ })
11
+ .describe("One side of the diff: pass repoPath OR configContent.");
12
+ async function resolveSide(label, input, options) {
13
+ if (!input.repoPath && !input.configContent) {
14
+ return { ok: false, error: `Provide either repoPath or configContent for ${label}.` };
15
+ }
16
+ let source;
17
+ let sourcePath;
18
+ if (input.configContent) {
19
+ source = input.configContent;
20
+ }
21
+ else {
22
+ const located = await locateConfig(input.repoPath);
23
+ if (!located) {
24
+ return { ok: false, error: `No Renovate configuration found in ${input.repoPath} (${label}).` };
25
+ }
26
+ source = located.config;
27
+ sourcePath = located.relPath;
28
+ }
29
+ const result = await resolveConfig(source, options);
30
+ return { ok: true, result, path: sourcePath };
31
+ }
32
+ export function registerResolveConfigDiff(server) {
33
+ server.registerTool("resolve_config_diff", {
34
+ title: "Diff two resolved Renovate configs",
35
+ description: "Compute a structural diff between two *fully-resolved* Renovate configs — the `before` and `after` of a config refactor — entirely offline. This is the resolve-level counterpart to `dry_run_diff`: where `dry_run_diff` shows how the *proposed PRs* change (and collapses to a vacuous 0-vs-0 when the environment can't reach registries), this answers \"does the new config produce the same effective settings as the old one, modulo the intended changes?\". Each side accepts `repoPath` (locates the repo's config) OR `configContent` (an inline config object); both sides are resolved with `resolve_config`'s preset expansion + faithful merge. Non-array top-level keys are deep-compared and reported with before/after values; array-valued top-level keys (`packageRules`, `customManagers`, `matchManagers`, …) get an order-insensitive set diff (members only in `before` are `removed`, only in `after` are `added` — a tweaked rule shows as one of each). Shared `externalPresets` / `endpoint` / `platform` knobs apply to both sides. Returns a structured diff (`summary`, `fieldChanges`, `arrayChanges`) plus a human-readable `text` rendering and per-side `resolution` metadata (`mergeQuality`, `presetsUnresolved`, `warnings`).",
36
+ inputSchema: {
37
+ before: sideSchema,
38
+ after: sideSchema,
39
+ externalPresets: z
40
+ .boolean()
41
+ .optional()
42
+ .describe("When true, fetch external presets (github>, gitlab>) over HTTPS for BOTH sides. Credentials come from RENOVATE_TOKEN (preferred) or GITHUB_TOKEN / GITLAB_TOKEN as platform-specific fallbacks, set on the MCP server process. Default false (fully offline)."),
43
+ endpoint: endpointString("API base URL for github>/gitlab> fetches, applied to both sides. Use for GitHub Enterprise (e.g. https://ghe.example.com/api/v3) or self-hosted GitLab (e.g. https://gitlab.example.com/api/v4).").optional(),
44
+ platform: z
45
+ .enum(["github", "gitlab"])
46
+ .optional()
47
+ .describe("Platform flavour of `endpoint`, applied to both sides. When set, `local>owner/repo` presets are fetched as if they were `<platform>>owner/repo`."),
48
+ },
49
+ }, async ({ before, after, externalPresets, endpoint, platform }) => {
50
+ const options = { fetchExternal: externalPresets ?? false, endpoint, platform };
51
+ const [beforeRes, afterRes] = await Promise.all([
52
+ resolveSide("before", before, options),
53
+ resolveSide("after", after, options),
54
+ ]);
55
+ if (!beforeRes.ok) {
56
+ return { isError: true, content: [{ type: "text", text: beforeRes.error }] };
57
+ }
58
+ if (!afterRes.ok) {
59
+ return { isError: true, content: [{ type: "text", text: afterRes.error }] };
60
+ }
61
+ const diff = diffResolvedConfigs(beforeRes.result.resolved, afterRes.result.resolved);
62
+ const resolution = {
63
+ before: sideMeta(beforeRes.result, beforeRes.path),
64
+ after: sideMeta(afterRes.result, afterRes.path),
65
+ };
66
+ const advisory = degradedAdvisory(beforeRes.result, afterRes.result);
67
+ const text = advisory ? `${advisory}\n\n${diff.text}` : diff.text;
68
+ return {
69
+ content: [
70
+ {
71
+ type: "text",
72
+ text: JSON.stringify({ ...diff, text, resolution }, null, 2),
73
+ },
74
+ ],
75
+ };
76
+ });
77
+ }
78
+ function sideMeta(result, path) {
79
+ return {
80
+ ...(path ? { path } : {}),
81
+ mergeQuality: result.mergeQuality,
82
+ presetsUnresolved: result.presetsUnresolved,
83
+ warnings: result.warnings,
84
+ };
85
+ }
86
+ function degradedAdvisory(before, after) {
87
+ const notes = [];
88
+ if (before.mergeQuality === "preview" || after.mergeQuality === "preview") {
89
+ notes.push("one or both sides used the approximate in-process merge (mergeQuality: \"preview\") — the diff may not be authoritative");
90
+ }
91
+ if (before.presetsUnresolved.length > 0 || after.presetsUnresolved.length > 0) {
92
+ notes.push("one or both sides have unresolved presets — fields they would contribute are absent from this diff (see resolution.presetsUnresolved)");
93
+ }
94
+ return notes.length > 0 ? `⚠ ${notes.join("; ")}.` : null;
95
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "renovate-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for designing Renovate configurations interactively.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -64,7 +64,7 @@
64
64
  "json5": "^2.2.3",
65
65
  "jsonata": "2.2.1",
66
66
  "jsonc-parser": "^3.3.1",
67
- "renovate": "^43.220.0",
67
+ "renovate": "^43.233.3",
68
68
  "smol-toml": "^1.6.1",
69
69
  "yaml": "^2.9.0",
70
70
  "zod": "^4.0.0"