renovate-mcp 0.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.
@@ -0,0 +1,204 @@
1
+ import { PRESETS } from "../data/presets.generated.js";
2
+ import { fetchExternalPreset } from "./externalPresetFetcher.js";
3
+ /**
4
+ * Resolve a Renovate config by expanding every preset in `extends`. Built-in
5
+ * presets resolve offline against the committed catalogue; external presets
6
+ * (`github>`, `gitlab>`, …) are only fetched when `fetchExternal` is true —
7
+ * otherwise they land in `presetsUnresolved` with a network reason.
8
+ */
9
+ export async function resolveConfig(config, options = {}) {
10
+ const presetsResolved = [];
11
+ const presetsUnresolved = [];
12
+ const stack = [];
13
+ const ctx = {
14
+ fetchExternal: options.fetchExternal ?? false,
15
+ timeoutMs: options.timeoutMs,
16
+ cache: new Map(),
17
+ };
18
+ const resolved = await expand(config, presetsResolved, presetsUnresolved, stack, ctx);
19
+ return { resolved, presetsResolved, presetsUnresolved };
20
+ }
21
+ async function expand(input, resolvedList, unresolvedList, stack, ctx) {
22
+ const rawExtends = input.extends;
23
+ if (!Array.isArray(rawExtends) || rawExtends.length === 0) {
24
+ const { extends: _drop, ...rest } = input;
25
+ return rest;
26
+ }
27
+ let accumulated = {};
28
+ for (const entry of rawExtends) {
29
+ if (typeof entry !== "string") {
30
+ unresolvedList.push({
31
+ preset: String(entry),
32
+ reason: "Preset entry must be a string.",
33
+ });
34
+ continue;
35
+ }
36
+ const parsed = parsePreset(entry);
37
+ if (stack.includes(parsed.key)) {
38
+ unresolvedList.push({
39
+ preset: entry,
40
+ reason: `Cycle detected: ${[...stack, parsed.key].join(" → ")}`,
41
+ });
42
+ continue;
43
+ }
44
+ const body = await loadPresetBody(parsed, ctx, unresolvedList);
45
+ if (!body)
46
+ continue;
47
+ const substituted = applyArgs(body, parsed.args);
48
+ stack.push(parsed.key);
49
+ const subResolved = await expand(substituted, resolvedList, unresolvedList, stack, ctx);
50
+ stack.pop();
51
+ accumulated = mergeConfig(accumulated, subResolved);
52
+ resolvedList.push(entry);
53
+ }
54
+ const { extends: _drop, ...ownKeys } = input;
55
+ return mergeConfig(accumulated, ownKeys);
56
+ }
57
+ async function loadPresetBody(parsed, ctx, unresolvedList) {
58
+ if (!parsed.source) {
59
+ const preset = PRESETS[parsed.key];
60
+ if (!preset) {
61
+ unresolvedList.push({
62
+ preset: parsed.original,
63
+ reason: `Unknown built-in preset. Not present in the committed catalogue.`,
64
+ });
65
+ return null;
66
+ }
67
+ return preset.body;
68
+ }
69
+ if (!ctx.fetchExternal) {
70
+ unresolvedList.push({
71
+ preset: parsed.original,
72
+ reason: "External preset (github>, gitlab>, bitbucket>, gitea>, local>, or npm). Fetching requires network access and potentially credentials; pass externalPresets: true to enable.",
73
+ });
74
+ return null;
75
+ }
76
+ const result = await fetchExternalPreset(parsed, {
77
+ timeoutMs: ctx.timeoutMs,
78
+ cache: ctx.cache,
79
+ });
80
+ if (!result.ok) {
81
+ unresolvedList.push({ preset: parsed.original, reason: result.reason });
82
+ return null;
83
+ }
84
+ return result.body;
85
+ }
86
+ const SOURCE_PREFIX_RE = /^([a-z]+)>/i;
87
+ const KNOWN_SOURCES = new Set([
88
+ "github",
89
+ "gitlab",
90
+ "bitbucket",
91
+ "gitea",
92
+ "local",
93
+ "npm",
94
+ ]);
95
+ export function parsePreset(raw) {
96
+ const original = raw;
97
+ let rest = raw.trim();
98
+ // Trailing "(arg1, arg2)"
99
+ let args = [];
100
+ const argMatch = /\(([^)]*)\)\s*$/.exec(rest);
101
+ if (argMatch) {
102
+ args = argMatch[1]
103
+ .split(",")
104
+ .map((a) => a.trim())
105
+ .filter((a) => a.length > 0);
106
+ rest = rest.slice(0, argMatch.index).trim();
107
+ }
108
+ const srcMatch = SOURCE_PREFIX_RE.exec(rest);
109
+ if (srcMatch) {
110
+ return parseExternal(rest, original, args, srcMatch[1].toLowerCase());
111
+ }
112
+ // Built-in: `:foo` is shorthand for `default:foo`.
113
+ const key = rest.startsWith(":") ? `default${rest}` : rest;
114
+ // An entry with no `>` prefix and no `:` namespace is an npm preset.
115
+ if (!key.includes(":")) {
116
+ return {
117
+ key: `npm>${key}`,
118
+ original,
119
+ args,
120
+ source: "npm",
121
+ repoPath: key,
122
+ };
123
+ }
124
+ return { key, original, args };
125
+ }
126
+ function parseExternal(rest, original, args, sourceRaw) {
127
+ const source = (KNOWN_SOURCES.has(sourceRaw)
128
+ ? sourceRaw
129
+ : sourceRaw);
130
+ let spec = rest.slice(sourceRaw.length + 1);
131
+ let ref;
132
+ const hashIdx = spec.indexOf("#");
133
+ if (hashIdx !== -1) {
134
+ ref = spec.slice(hashIdx + 1);
135
+ spec = spec.slice(0, hashIdx);
136
+ }
137
+ let subpath;
138
+ const slashSlashIdx = spec.indexOf("//");
139
+ if (slashSlashIdx !== -1) {
140
+ subpath = spec.slice(slashSlashIdx + 2);
141
+ spec = spec.slice(0, slashSlashIdx);
142
+ }
143
+ let presetName;
144
+ const colonIdx = spec.indexOf(":");
145
+ if (colonIdx !== -1) {
146
+ presetName = spec.slice(colonIdx + 1);
147
+ spec = spec.slice(0, colonIdx);
148
+ }
149
+ const repoPath = spec;
150
+ let key = `${source}>${repoPath}`;
151
+ if (presetName)
152
+ key += `:${presetName}`;
153
+ if (subpath)
154
+ key += `//${subpath}`;
155
+ if (ref)
156
+ key += `#${ref}`;
157
+ return { key, original, args, source, repoPath, presetName, subpath, ref };
158
+ }
159
+ function applyArgs(value, args) {
160
+ if (typeof value === "string") {
161
+ return value.replace(/\{\{\s*arg(\d+)\s*\}\}/g, (_, idx) => {
162
+ const i = Number(idx);
163
+ return i < args.length ? args[i] : "";
164
+ });
165
+ }
166
+ if (Array.isArray(value)) {
167
+ return value.map((v) => applyArgs(v, args));
168
+ }
169
+ if (value && typeof value === "object") {
170
+ const out = {};
171
+ for (const [k, v] of Object.entries(value)) {
172
+ out[k] = applyArgs(v, args);
173
+ }
174
+ return out;
175
+ }
176
+ return value;
177
+ }
178
+ /**
179
+ * Merge two Renovate config objects. Rules:
180
+ * - arrays → concatenate (matches `packageRules`, `matchManagers`, etc.)
181
+ * - plain objects → recursive merge
182
+ * - everything else → `b` wins
183
+ * Close enough to Renovate's `mergeChildConfig` for the purpose of showing a
184
+ * user what their config resolves to; not bit-identical.
185
+ */
186
+ function mergeConfig(a, b) {
187
+ const out = { ...a };
188
+ for (const [key, bVal] of Object.entries(b)) {
189
+ const aVal = out[key];
190
+ if (Array.isArray(aVal) && Array.isArray(bVal)) {
191
+ out[key] = [...aVal, ...bVal];
192
+ }
193
+ else if (isPlainObject(aVal) && isPlainObject(bVal)) {
194
+ out[key] = mergeConfig(aVal, bVal);
195
+ }
196
+ else {
197
+ out[key] = bVal;
198
+ }
199
+ }
200
+ return out;
201
+ }
202
+ function isPlainObject(v) {
203
+ return typeof v === "object" && v !== null && !Array.isArray(v);
204
+ }
@@ -0,0 +1,31 @@
1
+ export interface RunResult {
2
+ stdout: string;
3
+ stderr: string;
4
+ exitCode: number;
5
+ }
6
+ export interface RunOptions {
7
+ cwd?: string;
8
+ env?: NodeJS.ProcessEnv;
9
+ timeoutMs?: number;
10
+ stdin?: string;
11
+ }
12
+ /**
13
+ * Spawn a command and capture stdout/stderr. Never throws on non-zero exit —
14
+ * caller inspects exitCode. Throws only for spawn errors (ENOENT etc.) and
15
+ * timeouts.
16
+ */
17
+ export declare function run(cmd: string, args: string[], opts?: RunOptions): Promise<RunResult>;
18
+ /**
19
+ * Resolve how to invoke a Renovate CLI tool. Users can override via env:
20
+ * RENOVATE_BIN — path to the `renovate` binary
21
+ * RENOVATE_CONFIG_VALIDATOR_BIN — path to the validator binary
22
+ * Otherwise we rely on PATH resolution (globally installed Renovate, or a
23
+ * `npm exec` from a project with Renovate installed locally).
24
+ */
25
+ export declare function resolveRenovateTool(tool: "renovate" | "renovate-config-validator"): string;
26
+ /**
27
+ * Centralized message for when a Renovate CLI binary can't be spawned (ENOENT,
28
+ * permission denied, etc.). Used by all tools that shell out so users get
29
+ * consistent, actionable hints instead of raw spawn errors.
30
+ */
31
+ export declare function formatMissingBinaryError(tool: "renovate" | "renovate-config-validator", cause: Error): string;
@@ -0,0 +1,75 @@
1
+ import { spawn } from "node:child_process";
2
+ /**
3
+ * Spawn a command and capture stdout/stderr. Never throws on non-zero exit —
4
+ * caller inspects exitCode. Throws only for spawn errors (ENOENT etc.) and
5
+ * timeouts.
6
+ */
7
+ export function run(cmd, args, opts = {}) {
8
+ return new Promise((resolve, reject) => {
9
+ const child = spawn(cmd, args, {
10
+ cwd: opts.cwd,
11
+ env: { ...process.env, ...opts.env },
12
+ stdio: ["pipe", "pipe", "pipe"],
13
+ });
14
+ let stdout = "";
15
+ let stderr = "";
16
+ let timer;
17
+ let killed = false;
18
+ if (opts.timeoutMs) {
19
+ timer = setTimeout(() => {
20
+ killed = true;
21
+ child.kill("SIGKILL");
22
+ }, opts.timeoutMs);
23
+ }
24
+ child.stdout.on("data", (d) => {
25
+ stdout += d.toString();
26
+ });
27
+ child.stderr.on("data", (d) => {
28
+ stderr += d.toString();
29
+ });
30
+ child.on("error", (err) => {
31
+ if (timer)
32
+ clearTimeout(timer);
33
+ reject(err);
34
+ });
35
+ child.on("close", (code) => {
36
+ if (timer)
37
+ clearTimeout(timer);
38
+ if (killed) {
39
+ reject(new Error(`Command timed out after ${opts.timeoutMs}ms: ${cmd} ${args.join(" ")}`));
40
+ return;
41
+ }
42
+ resolve({ stdout, stderr, exitCode: code ?? -1 });
43
+ });
44
+ if (opts.stdin != null) {
45
+ child.stdin.end(opts.stdin);
46
+ }
47
+ else {
48
+ child.stdin.end();
49
+ }
50
+ });
51
+ }
52
+ /**
53
+ * Resolve how to invoke a Renovate CLI tool. Users can override via env:
54
+ * RENOVATE_BIN — path to the `renovate` binary
55
+ * RENOVATE_CONFIG_VALIDATOR_BIN — path to the validator binary
56
+ * Otherwise we rely on PATH resolution (globally installed Renovate, or a
57
+ * `npm exec` from a project with Renovate installed locally).
58
+ */
59
+ export function resolveRenovateTool(tool) {
60
+ const envKey = tool === "renovate" ? "RENOVATE_BIN" : "RENOVATE_CONFIG_VALIDATOR_BIN";
61
+ return process.env[envKey] || tool;
62
+ }
63
+ /**
64
+ * Centralized message for when a Renovate CLI binary can't be spawned (ENOENT,
65
+ * permission denied, etc.). Used by all tools that shell out so users get
66
+ * consistent, actionable hints instead of raw spawn errors.
67
+ */
68
+ export function formatMissingBinaryError(tool, cause) {
69
+ const envKey = tool === "renovate" ? "RENOVATE_BIN" : "RENOVATE_CONFIG_VALIDATOR_BIN";
70
+ return [
71
+ `Failed to run \`${tool}\`: ${cause.message}.`,
72
+ `Install Renovate globally with \`npm i -g renovate\`, or set ${envKey} to point at an existing binary.`,
73
+ "Call the `check_setup` tool for a full diagnostic.",
74
+ ].join(" ");
75
+ }
@@ -0,0 +1,18 @@
1
+ export type RenovateBinary = "renovate" | "renovate-config-validator";
2
+ export interface BinaryStatus {
3
+ tool: RenovateBinary;
4
+ command: string;
5
+ found: boolean;
6
+ version?: string;
7
+ error?: string;
8
+ }
9
+ export interface SetupStatus {
10
+ node: string;
11
+ renovate: BinaryStatus;
12
+ renovateConfigValidator: BinaryStatus;
13
+ envOverrides: Record<string, string>;
14
+ ok: boolean;
15
+ hints: string[];
16
+ }
17
+ export declare function checkSetup(): Promise<SetupStatus>;
18
+ export declare function describeSetup(status: SetupStatus): string;
@@ -0,0 +1,82 @@
1
+ import { run, resolveRenovateTool } from "./renovateCli.js";
2
+ const VERSION_TIMEOUT_MS = 10_000;
3
+ const INSTALL_HINT = "Install Renovate globally with `npm i -g renovate`, or set RENOVATE_BIN / RENOVATE_CONFIG_VALIDATOR_BIN to existing binaries.";
4
+ async function checkBinary(tool) {
5
+ const command = resolveRenovateTool(tool);
6
+ try {
7
+ const result = await run(command, ["--version"], { timeoutMs: VERSION_TIMEOUT_MS });
8
+ if (result.exitCode !== 0) {
9
+ return {
10
+ tool,
11
+ command,
12
+ found: false,
13
+ error: (result.stderr || result.stdout).trim() || `exit code ${result.exitCode}`,
14
+ };
15
+ }
16
+ return {
17
+ tool,
18
+ command,
19
+ found: true,
20
+ version: result.stdout.trim() || undefined,
21
+ };
22
+ }
23
+ catch (err) {
24
+ return {
25
+ tool,
26
+ command,
27
+ found: false,
28
+ error: err.message,
29
+ };
30
+ }
31
+ }
32
+ export async function checkSetup() {
33
+ const [renovate, renovateConfigValidator] = await Promise.all([
34
+ checkBinary("renovate"),
35
+ checkBinary("renovate-config-validator"),
36
+ ]);
37
+ const envOverrides = {};
38
+ for (const key of ["RENOVATE_BIN", "RENOVATE_CONFIG_VALIDATOR_BIN"]) {
39
+ const v = process.env[key];
40
+ if (v)
41
+ envOverrides[key] = v;
42
+ }
43
+ const hints = [];
44
+ if (!renovate.found) {
45
+ hints.push(`renovate not reachable at \`${renovate.command}\`. ${INSTALL_HINT}`);
46
+ }
47
+ if (!renovateConfigValidator.found) {
48
+ hints.push(`renovate-config-validator not reachable at \`${renovateConfigValidator.command}\`. ${INSTALL_HINT}`);
49
+ }
50
+ return {
51
+ node: process.version,
52
+ renovate,
53
+ renovateConfigValidator,
54
+ envOverrides,
55
+ ok: renovate.found && renovateConfigValidator.found,
56
+ hints,
57
+ };
58
+ }
59
+ export function describeSetup(status) {
60
+ const lines = [];
61
+ lines.push(`Node: ${status.node}`);
62
+ lines.push(`renovate: ${formatBinary(status.renovate)}`);
63
+ lines.push(`renovate-config-validator: ${formatBinary(status.renovateConfigValidator)}`);
64
+ const overrideKeys = Object.keys(status.envOverrides);
65
+ if (overrideKeys.length > 0) {
66
+ lines.push("Env overrides:");
67
+ for (const key of overrideKeys)
68
+ lines.push(` ${key}=${status.envOverrides[key]}`);
69
+ }
70
+ if (status.hints.length > 0) {
71
+ lines.push("");
72
+ lines.push("Hints:");
73
+ for (const h of status.hints)
74
+ lines.push(` - ${h}`);
75
+ }
76
+ return lines.join("\n");
77
+ }
78
+ function formatBinary(s) {
79
+ if (s.found)
80
+ return `${s.version ?? "(version unknown)"} (${s.command})`;
81
+ return `MISSING — ${s.error ?? "unknown error"}`;
82
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerPresetResources(server: McpServer): void;
@@ -0,0 +1,82 @@
1
+ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { PRESETS, PRESET_NAMES, RENOVATE_VERSION, } from "../data/presets.generated.js";
3
+ export function registerPresetResources(server) {
4
+ server.registerResource("renovate-presets", "renovate://presets", {
5
+ title: "Renovate built-in preset index",
6
+ description: `Markdown index of all ${PRESET_NAMES.length} built-in Renovate presets (from renovate v${RENOVATE_VERSION}). Grouped by namespace. Fetch renovate://preset/{name} for a single preset's expanded JSON.`,
7
+ mimeType: "text/markdown",
8
+ }, async () => ({
9
+ contents: [
10
+ {
11
+ uri: "renovate://presets",
12
+ mimeType: "text/markdown",
13
+ text: renderIndex(),
14
+ },
15
+ ],
16
+ }));
17
+ server.registerResource("renovate-preset", new ResourceTemplate("renovate://preset/{name}", {
18
+ list: async () => ({
19
+ resources: PRESET_NAMES.map((name) => {
20
+ const preset = PRESETS[name];
21
+ return {
22
+ uri: `renovate://preset/${name}`,
23
+ name,
24
+ description: preset.description ?? undefined,
25
+ mimeType: "application/json",
26
+ };
27
+ }),
28
+ }),
29
+ }), {
30
+ title: "Renovate preset (expanded JSON)",
31
+ description: "Expanded JSON of a single built-in Renovate preset — its description and body (extends, packageRules, etc.).",
32
+ mimeType: "application/json",
33
+ }, async (uri, variables) => {
34
+ const raw = variables.name;
35
+ const name = typeof raw === "string" ? decodeURIComponent(raw) : "";
36
+ const preset = PRESETS[name];
37
+ if (!preset) {
38
+ throw new Error(`Unknown preset: ${name}. Fetch renovate://presets for the full list.`);
39
+ }
40
+ return {
41
+ contents: [
42
+ {
43
+ uri: uri.href,
44
+ mimeType: "application/json",
45
+ text: JSON.stringify({
46
+ name,
47
+ namespace: preset.namespace,
48
+ description: preset.description,
49
+ body: preset.body,
50
+ }, null, 2),
51
+ },
52
+ ],
53
+ };
54
+ });
55
+ }
56
+ function renderIndex() {
57
+ const byNamespace = new Map();
58
+ for (const name of PRESET_NAMES) {
59
+ const preset = PRESETS[name];
60
+ const bucket = byNamespace.get(preset.namespace) ?? [];
61
+ bucket.push({ name, description: preset.description });
62
+ byNamespace.set(preset.namespace, bucket);
63
+ }
64
+ const namespaces = Array.from(byNamespace.keys()).sort();
65
+ const lines = [
66
+ "# Renovate built-in presets",
67
+ "",
68
+ `Snapshot from renovate v${RENOVATE_VERSION} — **${PRESET_NAMES.length} presets** across ${namespaces.length} namespaces.`,
69
+ "",
70
+ "Reference any of these in the `extends` array of your config. Fetch `renovate://preset/<name>` for the expanded JSON of a single preset.",
71
+ "",
72
+ ];
73
+ for (const ns of namespaces) {
74
+ const items = byNamespace.get(ns);
75
+ lines.push(`## \`${ns}\` (${items.length})`, "");
76
+ for (const { name, description } of items) {
77
+ lines.push(description ? `- \`${name}\` — ${description}` : `- \`${name}\``);
78
+ }
79
+ lines.push("");
80
+ }
81
+ return lines.join("\n");
82
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerCheckSetup(server: McpServer): void;
@@ -0,0 +1,21 @@
1
+ import { checkSetup, describeSetup } from "../lib/setupCheck.js";
2
+ export function registerCheckSetup(server) {
3
+ server.registerTool("check_setup", {
4
+ title: "Check Renovate MCP setup",
5
+ description: "Report whether the Renovate CLI and config validator are reachable, their versions, and any env overrides in effect. Call this first when other tools fail with a spawn error or unexpected validation output.",
6
+ inputSchema: {},
7
+ }, async () => {
8
+ const status = await checkSetup();
9
+ const summary = describeSetup(status);
10
+ const payload = JSON.stringify(status, null, 2);
11
+ return {
12
+ content: [
13
+ {
14
+ type: "text",
15
+ text: `${summary}\n\n${payload}`,
16
+ },
17
+ ],
18
+ isError: !status.ok,
19
+ };
20
+ });
21
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerDryRun(server: McpServer): void;
@@ -0,0 +1,88 @@
1
+ import { z } from "zod";
2
+ import path from "node:path";
3
+ import { promises as fs } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { randomUUID } from "node:crypto";
6
+ import { run, resolveRenovateTool, formatMissingBinaryError } from "../lib/renovateCli.js";
7
+ export function registerDryRun(server) {
8
+ server.registerTool("dry_run", {
9
+ title: "Dry-run Renovate",
10
+ 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.",
11
+ inputSchema: {
12
+ repoPath: z.string().describe("Absolute path to the repository root"),
13
+ dryRunMode: z
14
+ .enum(["extract", "lookup", "full"])
15
+ .default("full")
16
+ .describe("extract = detect manifests only; lookup = resolve latest versions; full = full simulation including branches/PRs (default)"),
17
+ logLevel: z.enum(["info", "debug"]).default("info"),
18
+ timeoutMs: z
19
+ .number()
20
+ .int()
21
+ .positive()
22
+ .max(15 * 60_000)
23
+ .default(5 * 60_000)
24
+ .describe("Max wait time in ms (default 5 minutes, capped at 15)"),
25
+ },
26
+ }, async ({ repoPath, dryRunMode, logLevel, timeoutMs }) => {
27
+ const reportPath = path.join(tmpdir(), `renovate-mcp-report-${randomUUID()}.json`);
28
+ const bin = resolveRenovateTool("renovate");
29
+ try {
30
+ const result = await run(bin, [
31
+ "--platform=local",
32
+ `--dry-run=${dryRunMode}`,
33
+ "--report-type=file",
34
+ `--report-path=${reportPath}`,
35
+ ], {
36
+ cwd: repoPath,
37
+ env: { LOG_LEVEL: logLevel, LOG_FORMAT: "json" },
38
+ timeoutMs,
39
+ });
40
+ let report = null;
41
+ try {
42
+ const raw = await fs.readFile(reportPath, "utf8");
43
+ report = JSON.parse(raw);
44
+ }
45
+ catch {
46
+ // no report produced (e.g., renovate errored before writing)
47
+ }
48
+ const summary = {
49
+ exitCode: result.exitCode,
50
+ hasReport: report !== null,
51
+ report,
52
+ };
53
+ // If no structured report, surface the last bit of stderr so Claude can
54
+ // debug without blowing up the context with Renovate's verbose logs.
55
+ if (!report) {
56
+ const tail = (result.stderr || result.stdout)
57
+ .split(/\r?\n/)
58
+ .filter(Boolean)
59
+ .slice(-40)
60
+ .join("\n");
61
+ summary.logTail = tail;
62
+ }
63
+ return {
64
+ content: [
65
+ {
66
+ type: "text",
67
+ text: JSON.stringify(summary, null, 2),
68
+ },
69
+ ],
70
+ isError: result.exitCode !== 0,
71
+ };
72
+ }
73
+ catch (err) {
74
+ return {
75
+ isError: true,
76
+ content: [
77
+ {
78
+ type: "text",
79
+ text: formatMissingBinaryError("renovate", err),
80
+ },
81
+ ],
82
+ };
83
+ }
84
+ finally {
85
+ await fs.unlink(reportPath).catch(() => undefined);
86
+ }
87
+ });
88
+ }
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerPreviewCustomManager(server: McpServer): void;