@dokploy/mcp 0.29.13 → 0.30.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 +52 -3
- package/build/generated/tools.js +783 -45
- package/build/server.js +85 -10
- package/build/utils/clientConfig.js +1 -1
- package/build/utils/redactSensitive.js +7 -6
- package/package.json +1 -1
package/build/server.js
CHANGED
|
@@ -6,21 +6,61 @@ import { createHandler } from "./handler.js";
|
|
|
6
6
|
import { createLogger } from "./utils/logger.js";
|
|
7
7
|
const logger = createLogger("MCP-Server");
|
|
8
8
|
const JSON_SCHEMA_2020_12 = "https://json-schema.org/draft/2020-12/schema";
|
|
9
|
+
const LARGE_TOOLSET_WARNING_THRESHOLD = 150;
|
|
10
|
+
const TOOL_PRESETS = {
|
|
11
|
+
all: null,
|
|
12
|
+
minimal: "project,application",
|
|
13
|
+
core: "project,server,application",
|
|
14
|
+
deploy: "project,environment,server,application,compose,domain,deployment",
|
|
15
|
+
databases: "postgres,redis,mysql,mariadb,mongo,libsql",
|
|
16
|
+
git: "github,gitlab,bitbucket,gitea,gitProvider,registry,sshKey",
|
|
17
|
+
};
|
|
18
|
+
function parseTagList(value) {
|
|
19
|
+
return new Set((value ?? "")
|
|
20
|
+
.split(",")
|
|
21
|
+
.map((tag) => tag.trim().toLowerCase())
|
|
22
|
+
.filter(Boolean));
|
|
23
|
+
}
|
|
24
|
+
function isToolPreset(value) {
|
|
25
|
+
return Object.hasOwn(TOOL_PRESETS, value);
|
|
26
|
+
}
|
|
9
27
|
function getEnabledTools() {
|
|
10
28
|
const enabledTags = process.env.DOKPLOY_ENABLED_TAGS;
|
|
11
|
-
|
|
12
|
-
|
|
29
|
+
const disabledTags = parseTagList(process.env.DOKPLOY_DISABLED_TAGS);
|
|
30
|
+
const requestedPreset = process.env.DOKPLOY_TOOL_PRESET?.trim().toLowerCase() || "all";
|
|
31
|
+
const preset = isToolPreset(requestedPreset) ? requestedPreset : "all";
|
|
32
|
+
if (!isToolPreset(requestedPreset)) {
|
|
33
|
+
logger.warn("Unknown tool preset, falling back to all tools", {
|
|
34
|
+
requestedPreset,
|
|
35
|
+
availablePresets: Object.keys(TOOL_PRESETS),
|
|
36
|
+
});
|
|
13
37
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
38
|
+
let selectedTags = parseTagList(enabledTags);
|
|
39
|
+
const source = selectedTags.size > 0 ? "enabled-tags" : "preset";
|
|
40
|
+
if (selectedTags.size === 0) {
|
|
41
|
+
selectedTags = parseTagList(TOOL_PRESETS[preset] ?? undefined);
|
|
42
|
+
}
|
|
43
|
+
let filtered = selectedTags.size > 0
|
|
44
|
+
? generatedTools.filter((tool) => selectedTags.has(tool.tag.toLowerCase()))
|
|
45
|
+
: generatedTools;
|
|
46
|
+
if (disabledTags.size > 0) {
|
|
47
|
+
filtered = filtered.filter((tool) => !disabledTags.has(tool.tag.toLowerCase()));
|
|
48
|
+
}
|
|
49
|
+
const context = {
|
|
21
50
|
total: generatedTools.length,
|
|
22
51
|
loaded: filtered.length,
|
|
23
|
-
|
|
52
|
+
source,
|
|
53
|
+
preset,
|
|
54
|
+
enabledTags: [...selectedTags],
|
|
55
|
+
disabledTags: [...disabledTags],
|
|
56
|
+
};
|
|
57
|
+
logger.info("Loaded tools", context);
|
|
58
|
+
if (filtered.length > LARGE_TOOLSET_WARNING_THRESHOLD) {
|
|
59
|
+
logger.warn("Large toolset loaded; some MCP clients or LLM providers may time out", {
|
|
60
|
+
...context,
|
|
61
|
+
recommendation: "Set DOKPLOY_TOOL_PRESET=minimal or DOKPLOY_ENABLED_TAGS to reduce tool count",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
24
64
|
return filtered;
|
|
25
65
|
}
|
|
26
66
|
function stripNestedSchemaKeys(value) {
|
|
@@ -41,6 +81,40 @@ function stripNestedSchemaKeys(value) {
|
|
|
41
81
|
}
|
|
42
82
|
}
|
|
43
83
|
}
|
|
84
|
+
// Provider schema validators (OpenAI, Sonnet strict, DeepSeek) accept only a
|
|
85
|
+
// conservative subset of ECMA-262: lookarounds and loosely-escaped character
|
|
86
|
+
// classes (e.g. an unescaped "[" inside a class) are rejected even though JS
|
|
87
|
+
// allows them. Lookarounds compile fine under the strict "v" flag, so they
|
|
88
|
+
// need their own check; everything else is caught by the "v" compile test.
|
|
89
|
+
function usesUnsupportedRegexSyntax(pattern) {
|
|
90
|
+
if (typeof pattern !== "string")
|
|
91
|
+
return false;
|
|
92
|
+
if (/\(\?<?[=!]/.test(pattern))
|
|
93
|
+
return true;
|
|
94
|
+
try {
|
|
95
|
+
new RegExp(pattern, "v");
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function stripUnsupportedRegexPatterns(value) {
|
|
103
|
+
if (value === null || typeof value !== "object")
|
|
104
|
+
return;
|
|
105
|
+
if (Array.isArray(value)) {
|
|
106
|
+
for (const item of value)
|
|
107
|
+
stripUnsupportedRegexPatterns(item);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const record = value;
|
|
111
|
+
if (usesUnsupportedRegexSyntax(record.pattern)) {
|
|
112
|
+
delete record.pattern;
|
|
113
|
+
}
|
|
114
|
+
for (const child of Object.values(record)) {
|
|
115
|
+
stripUnsupportedRegexPatterns(child);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
44
118
|
// Claude's API requires JSON Schema draft 2020-12. The MCP SDK's built-in
|
|
45
119
|
// Zod→JSON Schema converter emits draft-07 by default, which causes a 400
|
|
46
120
|
// error on tools/list. We bypass the SDK's auto-generated handler by
|
|
@@ -52,6 +126,7 @@ function toDraft2020_12JsonSchema(schema) {
|
|
|
52
126
|
strictUnions: true,
|
|
53
127
|
});
|
|
54
128
|
stripNestedSchemaKeys(result);
|
|
129
|
+
stripUnsupportedRegexPatterns(result);
|
|
55
130
|
result.$schema = JSON_SCHEMA_2020_12;
|
|
56
131
|
return result;
|
|
57
132
|
}
|
|
@@ -54,7 +54,7 @@ class ConfigManager {
|
|
|
54
54
|
if (!authToken) {
|
|
55
55
|
throw new Error("Environment variable DOKPLOY_API_KEY is not defined");
|
|
56
56
|
}
|
|
57
|
-
const redactEnv = parseBoolean(process.env.DOKPLOY_REDACT_ENV,
|
|
57
|
+
const redactEnv = parseBoolean(process.env.DOKPLOY_REDACT_ENV, true);
|
|
58
58
|
const parsedFields = process.env.DOKPLOY_REDACT_FIELDS?.split(",")
|
|
59
59
|
.map((f) => f.trim())
|
|
60
60
|
.filter((f) => f.length > 0) ?? [];
|
|
@@ -43,8 +43,8 @@ const REDACTED_PLACEHOLDER = "[REDACTED]";
|
|
|
43
43
|
export function redactSensitive(data, fields) {
|
|
44
44
|
if (fields.length === 0)
|
|
45
45
|
return data;
|
|
46
|
-
const
|
|
47
|
-
return walk(data,
|
|
46
|
+
const suffixes = fields.map((f) => f.toLowerCase());
|
|
47
|
+
return walk(data, suffixes, new WeakSet());
|
|
48
48
|
}
|
|
49
49
|
function isPlainObject(value) {
|
|
50
50
|
if (value === null || typeof value !== "object")
|
|
@@ -52,12 +52,12 @@ function isPlainObject(value) {
|
|
|
52
52
|
const proto = Object.getPrototypeOf(value);
|
|
53
53
|
return proto === Object.prototype || proto === null;
|
|
54
54
|
}
|
|
55
|
-
function walk(value,
|
|
55
|
+
function walk(value, suffixes, seen) {
|
|
56
56
|
if (Array.isArray(value)) {
|
|
57
57
|
if (seen.has(value))
|
|
58
58
|
return value;
|
|
59
59
|
seen.add(value);
|
|
60
|
-
return value.map((item) => walk(item,
|
|
60
|
+
return value.map((item) => walk(item, suffixes, seen));
|
|
61
61
|
}
|
|
62
62
|
if (isPlainObject(value)) {
|
|
63
63
|
if (seen.has(value))
|
|
@@ -67,11 +67,12 @@ function walk(value, fields, seen) {
|
|
|
67
67
|
for (const [key, val] of Object.entries(value)) {
|
|
68
68
|
if (key === "__proto__" || key === "constructor" || key === "prototype")
|
|
69
69
|
continue;
|
|
70
|
-
|
|
70
|
+
const loweredKey = key.toLowerCase();
|
|
71
|
+
if (suffixes.some((suffix) => loweredKey.endsWith(suffix))) {
|
|
71
72
|
out[key] = val === null || val === undefined ? val : REDACTED_PLACEHOLDER;
|
|
72
73
|
}
|
|
73
74
|
else {
|
|
74
|
-
out[key] = walk(val,
|
|
75
|
+
out[key] = walk(val, suffixes, seen);
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
return out;
|