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.
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/dist/data/presets.generated.d.ts +8 -0
- package/dist/data/presets.generated.js +20416 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +42 -0
- package/dist/lib/configLocations.d.ts +9 -0
- package/dist/lib/configLocations.js +61 -0
- package/dist/lib/customManagerPreview.d.ts +49 -0
- package/dist/lib/customManagerPreview.js +153 -0
- package/dist/lib/externalPresetFetcher.d.ts +15 -0
- package/dist/lib/externalPresetFetcher.js +130 -0
- package/dist/lib/presetResolver.d.ts +41 -0
- package/dist/lib/presetResolver.js +204 -0
- package/dist/lib/renovateCli.d.ts +31 -0
- package/dist/lib/renovateCli.js +75 -0
- package/dist/lib/setupCheck.d.ts +18 -0
- package/dist/lib/setupCheck.js +82 -0
- package/dist/resources/presets.d.ts +2 -0
- package/dist/resources/presets.js +82 -0
- package/dist/tools/checkSetup.d.ts +2 -0
- package/dist/tools/checkSetup.js +21 -0
- package/dist/tools/dryRun.d.ts +2 -0
- package/dist/tools/dryRun.js +88 -0
- package/dist/tools/previewCustomManager.d.ts +2 -0
- package/dist/tools/previewCustomManager.js +85 -0
- package/dist/tools/readConfig.d.ts +2 -0
- package/dist/tools/readConfig.js +35 -0
- package/dist/tools/resolveConfig.d.ts +2 -0
- package/dist/tools/resolveConfig.js +68 -0
- package/dist/tools/validateConfig.d.ts +2 -0
- package/dist/tools/validateConfig.js +78 -0
- package/dist/tools/writeConfig.d.ts +2 -0
- package/dist/tools/writeConfig.js +83 -0
- package/package.json +63 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { previewCustomManager, } from "../lib/customManagerPreview.js";
|
|
3
|
+
const managerSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
customType: z.string(),
|
|
6
|
+
fileMatch: z.array(z.string()).min(1),
|
|
7
|
+
matchStrings: z.array(z.string()).min(1),
|
|
8
|
+
matchStringsStrategy: z.string().optional(),
|
|
9
|
+
depNameTemplate: z.string().optional(),
|
|
10
|
+
packageNameTemplate: z.string().optional(),
|
|
11
|
+
currentValueTemplate: z.string().optional(),
|
|
12
|
+
currentDigestTemplate: z.string().optional(),
|
|
13
|
+
datasourceTemplate: z.string().optional(),
|
|
14
|
+
versioningTemplate: z.string().optional(),
|
|
15
|
+
registryUrlTemplate: z.string().optional(),
|
|
16
|
+
depTypeTemplate: z.string().optional(),
|
|
17
|
+
extractVersionTemplate: z.string().optional(),
|
|
18
|
+
autoReplaceStringTemplate: z.string().optional(),
|
|
19
|
+
})
|
|
20
|
+
.passthrough();
|
|
21
|
+
export function registerPreviewCustomManager(server) {
|
|
22
|
+
server.registerTool("preview_custom_manager", {
|
|
23
|
+
title: "Preview a Renovate custom manager (regex)",
|
|
24
|
+
description: [
|
|
25
|
+
"Preview a Renovate `customManagers` entry against a local repo — fast, offline, no `renovate` invocation. Designed for iterating on a regex: shows which files match `fileMatch`, which lines match each `matchStrings` regex (with named capture groups), and what dep info the template fields produce.",
|
|
26
|
+
"",
|
|
27
|
+
"Limitations vs. a real Renovate run:",
|
|
28
|
+
" - Only `customType: \"regex\"` is supported.",
|
|
29
|
+
" - `matchStringsStrategy` other than `any` (the default) is not implemented; a warning is emitted.",
|
|
30
|
+
" - Template substitution handles only `{{groupName}}` references, not full Handlebars helpers.",
|
|
31
|
+
" - `.gitignore` is not honored; `node_modules/` and `.git/` are always skipped.",
|
|
32
|
+
"",
|
|
33
|
+
"Run the `dry_run` tool afterwards for full-fidelity confirmation.",
|
|
34
|
+
].join("\n"),
|
|
35
|
+
inputSchema: {
|
|
36
|
+
repoPath: z.string().describe("Absolute path to the repository root"),
|
|
37
|
+
manager: managerSchema.describe("A single Renovate customManagers entry. NOTE: `fileMatch` is an array of REGEX strings matched against POSIX-style relative paths (not globs)."),
|
|
38
|
+
maxFilesScanned: z
|
|
39
|
+
.number()
|
|
40
|
+
.int()
|
|
41
|
+
.positive()
|
|
42
|
+
.max(100_000)
|
|
43
|
+
.optional()
|
|
44
|
+
.describe("Safety cap on files walked (default 2000)"),
|
|
45
|
+
maxHitsPerFile: z
|
|
46
|
+
.number()
|
|
47
|
+
.int()
|
|
48
|
+
.positive()
|
|
49
|
+
.max(10_000)
|
|
50
|
+
.optional()
|
|
51
|
+
.describe("Safety cap on matches per file (default 100)"),
|
|
52
|
+
},
|
|
53
|
+
}, async ({ repoPath, manager, maxFilesScanned, maxHitsPerFile }) => {
|
|
54
|
+
if (manager.customType !== "regex") {
|
|
55
|
+
return {
|
|
56
|
+
isError: true,
|
|
57
|
+
content: [
|
|
58
|
+
{
|
|
59
|
+
type: "text",
|
|
60
|
+
text: `preview_custom_manager only supports customType=\"regex\" (got \"${manager.customType}\"). For other custom manager types, use dry_run.`,
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
const result = await previewCustomManager(repoPath, manager, {
|
|
67
|
+
maxFilesScanned,
|
|
68
|
+
maxHitsPerFile,
|
|
69
|
+
});
|
|
70
|
+
return {
|
|
71
|
+
content: [
|
|
72
|
+
{ type: "text", text: JSON.stringify(result, null, 2) },
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
return {
|
|
78
|
+
isError: true,
|
|
79
|
+
content: [
|
|
80
|
+
{ type: "text", text: err.message },
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { locateConfig } from "../lib/configLocations.js";
|
|
3
|
+
export function registerReadConfig(server) {
|
|
4
|
+
server.registerTool("read_config", {
|
|
5
|
+
title: "Read Renovate config",
|
|
6
|
+
description: "Locate and parse the Renovate configuration in a repository. Searches for renovate.json, renovate.json5, .renovaterc(.json|.json5), .github/renovate.json, .gitlab/renovate.json, and the 'renovate' field of package.json — in that priority order.",
|
|
7
|
+
inputSchema: {
|
|
8
|
+
repoPath: z.string().describe("Absolute path to the repository root"),
|
|
9
|
+
},
|
|
10
|
+
}, async ({ repoPath }) => {
|
|
11
|
+
const located = await locateConfig(repoPath);
|
|
12
|
+
if (!located) {
|
|
13
|
+
return {
|
|
14
|
+
content: [
|
|
15
|
+
{
|
|
16
|
+
type: "text",
|
|
17
|
+
text: `No Renovate configuration found in ${repoPath}. Create one with write_config.`,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
content: [
|
|
24
|
+
{
|
|
25
|
+
type: "text",
|
|
26
|
+
text: JSON.stringify({
|
|
27
|
+
path: located.relPath,
|
|
28
|
+
format: located.format,
|
|
29
|
+
config: located.config,
|
|
30
|
+
}, null, 2),
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { locateConfig } from "../lib/configLocations.js";
|
|
3
|
+
import { resolveConfig } from "../lib/presetResolver.js";
|
|
4
|
+
export function registerResolveConfig(server) {
|
|
5
|
+
server.registerTool("resolve_config", {
|
|
6
|
+
title: "Resolve Renovate config (expand presets)",
|
|
7
|
+
description: "Expand every preset referenced by `extends` and return the fully resolved config. Built-in presets resolve offline against the committed catalogue. External presets (`github>`, `gitlab>`, `bitbucket>`, `gitea>`, `local>`, npm) are listed in `presetsUnresolved` by default; pass `externalPresets: true` to fetch `github>` / `gitlab>` over HTTPS (with optional `GITHUB_TOKEN` / `GITLAB_TOKEN` / `RENOVATE_TOKEN` for private repos). Pass either `repoPath` (reads the repo's config) or `configContent` (an inline config object).",
|
|
8
|
+
inputSchema: {
|
|
9
|
+
repoPath: z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe("Absolute path to the repository root. The tool will locate the repo's renovate config automatically."),
|
|
13
|
+
configContent: z
|
|
14
|
+
.record(z.string(), z.unknown())
|
|
15
|
+
.optional()
|
|
16
|
+
.describe("Inline config object to resolve — use instead of repoPath."),
|
|
17
|
+
externalPresets: z
|
|
18
|
+
.boolean()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe("When true, fetch external presets (github>, gitlab>) over HTTPS. Credentials are read from GITHUB_TOKEN / GITLAB_TOKEN / RENOVATE_TOKEN env vars. Default false."),
|
|
21
|
+
},
|
|
22
|
+
}, async ({ repoPath, configContent, externalPresets }) => {
|
|
23
|
+
if (!repoPath && !configContent) {
|
|
24
|
+
return {
|
|
25
|
+
isError: true,
|
|
26
|
+
content: [
|
|
27
|
+
{ type: "text", text: "Provide either repoPath or configContent." },
|
|
28
|
+
],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
let source;
|
|
32
|
+
let sourcePath;
|
|
33
|
+
if (configContent) {
|
|
34
|
+
source = configContent;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const located = await locateConfig(repoPath);
|
|
38
|
+
if (!located) {
|
|
39
|
+
return {
|
|
40
|
+
content: [
|
|
41
|
+
{
|
|
42
|
+
type: "text",
|
|
43
|
+
text: `No Renovate configuration found in ${repoPath}.`,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
source = located.config;
|
|
49
|
+
sourcePath = located.relPath;
|
|
50
|
+
}
|
|
51
|
+
const { resolved, presetsResolved, presetsUnresolved } = await resolveConfig(source, {
|
|
52
|
+
fetchExternal: externalPresets ?? false,
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: "text",
|
|
58
|
+
text: JSON.stringify({
|
|
59
|
+
...(sourcePath ? { path: sourcePath } : {}),
|
|
60
|
+
resolved,
|
|
61
|
+
presetsResolved,
|
|
62
|
+
presetsUnresolved,
|
|
63
|
+
}, null, 2),
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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 registerValidateConfig(server) {
|
|
8
|
+
server.registerTool("validate_config", {
|
|
9
|
+
title: "Validate Renovate config",
|
|
10
|
+
description: "Validate a Renovate configuration against the official schema using renovate-config-validator. Pass either configPath (file on disk) or configContent (inline JSON object). Returns validation output and a boolean `valid`.",
|
|
11
|
+
inputSchema: {
|
|
12
|
+
configPath: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("Absolute path to a config file to validate"),
|
|
16
|
+
configContent: z
|
|
17
|
+
.record(z.string(), z.unknown())
|
|
18
|
+
.optional()
|
|
19
|
+
.describe("Inline config object to validate (written to a temp file)"),
|
|
20
|
+
strict: z
|
|
21
|
+
.boolean()
|
|
22
|
+
.optional()
|
|
23
|
+
.describe("Treat warnings as errors"),
|
|
24
|
+
},
|
|
25
|
+
}, async ({ configPath, configContent, strict }) => {
|
|
26
|
+
if (!configPath && !configContent) {
|
|
27
|
+
return {
|
|
28
|
+
isError: true,
|
|
29
|
+
content: [
|
|
30
|
+
{ type: "text", text: "Provide either configPath or configContent." },
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
let target = configPath;
|
|
35
|
+
let cleanup;
|
|
36
|
+
if (!target) {
|
|
37
|
+
const tmp = path.join(tmpdir(), `renovate-mcp-${randomUUID()}.json`);
|
|
38
|
+
await fs.writeFile(tmp, JSON.stringify(configContent, null, 2));
|
|
39
|
+
target = tmp;
|
|
40
|
+
cleanup = async () => {
|
|
41
|
+
await fs.unlink(tmp).catch(() => undefined);
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const bin = resolveRenovateTool("renovate-config-validator");
|
|
46
|
+
const args = [];
|
|
47
|
+
if (strict)
|
|
48
|
+
args.push("--strict");
|
|
49
|
+
args.push(target);
|
|
50
|
+
const result = await run(bin, args, { timeoutMs: 30_000 });
|
|
51
|
+
const valid = result.exitCode === 0;
|
|
52
|
+
const output = (result.stdout + result.stderr).trim();
|
|
53
|
+
return {
|
|
54
|
+
content: [
|
|
55
|
+
{
|
|
56
|
+
type: "text",
|
|
57
|
+
text: JSON.stringify({ valid, output }, null, 2),
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
isError: !valid,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
return {
|
|
65
|
+
isError: true,
|
|
66
|
+
content: [
|
|
67
|
+
{
|
|
68
|
+
type: "text",
|
|
69
|
+
text: formatMissingBinaryError("renovate-config-validator", err),
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
await cleanup?.();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
import { run, resolveRenovateTool, formatMissingBinaryError } from "../lib/renovateCli.js";
|
|
5
|
+
export function registerWriteConfig(server) {
|
|
6
|
+
server.registerTool("write_config", {
|
|
7
|
+
title: "Write Renovate config",
|
|
8
|
+
description: "Write a Renovate config to disk. Runs renovate-config-validator first — refuses to write if validation fails unless force=true.",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
repoPath: z.string().describe("Absolute path to the repository root"),
|
|
11
|
+
config: z.record(z.string(), z.unknown()).describe("The Renovate config object"),
|
|
12
|
+
filename: z
|
|
13
|
+
.string()
|
|
14
|
+
.default("renovate.json")
|
|
15
|
+
.describe("Target filename relative to repoPath (default renovate.json)"),
|
|
16
|
+
force: z
|
|
17
|
+
.boolean()
|
|
18
|
+
.default(false)
|
|
19
|
+
.describe("Write even if validation fails"),
|
|
20
|
+
},
|
|
21
|
+
}, async ({ repoPath, config, filename, force }) => {
|
|
22
|
+
const repoAbs = path.resolve(repoPath);
|
|
23
|
+
const target = path.resolve(repoAbs, filename);
|
|
24
|
+
const rel = path.relative(repoAbs, target);
|
|
25
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
26
|
+
return {
|
|
27
|
+
isError: true,
|
|
28
|
+
content: [{ type: "text", text: "filename escapes repoPath" }],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
const payload = JSON.stringify(config, null, 2) + "\n";
|
|
32
|
+
const tmp = `${target}.renovate-mcp-tmp`;
|
|
33
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
34
|
+
await fs.writeFile(tmp, payload);
|
|
35
|
+
let valid = false;
|
|
36
|
+
let validationOutput = "";
|
|
37
|
+
let validatorMissing = false;
|
|
38
|
+
try {
|
|
39
|
+
const bin = resolveRenovateTool("renovate-config-validator");
|
|
40
|
+
const v = await run(bin, [tmp], { timeoutMs: 30_000 });
|
|
41
|
+
validationOutput = (v.stdout + v.stderr).trim();
|
|
42
|
+
valid = v.exitCode === 0;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
validatorMissing = true;
|
|
46
|
+
validationOutput = formatMissingBinaryError("renovate-config-validator", err);
|
|
47
|
+
}
|
|
48
|
+
if (!valid && !force) {
|
|
49
|
+
await fs.unlink(tmp).catch(() => undefined);
|
|
50
|
+
return {
|
|
51
|
+
isError: true,
|
|
52
|
+
content: [
|
|
53
|
+
{
|
|
54
|
+
type: "text",
|
|
55
|
+
text: JSON.stringify({
|
|
56
|
+
wrote: false,
|
|
57
|
+
reason: validatorMissing ? "validator-unavailable" : "validation-failed",
|
|
58
|
+
validationOutput,
|
|
59
|
+
hint: validatorMissing
|
|
60
|
+
? "Install renovate-config-validator (or set RENOVATE_CONFIG_VALIDATOR_BIN), then retry. Pass force=true to skip validation entirely."
|
|
61
|
+
: "Pass force=true to write anyway.",
|
|
62
|
+
}, null, 2),
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
await fs.rename(tmp, target);
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: JSON.stringify({
|
|
73
|
+
wrote: true,
|
|
74
|
+
path: rel,
|
|
75
|
+
bytes: payload.length,
|
|
76
|
+
valid,
|
|
77
|
+
validationOutput: valid ? undefined : validationOutput,
|
|
78
|
+
}, null, 2),
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "renovate-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for designing Renovate configurations interactively.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"renovate-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.build.json",
|
|
17
|
+
"dev": "tsc -p tsconfig.build.json --watch",
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"pretest": "npm run build",
|
|
21
|
+
"test": "vitest run",
|
|
22
|
+
"test:watch": "vitest",
|
|
23
|
+
"generate:presets": "node scripts/generate-presets.mjs",
|
|
24
|
+
"prepack": "npm run build && chmod +x dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"mcp",
|
|
28
|
+
"model-context-protocol",
|
|
29
|
+
"renovate",
|
|
30
|
+
"renovatebot",
|
|
31
|
+
"claude",
|
|
32
|
+
"llm",
|
|
33
|
+
"dependencies"
|
|
34
|
+
],
|
|
35
|
+
"author": "Timo Hankamer",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/tibuntu/renovate-mcp.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/tibuntu/renovate-mcp#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/tibuntu/renovate-mcp/issues"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public",
|
|
46
|
+
"provenance": true
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=24"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
53
|
+
"json5": "^2.2.3",
|
|
54
|
+
"zod": "^4.0.0"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"@types/node": "^24.0.0",
|
|
58
|
+
"renovate": "^43.139.5",
|
|
59
|
+
"typescript": "^6.0.0",
|
|
60
|
+
"vitest": "^4.1.0"
|
|
61
|
+
},
|
|
62
|
+
"license": "MIT"
|
|
63
|
+
}
|