@directus/api 29.1.1 → 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.
Files changed (105) hide show
  1. package/dist/app.js +5 -0
  2. package/dist/auth/drivers/oauth2.js +17 -3
  3. package/dist/auth/drivers/openid.js +17 -3
  4. package/dist/constants.d.ts +1 -1
  5. package/dist/constants.js +9 -1
  6. package/dist/controllers/items.js +3 -4
  7. package/dist/controllers/mcp.d.ts +2 -0
  8. package/dist/controllers/mcp.js +33 -0
  9. package/dist/controllers/users.js +17 -7
  10. package/dist/controllers/versions.js +3 -2
  11. package/dist/database/errors/dialects/mssql.d.ts +1 -1
  12. package/dist/database/errors/dialects/mssql.js +18 -10
  13. package/dist/database/migrations/20250813A-add-mcp.d.ts +3 -0
  14. package/dist/database/migrations/20250813A-add-mcp.js +18 -0
  15. package/dist/database/run-ast/README.md +46 -0
  16. package/dist/mailer.js +3 -3
  17. package/dist/mcp/define.d.ts +2 -0
  18. package/dist/mcp/define.js +3 -0
  19. package/dist/mcp/index.d.ts +1 -0
  20. package/dist/mcp/index.js +1 -0
  21. package/dist/mcp/schema.d.ts +485 -0
  22. package/dist/mcp/schema.js +219 -0
  23. package/dist/mcp/server.d.ts +97 -0
  24. package/dist/mcp/server.js +310 -0
  25. package/dist/mcp/tools/assets.d.ts +3 -0
  26. package/dist/mcp/tools/assets.js +54 -0
  27. package/dist/mcp/tools/collections.d.ts +84 -0
  28. package/dist/mcp/tools/collections.js +90 -0
  29. package/dist/mcp/tools/fields.d.ts +101 -0
  30. package/dist/mcp/tools/fields.js +157 -0
  31. package/dist/mcp/tools/files.d.ts +235 -0
  32. package/dist/mcp/tools/files.js +103 -0
  33. package/dist/mcp/tools/flows.d.ts +323 -0
  34. package/dist/mcp/tools/flows.js +85 -0
  35. package/dist/mcp/tools/folders.d.ts +95 -0
  36. package/dist/mcp/tools/folders.js +96 -0
  37. package/dist/mcp/tools/index.d.ts +15 -0
  38. package/dist/mcp/tools/index.js +29 -0
  39. package/dist/mcp/tools/items.d.ts +87 -0
  40. package/dist/mcp/tools/items.js +141 -0
  41. package/dist/mcp/tools/operations.d.ts +171 -0
  42. package/dist/mcp/tools/operations.js +77 -0
  43. package/dist/mcp/tools/prompts/assets.md +8 -0
  44. package/dist/mcp/tools/prompts/collections.md +336 -0
  45. package/dist/mcp/tools/prompts/fields.md +521 -0
  46. package/dist/mcp/tools/prompts/files.md +180 -0
  47. package/dist/mcp/tools/prompts/flows.md +495 -0
  48. package/dist/mcp/tools/prompts/folders.md +34 -0
  49. package/dist/mcp/tools/prompts/index.d.ts +16 -0
  50. package/dist/mcp/tools/prompts/index.js +19 -0
  51. package/dist/mcp/tools/prompts/items.md +317 -0
  52. package/dist/mcp/tools/prompts/operations.md +721 -0
  53. package/dist/mcp/tools/prompts/relations.md +386 -0
  54. package/dist/mcp/tools/prompts/schema.md +130 -0
  55. package/dist/mcp/tools/prompts/system-prompt-description.md +1 -0
  56. package/dist/mcp/tools/prompts/system-prompt.md +44 -0
  57. package/dist/mcp/tools/prompts/trigger-flow.md +214 -0
  58. package/dist/mcp/tools/relations.d.ts +73 -0
  59. package/dist/mcp/tools/relations.js +93 -0
  60. package/dist/mcp/tools/schema.d.ts +54 -0
  61. package/dist/mcp/tools/schema.js +317 -0
  62. package/dist/mcp/tools/system.d.ts +3 -0
  63. package/dist/mcp/tools/system.js +22 -0
  64. package/dist/mcp/tools/trigger-flow.d.ts +8 -0
  65. package/dist/mcp/tools/trigger-flow.js +48 -0
  66. package/dist/mcp/transport.d.ts +13 -0
  67. package/dist/mcp/transport.js +18 -0
  68. package/dist/mcp/types.d.ts +56 -0
  69. package/dist/mcp/types.js +1 -0
  70. package/dist/middleware/respond.js +2 -2
  71. package/dist/services/authentication.js +36 -0
  72. package/dist/services/fields.js +4 -4
  73. package/dist/services/graphql/index.d.ts +2 -2
  74. package/dist/services/graphql/index.js +6 -5
  75. package/dist/services/graphql/resolvers/query.js +4 -39
  76. package/dist/services/items.js +38 -12
  77. package/dist/services/payload.d.ts +7 -3
  78. package/dist/services/payload.js +70 -12
  79. package/dist/services/server.js +1 -0
  80. package/dist/services/tfa.d.ts +1 -1
  81. package/dist/services/tfa.js +20 -5
  82. package/dist/services/versions.d.ts +6 -4
  83. package/dist/services/versions.js +84 -25
  84. package/dist/types/auth.d.ts +2 -1
  85. package/dist/utils/deep-map-response.d.ts +17 -0
  86. package/dist/utils/deep-map-response.js +61 -0
  87. package/dist/utils/get-relation-info.d.ts +1 -2
  88. package/dist/utils/permissions-cacheable.d.ts +8 -0
  89. package/dist/utils/{permissions-cachable.js → permissions-cacheable.js} +8 -6
  90. package/dist/utils/transaction.d.ts +1 -1
  91. package/dist/utils/transaction.js +18 -2
  92. package/dist/utils/versioning/deep-map-with-schema.d.ts +23 -0
  93. package/dist/utils/versioning/deep-map-with-schema.js +81 -0
  94. package/dist/utils/versioning/handle-version.d.ts +3 -0
  95. package/dist/utils/versioning/handle-version.js +96 -0
  96. package/dist/utils/versioning/merge-version-data.d.ts +2 -0
  97. package/dist/utils/versioning/merge-version-data.js +10 -0
  98. package/dist/utils/versioning/split-recursive.d.ts +4 -0
  99. package/dist/utils/versioning/split-recursive.js +27 -0
  100. package/package.json +31 -30
  101. package/dist/middleware/merge-content-versions.d.ts +0 -2
  102. package/dist/middleware/merge-content-versions.js +0 -26
  103. package/dist/utils/merge-version-data.d.ts +0 -3
  104. package/dist/utils/merge-version-data.js +0 -134
  105. package/dist/utils/permissions-cachable.d.ts +0 -8
@@ -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,3 @@
1
+ export declare const system: import("../types.js").ToolConfig<{
2
+ promptOverride?: string | null | undefined;
3
+ }>;
@@ -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 {};
@@ -10,7 +10,7 @@ import { getCacheKey } from '../utils/get-cache-key.js';
10
10
  import { getDateFormatted } from '../utils/get-date-formatted.js';
11
11
  import { getMilliseconds } from '../utils/get-milliseconds.js';
12
12
  import { stringByteSize } from '../utils/get-string-byte-size.js';
13
- import { permissionsCachable } from '../utils/permissions-cachable.js';
13
+ import { permissionsCacheable } from '../utils/permissions-cacheable.js';
14
14
  export const respond = asyncHandler(async (req, res) => {
15
15
  const env = useEnv();
16
16
  const logger = useLogger();
@@ -29,7 +29,7 @@ export const respond = asyncHandler(async (req, res) => {
29
29
  !req.sanitizedQuery.export &&
30
30
  res.locals['cache'] !== false &&
31
31
  exceedsMaxSize === false &&
32
- (await permissionsCachable(req.collection, {
32
+ (await permissionsCacheable(req.collection, {
33
33
  knex: getDatabase(),
34
34
  schema: req.schema,
35
35
  }, req.accountability))) {
@@ -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) {
@@ -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);
@@ -1,4 +1,4 @@
1
- import type { AbstractServiceOptions, Accountability, GraphQLParams, GQLScope, Item, Query, SchemaOverview } from '@directus/types';
1
+ import type { AbstractServiceOptions, Accountability, GraphQLParams, GQLScope, Item, Query, SchemaOverview, PrimaryKey } from '@directus/types';
2
2
  import type { FormattedExecutionResult, GraphQLSchema } from 'graphql';
3
3
  import type { Knex } from 'knex';
4
4
  export declare class GraphQLService {
@@ -22,7 +22,7 @@ export declare class GraphQLService {
22
22
  /**
23
23
  * Execute the read action on the correct service. Checks for singleton as well.
24
24
  */
25
- read(collection: string, query: Query): Promise<Partial<Item>>;
25
+ read(collection: string, query: Query, id?: PrimaryKey): Promise<Partial<Item>>;
26
26
  /**
27
27
  * Upsert and read singleton item
28
28
  */
@@ -61,16 +61,17 @@ export class GraphQLService {
61
61
  /**
62
62
  * Execute the read action on the correct service. Checks for singleton as well.
63
63
  */
64
- async read(collection, query) {
64
+ async read(collection, query, id) {
65
65
  const service = getService(collection, {
66
66
  knex: this.knex,
67
67
  accountability: this.accountability,
68
68
  schema: this.schema,
69
69
  });
70
- const result = this.schema.collections[collection].singleton
71
- ? await service.readSingleton(query, { stripNonRequested: false })
72
- : await service.readByQuery(query, { stripNonRequested: false });
73
- return result;
70
+ if (this.schema.collections[collection].singleton)
71
+ return await service.readSingleton(query, { stripNonRequested: false });
72
+ if (id)
73
+ return await service.readOne(id, query, { stripNonRequested: false });
74
+ return await service.readByQuery(query, { stripNonRequested: false });
74
75
  }
75
76
  /**
76
77
  * Upsert and read singleton item