@vibetools/dokploy-mcp 0.5.0 → 1.2.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.
Files changed (55) hide show
  1. package/README.md +39 -13
  2. package/dist/api/client.d.ts +2 -0
  3. package/dist/api/client.js +51 -16
  4. package/dist/config/resolver.js +33 -50
  5. package/dist/server.js +1 -1
  6. package/dist/tools/_database.d.ts +12 -0
  7. package/dist/tools/_database.js +115 -0
  8. package/dist/tools/_factory.d.ts +3 -1
  9. package/dist/tools/_factory.js +36 -17
  10. package/dist/tools/application.js +219 -82
  11. package/dist/tools/backup.js +30 -0
  12. package/dist/tools/compose.js +273 -35
  13. package/dist/tools/deployment.js +82 -2
  14. package/dist/tools/docker.js +62 -2
  15. package/dist/tools/domain.js +15 -2
  16. package/dist/tools/environment.d.ts +2 -0
  17. package/dist/tools/environment.js +104 -0
  18. package/dist/tools/git-provider.d.ts +2 -0
  19. package/dist/tools/git-provider.js +22 -0
  20. package/dist/tools/github.d.ts +2 -0
  21. package/dist/tools/github.js +66 -0
  22. package/dist/tools/gitlab.d.ts +2 -0
  23. package/dist/tools/gitlab.js +98 -0
  24. package/dist/tools/index.js +24 -0
  25. package/dist/tools/mariadb.d.ts +1 -2
  26. package/dist/tools/mariadb.js +9 -165
  27. package/dist/tools/mongo.d.ts +1 -2
  28. package/dist/tools/mongo.js +9 -164
  29. package/dist/tools/mounts.js +53 -9
  30. package/dist/tools/mysql.d.ts +1 -2
  31. package/dist/tools/mysql.js +9 -165
  32. package/dist/tools/notification.d.ts +2 -0
  33. package/dist/tools/notification.js +559 -0
  34. package/dist/tools/patch.d.ts +2 -0
  35. package/dist/tools/patch.js +179 -0
  36. package/dist/tools/postgres.d.ts +1 -2
  37. package/dist/tools/postgres.js +8 -164
  38. package/dist/tools/preview-deployment.d.ts +2 -0
  39. package/dist/tools/preview-deployment.js +50 -0
  40. package/dist/tools/project.js +32 -1
  41. package/dist/tools/redis.d.ts +1 -2
  42. package/dist/tools/redis.js +8 -164
  43. package/dist/tools/rollback.d.ts +2 -0
  44. package/dist/tools/rollback.js +28 -0
  45. package/dist/tools/schedule.d.ts +2 -0
  46. package/dist/tools/schedule.js +92 -0
  47. package/dist/tools/server.d.ts +2 -0
  48. package/dist/tools/server.js +192 -0
  49. package/dist/tools/settings.js +251 -0
  50. package/dist/tools/ssh-key.d.ts +2 -0
  51. package/dist/tools/ssh-key.js +74 -0
  52. package/dist/tools/user.js +75 -2
  53. package/dist/tools/volume-backups.d.ts +2 -0
  54. package/dist/tools/volume-backups.js +96 -0
  55. package/package.json +7 -2
package/README.md CHANGED
@@ -4,7 +4,9 @@
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
5
5
  [![Node >= 22](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org/)
6
6
 
7
- MCP server for the Dokploy API. 196 tools across 23 modules. Your AI agent can now deploy apps, manage databases, configure domains, and handle backups -- without you touching a dashboard.
7
+ MCP server for the Dokploy API. 377 tools across 35 modules. Your AI agent can now deploy apps, manage databases, configure domains, and handle backups -- without you touching a dashboard.
8
+
9
+ Forked from [Dokploy/mcp](https://github.com/Dokploy/mcp) and rebuilt with expanded API coverage, tool annotations, Zod v4 schemas, lazy config loading, and a setup wizard. The original had 67 tools. This one has 377. Standing on shoulders, etc.
8
10
 
9
11
  ## Quick Start
10
12
 
@@ -43,7 +45,7 @@ If you already have the [Dokploy CLI](https://github.com/Dokploy/cli) installed
43
45
 
44
46
  ## Features
45
47
 
46
- - **196 tools, 23 modules** -- applications, compose, databases (Postgres/MySQL/MariaDB/MongoDB/Redis), domains, backups, Docker, settings, and more
48
+ - **377 tools, 35 modules** -- applications, compose, environments, servers, Git providers, notifications, databases (Postgres/MySQL/MariaDB/MongoDB/Redis), domains, backups, deployment queues, rollback, patching, Docker, settings, preview deployments, schedules, and more
47
49
  - **Tool annotations** -- `readOnlyHint`, `destructiveHint`, `idempotentHint` so clients can warn before you nuke something
48
50
  - **Type-safe schemas** -- Zod v4 validation on every parameter
49
51
  - **Lazy config loading** -- validates credentials on first API call, not at startup
@@ -134,18 +136,24 @@ Already ran `setup` or have Dokploy CLI authenticated? Drop the `env` block enti
134
136
 
135
137
  | Module | Tools | Module | Tools |
136
138
  |--------|-------|--------|-------|
137
- | Project | 6 | Deployment | 2 |
138
- | Application | 26 | Docker | 4 |
139
- | Compose | 14 | Certificates | 4 |
140
- | Domain | 8 | Registry | 6 |
141
- | PostgreSQL | 13 | Destination | 6 |
142
- | MySQL | 13 | Backup | 8 |
143
- | MariaDB | 13 | Mounts | 4 |
144
- | MongoDB | 13 | Port | 4 |
145
- | Redis | 13 | Redirects | 4 |
139
+ | Project | 8 | Deployment | 8 |
140
+ | Environment | 7 | Docker | 7 |
141
+ | Application | 29 | Server | 16 |
142
+ | Compose | 28 | Certificates | 4 |
143
+ | Domain | 9 | Registry | 6 |
144
+ | Patch | 12 | SSH Key | 6 |
145
+ | Git Provider | 2 | GitHub | 6 |
146
+ | GitLab | 7 | PostgreSQL | 14 |
147
+ | Notification | 38 | MySQL | 14 |
148
+ | Destination | 6 | MariaDB | 14 |
149
+ | Backup | 11 | MongoDB | 14 |
150
+ | Mounts | 6 | Redis | 14 |
151
+ | Port | 4 | Volume Backups | 6 |
152
+ | Redirects | 4 | Rollback | 2 |
153
+ | Preview Deployment | 4 | Schedule | 6 |
146
154
  | Security | 4 | Cluster | 4 |
147
- | Settings | 25 | Admin | 1 |
148
- | User | 1 | | |
155
+ | Settings | 49 | Admin | 1 |
156
+ | User | 7 | | |
149
157
 
150
158
  Full reference with parameters and descriptions: **[docs/tools.md](docs/tools.md)**
151
159
 
@@ -207,6 +215,24 @@ Test with the MCP Inspector:
207
215
  npx @modelcontextprotocol/inspector node dist/index.js
208
216
  ```
209
217
 
218
+ ## Standing on the Shoulders of People Who Actually Did the Work
219
+
220
+ This project is a fork of [Dokploy/mcp](https://github.com/Dokploy/mcp). I rewrote most of it, tripled the tool count, and added things like a setup wizard and config resolution chain -- but "rewrote" is easy when someone else already built the thing you're rewriting.
221
+
222
+ [Mauricio Siu](https://github.com/Siumauricio) created [Dokploy](https://dokploy.com) itself -- a genuinely impressive open-source PaaS -- and kicked off the MCP server repo. Without Dokploy, there's no API. Without the API, there's no MCP server. Without the MCP server, I'd have had to start from zero instead of "from scratch."
223
+
224
+ [Henrique Andrade](https://github.com/andradehenrique) did the actual heavy lifting on the original MCP. Projects, applications, PostgreSQL, MySQL, domains -- that was all him. 15 commits, every merged PR. The kind of contributor who doesn't just open issues, he closes them.
225
+
226
+ And to everyone who opened PRs on the original repo -- merged or not -- your code and ideas shaped what this became:
227
+
228
+ [Joshua Macauley](https://github.com/Macawls) · [lucasleal-developer](https://github.com/lucasleal-developer) · [Nour Eddine Hamaidi](https://github.com/HenkDz) · [Corey](https://github.com/limehawk) · [Azil0ne](https://github.com/Azilone)
229
+
230
+ Unmerged PRs are still blueprints. Someone reads your compose tools PR and thinks "right, I should cover that." Someone sees your consolidation approach and borrows the idea. That's how open source actually works -- not through clean merge histories, but through stolen inspiration with better commit messages.
231
+
232
+ Cheers to all of you. I owe you mass-produced coffee at minimum.
233
+
210
234
  ## License
211
235
 
212
236
  MIT - [Vibe Code](https://vcode.sh)
237
+
238
+ Original work by [Henrique Andrade](https://github.com/andradehenrique) under Apache 2.0 -- see [LICENSE-ORIGINAL](LICENSE-ORIGINAL).
@@ -1,3 +1,4 @@
1
+ export declare function unwrapTrpcResponse(data: unknown): unknown;
1
2
  export declare class ApiError extends Error {
2
3
  readonly status: number;
3
4
  readonly statusText: string;
@@ -5,6 +6,7 @@ export declare class ApiError extends Error {
5
6
  readonly endpoint: string;
6
7
  constructor(status: number, statusText: string, body: unknown, endpoint: string);
7
8
  }
9
+ export declare function buildQueryString(body: unknown): string;
8
10
  export declare const api: {
9
11
  get: <T = unknown>(path: string, params?: Record<string, unknown>) => Promise<T>;
10
12
  post: <T = unknown>(path: string, body?: unknown) => Promise<T>;
@@ -1,4 +1,38 @@
1
1
  import { resolveConfig } from '../config/resolver.js';
2
+ const DEFAULT_TIMEOUT = 30_000;
3
+ function getErrorMessage(body, statusText) {
4
+ if (typeof body !== 'object' || body === null) {
5
+ return statusText;
6
+ }
7
+ if ('message' in body && typeof body.message === 'string') {
8
+ return body.message;
9
+ }
10
+ if ('error' in body && typeof body.error === 'object' && body.error !== null) {
11
+ const error = body.error;
12
+ if ('message' in error && typeof error.message === 'string') {
13
+ return error.message;
14
+ }
15
+ if ('json' in error && typeof error.json === 'object' && error.json !== null) {
16
+ const json = error.json;
17
+ if ('message' in json && typeof json.message === 'string') {
18
+ return json.message;
19
+ }
20
+ }
21
+ }
22
+ return statusText;
23
+ }
24
+ export function unwrapTrpcResponse(data) {
25
+ if (typeof data !== 'object' || data === null)
26
+ return data;
27
+ const outer = data;
28
+ if (typeof outer.result !== 'object' || outer.result === null)
29
+ return data;
30
+ const result = outer.result;
31
+ if (typeof result.data !== 'object' || result.data === null)
32
+ return data;
33
+ const inner = result.data;
34
+ return 'json' in inner ? inner.json : data;
35
+ }
2
36
  function getConfig() {
3
37
  const resolved = resolveConfig();
4
38
  if (!resolved) {
@@ -14,7 +48,7 @@ function getConfig() {
14
48
  return {
15
49
  baseUrl: resolved.url.replace(/\/+$/, ''),
16
50
  apiKey: resolved.apiKey,
17
- timeout: resolved.timeout,
51
+ timeout: resolved.timeout || DEFAULT_TIMEOUT,
18
52
  };
19
53
  }
20
54
  let _config = null;
@@ -28,9 +62,7 @@ export class ApiError extends Error {
28
62
  body;
29
63
  endpoint;
30
64
  constructor(status, statusText, body, endpoint) {
31
- const msg = typeof body === 'object' && body !== null && 'message' in body
32
- ? body.message
33
- : statusText;
65
+ const msg = getErrorMessage(body, statusText);
34
66
  super(`Dokploy API error (${status}): ${msg}`);
35
67
  this.status = status;
36
68
  this.statusText = statusText;
@@ -39,18 +71,21 @@ export class ApiError extends Error {
39
71
  this.name = 'ApiError';
40
72
  }
41
73
  }
42
- function buildQueryString(body) {
43
- if (!body || typeof body !== 'object') {
74
+ export function buildQueryString(body) {
75
+ if (body == null)
44
76
  return '';
45
- }
46
- const params = new URLSearchParams();
47
- for (const [k, v] of Object.entries(body)) {
48
- if (v !== undefined && v !== null) {
49
- params.set(k, String(v));
50
- }
51
- }
52
- return params.toString();
77
+ if (typeof body !== 'object')
78
+ return '';
79
+ const params = Object.fromEntries(Object.entries(body).filter(([, value]) => value != null));
80
+ return new URLSearchParams({
81
+ input: JSON.stringify({ json: params }),
82
+ }).toString();
53
83
  }
84
+ /**
85
+ * Checks whether an error was caused by an aborted fetch.
86
+ * Both checks are needed: older Node versions throw DOMException,
87
+ * while newer versions throw an Error with name 'AbortError'.
88
+ */
54
89
  function isAbortError(error) {
55
90
  return error instanceof DOMException || (error instanceof Error && error.name === 'AbortError');
56
91
  }
@@ -68,7 +103,7 @@ async function request(method, path, body) {
68
103
  Accept: 'application/json',
69
104
  'x-api-key': apiKey,
70
105
  },
71
- body: method === 'POST' && body ? JSON.stringify(body) : undefined,
106
+ body: method === 'POST' && body ? JSON.stringify({ json: body }) : undefined,
72
107
  signal: controller.signal,
73
108
  });
74
109
  const text = await response.text();
@@ -82,7 +117,7 @@ async function request(method, path, body) {
82
117
  if (!response.ok) {
83
118
  throw new ApiError(response.status, response.statusText, data, path);
84
119
  }
85
- return data;
120
+ return unwrapTrpcResponse(data);
86
121
  }
87
122
  catch (error) {
88
123
  if (error instanceof ApiError) {
@@ -1,7 +1,28 @@
1
1
  import { execSync } from 'node:child_process';
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
+ import { z } from 'zod';
4
5
  import { getConfigDir, getConfigFilePath } from './types.js';
6
+ const configFileSchema = z.object({
7
+ url: z.string().min(1),
8
+ apiKey: z.string().min(1),
9
+ });
10
+ const dokployCliSchema = z.object({
11
+ url: z.string().min(1),
12
+ token: z.string().min(1),
13
+ });
14
+ const userSchema = z
15
+ .object({
16
+ email: z.string().optional(),
17
+ user: z
18
+ .object({
19
+ email: z.string().optional(),
20
+ firstName: z.string().optional(),
21
+ })
22
+ .optional(),
23
+ })
24
+ .passthrough();
25
+ const versionSchema = z.union([z.string(), z.object({ version: z.string() }).passthrough()]);
5
26
  /**
6
27
  * Normalizes a Dokploy URL to the tRPC API base.
7
28
  * Accepts any of these formats:
@@ -74,20 +95,8 @@ function readConfigFile() {
74
95
  try {
75
96
  const content = readFileSync(filePath, 'utf8');
76
97
  const parsed = JSON.parse(content);
77
- if (typeof parsed !== 'object' ||
78
- parsed === null ||
79
- !('url' in parsed) ||
80
- !('apiKey' in parsed)) {
81
- return null;
82
- }
83
- const record = parsed;
84
- if (typeof record.url !== 'string' || typeof record.apiKey !== 'string') {
85
- return null;
86
- }
87
- if (!(record.url && record.apiKey)) {
88
- return null;
89
- }
90
- return { url: record.url, apiKey: record.apiKey };
98
+ const result = configFileSchema.safeParse(parsed);
99
+ return result.success ? result.data : null;
91
100
  }
92
101
  catch {
93
102
  return null;
@@ -107,20 +116,8 @@ function readDokployCliConfig() {
107
116
  }
108
117
  const content = readFileSync(cliConfigPath, 'utf8');
109
118
  const parsed = JSON.parse(content);
110
- if (typeof parsed !== 'object' ||
111
- parsed === null ||
112
- !('url' in parsed) ||
113
- !('token' in parsed)) {
114
- return null;
115
- }
116
- const record = parsed;
117
- if (typeof record.url !== 'string' || typeof record.token !== 'string') {
118
- return null;
119
- }
120
- if (!(record.url && record.token)) {
121
- return null;
122
- }
123
- return { url: record.url, apiKey: record.token };
119
+ const result = dokployCliSchema.safeParse(parsed);
120
+ return result.success ? { url: result.data.url, apiKey: result.data.token } : null;
124
121
  }
125
122
  catch {
126
123
  return null;
@@ -212,21 +209,11 @@ function unwrapTrpc(data) {
212
209
  }
213
210
  function parseUser(data) {
214
211
  const unwrapped = unwrapTrpc(data);
215
- if (typeof unwrapped !== 'object' || unwrapped === null)
212
+ const result = userSchema.safeParse(unwrapped);
213
+ if (!result.success)
216
214
  return undefined;
217
- const record = unwrapped;
218
- // Top-level email/name
219
- if (typeof record.email === 'string')
220
- return record.email;
221
- // Nested user object (tRPC user.get response)
222
- if (typeof record.user === 'object' && record.user !== null) {
223
- const user = record.user;
224
- if (typeof user.email === 'string')
225
- return user.email;
226
- if (typeof user.firstName === 'string')
227
- return user.firstName;
228
- }
229
- return undefined;
215
+ const { email, user } = result.data;
216
+ return email ?? user?.email ?? user?.firstName;
230
217
  }
231
218
  async function fetchVersion(baseUrl, apiKey) {
232
219
  const controller = new AbortController();
@@ -241,14 +228,10 @@ async function fetchVersion(baseUrl, apiKey) {
241
228
  return undefined;
242
229
  const data = await response.json();
243
230
  const unwrapped = unwrapTrpc(data);
244
- if (typeof unwrapped === 'string')
245
- return unwrapped;
246
- if (typeof unwrapped === 'object' && unwrapped !== null) {
247
- const record = unwrapped;
248
- if (typeof record.version === 'string')
249
- return record.version;
250
- }
251
- return undefined;
231
+ const result = versionSchema.safeParse(unwrapped);
232
+ if (!result.success)
233
+ return undefined;
234
+ return typeof result.data === 'string' ? result.data : result.data.version;
252
235
  }
253
236
  catch {
254
237
  return undefined;
package/dist/server.js CHANGED
@@ -3,7 +3,7 @@ import { allTools } from './tools/index.js';
3
3
  export function createServer() {
4
4
  const server = new McpServer({
5
5
  name: 'dokploy-mcp-server',
6
- version: '0.1.0',
6
+ version: '1.0.0',
7
7
  });
8
8
  for (const tool of allTools) {
9
9
  server.registerTool(tool.name, {
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+ import { type ToolDefinition } from './_factory.js';
3
+ type AnyZodObject = z.ZodObject;
4
+ export interface DatabaseConfig {
5
+ type: string;
6
+ idField: string;
7
+ displayName: string;
8
+ defaultImage: string;
9
+ createFields: AnyZodObject;
10
+ }
11
+ export declare function createDatabaseTools(config: DatabaseConfig): ToolDefinition[];
12
+ export {};
@@ -0,0 +1,115 @@
1
+ import { z } from 'zod';
2
+ import { getTool, postTool } from './_factory.js';
3
+ export function createDatabaseTools(config) {
4
+ const { type, idField, displayName, defaultImage, createFields } = config;
5
+ const idSchema = z
6
+ .object({ [idField]: z.string().min(1).describe(`Unique ${displayName} database ID`) })
7
+ .strict();
8
+ function tool(action, title, description, schema, opts = {}) {
9
+ const endpoint = `/${type}.${action}`;
10
+ const name = `dokploy_${type}_${action.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)}`;
11
+ if (opts.get) {
12
+ return getTool({ name, title, description, schema, endpoint, annotations: opts.annotations });
13
+ }
14
+ return postTool({ name, title, description, schema, endpoint, annotations: opts.annotations });
15
+ }
16
+ const one = tool('one', `Get ${displayName} Details`, `Retrieve detailed information about a specific ${displayName} database managed by Dokploy. Returns the full configuration including connection settings, resource limits, environment variables, and current status. Requires the unique ${displayName} database ID.`, idSchema, { get: true });
17
+ const create = tool('create', `Create ${displayName} Database`, `Create a new ${displayName} database instance inside a Dokploy environment. Requires a display name and the target environment ID. Optionally specify an app-level identifier, Docker image, description, or remote server. Returns the newly created database record.`, z
18
+ .object({
19
+ name: z.string().min(1).describe('Display name for the database'),
20
+ appName: z
21
+ .string()
22
+ .min(1)
23
+ .max(63)
24
+ .regex(/^[a-zA-Z0-9._-]+$/)
25
+ .optional()
26
+ .describe('Unique app-level identifier'),
27
+ ...createFields.shape,
28
+ environmentId: z.string().min(1).describe('Environment ID to create the database in'),
29
+ dockerImage: z.string().optional().describe(`Docker image (default: ${defaultImage})`),
30
+ description: z.string().nullable().optional().describe('Optional description'),
31
+ serverId: z.string().nullable().optional().describe('Target server ID (null for local)'),
32
+ })
33
+ .strict());
34
+ const update = tool('update', `Update ${displayName} Database`, `Update the configuration of an existing ${displayName} database in Dokploy. Supports modifying the display name, Docker image, resource limits (CPU and memory), custom start command, environment variables, and external port. Requires the ${displayName} database ID. Only the provided fields are updated.`, z
35
+ .object({
36
+ [idField]: z.string().min(1).describe(`Unique ${displayName} database ID`),
37
+ name: z.string().min(1).optional().describe('Display name'),
38
+ appName: z.string().min(1).optional().describe('App-level identifier'),
39
+ description: z.string().nullable().optional().describe('Description'),
40
+ dockerImage: z.string().optional().describe('Docker image'),
41
+ memoryReservation: z.number().nullable().optional().describe('Memory reservation in MB'),
42
+ memoryLimit: z.number().nullable().optional().describe('Memory limit in MB'),
43
+ cpuReservation: z.number().nullable().optional().describe('CPU reservation'),
44
+ cpuLimit: z.number().nullable().optional().describe('CPU limit'),
45
+ command: z.string().nullable().optional().describe('Custom start command'),
46
+ env: z.string().nullable().optional().describe('Environment variables'),
47
+ externalPort: z.number().nullable().optional().describe('External port'),
48
+ })
49
+ .strict());
50
+ const remove = tool('remove', `Remove ${displayName} Database`, `Permanently delete a ${displayName} database from Dokploy. This action removes the database container, its data, and all associated configuration. Requires the ${displayName} database ID. This operation is destructive and cannot be undone.`, idSchema, { annotations: { destructiveHint: true } });
51
+ const move = tool('move', `Move ${displayName} Database`, `Move a ${displayName} database from its current environment to a different environment within Dokploy. Requires the ${displayName} database ID and the destination environment ID. The database configuration and data are preserved during the move.`, z
52
+ .object({
53
+ [idField]: z.string().min(1).describe(`Unique ${displayName} database ID`),
54
+ targetEnvironmentId: z.string().min(1).describe('Destination environment ID'),
55
+ })
56
+ .strict());
57
+ const deploy = tool('deploy', `Deploy ${displayName} Database`, `Deploy a ${displayName} database container in Dokploy. Triggers the build and start process for the specified database. Requires the ${displayName} database ID. Returns the deployment status.`, idSchema);
58
+ const start = tool('start', `Start ${displayName} Database`, `Start a previously stopped ${displayName} database container in Dokploy. The database must already be deployed. Requires the ${displayName} database ID. Returns the updated status after starting.`, idSchema);
59
+ const stop = tool('stop', `Stop ${displayName} Database`, `Stop a running ${displayName} database container in Dokploy. The database data is preserved but the container will no longer accept connections. Requires the ${displayName} database ID. This is a destructive action as it interrupts active connections.`, idSchema, { annotations: { destructiveHint: true } });
60
+ const reload = tool('reload', `Reload ${displayName} Database`, `Reload the ${displayName} database container in Dokploy without a full restart. Applies configuration changes that do not require a rebuild. Requires the ${displayName} database ID and the app-level identifier. Returns the reload status.`, z
61
+ .object({
62
+ [idField]: z.string().min(1).describe(`Unique ${displayName} database ID`),
63
+ appName: z.string().min(1).describe('App-level identifier'),
64
+ })
65
+ .strict());
66
+ const rebuild = tool('rebuild', `Rebuild ${displayName} Database`, `Rebuild the ${displayName} database container from scratch in Dokploy. This tears down the existing container and recreates it with the current configuration. Requires the ${displayName} database ID. Useful after changing the Docker image or when the container is in a broken state.`, idSchema);
67
+ const changeStatus = tool('changeStatus', `Change ${displayName} Status`, `Manually set the application status of a ${displayName} database in Dokploy. Accepts one of: idle, running, done, or error. Requires the ${displayName} database ID and the new status value. Useful for correcting a stale or incorrect status.`, z
68
+ .object({
69
+ [idField]: z.string().min(1).describe(`Unique ${displayName} database ID`),
70
+ applicationStatus: z
71
+ .enum(['idle', 'running', 'done', 'error'])
72
+ .describe('New application status'),
73
+ })
74
+ .strict());
75
+ const saveExternalPort = tool('saveExternalPort', `Save ${displayName} External Port`, `Set or clear the external port mapping for a ${displayName} database in Dokploy. When set, the database is accessible from outside the Docker network on the specified port. Pass null to remove the external port. Requires the ${displayName} database ID.`, z
76
+ .object({
77
+ [idField]: z.string().min(1).describe(`Unique ${displayName} database ID`),
78
+ externalPort: z.number().nullable().describe('External port number (null to remove)'),
79
+ })
80
+ .strict());
81
+ const saveEnvironment = tool('saveEnvironment', `Save ${displayName} Environment`, `Overwrite the environment variables for a ${displayName} database in Dokploy. Replaces all existing environment variables with the provided value. Pass the variables as a single string (one per line, KEY=VALUE format). Requires the ${displayName} database ID.`, z
82
+ .object({
83
+ [idField]: z.string().min(1).describe(`Unique ${displayName} database ID`),
84
+ env: z.string().nullable().optional().describe('Environment variables as a string'),
85
+ })
86
+ .strict());
87
+ const search = tool('search', `Search ${displayName} Databases`, `Search ${displayName} databases in Dokploy by free text or field-specific filters. Supports pagination through limit and offset.`, z
88
+ .object({
89
+ q: z.string().optional().describe('Free-text query'),
90
+ name: z.string().optional().describe('Display name'),
91
+ appName: z.string().optional().describe('App-level identifier'),
92
+ description: z.string().optional().describe('Description'),
93
+ projectId: z.string().optional().describe('Project ID'),
94
+ environmentId: z.string().optional().describe('Environment ID'),
95
+ limit: z.number().min(1).max(100).optional().describe('Maximum number of results'),
96
+ offset: z.number().min(0).optional().describe('Number of results to skip'),
97
+ })
98
+ .strict(), { get: true });
99
+ return [
100
+ one,
101
+ create,
102
+ update,
103
+ remove,
104
+ move,
105
+ deploy,
106
+ start,
107
+ stop,
108
+ reload,
109
+ rebuild,
110
+ changeStatus,
111
+ saveExternalPort,
112
+ saveEnvironment,
113
+ search,
114
+ ];
115
+ }
@@ -12,6 +12,8 @@ export interface ToolDefinition {
12
12
  name: string;
13
13
  title: string;
14
14
  description: string;
15
+ endpoint?: string;
16
+ method?: 'GET' | 'POST';
15
17
  schema: AnyZodObject;
16
18
  annotations: ToolAnnotations;
17
19
  handler: (input: Record<string, unknown>) => Promise<{
@@ -28,7 +30,7 @@ export declare function createTool<T extends AnyZodObject>(def: {
28
30
  title: string;
29
31
  description: string;
30
32
  schema: T;
31
- annotations: ToolAnnotations;
33
+ annotations?: Partial<ToolAnnotations>;
32
34
  handler: (params: {
33
35
  input: z.infer<T>;
34
36
  api: typeof api;
@@ -1,8 +1,15 @@
1
1
  import { ApiError, api } from '../api/client.js';
2
+ function wrapStructured(data) {
3
+ if (Array.isArray(data))
4
+ return { items: data };
5
+ if (data === null || data === undefined || typeof data !== 'object')
6
+ return { value: data };
7
+ return data;
8
+ }
2
9
  function success(data) {
3
10
  return {
4
11
  content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
5
- structuredContent: data,
12
+ structuredContent: wrapStructured(data),
6
13
  };
7
14
  }
8
15
  function error(message, details) {
@@ -16,19 +23,22 @@ function error(message, details) {
16
23
  isError: true,
17
24
  };
18
25
  }
26
+ const ERROR_MAP = {
27
+ 401: ['Authentication failed', () => 'Check your DOKPLOY_API_KEY environment variable.'],
28
+ 403: ['Permission denied', () => 'Your API key lacks permission for this operation.'],
29
+ 404: ['Resource not found', (err) => err.message],
30
+ 422: [
31
+ 'Validation error',
32
+ (err) => typeof err.body === 'object' && err.body !== null ? JSON.stringify(err.body) : err.message,
33
+ ],
34
+ };
19
35
  function mapApiError(err) {
20
- switch (err.status) {
21
- case 401:
22
- return error('Authentication failed', 'Check your DOKPLOY_API_KEY environment variable.');
23
- case 403:
24
- return error('Permission denied', 'Your API key lacks permission for this operation.');
25
- case 404:
26
- return error('Resource not found', err.message);
27
- case 422:
28
- return error('Validation error', typeof err.body === 'object' && err.body !== null ? JSON.stringify(err.body) : err.message);
29
- default:
30
- return error(`Dokploy API error (${err.status})`, err.message);
36
+ const entry = ERROR_MAP[err.status];
37
+ if (entry) {
38
+ const [message, getDetails] = entry;
39
+ return error(message, getDetails(err));
31
40
  }
41
+ return error(`Dokploy API error (${err.status})`, err.message);
32
42
  }
33
43
  export function createTool(def) {
34
44
  return {
@@ -52,17 +62,22 @@ export function createTool(def) {
52
62
  };
53
63
  }
54
64
  export function postTool(opts) {
55
- return createTool({
65
+ const tool = createTool({
56
66
  name: opts.name,
57
67
  title: opts.title,
58
68
  description: opts.description,
59
69
  schema: opts.schema,
60
- annotations: { openWorldHint: true, ...opts.annotations },
70
+ annotations: opts.annotations,
61
71
  handler: async ({ input, api }) => api.post(opts.endpoint, input),
62
72
  });
73
+ return {
74
+ ...tool,
75
+ endpoint: opts.endpoint,
76
+ method: 'POST',
77
+ };
63
78
  }
64
79
  export function getTool(opts) {
65
- return createTool({
80
+ const tool = createTool({
66
81
  name: opts.name,
67
82
  title: opts.title,
68
83
  description: opts.description,
@@ -70,7 +85,6 @@ export function getTool(opts) {
70
85
  annotations: {
71
86
  readOnlyHint: true,
72
87
  idempotentHint: true,
73
- openWorldHint: true,
74
88
  ...opts.annotations,
75
89
  },
76
90
  handler: async ({ input, api }) => {
@@ -80,7 +94,12 @@ export function getTool(opts) {
80
94
  params[k] = v;
81
95
  }
82
96
  }
83
- return api.get(opts.endpoint, Object.keys(params).length > 0 ? params : undefined);
97
+ return api.get(opts.endpoint, params);
84
98
  },
85
99
  });
100
+ return {
101
+ ...tool,
102
+ endpoint: opts.endpoint,
103
+ method: 'GET',
104
+ };
86
105
  }