@vibetools/dokploy-mcp 2.1.0 → 2.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.
@@ -0,0 +1,166 @@
1
+ import { procedureSchemas } from '../../generated/dokploy-schemas.js';
2
+ const applicationOneMcpOnlyKeys = new Set(['select', 'includeDeployments', 'deploymentLimit']);
3
+ const applicationOneInputSchema = {
4
+ type: 'object',
5
+ properties: {
6
+ applicationId: {
7
+ type: 'string',
8
+ minLength: 1,
9
+ },
10
+ select: {
11
+ type: 'array',
12
+ items: {
13
+ type: 'string',
14
+ },
15
+ },
16
+ includeDeployments: {
17
+ type: 'boolean',
18
+ },
19
+ deploymentLimit: {
20
+ type: 'integer',
21
+ },
22
+ },
23
+ required: ['applicationId'],
24
+ additionalProperties: false,
25
+ };
26
+ function isRecord(value) {
27
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
28
+ }
29
+ function normalizeSelectedFields(select) {
30
+ if (!Array.isArray(select)) {
31
+ return null;
32
+ }
33
+ const normalized = [];
34
+ const seen = new Set();
35
+ for (const entry of select) {
36
+ if (typeof entry !== 'string') {
37
+ continue;
38
+ }
39
+ const field = entry.trim();
40
+ if (field.length === 0 || seen.has(field)) {
41
+ continue;
42
+ }
43
+ seen.add(field);
44
+ normalized.push(field);
45
+ }
46
+ return normalized;
47
+ }
48
+ function pickSelectedFields(value, select) {
49
+ const normalized = normalizeSelectedFields(select);
50
+ if (!normalized) {
51
+ return { ...value };
52
+ }
53
+ return Object.fromEntries(normalized.filter((field) => Object.hasOwn(value, field)).map((field) => [field, value[field]]));
54
+ }
55
+ function applyDeploymentControls(value, input) {
56
+ if (input.includeDeployments === false) {
57
+ const { deployments: _deployments, ...rest } = value;
58
+ return rest;
59
+ }
60
+ if (typeof input.deploymentLimit === 'number' && Array.isArray(value.deployments)) {
61
+ return {
62
+ ...value,
63
+ deployments: value.deployments.slice(0, input.deploymentLimit),
64
+ };
65
+ }
66
+ return value;
67
+ }
68
+ function validateApplicationOneInput(input) {
69
+ const errors = [];
70
+ if ('select' in input) {
71
+ if (!Array.isArray(input.select) || input.select.length === 0) {
72
+ errors.push('select must be a non-empty array of field names');
73
+ }
74
+ else {
75
+ for (const [index, field] of input.select.entries()) {
76
+ if (typeof field !== 'string' || field.trim().length === 0) {
77
+ errors.push(`select[${index}] must be a non-empty string`);
78
+ }
79
+ }
80
+ }
81
+ }
82
+ if ('deploymentLimit' in input) {
83
+ if (typeof input.deploymentLimit !== 'number' ||
84
+ !Number.isInteger(input.deploymentLimit) ||
85
+ input.deploymentLimit < 0) {
86
+ errors.push('deploymentLimit must be a non-negative integer');
87
+ }
88
+ }
89
+ if (input.includeDeployments === false && input.deploymentLimit !== undefined) {
90
+ errors.push('deploymentLimit cannot be used when includeDeployments is false');
91
+ }
92
+ return errors;
93
+ }
94
+ function mapApplicationOneInput(input) {
95
+ return Object.fromEntries(Object.entries(input).filter(([key]) => !applicationOneMcpOnlyKeys.has(key)));
96
+ }
97
+ function transformApplicationOneResponse(data, input) {
98
+ if (!isRecord(data)) {
99
+ return data;
100
+ }
101
+ const selected = pickSelectedFields(data, input.select);
102
+ return applyDeploymentControls(selected, input);
103
+ }
104
+ const procedureOverrides = {
105
+ 'application.one': {
106
+ inputSchema: applicationOneInputSchema,
107
+ mapInput: mapApplicationOneInput,
108
+ validateInput: validateApplicationOneInput,
109
+ transformResponse: transformApplicationOneResponse,
110
+ },
111
+ };
112
+ function getGeneratedProcedureSchema(procedure) {
113
+ return procedureSchemas[procedure];
114
+ }
115
+ function extractObjectInputMetadata(schema) {
116
+ if (!isRecord(schema) || schema.type !== 'object' || !isRecord(schema.properties)) {
117
+ return {
118
+ requiredInputs: [],
119
+ optionalInputs: [],
120
+ };
121
+ }
122
+ const required = Array.isArray(schema.required)
123
+ ? schema.required.filter((key) => typeof key === 'string')
124
+ : [];
125
+ const requiredSet = new Set(required);
126
+ const optional = Object.keys(schema.properties).filter((key) => !requiredSet.has(key));
127
+ return {
128
+ requiredInputs: required,
129
+ optionalInputs: optional,
130
+ };
131
+ }
132
+ export function getEffectiveProcedureSchema(procedure) {
133
+ const generated = getGeneratedProcedureSchema(procedure);
134
+ if (!generated) {
135
+ return null;
136
+ }
137
+ const override = procedureOverrides[procedure];
138
+ if (!override) {
139
+ return generated;
140
+ }
141
+ return {
142
+ ...generated,
143
+ inputSchema: override.inputSchema ?? generated.inputSchema,
144
+ };
145
+ }
146
+ export function applyProcedureInputMetadata(endpoint) {
147
+ const effectiveSchema = getEffectiveProcedureSchema(endpoint.procedure);
148
+ if (!effectiveSchema) {
149
+ return endpoint;
150
+ }
151
+ const metadata = extractObjectInputMetadata(effectiveSchema.inputSchema);
152
+ return {
153
+ ...endpoint,
154
+ requiredInputs: metadata.requiredInputs,
155
+ optionalInputs: metadata.optionalInputs,
156
+ };
157
+ }
158
+ export function mapProcedureInput(procedure, input) {
159
+ return procedureOverrides[procedure]?.mapInput?.(input) ?? input;
160
+ }
161
+ export function validateProcedureInput(procedure, input) {
162
+ return procedureOverrides[procedure]?.validateInput?.(input) ?? [];
163
+ }
164
+ export function transformProcedureResponse(procedure, input, data) {
165
+ return procedureOverrides[procedure]?.transformResponse?.(data, input) ?? data;
166
+ }
@@ -0,0 +1,29 @@
1
+ import type { CatalogEndpoint } from '../../generated/dokploy-catalog.js';
2
+ export interface VirtualCatalogEndpoint extends CatalogEndpoint {
3
+ virtual: true;
4
+ }
5
+ export interface VirtualProcedureSchema {
6
+ method: 'GET' | 'POST';
7
+ path: string;
8
+ tag: string;
9
+ inputKind: 'query' | 'body';
10
+ inputSchema: unknown;
11
+ outputSchema: unknown;
12
+ virtual: true;
13
+ }
14
+ interface VirtualProcedureContext {
15
+ call: (procedure: string, input?: Record<string, unknown>) => Promise<unknown>;
16
+ }
17
+ interface VirtualProcedureDefinition {
18
+ endpoint: VirtualCatalogEndpoint;
19
+ schema: VirtualProcedureSchema;
20
+ validateInput?: (input: Record<string, unknown>) => string[];
21
+ execute: (input: Record<string, unknown>, context: VirtualProcedureContext) => Promise<unknown>;
22
+ }
23
+ export declare function getVirtualProcedureDefinition(procedure: string): VirtualProcedureDefinition | null;
24
+ export declare function getVirtualProcedureSchema(procedure: string): VirtualProcedureSchema | null;
25
+ export declare function getVirtualCatalogEndpoints(): VirtualCatalogEndpoint[];
26
+ export declare function isVirtualProcedure(procedure: string): boolean;
27
+ export declare function validateVirtualProcedureInput(procedure: string, input: Record<string, unknown>): string[];
28
+ export declare function executeVirtualProcedure(procedure: string, input: Record<string, unknown>, context: VirtualProcedureContext): Promise<unknown>;
29
+ export {};
@@ -0,0 +1,330 @@
1
+ function createApplicationManyInputSchema() {
2
+ return {
3
+ type: 'object',
4
+ properties: {
5
+ applicationIds: {
6
+ type: 'array',
7
+ items: {
8
+ type: 'string',
9
+ },
10
+ },
11
+ select: {
12
+ type: 'array',
13
+ items: {
14
+ type: 'string',
15
+ },
16
+ },
17
+ includeDeployments: {
18
+ type: 'boolean',
19
+ },
20
+ deploymentLimit: {
21
+ type: 'integer',
22
+ },
23
+ },
24
+ required: ['applicationIds'],
25
+ additionalProperties: false,
26
+ };
27
+ }
28
+ function createApplicationManyOutputSchema() {
29
+ return {
30
+ type: 'object',
31
+ properties: {
32
+ items: {
33
+ type: 'array',
34
+ items: {
35
+ type: 'object',
36
+ additionalProperties: true,
37
+ },
38
+ },
39
+ total: {
40
+ type: 'integer',
41
+ },
42
+ },
43
+ required: ['items', 'total'],
44
+ additionalProperties: false,
45
+ };
46
+ }
47
+ function validateStringList(value, key, options = {}) {
48
+ const errors = [];
49
+ if (!Array.isArray(value)) {
50
+ return [`${key} must be an array of strings`];
51
+ }
52
+ if (options.requireNonEmptyArray && value.length === 0) {
53
+ errors.push(`${key} must be a non-empty array of field names`);
54
+ }
55
+ for (const [index, entry] of value.entries()) {
56
+ if (typeof entry !== 'string' || entry.trim().length === 0) {
57
+ errors.push(`${key}[${index}] must be a non-empty string`);
58
+ }
59
+ }
60
+ return errors;
61
+ }
62
+ function validateDeploymentControls(input) {
63
+ const errors = [];
64
+ if ('deploymentLimit' in input) {
65
+ if (typeof input.deploymentLimit !== 'number' ||
66
+ !Number.isInteger(input.deploymentLimit) ||
67
+ input.deploymentLimit < 0) {
68
+ errors.push('deploymentLimit must be a non-negative integer');
69
+ }
70
+ }
71
+ if (input.includeDeployments === false && input.deploymentLimit !== undefined) {
72
+ errors.push('deploymentLimit cannot be used when includeDeployments is false');
73
+ }
74
+ return errors;
75
+ }
76
+ function validateApplicationManyInput(input) {
77
+ const errors = [];
78
+ errors.push(...validateStringList(input.applicationIds, 'applicationIds'));
79
+ if ('select' in input) {
80
+ errors.push(...validateStringList(input.select, 'select', { requireNonEmptyArray: true }));
81
+ }
82
+ errors.push(...validateDeploymentControls(input));
83
+ return errors;
84
+ }
85
+ function buildApplicationOneInput(applicationId, input) {
86
+ const nextInput = { applicationId };
87
+ if ('select' in input) {
88
+ nextInput.select = input.select;
89
+ }
90
+ if ('includeDeployments' in input) {
91
+ nextInput.includeDeployments = input.includeDeployments;
92
+ }
93
+ if ('deploymentLimit' in input) {
94
+ nextInput.deploymentLimit = input.deploymentLimit;
95
+ }
96
+ return nextInput;
97
+ }
98
+ async function executeApplicationMany(input, context) {
99
+ const applicationIds = input.applicationIds?.map((applicationId) => applicationId.trim()) ??
100
+ [];
101
+ const items = [];
102
+ for (const applicationId of applicationIds) {
103
+ const item = await context.call('application.one', buildApplicationOneInput(applicationId, input));
104
+ items.push(item);
105
+ }
106
+ return {
107
+ items,
108
+ total: items.length,
109
+ };
110
+ }
111
+ function createProjectOverviewInputSchema() {
112
+ return {
113
+ type: 'object',
114
+ properties: {
115
+ projectId: {
116
+ type: 'string',
117
+ minLength: 1,
118
+ },
119
+ pageSize: {
120
+ type: 'integer',
121
+ },
122
+ },
123
+ required: ['projectId'],
124
+ additionalProperties: false,
125
+ };
126
+ }
127
+ function createProjectOverviewOutputSchema() {
128
+ return {
129
+ type: 'object',
130
+ properties: {
131
+ projectId: {
132
+ type: 'string',
133
+ },
134
+ name: {
135
+ anyOf: [{ type: 'string' }, { type: 'null' }],
136
+ },
137
+ environments: {
138
+ type: 'array',
139
+ items: {
140
+ type: 'object',
141
+ additionalProperties: true,
142
+ },
143
+ },
144
+ },
145
+ required: ['projectId', 'name', 'environments'],
146
+ additionalProperties: false,
147
+ };
148
+ }
149
+ function validateProjectOverviewInput(input) {
150
+ const errors = [];
151
+ if (typeof input.projectId !== 'string' || input.projectId.trim().length === 0) {
152
+ errors.push('projectId must be a non-empty string');
153
+ }
154
+ if ('pageSize' in input) {
155
+ if (typeof input.pageSize !== 'number' ||
156
+ !Number.isInteger(input.pageSize) ||
157
+ input.pageSize <= 0) {
158
+ errors.push('pageSize must be a positive integer');
159
+ }
160
+ }
161
+ return errors;
162
+ }
163
+ function isRecord(value) {
164
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
165
+ }
166
+ function getStringOrNull(value) {
167
+ return typeof value === 'string' ? value : null;
168
+ }
169
+ function getArray(value) {
170
+ return Array.isArray(value) ? value : [];
171
+ }
172
+ function buildProjectOverviewApplication(value) {
173
+ if (!isRecord(value)) {
174
+ return {
175
+ applicationId: null,
176
+ name: null,
177
+ appName: null,
178
+ applicationStatus: null,
179
+ domains: [],
180
+ mounts: [],
181
+ watchPaths: [],
182
+ lastDeployment: null,
183
+ };
184
+ }
185
+ const deployments = getArray(value.deployments);
186
+ return {
187
+ applicationId: getStringOrNull(value.applicationId),
188
+ name: getStringOrNull(value.name),
189
+ appName: getStringOrNull(value.appName),
190
+ applicationStatus: getStringOrNull(value.applicationStatus),
191
+ domains: getArray(value.domains),
192
+ mounts: getArray(value.mounts),
193
+ watchPaths: getArray(value.watchPaths),
194
+ lastDeployment: deployments[0] ?? null,
195
+ };
196
+ }
197
+ async function executeProjectOverview(input, context) {
198
+ const projectId = String(input.projectId);
199
+ const project = await context.call('project.one', { projectId });
200
+ const environments = await context.call('environment.byProjectId', { projectId });
201
+ const environmentItems = getArray(environments);
202
+ const overviewEnvironments = [];
203
+ for (const environment of environmentItems) {
204
+ if (!isRecord(environment)) {
205
+ continue;
206
+ }
207
+ const environmentId = getStringOrNull(environment.environmentId);
208
+ if (!environmentId) {
209
+ continue;
210
+ }
211
+ // Use environment.one to get application references (application.search
212
+ // does not reliably filter by environmentId in all Dokploy versions)
213
+ const envDetail = await context.call('environment.one', { environmentId });
214
+ const appRefs = isRecord(envDetail) ? getArray(envDetail.applications) : [];
215
+ const overviewApplications = [];
216
+ for (const appRef of appRefs) {
217
+ const applicationId = isRecord(appRef) ? getStringOrNull(appRef.applicationId) : null;
218
+ if (!applicationId) {
219
+ continue;
220
+ }
221
+ const detail = await context.call('application.one', {
222
+ applicationId,
223
+ select: [
224
+ 'applicationId',
225
+ 'name',
226
+ 'appName',
227
+ 'applicationStatus',
228
+ 'domains',
229
+ 'mounts',
230
+ 'watchPaths',
231
+ 'deployments',
232
+ ],
233
+ deploymentLimit: 1,
234
+ });
235
+ overviewApplications.push(buildProjectOverviewApplication(detail));
236
+ }
237
+ overviewEnvironments.push({
238
+ environmentId,
239
+ name: getStringOrNull(environment.name),
240
+ applications: overviewApplications,
241
+ });
242
+ }
243
+ return {
244
+ projectId,
245
+ name: isRecord(project) ? getStringOrNull(project.name) : null,
246
+ environments: overviewEnvironments,
247
+ };
248
+ }
249
+ const virtualProcedureDefinitions = {
250
+ 'application.many': {
251
+ endpoint: {
252
+ procedure: 'application.many',
253
+ method: 'GET',
254
+ path: '/virtual/application.many',
255
+ tag: 'application',
256
+ summary: 'Read multiple applications in one execute workflow',
257
+ description: 'MCP-only virtual helper that fans out to application.one while preserving input order and execute call budgeting.',
258
+ inputKind: 'body',
259
+ requiredInputs: ['applicationIds'],
260
+ optionalInputs: ['select', 'includeDeployments', 'deploymentLimit'],
261
+ response: {
262
+ type: 'object',
263
+ keys: ['items', 'total'],
264
+ },
265
+ virtual: true,
266
+ },
267
+ schema: {
268
+ method: 'GET',
269
+ path: '/virtual/application.many',
270
+ tag: 'application',
271
+ inputKind: 'body',
272
+ inputSchema: createApplicationManyInputSchema(),
273
+ outputSchema: createApplicationManyOutputSchema(),
274
+ virtual: true,
275
+ },
276
+ validateInput: validateApplicationManyInput,
277
+ execute: executeApplicationMany,
278
+ },
279
+ 'project.overview': {
280
+ endpoint: {
281
+ procedure: 'project.overview',
282
+ method: 'GET',
283
+ path: '/virtual/project.overview',
284
+ tag: 'project',
285
+ summary: 'Read an opinionated overview of one project',
286
+ description: 'MCP-only virtual helper that aggregates project, environment, application, mounts, watch paths, domains, and the latest deployment.',
287
+ inputKind: 'body',
288
+ requiredInputs: ['projectId'],
289
+ optionalInputs: ['pageSize'],
290
+ response: {
291
+ type: 'object',
292
+ keys: ['projectId', 'name', 'environments'],
293
+ },
294
+ virtual: true,
295
+ },
296
+ schema: {
297
+ method: 'GET',
298
+ path: '/virtual/project.overview',
299
+ tag: 'project',
300
+ inputKind: 'body',
301
+ inputSchema: createProjectOverviewInputSchema(),
302
+ outputSchema: createProjectOverviewOutputSchema(),
303
+ virtual: true,
304
+ },
305
+ validateInput: validateProjectOverviewInput,
306
+ execute: executeProjectOverview,
307
+ },
308
+ };
309
+ export function getVirtualProcedureDefinition(procedure) {
310
+ return virtualProcedureDefinitions[procedure] ?? null;
311
+ }
312
+ export function getVirtualProcedureSchema(procedure) {
313
+ return getVirtualProcedureDefinition(procedure)?.schema ?? null;
314
+ }
315
+ export function getVirtualCatalogEndpoints() {
316
+ return Object.values(virtualProcedureDefinitions).map((definition) => definition.endpoint);
317
+ }
318
+ export function isVirtualProcedure(procedure) {
319
+ return procedure in virtualProcedureDefinitions;
320
+ }
321
+ export function validateVirtualProcedureInput(procedure, input) {
322
+ return getVirtualProcedureDefinition(procedure)?.validateInput?.(input) ?? [];
323
+ }
324
+ export async function executeVirtualProcedure(procedure, input, context) {
325
+ const definition = getVirtualProcedureDefinition(procedure);
326
+ if (!definition) {
327
+ throw new Error(`Unknown virtual procedure: ${procedure}`);
328
+ }
329
+ return definition.execute(input, context);
330
+ }
@@ -1,6 +1,6 @@
1
- import { createGeneratedDokployRuntime } from '../../generated/dokploy-sdk.js';
2
- import { buildHelpers } from '../context/execute-context.js';
1
+ import { createExecuteContext } from '../context/execute-context.js';
3
2
  import { createSearchCatalogView } from '../context/search-context.js';
3
+ import { resolveSandboxLimits } from './limits.js';
4
4
  import { runSandboxedFunction } from './runner.js';
5
5
  const pendingCalls = new Map();
6
6
  let requestIdCounter = 0;
@@ -44,10 +44,27 @@ process.on('message', async (message) => {
44
44
  const limits = payload.limits ?? undefined;
45
45
  const context = payload.mode === 'search'
46
46
  ? { catalog: createSearchCatalogView() }
47
- : {
48
- dokploy: createGeneratedDokployRuntime(rpcCall),
49
- helpers: buildHelpers(),
50
- };
47
+ : (() => {
48
+ const rpcExecutor = async (procedure, input) => {
49
+ const data = await rpcCall(procedure, input ?? {});
50
+ return {
51
+ data: data,
52
+ trace: {
53
+ procedure,
54
+ method: 'GET',
55
+ startedAt: Date.now(),
56
+ finishedAt: Date.now(),
57
+ durationMs: 0,
58
+ },
59
+ };
60
+ };
61
+ const maxCalls = limits?.maxCalls ?? resolveSandboxLimits().maxCalls;
62
+ const ctx = createExecuteContext(rpcExecutor, maxCalls);
63
+ return {
64
+ dokploy: ctx.dokploy,
65
+ helpers: ctx.helpers,
66
+ };
67
+ })();
51
68
  const execution = await runSandboxedFunction({
52
69
  code: String(payload.code),
53
70
  context,