@dokploy/mcp 0.29.2 → 0.29.3
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 +9 -0
- package/build/handler.js +6 -2
- package/build/server.js +44 -0
- package/build/utils/apiClient.js +2 -1
- package/build/utils/clientConfig.js +49 -0
- package/build/utils/redactSensitive.js +80 -0
- package/build/utils/responseFormatter.js +1 -1
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -287,10 +287,19 @@ The configuration on Windows is slightly different compared to Linux or macOS. U
|
|
|
287
287
|
|----------|----------|-------------|
|
|
288
288
|
| `DOKPLOY_URL` | Yes | Your Dokploy server URL (e.g., `https://your-dokploy-server.com`) |
|
|
289
289
|
| `DOKPLOY_API_KEY` | Yes | Your Dokploy API authentication token |
|
|
290
|
+
| `DOKPLOY_CUSTOM_HEADERS` | No | JSON object of additional upstream request headers. Header names and values must be strings. Reserved headers cannot be set here: `x-api-key`, `content-type`, `accept`. |
|
|
290
291
|
| `DOKPLOY_ENABLED_TAGS` | No | Comma-separated list of tags to filter which tools are loaded (e.g., `project,application,postgres`) |
|
|
291
292
|
| `DOKPLOY_TIMEOUT` | No | Request timeout in milliseconds (default: `30000`) |
|
|
292
293
|
| `DOKPLOY_RETRY_ATTEMPTS` | No | Number of retry attempts (default: `3`) |
|
|
293
294
|
| `DOKPLOY_RETRY_DELAY` | No | Delay between retries in milliseconds (default: `1000`) |
|
|
295
|
+
| `DOKPLOY_REDACT_ENV` | No | When `true`, redacts secret-bearing fields from API responses before they reach the MCP client (default: `false`). Useful when an LLM consumes responses and you don't want env vars or compose files in its context. |
|
|
296
|
+
| `DOKPLOY_REDACT_FIELDS` | No | Comma-separated list of response field names to redact when `DOKPLOY_REDACT_ENV=true`. Matched case-insensitively at any nesting depth. Defaults to: `env`, `buildArgs`, `composeFile`, `dockerCompose`, `environment`, `buildSecrets`, `previewBuildSecrets`, `password`, `currentPassword`, `appPassword`, `databasePassword`, `databaseRootPassword`, `redisPassword`, `mariadbPassword`, `mongoPassword`, `mysqlPassword`, `postgresPassword`, `registryPassword`, `token`, `accessToken`, `appToken`, `apiToken`, `botToken`, `refreshToken`, `secret`, `clientSecret`, `apiKey`, `secretAccessKey`, `accessKey`, `licenseKey`, `userKey`, `privateKey`, `privateKeyPass`, `encPrivateKey`, `encPrivateKeyPass`, `sshKey`, `sshPrivateKey`, `customGitSSHKey`, `dockerAuth`. |
|
|
297
|
+
|
|
298
|
+
For Dokploy instances behind Cloudflare Access or a similar reverse proxy, pass service-token headers with placeholder values like this:
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
DOKPLOY_CUSTOM_HEADERS='{"CF-Access-Client-Id":"your-client-id.access","CF-Access-Client-Secret":"your-client-secret"}'
|
|
302
|
+
```
|
|
294
303
|
|
|
295
304
|
## Transport Modes
|
|
296
305
|
|
package/build/handler.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import apiClient from "./utils/apiClient.js";
|
|
2
|
+
import { getClientConfig } from "./utils/clientConfig.js";
|
|
2
3
|
import { createLogger } from "./utils/logger.js";
|
|
4
|
+
import { redactSensitive } from "./utils/redactSensitive.js";
|
|
3
5
|
import { ResponseFormatter } from "./utils/responseFormatter.js";
|
|
4
6
|
const logger = createLogger("ToolHandler");
|
|
5
7
|
export function createHandler(tool) {
|
|
6
8
|
return async (input) => {
|
|
9
|
+
const { redactEnv, redactFields } = getClientConfig();
|
|
10
|
+
const redact = (value) => (redactEnv ? redactSensitive(value, redactFields) : value);
|
|
7
11
|
try {
|
|
8
|
-
logger.info(`Executing tool: ${tool.name}`, { input });
|
|
12
|
+
logger.info(`Executing tool: ${tool.name}`, { input: redact(input) });
|
|
9
13
|
const response = tool.method === "GET"
|
|
10
14
|
? await apiClient.get(tool.path, { params: input })
|
|
11
15
|
: await apiClient.post(tool.path, input);
|
|
12
|
-
return ResponseFormatter.success(`${tool.name} completed successfully`, response.data);
|
|
16
|
+
return ResponseFormatter.success(`${tool.name} completed successfully`, redact(response.data));
|
|
13
17
|
}
|
|
14
18
|
catch (error) {
|
|
15
19
|
logger.error(`Tool execution failed: ${tool.name}`, {
|
package/build/server.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
2
4
|
import { generatedTools } from "./generated/tools.js";
|
|
3
5
|
import { createHandler } from "./handler.js";
|
|
4
6
|
import { createLogger } from "./utils/logger.js";
|
|
5
7
|
const logger = createLogger("MCP-Server");
|
|
8
|
+
const JSON_SCHEMA_2020_12 = "https://json-schema.org/draft/2020-12/schema";
|
|
6
9
|
function getEnabledTools() {
|
|
7
10
|
const enabledTags = process.env.DOKPLOY_ENABLED_TAGS;
|
|
8
11
|
if (!enabledTags) {
|
|
@@ -20,6 +23,38 @@ function getEnabledTools() {
|
|
|
20
23
|
});
|
|
21
24
|
return filtered;
|
|
22
25
|
}
|
|
26
|
+
function stripNestedSchemaKeys(value) {
|
|
27
|
+
if (value === null || typeof value !== "object")
|
|
28
|
+
return;
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
for (const item of value)
|
|
31
|
+
stripNestedSchemaKeys(item);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const record = value;
|
|
35
|
+
for (const key of Object.keys(record)) {
|
|
36
|
+
if (key === "$schema") {
|
|
37
|
+
delete record[key];
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
stripNestedSchemaKeys(record[key]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Claude's API requires JSON Schema draft 2020-12. The MCP SDK's built-in
|
|
45
|
+
// Zod→JSON Schema converter emits draft-07 by default, which causes a 400
|
|
46
|
+
// error on tools/list. We bypass the SDK's auto-generated handler by
|
|
47
|
+
// registering our own with pre-converted draft-2020-12 schemas.
|
|
48
|
+
// See https://github.com/Dokploy/mcp/issues/32
|
|
49
|
+
function toDraft2020_12JsonSchema(schema) {
|
|
50
|
+
const result = zodToJsonSchema(schema, {
|
|
51
|
+
target: "jsonSchema2019-09",
|
|
52
|
+
strictUnions: true,
|
|
53
|
+
});
|
|
54
|
+
stripNestedSchemaKeys(result);
|
|
55
|
+
result.$schema = JSON_SCHEMA_2020_12;
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
23
58
|
export function createServer() {
|
|
24
59
|
const server = new McpServer({
|
|
25
60
|
name: "dokploy",
|
|
@@ -29,5 +64,14 @@ export function createServer() {
|
|
|
29
64
|
for (const tool of tools) {
|
|
30
65
|
server.tool(tool.name, tool.description, tool.schema.shape, tool.annotations ?? {}, createHandler(tool));
|
|
31
66
|
}
|
|
67
|
+
const toolList = tools.map((tool) => ({
|
|
68
|
+
name: tool.name,
|
|
69
|
+
description: tool.description,
|
|
70
|
+
inputSchema: toDraft2020_12JsonSchema(tool.schema),
|
|
71
|
+
annotations: tool.annotations,
|
|
72
|
+
}));
|
|
73
|
+
server.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
74
|
+
tools: toolList,
|
|
75
|
+
}));
|
|
32
76
|
return server;
|
|
33
77
|
}
|
package/build/utils/apiClient.js
CHANGED
|
@@ -9,7 +9,8 @@ const config = getClientConfig();
|
|
|
9
9
|
const DEFAULT_HEADERS = {
|
|
10
10
|
"Content-Type": "application/json",
|
|
11
11
|
Accept: "application/json",
|
|
12
|
-
|
|
12
|
+
...config.customHeaders,
|
|
13
|
+
"x-api-key": config.authToken,
|
|
13
14
|
};
|
|
14
15
|
// Create axios instance with configuration from clientConfig
|
|
15
16
|
// Ensure baseURL includes /api prefix for Dokploy API routes
|
|
@@ -1,3 +1,34 @@
|
|
|
1
|
+
import { DEFAULT_REDACTED_FIELDS } from "./redactSensitive.js";
|
|
2
|
+
const RESERVED_CUSTOM_HEADER_NAMES = new Set(["x-api-key", "content-type", "accept"]);
|
|
3
|
+
export function parseCustomHeaders(rawHeaders) {
|
|
4
|
+
if (rawHeaders === undefined) {
|
|
5
|
+
return {};
|
|
6
|
+
}
|
|
7
|
+
let parsed;
|
|
8
|
+
try {
|
|
9
|
+
parsed = JSON.parse(rawHeaders);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
throw new Error("Environment variable DOKPLOY_CUSTOM_HEADERS must be valid JSON containing an object of string header names to string values", { cause: error });
|
|
13
|
+
}
|
|
14
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
15
|
+
throw new Error("Environment variable DOKPLOY_CUSTOM_HEADERS must be a JSON object of string header names to string values");
|
|
16
|
+
}
|
|
17
|
+
const customHeaders = {};
|
|
18
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
19
|
+
if (name.trim() === "") {
|
|
20
|
+
throw new Error("Environment variable DOKPLOY_CUSTOM_HEADERS contains an empty header name");
|
|
21
|
+
}
|
|
22
|
+
if (RESERVED_CUSTOM_HEADER_NAMES.has(name.toLowerCase())) {
|
|
23
|
+
throw new Error("Environment variable DOKPLOY_CUSTOM_HEADERS cannot override reserved headers x-api-key, content-type, or accept; configure Dokploy authentication with DOKPLOY_API_KEY");
|
|
24
|
+
}
|
|
25
|
+
if (typeof value !== "string") {
|
|
26
|
+
throw new Error("Environment variable DOKPLOY_CUSTOM_HEADERS must contain only string header values");
|
|
27
|
+
}
|
|
28
|
+
customHeaders[name] = value;
|
|
29
|
+
}
|
|
30
|
+
return customHeaders;
|
|
31
|
+
}
|
|
1
32
|
class ConfigManager {
|
|
2
33
|
static instance;
|
|
3
34
|
config = null;
|
|
@@ -23,15 +54,33 @@ class ConfigManager {
|
|
|
23
54
|
if (!authToken) {
|
|
24
55
|
throw new Error("Environment variable DOKPLOY_API_KEY is not defined");
|
|
25
56
|
}
|
|
57
|
+
const redactEnv = parseBoolean(process.env.DOKPLOY_REDACT_ENV, false);
|
|
58
|
+
const parsedFields = process.env.DOKPLOY_REDACT_FIELDS?.split(",")
|
|
59
|
+
.map((f) => f.trim())
|
|
60
|
+
.filter((f) => f.length > 0) ?? [];
|
|
61
|
+
const redactFields = parsedFields.length > 0 ? parsedFields : DEFAULT_REDACTED_FIELDS;
|
|
26
62
|
return {
|
|
27
63
|
dokployUrl,
|
|
28
64
|
authToken,
|
|
65
|
+
customHeaders: parseCustomHeaders(process.env.DOKPLOY_CUSTOM_HEADERS),
|
|
29
66
|
timeout: parseInt(process.env.DOKPLOY_TIMEOUT || "30000", 10),
|
|
30
67
|
retryAttempts: parseInt(process.env.DOKPLOY_RETRY_ATTEMPTS || "3", 10),
|
|
31
68
|
retryDelay: parseInt(process.env.DOKPLOY_RETRY_DELAY || "1000", 10),
|
|
69
|
+
redactEnv,
|
|
70
|
+
redactFields,
|
|
32
71
|
};
|
|
33
72
|
}
|
|
34
73
|
}
|
|
35
74
|
export function getClientConfig() {
|
|
36
75
|
return ConfigManager.getInstance().getConfig();
|
|
37
76
|
}
|
|
77
|
+
function parseBoolean(value, fallback) {
|
|
78
|
+
if (value === undefined)
|
|
79
|
+
return fallback;
|
|
80
|
+
const normalized = value.trim().toLowerCase();
|
|
81
|
+
if (["true", "1", "yes", "on"].includes(normalized))
|
|
82
|
+
return true;
|
|
83
|
+
if (["false", "0", "no", "off", ""].includes(normalized))
|
|
84
|
+
return false;
|
|
85
|
+
return fallback;
|
|
86
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export const DEFAULT_REDACTED_FIELDS = [
|
|
2
|
+
"env",
|
|
3
|
+
"buildArgs",
|
|
4
|
+
"composeFile",
|
|
5
|
+
"dockerCompose",
|
|
6
|
+
"environment",
|
|
7
|
+
"buildSecrets",
|
|
8
|
+
"previewBuildSecrets",
|
|
9
|
+
"password",
|
|
10
|
+
"currentPassword",
|
|
11
|
+
"appPassword",
|
|
12
|
+
"databasePassword",
|
|
13
|
+
"databaseRootPassword",
|
|
14
|
+
"redisPassword",
|
|
15
|
+
"mariadbPassword",
|
|
16
|
+
"mongoPassword",
|
|
17
|
+
"mysqlPassword",
|
|
18
|
+
"postgresPassword",
|
|
19
|
+
"registryPassword",
|
|
20
|
+
"token",
|
|
21
|
+
"accessToken",
|
|
22
|
+
"appToken",
|
|
23
|
+
"apiToken",
|
|
24
|
+
"botToken",
|
|
25
|
+
"refreshToken",
|
|
26
|
+
"secret",
|
|
27
|
+
"clientSecret",
|
|
28
|
+
"apiKey",
|
|
29
|
+
"secretAccessKey",
|
|
30
|
+
"accessKey",
|
|
31
|
+
"licenseKey",
|
|
32
|
+
"userKey",
|
|
33
|
+
"privateKey",
|
|
34
|
+
"privateKeyPass",
|
|
35
|
+
"encPrivateKey",
|
|
36
|
+
"encPrivateKeyPass",
|
|
37
|
+
"sshKey",
|
|
38
|
+
"sshPrivateKey",
|
|
39
|
+
"customGitSSHKey",
|
|
40
|
+
"dockerAuth",
|
|
41
|
+
];
|
|
42
|
+
const REDACTED_PLACEHOLDER = "[REDACTED]";
|
|
43
|
+
export function redactSensitive(data, fields) {
|
|
44
|
+
if (fields.length === 0)
|
|
45
|
+
return data;
|
|
46
|
+
const lowered = new Set(fields.map((f) => f.toLowerCase()));
|
|
47
|
+
return walk(data, lowered, new WeakSet());
|
|
48
|
+
}
|
|
49
|
+
function isPlainObject(value) {
|
|
50
|
+
if (value === null || typeof value !== "object")
|
|
51
|
+
return false;
|
|
52
|
+
const proto = Object.getPrototypeOf(value);
|
|
53
|
+
return proto === Object.prototype || proto === null;
|
|
54
|
+
}
|
|
55
|
+
function walk(value, fields, seen) {
|
|
56
|
+
if (Array.isArray(value)) {
|
|
57
|
+
if (seen.has(value))
|
|
58
|
+
return value;
|
|
59
|
+
seen.add(value);
|
|
60
|
+
return value.map((item) => walk(item, fields, seen));
|
|
61
|
+
}
|
|
62
|
+
if (isPlainObject(value)) {
|
|
63
|
+
if (seen.has(value))
|
|
64
|
+
return value;
|
|
65
|
+
seen.add(value);
|
|
66
|
+
const out = Object.create(null);
|
|
67
|
+
for (const [key, val] of Object.entries(value)) {
|
|
68
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype")
|
|
69
|
+
continue;
|
|
70
|
+
if (fields.has(key.toLowerCase())) {
|
|
71
|
+
out[key] = val === null || val === undefined ? val : REDACTED_PLACEHOLDER;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
out[key] = walk(val, fields, seen);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dokploy/mcp",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.3",
|
|
4
4
|
"description": "MCP Server for Dokploy API",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -30,14 +30,16 @@
|
|
|
30
30
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
31
31
|
"axios": "^1.9.0",
|
|
32
32
|
"hono": "^4.12.12",
|
|
33
|
-
"zod": "^3.25.28"
|
|
33
|
+
"zod": "^3.25.28",
|
|
34
|
+
"zod-to-json-schema": "^3.25.2"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@biomejs/biome": "^2.4.10",
|
|
37
38
|
"@types/node": "^22.15.21",
|
|
38
39
|
"json-schema-to-zod": "^2.8.1",
|
|
39
40
|
"tsx": "^4.21.0",
|
|
40
|
-
"typescript": "^5.8.3"
|
|
41
|
+
"typescript": "^5.8.3",
|
|
42
|
+
"vitest": "^4.1.6"
|
|
41
43
|
},
|
|
42
44
|
"scripts": {
|
|
43
45
|
"build": "pnpm run clean && tsc && chmod 755 build/index.js",
|
|
@@ -57,6 +59,6 @@
|
|
|
57
59
|
"type-check": "tsc --noEmit",
|
|
58
60
|
"clean": "rm -rf build",
|
|
59
61
|
"precommit": "biome check && pnpm run type-check",
|
|
60
|
-
"test": "
|
|
62
|
+
"test": "vitest run"
|
|
61
63
|
}
|
|
62
64
|
}
|