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