@dokploy/mcp 0.29.2 → 0.29.4

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
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  // AUTO-GENERATED FILE — DO NOT EDIT MANUALLY
2
- // Generated from openapi.json on 2026-04-25
2
+ // Generated from openapi.json on 2026-08-06
3
3
  // Run `pnpm generate` to regenerate
4
4
  import { z } from "zod";
5
5
  export const generatedTools = [
@@ -129,7 +129,7 @@ export const generatedTools = [
129
129
  tag: "application",
130
130
  method: "POST",
131
131
  path: "/application.saveGithubProvider",
132
- schema: z.object({ "applicationId": z.string(), "repository": z.union([z.string(), z.null()]), "branch": z.union([z.string(), z.null()]), "owner": z.union([z.string(), z.null()]), "buildPath": z.union([z.string(), z.null()]), "githubId": z.union([z.string(), z.null()]), "triggerType": z.enum(["push", "tag"]).default("push"), "enableSubmodules": z.boolean().optional(), "watchPaths": z.union([z.array(z.string()), z.null()]).optional() }),
132
+ schema: z.object({ "applicationId": z.string(), "repository": z.union([z.string(), z.null()]), "owner": z.union([z.string(), z.null()]), "buildPath": z.union([z.string(), z.null()]), "githubId": z.union([z.string(), z.null()]), "branch": z.string().regex(new RegExp("^[a-zA-Z0-9._\\-/]+$")).min(1), "triggerType": z.enum(["push", "tag"]).default("push"), "enableSubmodules": z.boolean().optional(), "watchPaths": z.union([z.array(z.string()), z.null()]).optional() }),
133
133
  annotations: {
134
134
  title: "Application SaveGithubProvider",
135
135
  ...{ "idempotentHint": true, "openWorldHint": true },
@@ -141,7 +141,7 @@ export const generatedTools = [
141
141
  tag: "application",
142
142
  method: "POST",
143
143
  path: "/application.saveGitlabProvider",
144
- schema: z.object({ "applicationId": z.string(), "gitlabBranch": z.union([z.string(), z.null()]), "gitlabBuildPath": z.union([z.string(), z.null()]), "gitlabOwner": z.union([z.string(), z.null()]), "gitlabRepository": z.union([z.string(), z.null()]), "gitlabId": z.union([z.string(), z.null()]), "gitlabProjectId": z.union([z.number(), z.null()]), "gitlabPathNamespace": z.union([z.string(), z.null()]), "enableSubmodules": z.boolean().optional(), "watchPaths": z.union([z.array(z.string()), z.null()]).optional() }),
144
+ schema: z.object({ "applicationId": z.string(), "gitlabBuildPath": z.union([z.string(), z.null()]), "gitlabOwner": z.union([z.string(), z.null()]), "gitlabRepository": z.union([z.string(), z.null()]), "gitlabId": z.union([z.string(), z.null()]), "gitlabProjectId": z.union([z.number(), z.null()]), "gitlabPathNamespace": z.union([z.string(), z.null()]), "gitlabBranch": z.string().regex(new RegExp("^[a-zA-Z0-9._\\-/]+$")).min(1), "enableSubmodules": z.boolean().optional(), "watchPaths": z.union([z.array(z.string()), z.null()]).optional() }),
145
145
  annotations: {
146
146
  title: "Application SaveGitlabProvider",
147
147
  ...{ "idempotentHint": true, "openWorldHint": true },
@@ -153,7 +153,7 @@ export const generatedTools = [
153
153
  tag: "application",
154
154
  method: "POST",
155
155
  path: "/application.saveBitbucketProvider",
156
- schema: z.object({ "bitbucketBranch": z.union([z.string(), z.null()]), "bitbucketBuildPath": z.union([z.string(), z.null()]), "bitbucketOwner": z.union([z.string(), z.null()]), "bitbucketRepository": z.union([z.string(), z.null()]), "bitbucketRepositorySlug": z.union([z.string(), z.null()]), "bitbucketId": z.union([z.string(), z.null()]), "applicationId": z.string(), "enableSubmodules": z.boolean().optional(), "watchPaths": z.union([z.array(z.string()), z.null()]).optional() }),
156
+ schema: z.object({ "bitbucketBuildPath": z.union([z.string(), z.null()]), "bitbucketOwner": z.union([z.string(), z.null()]), "bitbucketRepository": z.union([z.string(), z.null()]), "bitbucketRepositorySlug": z.union([z.string(), z.null()]), "bitbucketId": z.union([z.string(), z.null()]), "applicationId": z.string(), "bitbucketBranch": z.string().regex(new RegExp("^[a-zA-Z0-9._\\-/]+$")).min(1), "enableSubmodules": z.boolean().optional(), "watchPaths": z.union([z.array(z.string()), z.null()]).optional() }),
157
157
  annotations: {
158
158
  title: "Application SaveBitbucketProvider",
159
159
  ...{ "idempotentHint": true, "openWorldHint": true },
@@ -165,7 +165,7 @@ export const generatedTools = [
165
165
  tag: "application",
166
166
  method: "POST",
167
167
  path: "/application.saveGiteaProvider",
168
- schema: z.object({ "applicationId": z.string(), "giteaBranch": z.union([z.string(), z.null()]), "giteaBuildPath": z.union([z.string(), z.null()]), "giteaOwner": z.union([z.string(), z.null()]), "giteaRepository": z.union([z.string(), z.null()]), "giteaId": z.union([z.string(), z.null()]), "enableSubmodules": z.boolean().optional(), "watchPaths": z.union([z.array(z.string()), z.null()]).optional() }),
168
+ schema: z.object({ "applicationId": z.string(), "giteaBuildPath": z.union([z.string(), z.null()]), "giteaOwner": z.union([z.string(), z.null()]), "giteaRepository": z.union([z.string(), z.null()]), "giteaId": z.union([z.string(), z.null()]), "giteaBranch": z.string().regex(new RegExp("^[a-zA-Z0-9._\\-/]+$")).min(1), "enableSubmodules": z.boolean().optional(), "watchPaths": z.union([z.array(z.string()), z.null()]).optional() }),
169
169
  annotations: {
170
170
  title: "Application SaveGiteaProvider",
171
171
  ...{ "idempotentHint": true, "openWorldHint": true },
@@ -189,7 +189,7 @@ export const generatedTools = [
189
189
  tag: "application",
190
190
  method: "POST",
191
191
  path: "/application.saveGitProvider",
192
- schema: z.object({ "customGitBranch": z.union([z.string(), z.null()]), "applicationId": z.string(), "customGitBuildPath": z.union([z.string(), z.null()]), "customGitUrl": z.union([z.string(), z.null()]), "watchPaths": z.union([z.array(z.string()), z.null()]), "enableSubmodules": z.boolean().optional(), "customGitSSHKeyId": z.union([z.string(), z.null()]).optional() }),
192
+ schema: z.object({ "applicationId": z.string(), "customGitBuildPath": z.union([z.string(), z.null()]), "customGitUrl": z.union([z.string(), z.null()]), "watchPaths": z.union([z.array(z.string()), z.null()]), "enableSubmodules": z.boolean().optional(), "customGitBranch": z.string().regex(new RegExp("^[a-zA-Z0-9._\\-/]+$")).min(1), "customGitSSHKeyId": z.union([z.string(), z.null()]).optional() }),
193
193
  annotations: {
194
194
  title: "Application SaveGitProvider",
195
195
  ...{ "idempotentHint": true, "openWorldHint": true },
@@ -5805,7 +5805,7 @@ export const generatedTools = [
5805
5805
  tag: "schedule",
5806
5806
  method: "POST",
5807
5807
  path: "/schedule.create",
5808
- schema: z.object({ "scheduleId": z.string().optional(), "name": z.string(), "cronExpression": z.string(), "appName": z.string().optional(), "serviceName": z.union([z.string(), z.null()]).optional(), "shellType": z.enum(["bash", "sh"]).optional(), "scheduleType": z.enum(["application", "compose", "server", "dokploy-server"]).optional(), "command": z.string(), "script": z.union([z.string(), z.null()]).optional(), "applicationId": z.union([z.string(), z.null()]).optional(), "composeId": z.union([z.string(), z.null()]).optional(), "serverId": z.union([z.string(), z.null()]).optional(), "userId": z.union([z.string(), z.null()]).optional(), "enabled": z.boolean().optional(), "timezone": z.union([z.string(), z.null()]).optional(), "createdAt": z.string().optional() }),
5808
+ schema: z.object({ "scheduleId": z.string().optional(), "name": z.string(), "description": z.union([z.string(), z.null()]).optional(), "cronExpression": z.string(), "appName": z.string().optional(), "serviceName": z.union([z.string(), z.null()]).optional(), "shellType": z.enum(["bash", "sh"]).optional(), "scheduleType": z.enum(["application", "compose", "server", "dokploy-server"]).optional(), "command": z.string(), "script": z.union([z.string(), z.null()]).optional(), "applicationId": z.union([z.string(), z.null()]).optional(), "composeId": z.union([z.string(), z.null()]).optional(), "serverId": z.union([z.string(), z.null()]).optional(), "userId": z.union([z.string(), z.null()]).optional(), "enabled": z.boolean().optional(), "timezone": z.union([z.string(), z.null()]).optional(), "createdAt": z.string().optional() }),
5809
5809
  annotations: {
5810
5810
  title: "Schedule Create",
5811
5811
  ...{ "openWorldHint": true },
@@ -5817,7 +5817,7 @@ export const generatedTools = [
5817
5817
  tag: "schedule",
5818
5818
  method: "POST",
5819
5819
  path: "/schedule.update",
5820
- schema: z.object({ "scheduleId": z.string().min(1), "name": z.string(), "cronExpression": z.string(), "appName": z.string().optional(), "serviceName": z.union([z.string(), z.null()]).optional(), "shellType": z.enum(["bash", "sh"]).optional(), "scheduleType": z.enum(["application", "compose", "server", "dokploy-server"]).optional(), "command": z.string(), "script": z.union([z.string(), z.null()]).optional(), "applicationId": z.union([z.string(), z.null()]).optional(), "composeId": z.union([z.string(), z.null()]).optional(), "serverId": z.union([z.string(), z.null()]).optional(), "userId": z.union([z.string(), z.null()]).optional(), "enabled": z.boolean().optional(), "timezone": z.union([z.string(), z.null()]).optional(), "createdAt": z.string().optional() }),
5820
+ schema: z.object({ "scheduleId": z.string().min(1), "name": z.string(), "description": z.union([z.string(), z.null()]).optional(), "cronExpression": z.string(), "appName": z.string().optional(), "serviceName": z.union([z.string(), z.null()]).optional(), "shellType": z.enum(["bash", "sh"]).optional(), "scheduleType": z.enum(["application", "compose", "server", "dokploy-server"]).optional(), "command": z.string(), "script": z.union([z.string(), z.null()]).optional(), "applicationId": z.union([z.string(), z.null()]).optional(), "composeId": z.union([z.string(), z.null()]).optional(), "serverId": z.union([z.string(), z.null()]).optional(), "userId": z.union([z.string(), z.null()]).optional(), "enabled": z.boolean().optional(), "timezone": z.union([z.string(), z.null()]).optional(), "createdAt": z.string().optional() }),
5821
5821
  annotations: {
5822
5822
  title: "Schedule Update",
5823
5823
  ...{ "idempotentHint": true, "openWorldHint": true },
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
  }
@@ -9,7 +9,8 @@ const config = getClientConfig();
9
9
  const DEFAULT_HEADERS = {
10
10
  "Content-Type": "application/json",
11
11
  Accept: "application/json",
12
- "x-api-key": config.authToken, // Use the same auth mechanism as httpClient
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
+ }
@@ -3,7 +3,7 @@ export class ResponseFormatter {
3
3
  const responseData = {
4
4
  success: true,
5
5
  message,
6
- ...(data && typeof data === "object" && data !== null ? { data } : {}),
6
+ ...(data !== undefined && data !== null ? { data } : {}),
7
7
  };
8
8
  return {
9
9
  content: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dokploy/mcp",
3
- "version": "0.29.2",
3
+ "version": "0.29.4",
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": "echo \"Error: no test spcified\" && exit 1"
62
+ "test": "vitest run"
61
63
  }
62
64
  }