@directus/api 30.0.0 → 31.0.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.
- package/dist/app.js +5 -0
- package/dist/auth/drivers/oauth2.js +17 -3
- package/dist/auth/drivers/openid.js +17 -3
- package/dist/controllers/mcp.d.ts +2 -0
- package/dist/controllers/mcp.js +33 -0
- package/dist/controllers/users.js +17 -7
- package/dist/controllers/versions.js +3 -2
- package/dist/database/errors/dialects/mssql.d.ts +1 -1
- package/dist/database/errors/dialects/mssql.js +18 -10
- package/dist/database/migrations/20250813A-add-mcp.d.ts +3 -0
- package/dist/database/migrations/20250813A-add-mcp.js +18 -0
- package/dist/database/run-ast/README.md +46 -0
- package/dist/mcp/define.d.ts +2 -0
- package/dist/mcp/define.js +3 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/schema.d.ts +485 -0
- package/dist/mcp/schema.js +219 -0
- package/dist/mcp/server.d.ts +97 -0
- package/dist/mcp/server.js +310 -0
- package/dist/mcp/tools/assets.d.ts +3 -0
- package/dist/mcp/tools/assets.js +54 -0
- package/dist/mcp/tools/collections.d.ts +84 -0
- package/dist/mcp/tools/collections.js +90 -0
- package/dist/mcp/tools/fields.d.ts +101 -0
- package/dist/mcp/tools/fields.js +157 -0
- package/dist/mcp/tools/files.d.ts +235 -0
- package/dist/mcp/tools/files.js +103 -0
- package/dist/mcp/tools/flows.d.ts +323 -0
- package/dist/mcp/tools/flows.js +85 -0
- package/dist/mcp/tools/folders.d.ts +95 -0
- package/dist/mcp/tools/folders.js +96 -0
- package/dist/mcp/tools/index.d.ts +15 -0
- package/dist/mcp/tools/index.js +29 -0
- package/dist/mcp/tools/items.d.ts +87 -0
- package/dist/mcp/tools/items.js +141 -0
- package/dist/mcp/tools/operations.d.ts +171 -0
- package/dist/mcp/tools/operations.js +77 -0
- package/dist/mcp/tools/prompts/assets.md +8 -0
- package/dist/mcp/tools/prompts/collections.md +336 -0
- package/dist/mcp/tools/prompts/fields.md +521 -0
- package/dist/mcp/tools/prompts/files.md +180 -0
- package/dist/mcp/tools/prompts/flows.md +495 -0
- package/dist/mcp/tools/prompts/folders.md +34 -0
- package/dist/mcp/tools/prompts/index.d.ts +16 -0
- package/dist/mcp/tools/prompts/index.js +19 -0
- package/dist/mcp/tools/prompts/items.md +317 -0
- package/dist/mcp/tools/prompts/operations.md +721 -0
- package/dist/mcp/tools/prompts/relations.md +386 -0
- package/dist/mcp/tools/prompts/schema.md +130 -0
- package/dist/mcp/tools/prompts/system-prompt-description.md +1 -0
- package/dist/mcp/tools/prompts/system-prompt.md +44 -0
- package/dist/mcp/tools/prompts/trigger-flow.md +214 -0
- package/dist/mcp/tools/relations.d.ts +73 -0
- package/dist/mcp/tools/relations.js +93 -0
- package/dist/mcp/tools/schema.d.ts +54 -0
- package/dist/mcp/tools/schema.js +317 -0
- package/dist/mcp/tools/system.d.ts +3 -0
- package/dist/mcp/tools/system.js +22 -0
- package/dist/mcp/tools/trigger-flow.d.ts +8 -0
- package/dist/mcp/tools/trigger-flow.js +48 -0
- package/dist/mcp/transport.d.ts +13 -0
- package/dist/mcp/transport.js +18 -0
- package/dist/mcp/types.d.ts +56 -0
- package/dist/mcp/types.js +1 -0
- package/dist/services/authentication.js +36 -0
- package/dist/services/fields.js +4 -4
- package/dist/services/items.js +14 -4
- package/dist/services/payload.d.ts +7 -3
- package/dist/services/payload.js +26 -12
- package/dist/services/server.js +1 -0
- package/dist/services/tfa.d.ts +1 -1
- package/dist/services/tfa.js +20 -5
- package/dist/services/versions.d.ts +6 -4
- package/dist/services/versions.js +84 -25
- package/dist/types/auth.d.ts +2 -1
- package/dist/utils/versioning/deep-map-with-schema.d.ts +23 -0
- package/dist/utils/versioning/deep-map-with-schema.js +81 -0
- package/dist/utils/versioning/handle-version.d.ts +2 -2
- package/dist/utils/versioning/handle-version.js +47 -43
- package/dist/utils/versioning/split-recursive.d.ts +4 -0
- package/dist/utils/versioning/split-recursive.js +27 -0
- package/package.json +30 -29
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { CollectionsService } from '../../services/collections.js';
|
|
3
|
+
import { FieldsService } from '../../services/fields.js';
|
|
4
|
+
import { RelationsService } from '../../services/relations.js';
|
|
5
|
+
import { defineTool } from '../define.js';
|
|
6
|
+
import prompts from './prompts/index.js';
|
|
7
|
+
export const SchemaValidateSchema = z.strictObject({
|
|
8
|
+
keys: z.array(z.string()).optional(),
|
|
9
|
+
});
|
|
10
|
+
export const SchemaInputSchema = z.object({
|
|
11
|
+
keys: z
|
|
12
|
+
.array(z.string())
|
|
13
|
+
.optional()
|
|
14
|
+
.describe('Collection names to get detailed schema for. If omitted, returns a lightweight list of all collections.'),
|
|
15
|
+
});
|
|
16
|
+
export const schema = defineTool({
|
|
17
|
+
name: 'schema',
|
|
18
|
+
description: prompts.schema,
|
|
19
|
+
annotations: {
|
|
20
|
+
title: 'Directus - Schema',
|
|
21
|
+
},
|
|
22
|
+
inputSchema: SchemaInputSchema,
|
|
23
|
+
validateSchema: SchemaValidateSchema,
|
|
24
|
+
async handler({ args, accountability, schema }) {
|
|
25
|
+
const serviceOptions = {
|
|
26
|
+
schema,
|
|
27
|
+
accountability,
|
|
28
|
+
};
|
|
29
|
+
const collectionsService = new CollectionsService(serviceOptions);
|
|
30
|
+
const collections = await collectionsService.readByQuery();
|
|
31
|
+
// If no keys provided, return lightweight collection list
|
|
32
|
+
if (!args.keys || args.keys.length === 0) {
|
|
33
|
+
const lightweightOverview = {
|
|
34
|
+
collections: [],
|
|
35
|
+
collection_folders: [],
|
|
36
|
+
notes: {},
|
|
37
|
+
};
|
|
38
|
+
collections.forEach((collection) => {
|
|
39
|
+
// Separate folders from real collections
|
|
40
|
+
if (!collection.schema) {
|
|
41
|
+
lightweightOverview.collection_folders.push(collection.collection);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
lightweightOverview.collections.push(collection.collection);
|
|
45
|
+
}
|
|
46
|
+
// Extract note if exists (for both collections and folders)
|
|
47
|
+
if (collection.meta?.note && !collection.meta.note.startsWith('$t')) {
|
|
48
|
+
lightweightOverview.notes[collection.collection] = collection.meta.note;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
type: 'text',
|
|
53
|
+
data: lightweightOverview,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// If keys provided, return detailed schema for requested collections
|
|
57
|
+
const overview = {};
|
|
58
|
+
const fieldsService = new FieldsService(serviceOptions);
|
|
59
|
+
const fields = await fieldsService.readAll();
|
|
60
|
+
const relationsService = new RelationsService(serviceOptions);
|
|
61
|
+
const relations = await relationsService.readAll();
|
|
62
|
+
const snapshot = {
|
|
63
|
+
collections,
|
|
64
|
+
fields,
|
|
65
|
+
relations,
|
|
66
|
+
};
|
|
67
|
+
fields.forEach((field) => {
|
|
68
|
+
// Skip collections not requested
|
|
69
|
+
if (!args.keys?.includes(field.collection))
|
|
70
|
+
return;
|
|
71
|
+
// Skip UI-only fields
|
|
72
|
+
if (field.type === 'alias' && field.meta?.special?.includes('no-data'))
|
|
73
|
+
return;
|
|
74
|
+
if (!overview[field.collection]) {
|
|
75
|
+
overview[field.collection] = {};
|
|
76
|
+
}
|
|
77
|
+
const fieldOverview = {
|
|
78
|
+
type: field.type,
|
|
79
|
+
};
|
|
80
|
+
if (field.schema?.is_primary_key) {
|
|
81
|
+
fieldOverview.primary_key = field.schema?.is_primary_key;
|
|
82
|
+
}
|
|
83
|
+
if (field.meta?.required) {
|
|
84
|
+
fieldOverview.required = field.meta.required;
|
|
85
|
+
}
|
|
86
|
+
if (field.meta?.readonly) {
|
|
87
|
+
fieldOverview.readonly = field.meta.readonly;
|
|
88
|
+
}
|
|
89
|
+
if (field.meta?.note) {
|
|
90
|
+
fieldOverview.note = field.meta.note;
|
|
91
|
+
}
|
|
92
|
+
if (field.meta?.interface) {
|
|
93
|
+
fieldOverview.interface = {
|
|
94
|
+
type: field.meta.interface,
|
|
95
|
+
};
|
|
96
|
+
if (field.meta.options?.['choices']) {
|
|
97
|
+
fieldOverview.interface.choices = field.meta.options['choices'].map(
|
|
98
|
+
// Only return the value of the choice to reduce size and potential for confusion.
|
|
99
|
+
(choice) => choice.value);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Process nested fields for JSON fields with options.fields (like repeaters)
|
|
103
|
+
if (field.type === 'json' && field.meta?.options?.['fields']) {
|
|
104
|
+
const nestedFields = field.meta.options['fields'];
|
|
105
|
+
fieldOverview.fields = processNestedFields({
|
|
106
|
+
fields: nestedFields,
|
|
107
|
+
maxDepth: 5,
|
|
108
|
+
currentDepth: 0,
|
|
109
|
+
snapshot,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// Handle collection-item-dropdown interface
|
|
113
|
+
if (field.type === 'json' && field.meta?.interface === 'collection-item-dropdown') {
|
|
114
|
+
fieldOverview.fields = processCollectionItemDropdown({
|
|
115
|
+
field,
|
|
116
|
+
snapshot,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// Handle relationships
|
|
120
|
+
if (field.meta?.special) {
|
|
121
|
+
const relationshipType = getRelationType(field.meta.special);
|
|
122
|
+
if (relationshipType) {
|
|
123
|
+
fieldOverview.relation = buildRelationInfo(field, relationshipType, snapshot);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
overview[field.collection][field.field] = fieldOverview;
|
|
127
|
+
});
|
|
128
|
+
return {
|
|
129
|
+
type: 'text',
|
|
130
|
+
data: overview,
|
|
131
|
+
};
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
// Helpers
|
|
135
|
+
function processNestedFields(options) {
|
|
136
|
+
const { fields, maxDepth = 5, currentDepth = 0, snapshot } = options;
|
|
137
|
+
const result = {};
|
|
138
|
+
if (currentDepth >= maxDepth) {
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
if (!Array.isArray(fields)) {
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
for (const field of fields) {
|
|
145
|
+
const fieldKey = field.field || field.name;
|
|
146
|
+
if (!fieldKey)
|
|
147
|
+
continue;
|
|
148
|
+
const fieldOverview = {
|
|
149
|
+
type: field.type ?? 'any',
|
|
150
|
+
};
|
|
151
|
+
if (field.meta) {
|
|
152
|
+
const { required, readonly, note, interface: interfaceConfig, options } = field.meta;
|
|
153
|
+
if (required)
|
|
154
|
+
fieldOverview.required = required;
|
|
155
|
+
if (readonly)
|
|
156
|
+
fieldOverview.readonly = readonly;
|
|
157
|
+
if (note)
|
|
158
|
+
fieldOverview.note = note;
|
|
159
|
+
if (interfaceConfig) {
|
|
160
|
+
fieldOverview.interface = { type: interfaceConfig };
|
|
161
|
+
if (options?.choices) {
|
|
162
|
+
fieldOverview.interface.choices = options.choices;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Handle nested fields recursively
|
|
167
|
+
const nestedFields = field.meta?.options?.fields || field.options?.fields;
|
|
168
|
+
if (field.type === 'json' && nestedFields) {
|
|
169
|
+
fieldOverview.fields = processNestedFields({
|
|
170
|
+
fields: nestedFields,
|
|
171
|
+
maxDepth,
|
|
172
|
+
currentDepth: currentDepth + 1,
|
|
173
|
+
snapshot,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// Handle collection-item-dropdown interface
|
|
177
|
+
if (field.type === 'json' && field.meta?.interface === 'collection-item-dropdown') {
|
|
178
|
+
fieldOverview.fields = processCollectionItemDropdown({
|
|
179
|
+
field,
|
|
180
|
+
snapshot,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
result[fieldKey] = fieldOverview;
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
function processCollectionItemDropdown(options) {
|
|
188
|
+
const { field, snapshot } = options;
|
|
189
|
+
const selectedCollection = field.meta?.options?.['selectedCollection'];
|
|
190
|
+
let keyType = 'string | number | uuid';
|
|
191
|
+
// Find the primary key type for the selected collection
|
|
192
|
+
if (selectedCollection && snapshot?.fields) {
|
|
193
|
+
const primaryKeyField = snapshot.fields.find((f) => f.collection === selectedCollection && f.schema?.is_primary_key);
|
|
194
|
+
if (primaryKeyField) {
|
|
195
|
+
keyType = primaryKeyField.type;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
collection: {
|
|
200
|
+
value: selectedCollection,
|
|
201
|
+
type: 'string',
|
|
202
|
+
},
|
|
203
|
+
key: {
|
|
204
|
+
type: keyType,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
function getRelationType(special) {
|
|
209
|
+
if (special.includes('m2o') || special.includes('file'))
|
|
210
|
+
return 'm2o';
|
|
211
|
+
if (special.includes('o2m'))
|
|
212
|
+
return 'o2m';
|
|
213
|
+
if (special.includes('m2m') || special.includes('files'))
|
|
214
|
+
return 'm2m';
|
|
215
|
+
if (special.includes('m2a'))
|
|
216
|
+
return 'm2a';
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
function buildRelationInfo(field, type, snapshot) {
|
|
220
|
+
switch (type) {
|
|
221
|
+
case 'm2o':
|
|
222
|
+
return buildManyToOneRelation(field, snapshot);
|
|
223
|
+
case 'o2m':
|
|
224
|
+
return buildOneToManyRelation(field, snapshot);
|
|
225
|
+
case 'm2m':
|
|
226
|
+
return buildManyToManyRelation(field, snapshot);
|
|
227
|
+
case 'm2a':
|
|
228
|
+
return buildManyToAnyRelation(field, snapshot);
|
|
229
|
+
default:
|
|
230
|
+
return { type };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function buildManyToOneRelation(field, snapshot) {
|
|
234
|
+
// For M2O, the relation is directly on this field
|
|
235
|
+
const relation = snapshot.relations.find((r) => r.collection === field.collection && r.field === field.field);
|
|
236
|
+
// The target collection is either in related_collection or foreign_key_table
|
|
237
|
+
const targetCollection = relation?.related_collection || relation?.schema?.foreign_key_table || field.schema?.foreign_key_table;
|
|
238
|
+
return {
|
|
239
|
+
type: 'm2o',
|
|
240
|
+
collection: targetCollection,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function buildOneToManyRelation(field, snapshot) {
|
|
244
|
+
// For O2M, we need to find the relation that points BACK to this field
|
|
245
|
+
// The relation will have this field stored in meta.one_field
|
|
246
|
+
const reverseRelation = snapshot.relations.find((r) => r.meta?.one_collection === field.collection && r.meta?.one_field === field.field);
|
|
247
|
+
if (!reverseRelation) {
|
|
248
|
+
return { type: 'o2m' };
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
type: 'o2m',
|
|
252
|
+
collection: reverseRelation.collection,
|
|
253
|
+
many_field: reverseRelation.field,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function buildManyToManyRelation(field, snapshot) {
|
|
257
|
+
// Find the junction table relation that references this field
|
|
258
|
+
// This relation will have our field as meta.one_field
|
|
259
|
+
const junctionRelation = snapshot.relations.find((r) => r.meta?.one_field === field.field &&
|
|
260
|
+
r.meta?.one_collection === field.collection &&
|
|
261
|
+
r.collection !== field.collection);
|
|
262
|
+
if (!junctionRelation) {
|
|
263
|
+
return { type: 'm2m' };
|
|
264
|
+
}
|
|
265
|
+
// Find the other side of the junction (pointing to the target collection)
|
|
266
|
+
// This is stored in meta.junction_field
|
|
267
|
+
const targetRelation = snapshot.relations.find((r) => r.collection === junctionRelation.collection && r.field === junctionRelation.meta?.junction_field);
|
|
268
|
+
const targetCollection = targetRelation?.related_collection || 'directus_files';
|
|
269
|
+
const result = {
|
|
270
|
+
type: 'm2m',
|
|
271
|
+
collection: targetCollection,
|
|
272
|
+
junction: {
|
|
273
|
+
collection: junctionRelation.collection,
|
|
274
|
+
many_field: junctionRelation.field,
|
|
275
|
+
junction_field: junctionRelation.meta?.junction_field,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
if (junctionRelation.meta?.sort_field) {
|
|
279
|
+
result.junction.sort_field = junctionRelation.meta.sort_field;
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
function buildManyToAnyRelation(field, snapshot) {
|
|
284
|
+
// Find the junction table relation that references this field
|
|
285
|
+
// This relation will have our field as meta.one_field
|
|
286
|
+
const junctionRelation = snapshot.relations.find((r) => r.meta?.one_field === field.field && r.meta?.one_collection === field.collection);
|
|
287
|
+
if (!junctionRelation) {
|
|
288
|
+
return { type: 'm2a' };
|
|
289
|
+
}
|
|
290
|
+
// Find the polymorphic relation in the junction table
|
|
291
|
+
// This relation will have one_allowed_collections set
|
|
292
|
+
const polymorphicRelation = snapshot.relations.find((r) => r.collection === junctionRelation.collection &&
|
|
293
|
+
r.meta?.one_allowed_collections &&
|
|
294
|
+
r.meta.one_allowed_collections.length > 0);
|
|
295
|
+
if (!polymorphicRelation) {
|
|
296
|
+
return { type: 'm2a' };
|
|
297
|
+
}
|
|
298
|
+
// Find the relation back to our parent collection
|
|
299
|
+
const parentRelation = snapshot.relations.find((r) => r.collection === junctionRelation.collection &&
|
|
300
|
+
r.related_collection === field.collection &&
|
|
301
|
+
r.field !== polymorphicRelation.field);
|
|
302
|
+
const result = {
|
|
303
|
+
type: 'm2a',
|
|
304
|
+
one_allowed_collections: polymorphicRelation.meta?.one_allowed_collections,
|
|
305
|
+
junction: {
|
|
306
|
+
collection: junctionRelation.collection,
|
|
307
|
+
many_field: parentRelation?.field || `${field.collection}_id`,
|
|
308
|
+
junction_field: polymorphicRelation.field,
|
|
309
|
+
one_collection_field: polymorphicRelation.meta?.one_collection_field || 'collection',
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
const sortField = parentRelation?.meta?.sort_field || polymorphicRelation.meta?.sort_field;
|
|
313
|
+
if (sortField) {
|
|
314
|
+
result.junction.sort_field = sortField;
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { defineTool } from '../define.js';
|
|
3
|
+
import prompts from './prompts/index.js';
|
|
4
|
+
const SystemPromptInputSchema = z.object({});
|
|
5
|
+
const SystemPromptValidateSchema = z.object({
|
|
6
|
+
promptOverride: z.union([z.string(), z.null()]).optional(),
|
|
7
|
+
});
|
|
8
|
+
export const system = defineTool({
|
|
9
|
+
name: 'system-prompt',
|
|
10
|
+
description: prompts.systemPromptDescription,
|
|
11
|
+
annotations: {
|
|
12
|
+
title: 'Directus - System Prompt',
|
|
13
|
+
},
|
|
14
|
+
inputSchema: SystemPromptInputSchema,
|
|
15
|
+
validateSchema: SystemPromptValidateSchema,
|
|
16
|
+
async handler({ args }) {
|
|
17
|
+
return {
|
|
18
|
+
type: 'text',
|
|
19
|
+
data: args.promptOverride || prompts.systemPrompt,
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const triggerFlow: import("../types.js").ToolConfig<{
|
|
2
|
+
id: string | number;
|
|
3
|
+
collection: string;
|
|
4
|
+
keys?: (string | number)[] | undefined;
|
|
5
|
+
query?: Record<string, any> | undefined;
|
|
6
|
+
headers?: Record<string, any> | undefined;
|
|
7
|
+
data?: Record<string, any> | undefined;
|
|
8
|
+
}>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { InvalidPayloadError } from '@directus/errors';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { getFlowManager } from '../../flows.js';
|
|
4
|
+
import { FlowsService } from '../../services/flows.js';
|
|
5
|
+
import { defineTool } from '../define.js';
|
|
6
|
+
import { TriggerFlowInputSchema, TriggerFlowValidateSchema } from '../schema.js';
|
|
7
|
+
import prompts from './prompts/index.js';
|
|
8
|
+
export const triggerFlow = defineTool({
|
|
9
|
+
name: 'trigger-flow',
|
|
10
|
+
description: prompts.triggerFlow,
|
|
11
|
+
annotations: {
|
|
12
|
+
title: 'Directus - Trigger Flow',
|
|
13
|
+
},
|
|
14
|
+
inputSchema: TriggerFlowInputSchema,
|
|
15
|
+
validateSchema: TriggerFlowValidateSchema,
|
|
16
|
+
async handler({ args, schema, accountability }) {
|
|
17
|
+
const flowsService = new FlowsService({ schema, accountability });
|
|
18
|
+
const flow = await flowsService.readOne(args.id, {
|
|
19
|
+
filter: { status: { _eq: 'active' }, trigger: { _eq: 'manual' } },
|
|
20
|
+
fields: ['options'],
|
|
21
|
+
});
|
|
22
|
+
/**
|
|
23
|
+
* Collection and Required selection are validated by the server.
|
|
24
|
+
* Required fields is an additional validation we do.
|
|
25
|
+
*/
|
|
26
|
+
const requiredFields = (flow.options?.['fields'] ?? [])
|
|
27
|
+
.filter((field) => field.meta?.required)
|
|
28
|
+
.map((field) => field.field);
|
|
29
|
+
for (const fieldName of requiredFields) {
|
|
30
|
+
if (!args.data || !(fieldName in args.data)) {
|
|
31
|
+
throw new InvalidPayloadError({ reason: `Required field "${fieldName}" is missing` });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const flowManager = getFlowManager();
|
|
35
|
+
const { result } = await flowManager.runWebhookFlow(`POST-${args.id}`, {
|
|
36
|
+
path: `/trigger/${args.id}`,
|
|
37
|
+
query: args.query ?? {},
|
|
38
|
+
method: 'POST',
|
|
39
|
+
body: {
|
|
40
|
+
collection: args.collection,
|
|
41
|
+
keys: args.keys,
|
|
42
|
+
...(args.data ?? {}),
|
|
43
|
+
},
|
|
44
|
+
headers: args.headers ?? {},
|
|
45
|
+
}, { accountability, schema });
|
|
46
|
+
return { type: 'text', data: result };
|
|
47
|
+
},
|
|
48
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
2
|
+
import type { JSONRPCMessage, MessageExtraInfo } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import type { Response } from 'express';
|
|
4
|
+
export declare class DirectusTransport implements Transport {
|
|
5
|
+
res: Response;
|
|
6
|
+
onerror?: (error: Error) => void;
|
|
7
|
+
onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void;
|
|
8
|
+
onclose?: () => void;
|
|
9
|
+
constructor(res: Response);
|
|
10
|
+
start(): Promise<void>;
|
|
11
|
+
send(message: JSONRPCMessage): Promise<void>;
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export class DirectusTransport {
|
|
2
|
+
res;
|
|
3
|
+
onerror;
|
|
4
|
+
onmessage;
|
|
5
|
+
onclose;
|
|
6
|
+
constructor(res) {
|
|
7
|
+
this.res = res;
|
|
8
|
+
}
|
|
9
|
+
async start() {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
async send(message) {
|
|
13
|
+
this.res.json(message);
|
|
14
|
+
}
|
|
15
|
+
async close() {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Accountability, Query, SchemaOverview } from '@directus/types';
|
|
2
|
+
import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import type { ZodType } from 'zod';
|
|
4
|
+
export type ToolResultBase = {
|
|
5
|
+
type?: 'text' | 'image' | 'audio';
|
|
6
|
+
url?: string | undefined;
|
|
7
|
+
};
|
|
8
|
+
export type TextToolResult = ToolResultBase & {
|
|
9
|
+
type: 'text';
|
|
10
|
+
data: unknown;
|
|
11
|
+
};
|
|
12
|
+
export type AssetToolResult = ToolResultBase & {
|
|
13
|
+
type: 'image' | 'audio';
|
|
14
|
+
data: string;
|
|
15
|
+
mimeType: string;
|
|
16
|
+
};
|
|
17
|
+
export type ToolResult = TextToolResult | AssetToolResult;
|
|
18
|
+
export type ToolHandler<T> = {
|
|
19
|
+
(options: {
|
|
20
|
+
args: T;
|
|
21
|
+
sanitizedQuery: Query;
|
|
22
|
+
schema: SchemaOverview;
|
|
23
|
+
accountability: Accountability | undefined;
|
|
24
|
+
}): Promise<ToolResult | undefined>;
|
|
25
|
+
};
|
|
26
|
+
export type ToolEndpoint<T> = {
|
|
27
|
+
(options: {
|
|
28
|
+
input: T;
|
|
29
|
+
data: unknown;
|
|
30
|
+
}): string[] | undefined;
|
|
31
|
+
};
|
|
32
|
+
export interface ToolConfig<T> {
|
|
33
|
+
name: string;
|
|
34
|
+
description: string;
|
|
35
|
+
endpoint?: ToolEndpoint<T>;
|
|
36
|
+
admin?: boolean;
|
|
37
|
+
inputSchema: ZodType<any>;
|
|
38
|
+
validateSchema?: ZodType<T>;
|
|
39
|
+
annotations?: ToolAnnotations;
|
|
40
|
+
handler: ToolHandler<T>;
|
|
41
|
+
}
|
|
42
|
+
export interface Prompt {
|
|
43
|
+
name: string;
|
|
44
|
+
system_prompt?: string | null;
|
|
45
|
+
description?: string;
|
|
46
|
+
messages: {
|
|
47
|
+
role: 'user' | 'assistant';
|
|
48
|
+
text: string;
|
|
49
|
+
}[];
|
|
50
|
+
}
|
|
51
|
+
export interface MCPOptions {
|
|
52
|
+
promptsCollection?: string;
|
|
53
|
+
allowDeletes?: boolean;
|
|
54
|
+
systemPromptEnabled?: boolean;
|
|
55
|
+
systemPrompt?: string | null;
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -15,6 +15,7 @@ import { getMilliseconds } from '../utils/get-milliseconds.js';
|
|
|
15
15
|
import { getSecret } from '../utils/get-secret.js';
|
|
16
16
|
import { stall } from '../utils/stall.js';
|
|
17
17
|
import { ActivityService } from './activity.js';
|
|
18
|
+
import { RevisionsService } from './revisions.js';
|
|
18
19
|
import { SettingsService } from './settings.js';
|
|
19
20
|
import { TFAService } from './tfa.js';
|
|
20
21
|
const env = useEnv();
|
|
@@ -96,6 +97,25 @@ export class AuthenticationService {
|
|
|
96
97
|
if (error instanceof RateLimiterRes && error.remainingPoints === 0) {
|
|
97
98
|
await this.knex('directus_users').update({ status: 'suspended' }).where({ id: user.id });
|
|
98
99
|
user.status = 'suspended';
|
|
100
|
+
if (this.accountability) {
|
|
101
|
+
const activity = await this.activityService.createOne({
|
|
102
|
+
action: Action.UPDATE,
|
|
103
|
+
user: user.id,
|
|
104
|
+
ip: this.accountability.ip,
|
|
105
|
+
user_agent: this.accountability.userAgent,
|
|
106
|
+
origin: this.accountability.origin,
|
|
107
|
+
collection: 'directus_users',
|
|
108
|
+
item: user.id,
|
|
109
|
+
});
|
|
110
|
+
const revisionsService = new RevisionsService({ knex: this.knex, schema: this.schema });
|
|
111
|
+
await revisionsService.createOne({
|
|
112
|
+
activity: activity,
|
|
113
|
+
collection: 'directus_users',
|
|
114
|
+
item: user.id,
|
|
115
|
+
data: user,
|
|
116
|
+
delta: { status: 'suspended' },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
99
119
|
// This means that new attempts after the user has been re-activated will be accepted
|
|
100
120
|
await loginAttemptsLimiter.set(user.id, 0, 0);
|
|
101
121
|
}
|
|
@@ -137,6 +157,22 @@ export class AuthenticationService {
|
|
|
137
157
|
app_access: globalAccess.app,
|
|
138
158
|
admin_access: globalAccess.admin,
|
|
139
159
|
};
|
|
160
|
+
// Add role-based enforcement to token payload for users who need to set up 2FA
|
|
161
|
+
if (!user.tfa_secret) {
|
|
162
|
+
// Check if user has role-based enforcement
|
|
163
|
+
const roleEnforcement = await this.knex
|
|
164
|
+
.select('directus_policies.enforce_tfa')
|
|
165
|
+
.from('directus_users')
|
|
166
|
+
.leftJoin('directus_roles', 'directus_users.role', 'directus_roles.id')
|
|
167
|
+
.leftJoin('directus_access', 'directus_roles.id', 'directus_access.role')
|
|
168
|
+
.leftJoin('directus_policies', 'directus_access.policy', 'directus_policies.id')
|
|
169
|
+
.where('directus_users.id', user.id)
|
|
170
|
+
.where('directus_policies.enforce_tfa', true)
|
|
171
|
+
.first();
|
|
172
|
+
if (roleEnforcement) {
|
|
173
|
+
tokenPayload.enforce_tfa = true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
140
176
|
const refreshToken = nanoid(64);
|
|
141
177
|
const refreshTokenExpiration = new Date(Date.now() + getMilliseconds(env['REFRESH_TOKEN_TTL'], 0));
|
|
142
178
|
if (options?.session) {
|
package/dist/services/fields.js
CHANGED
|
@@ -339,7 +339,7 @@ export class FieldsService {
|
|
|
339
339
|
await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
|
|
340
340
|
}
|
|
341
341
|
if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
|
|
342
|
-
const updatedSchema = await getSchema();
|
|
342
|
+
const updatedSchema = await getSchema({ database: this.knex });
|
|
343
343
|
for (const nestedActionEvent of nestedActionEvents) {
|
|
344
344
|
nestedActionEvent.context.schema = updatedSchema;
|
|
345
345
|
emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
|
|
@@ -451,7 +451,7 @@ export class FieldsService {
|
|
|
451
451
|
await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
|
|
452
452
|
}
|
|
453
453
|
if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
|
|
454
|
-
const updatedSchema = await getSchema();
|
|
454
|
+
const updatedSchema = await getSchema({ database: this.knex });
|
|
455
455
|
for (const nestedActionEvent of nestedActionEvents) {
|
|
456
456
|
nestedActionEvent.context.schema = updatedSchema;
|
|
457
457
|
emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
|
|
@@ -480,7 +480,7 @@ export class FieldsService {
|
|
|
480
480
|
await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
|
|
481
481
|
}
|
|
482
482
|
if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
|
|
483
|
-
const updatedSchema = await getSchema();
|
|
483
|
+
const updatedSchema = await getSchema({ database: this.knex });
|
|
484
484
|
for (const nestedActionEvent of nestedActionEvents) {
|
|
485
485
|
nestedActionEvent.context.schema = updatedSchema;
|
|
486
486
|
emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
|
|
@@ -621,7 +621,7 @@ export class FieldsService {
|
|
|
621
621
|
await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
|
|
622
622
|
}
|
|
623
623
|
if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
|
|
624
|
-
const updatedSchema = await getSchema();
|
|
624
|
+
const updatedSchema = await getSchema({ database: this.knex });
|
|
625
625
|
for (const nestedActionEvent of nestedActionEvents) {
|
|
626
626
|
nestedActionEvent.context.schema = updatedSchema;
|
|
627
627
|
emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
|
package/dist/services/items.js
CHANGED
|
@@ -148,6 +148,7 @@ export class ItemsService {
|
|
|
148
148
|
knex: trx,
|
|
149
149
|
schema: this.schema,
|
|
150
150
|
nested: this.nested,
|
|
151
|
+
overwriteDefaults: opts.overwriteDefaults,
|
|
151
152
|
});
|
|
152
153
|
const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, userIntegrityCheckFlags: userIntegrityCheckFlagsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
|
|
153
154
|
const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, userIntegrityCheckFlags: userIntegrityCheckFlagsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
|
|
@@ -338,6 +339,7 @@ export class ItemsService {
|
|
|
338
339
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
339
340
|
bypassEmitAction: (params) => nestedActionEvents.push(params),
|
|
340
341
|
mutationTracker: opts.mutationTracker,
|
|
342
|
+
overwriteDefaults: opts.overwriteDefaults?.[index],
|
|
341
343
|
bypassAutoIncrementSequenceReset,
|
|
342
344
|
});
|
|
343
345
|
primaryKeys.push(primaryKey);
|
|
@@ -435,7 +437,7 @@ export class ItemsService {
|
|
|
435
437
|
const queryWithKey = assign({}, query, { filter: filterWithKey });
|
|
436
438
|
let results = [];
|
|
437
439
|
if (query.version) {
|
|
438
|
-
results =
|
|
440
|
+
results = [await handleVersion(this, key, queryWithKey, opts)];
|
|
439
441
|
}
|
|
440
442
|
else {
|
|
441
443
|
results = await this.readByQuery(queryWithKey, opts);
|
|
@@ -497,13 +499,15 @@ export class ItemsService {
|
|
|
497
499
|
await transaction(this.knex, async (knex) => {
|
|
498
500
|
const service = this.fork({ knex });
|
|
499
501
|
let userIntegrityCheckFlags = opts.userIntegrityCheckFlags ?? UserIntegrityCheckFlag.None;
|
|
500
|
-
for (const
|
|
502
|
+
for (const index in data) {
|
|
503
|
+
const item = data[index];
|
|
501
504
|
const primaryKey = item[primaryKeyField];
|
|
502
505
|
if (!primaryKey)
|
|
503
506
|
throw new InvalidPayloadError({ reason: `Item in update misses primary key` });
|
|
504
507
|
const combinedOpts = {
|
|
505
508
|
autoPurgeCache: false,
|
|
506
509
|
...opts,
|
|
510
|
+
overwriteDefaults: opts.overwriteDefaults?.[index],
|
|
507
511
|
onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
|
|
508
512
|
};
|
|
509
513
|
keys.push(await service.updateOne(primaryKey, omit(item, primaryKeyField), combinedOpts));
|
|
@@ -591,6 +595,7 @@ export class ItemsService {
|
|
|
591
595
|
knex: trx,
|
|
592
596
|
schema: this.schema,
|
|
593
597
|
nested: this.nested,
|
|
598
|
+
overwriteDefaults: opts.overwriteDefaults,
|
|
594
599
|
});
|
|
595
600
|
const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, userIntegrityCheckFlags: userIntegrityCheckFlagsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
|
|
596
601
|
const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, userIntegrityCheckFlags: userIntegrityCheckFlagsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
|
|
@@ -752,8 +757,13 @@ export class ItemsService {
|
|
|
752
757
|
const primaryKeys = await transaction(this.knex, async (knex) => {
|
|
753
758
|
const service = this.fork({ knex });
|
|
754
759
|
const primaryKeys = [];
|
|
755
|
-
for (const
|
|
756
|
-
const
|
|
760
|
+
for (const index in payloads) {
|
|
761
|
+
const payload = payloads[index];
|
|
762
|
+
const primaryKey = await service.upsertOne(payload, {
|
|
763
|
+
...(opts || {}),
|
|
764
|
+
overwriteDefaults: opts.overwriteDefaults?.[index],
|
|
765
|
+
autoPurgeCache: false,
|
|
766
|
+
});
|
|
757
767
|
primaryKeys.push(primaryKey);
|
|
758
768
|
}
|
|
759
769
|
return primaryKeys;
|