@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.
Files changed (83) 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/controllers/mcp.d.ts +2 -0
  5. package/dist/controllers/mcp.js +33 -0
  6. package/dist/controllers/users.js +17 -7
  7. package/dist/controllers/versions.js +3 -2
  8. package/dist/database/errors/dialects/mssql.d.ts +1 -1
  9. package/dist/database/errors/dialects/mssql.js +18 -10
  10. package/dist/database/migrations/20250813A-add-mcp.d.ts +3 -0
  11. package/dist/database/migrations/20250813A-add-mcp.js +18 -0
  12. package/dist/database/run-ast/README.md +46 -0
  13. package/dist/mcp/define.d.ts +2 -0
  14. package/dist/mcp/define.js +3 -0
  15. package/dist/mcp/index.d.ts +1 -0
  16. package/dist/mcp/index.js +1 -0
  17. package/dist/mcp/schema.d.ts +485 -0
  18. package/dist/mcp/schema.js +219 -0
  19. package/dist/mcp/server.d.ts +97 -0
  20. package/dist/mcp/server.js +310 -0
  21. package/dist/mcp/tools/assets.d.ts +3 -0
  22. package/dist/mcp/tools/assets.js +54 -0
  23. package/dist/mcp/tools/collections.d.ts +84 -0
  24. package/dist/mcp/tools/collections.js +90 -0
  25. package/dist/mcp/tools/fields.d.ts +101 -0
  26. package/dist/mcp/tools/fields.js +157 -0
  27. package/dist/mcp/tools/files.d.ts +235 -0
  28. package/dist/mcp/tools/files.js +103 -0
  29. package/dist/mcp/tools/flows.d.ts +323 -0
  30. package/dist/mcp/tools/flows.js +85 -0
  31. package/dist/mcp/tools/folders.d.ts +95 -0
  32. package/dist/mcp/tools/folders.js +96 -0
  33. package/dist/mcp/tools/index.d.ts +15 -0
  34. package/dist/mcp/tools/index.js +29 -0
  35. package/dist/mcp/tools/items.d.ts +87 -0
  36. package/dist/mcp/tools/items.js +141 -0
  37. package/dist/mcp/tools/operations.d.ts +171 -0
  38. package/dist/mcp/tools/operations.js +77 -0
  39. package/dist/mcp/tools/prompts/assets.md +8 -0
  40. package/dist/mcp/tools/prompts/collections.md +336 -0
  41. package/dist/mcp/tools/prompts/fields.md +521 -0
  42. package/dist/mcp/tools/prompts/files.md +180 -0
  43. package/dist/mcp/tools/prompts/flows.md +495 -0
  44. package/dist/mcp/tools/prompts/folders.md +34 -0
  45. package/dist/mcp/tools/prompts/index.d.ts +16 -0
  46. package/dist/mcp/tools/prompts/index.js +19 -0
  47. package/dist/mcp/tools/prompts/items.md +317 -0
  48. package/dist/mcp/tools/prompts/operations.md +721 -0
  49. package/dist/mcp/tools/prompts/relations.md +386 -0
  50. package/dist/mcp/tools/prompts/schema.md +130 -0
  51. package/dist/mcp/tools/prompts/system-prompt-description.md +1 -0
  52. package/dist/mcp/tools/prompts/system-prompt.md +44 -0
  53. package/dist/mcp/tools/prompts/trigger-flow.md +214 -0
  54. package/dist/mcp/tools/relations.d.ts +73 -0
  55. package/dist/mcp/tools/relations.js +93 -0
  56. package/dist/mcp/tools/schema.d.ts +54 -0
  57. package/dist/mcp/tools/schema.js +317 -0
  58. package/dist/mcp/tools/system.d.ts +3 -0
  59. package/dist/mcp/tools/system.js +22 -0
  60. package/dist/mcp/tools/trigger-flow.d.ts +8 -0
  61. package/dist/mcp/tools/trigger-flow.js +48 -0
  62. package/dist/mcp/transport.d.ts +13 -0
  63. package/dist/mcp/transport.js +18 -0
  64. package/dist/mcp/types.d.ts +56 -0
  65. package/dist/mcp/types.js +1 -0
  66. package/dist/services/authentication.js +36 -0
  67. package/dist/services/fields.js +4 -4
  68. package/dist/services/items.js +14 -4
  69. package/dist/services/payload.d.ts +7 -3
  70. package/dist/services/payload.js +26 -12
  71. package/dist/services/server.js +1 -0
  72. package/dist/services/tfa.d.ts +1 -1
  73. package/dist/services/tfa.js +20 -5
  74. package/dist/services/versions.d.ts +6 -4
  75. package/dist/services/versions.js +84 -25
  76. package/dist/types/auth.d.ts +2 -1
  77. package/dist/utils/versioning/deep-map-with-schema.d.ts +23 -0
  78. package/dist/utils/versioning/deep-map-with-schema.js +81 -0
  79. package/dist/utils/versioning/handle-version.d.ts +2 -2
  80. package/dist/utils/versioning/handle-version.js +47 -43
  81. package/dist/utils/versioning/split-recursive.d.ts +4 -0
  82. package/dist/utils/versioning/split-recursive.js +27 -0
  83. package/package.json +30 -29
@@ -0,0 +1,219 @@
1
+ import { z } from 'zod';
2
+ // PK
3
+ export const PrimaryKeyInputSchema = z.union([z.number(), z.string()]);
4
+ export const PrimaryKeyValidateSchema = z.union([z.number(), z.string()]);
5
+ // item
6
+ export const ItemInputSchema = z.record(z.string(), z.any());
7
+ export const ItemValidateSchema = z.record(z.string(), z.any());
8
+ // query
9
+ export const QueryInputSchema = z
10
+ .object({
11
+ fields: z.array(z.string()),
12
+ sort: z.array(z.string()),
13
+ filter: z.record(z.string(), z.any()),
14
+ limit: z.number(),
15
+ offset: z.number(),
16
+ page: z.number(),
17
+ search: z.string(),
18
+ deep: z.record(z.string(), z.any()),
19
+ alias: z.record(z.string(), z.string()),
20
+ aggregate: z.object({
21
+ count: z.array(z.string()),
22
+ sum: z.array(z.string()),
23
+ avg: z.array(z.string()),
24
+ min: z.array(z.string()),
25
+ max: z.array(z.string()),
26
+ }),
27
+ backlink: z.boolean(),
28
+ version: z.string(),
29
+ versionRaw: z.boolean(),
30
+ export: z.string(),
31
+ group: z.array(z.string()),
32
+ })
33
+ .partial();
34
+ export const QueryValidateSchema = QueryInputSchema;
35
+ // field
36
+ export const RawFieldItemInputSchema = z.object({
37
+ field: z.string(),
38
+ type: z.string(),
39
+ name: z.string().optional(),
40
+ children: z.union([z.array(z.record(z.string(), z.any())), z.null()]).optional(),
41
+ collection: z.string().optional(),
42
+ schema: z.union([z.record(z.string(), z.any()), z.null()]).optional(),
43
+ meta: z.union([z.record(z.string(), z.any()), z.null()]).optional(),
44
+ });
45
+ export const RawFieldItemValidateSchema = RawFieldItemInputSchema;
46
+ export const FieldItemInputSchema = z.object({
47
+ field: z.string(),
48
+ type: z.string().nullable(),
49
+ name: z.string().optional(),
50
+ collection: z.string().optional(),
51
+ schema: z.union([z.record(z.string(), z.any()), z.null()]).optional(),
52
+ meta: z.union([z.record(z.string(), z.any()), z.null()]).optional(),
53
+ });
54
+ export const FieldItemValidateSchema = FieldItemInputSchema;
55
+ // collection
56
+ export const CollectionItemInputSchema = z.object({
57
+ collection: z.string(),
58
+ fields: z.array(RawFieldItemInputSchema).optional(),
59
+ meta: z.union([z.record(z.string(), z.any()), z.null()]).optional(),
60
+ schema: z
61
+ .union([z.object({}), z.null()])
62
+ .optional()
63
+ .describe('ALWAYS an empty object for new collections. Only send `null` or `undefined` for folder collections.'),
64
+ });
65
+ export const CollectionItemValidateCreateSchema = CollectionItemInputSchema;
66
+ export const CollectionItemValidateUpdateSchema = z.object({
67
+ collection: z.string(),
68
+ meta: z.union([z.record(z.string(), z.any()), z.null()]).optional(),
69
+ schema: z.union([z.record(z.string(), z.any()), z.null()]).optional(),
70
+ });
71
+ // file
72
+ export const FileItemInputSchema = z
73
+ .object({
74
+ id: z.string(),
75
+ storage: z.string(),
76
+ filename_disk: z.string(),
77
+ filename_download: z.string(),
78
+ title: z.union([z.string(), z.null()]),
79
+ type: z.union([z.string(), z.null()]),
80
+ folder: z.union([z.string(), z.null()]),
81
+ created_on: z.string(),
82
+ uploaded_by: z.union([z.string(), z.null()]),
83
+ uploaded_on: z.union([z.string(), z.null()]),
84
+ modified_by: z.union([z.string(), z.null()]),
85
+ modified_on: z.string(),
86
+ charset: z.union([z.string(), z.null()]),
87
+ filesize: z.number(),
88
+ width: z.union([z.number(), z.null()]),
89
+ height: z.union([z.number(), z.null()]),
90
+ duration: z.union([z.number(), z.null()]),
91
+ embed: z.union([z.string(), z.null()]),
92
+ description: z.union([z.string(), z.null()]),
93
+ location: z.union([z.string(), z.null()]),
94
+ tags: z.union([z.string(), z.null()]),
95
+ metadata: z.union([z.record(z.string(), z.any()), z.null()]),
96
+ focal_point_x: z.union([z.number(), z.null()]),
97
+ focal_point_y: z.union([z.number(), z.null()]),
98
+ tus_id: z.union([z.string(), z.null()]),
99
+ tus_data: z.union([z.record(z.string(), z.any()), z.null()]),
100
+ })
101
+ .partial();
102
+ export const FileItemValidateSchema = FileItemInputSchema;
103
+ export const FileImportItemInputSchema = z.object({
104
+ url: z.string(),
105
+ file: FileItemInputSchema,
106
+ });
107
+ export const FileImportItemValidateSchema = z.object({
108
+ url: z.string(),
109
+ file: FileItemValidateSchema,
110
+ });
111
+ // opertations
112
+ export const OperationItemInputSchema = z
113
+ .object({
114
+ id: z.string(),
115
+ name: z.union([z.string(), z.null()]),
116
+ key: z.string(),
117
+ type: z.string(),
118
+ position_x: z.number(),
119
+ position_y: z.number(),
120
+ options: z.record(z.string(), z.any()),
121
+ resolve: z.union([z.string(), z.null()]),
122
+ reject: z.union([z.string(), z.null()]),
123
+ flow: z.string(),
124
+ date_created: z.string(),
125
+ user_created: z.string(),
126
+ })
127
+ .partial();
128
+ export const OperationItemValidateSchema = OperationItemInputSchema;
129
+ // flow
130
+ export const FlowItemInputSchema = z
131
+ .object({
132
+ id: z.string(),
133
+ name: z.string(),
134
+ icon: z.union([z.string(), z.null()]),
135
+ color: z.union([z.string(), z.null()]),
136
+ description: z.union([z.string(), z.null()]),
137
+ status: z.enum(['active', 'inactive']),
138
+ trigger: z.union([z.enum(['event', 'schedule', 'operation', 'webhook', 'manual']), z.null()]),
139
+ options: z.union([z.record(z.string(), z.any()), z.null()]),
140
+ operation: z.union([z.string(), z.null()]),
141
+ operations: z.array(OperationItemInputSchema),
142
+ date_created: z.string(),
143
+ user_created: z.string(),
144
+ accountability: z.union([z.enum(['all', 'activity']), z.null()]),
145
+ })
146
+ .partial();
147
+ export const FlowItemValidateSchema = FlowItemInputSchema;
148
+ // trigger flow
149
+ export const TriggerFlowInputSchema = z.object({
150
+ id: PrimaryKeyInputSchema,
151
+ collection: z.string(),
152
+ keys: z.array(PrimaryKeyInputSchema).optional(),
153
+ headers: z.record(z.string(), z.any()).optional(),
154
+ query: z.record(z.string(), z.any()).optional(),
155
+ data: z.record(z.string(), z.any()).optional(),
156
+ });
157
+ export const TriggerFlowValidateSchema = z.strictObject({
158
+ id: PrimaryKeyValidateSchema,
159
+ collection: z.string(),
160
+ keys: z.array(PrimaryKeyValidateSchema).optional(),
161
+ query: z.record(z.string(), z.any()).optional(),
162
+ headers: z.record(z.string(), z.any()).optional(),
163
+ data: z.record(z.string(), z.any()).optional(),
164
+ });
165
+ // folder
166
+ export const FolderItemInputSchema = z.object({
167
+ id: PrimaryKeyInputSchema.optional(),
168
+ name: z.string(),
169
+ parent: z.string().optional(),
170
+ });
171
+ export const FolderItemValidateSchema = FolderItemInputSchema;
172
+ // relation
173
+ export const RelationItemInputSchema = z.object({
174
+ collection: z.string(),
175
+ field: z.string(),
176
+ related_collection: z.union([z.string(), z.null()]),
177
+ schema: z.union([z.record(z.string(), z.any()), z.null()]),
178
+ meta: z.union([z.record(z.string(), z.any()), z.null()]),
179
+ });
180
+ const RelationMetaSchema = z.object({
181
+ id: z.number(),
182
+ many_collection: z.string(),
183
+ many_field: z.string(),
184
+ one_collection: z.string().nullable(),
185
+ one_field: z.string().nullable(),
186
+ one_collection_field: z.string().nullable(),
187
+ one_allowed_collections: z.array(z.string()).nullable(),
188
+ one_deselect_action: z.enum(['nullify', 'delete']),
189
+ junction_field: z.string().nullable(),
190
+ sort_field: z.string().nullable(),
191
+ system: z.boolean().optional(),
192
+ });
193
+ const FkActionEnum = z.enum(['NO ACTION', 'RESTRICT', 'CASCADE', 'SET NULL', 'SET DEFAULT']);
194
+ export const ForeignKeySchema = z.object({
195
+ table: z.string(),
196
+ column: z.string(),
197
+ foreign_key_table: z.string(),
198
+ foreign_key_column: z.string(),
199
+ foreign_key_schema: z.string().optional(),
200
+ constraint_name: z.union([z.string(), z.null()]),
201
+ on_update: z.union([FkActionEnum, z.null()]),
202
+ on_delete: z.union([FkActionEnum, z.null()]),
203
+ });
204
+ export const RelationItemValidateCreateSchema = z.object({
205
+ collection: z.string(),
206
+ field: z.string(),
207
+ related_collection: z.string().nullable(),
208
+ schema: ForeignKeySchema.partial().nullable().optional(),
209
+ meta: RelationMetaSchema.partial().nullable(),
210
+ });
211
+ export const RelationItemValidateUpdateSchema = z
212
+ .object({
213
+ collection: z.string(),
214
+ field: z.string(),
215
+ related_collection: z.string().nullable().optional(),
216
+ schema: ForeignKeySchema.partial().nullable().optional(),
217
+ meta: RelationMetaSchema.partial().nullable().optional(),
218
+ })
219
+ .optional();
@@ -0,0 +1,97 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { type GetPromptResult } from '@modelcontextprotocol/sdk/types.js';
3
+ import type { Request, Response } from 'express';
4
+ import type { MCPOptions, ToolConfig, ToolResult } from './types.js';
5
+ export declare class DirectusMCP {
6
+ promptsCollection?: string | null;
7
+ systemPrompt?: string | null;
8
+ systemPromptEnabled?: boolean;
9
+ server: Server;
10
+ allowDeletes?: boolean;
11
+ constructor(options?: MCPOptions);
12
+ /**
13
+ * This handleRequest function is not awaiting lower level logic resulting in the actual
14
+ * response being an asynchronous side effect happening after the function has returned
15
+ */
16
+ handleRequest(req: Request, res: Response): void;
17
+ buildURL(tool: ToolConfig<unknown>, input: unknown, data: unknown): string | undefined;
18
+ toPromptResponse(result: {
19
+ description?: string | undefined;
20
+ messages: GetPromptResult['messages'];
21
+ }): GetPromptResult;
22
+ toToolResponse(result?: ToolResult): {
23
+ [x: string]: unknown;
24
+ content: ({
25
+ [x: string]: unknown;
26
+ type: "text";
27
+ text: string;
28
+ _meta?: {
29
+ [x: string]: unknown;
30
+ } | undefined;
31
+ } | {
32
+ [x: string]: unknown;
33
+ type: "image";
34
+ data: string;
35
+ mimeType: string;
36
+ _meta?: {
37
+ [x: string]: unknown;
38
+ } | undefined;
39
+ } | {
40
+ [x: string]: unknown;
41
+ type: "audio";
42
+ data: string;
43
+ mimeType: string;
44
+ _meta?: {
45
+ [x: string]: unknown;
46
+ } | undefined;
47
+ } | {
48
+ [x: string]: unknown;
49
+ type: "resource_link";
50
+ name: string;
51
+ uri: string;
52
+ title?: string | undefined;
53
+ description?: string | undefined;
54
+ mimeType?: string | undefined;
55
+ _meta?: {
56
+ [x: string]: unknown;
57
+ } | undefined;
58
+ } | {
59
+ [x: string]: unknown;
60
+ type: "resource";
61
+ resource: {
62
+ [x: string]: unknown;
63
+ text: string;
64
+ uri: string;
65
+ mimeType?: string | undefined;
66
+ _meta?: {
67
+ [x: string]: unknown;
68
+ } | undefined;
69
+ } | {
70
+ [x: string]: unknown;
71
+ blob: string;
72
+ uri: string;
73
+ mimeType?: string | undefined;
74
+ _meta?: {
75
+ [x: string]: unknown;
76
+ } | undefined;
77
+ };
78
+ _meta?: {
79
+ [x: string]: unknown;
80
+ } | undefined;
81
+ })[];
82
+ _meta?: {
83
+ [x: string]: unknown;
84
+ } | undefined;
85
+ structuredContent?: {
86
+ [x: string]: unknown;
87
+ } | undefined;
88
+ isError?: boolean | undefined;
89
+ };
90
+ toExecutionError(err: unknown): {
91
+ isError: boolean;
92
+ content: {
93
+ type: "text";
94
+ text: string;
95
+ }[];
96
+ };
97
+ }
@@ -0,0 +1,310 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { ForbiddenError, InvalidPayloadError, isDirectusError } from '@directus/errors';
3
+ import { isObject, parseJSON, toArray } from '@directus/utils';
4
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
5
+ import { CallToolRequestSchema, GetPromptRequestSchema, InitializedNotificationSchema, ErrorCode as JSONRPCErrorCode, JSONRPCMessageSchema, ListPromptsRequestSchema, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
6
+ import { render, tokenize } from 'micromustache';
7
+ import { z } from 'zod';
8
+ import { fromZodError } from 'zod-validation-error';
9
+ import { ItemsService } from '../services/index.js';
10
+ import { sanitizeQuery } from '../utils/sanitize-query.js';
11
+ import { Url } from '../utils/url.js';
12
+ import { findMcpTool, getAllMcpTools } from './tools/index.js';
13
+ import { DirectusTransport } from './transport.js';
14
+ export class DirectusMCP {
15
+ promptsCollection;
16
+ systemPrompt;
17
+ systemPromptEnabled;
18
+ server;
19
+ allowDeletes;
20
+ constructor(options = {}) {
21
+ this.promptsCollection = options.promptsCollection ?? null;
22
+ this.systemPromptEnabled = options.systemPromptEnabled ?? true;
23
+ this.systemPrompt = options.systemPrompt ?? null;
24
+ this.allowDeletes = options.allowDeletes ?? false;
25
+ this.server = new Server({
26
+ name: 'directus-mcp',
27
+ version: '0.1.0',
28
+ }, {
29
+ capabilities: {
30
+ tools: {},
31
+ prompts: {},
32
+ },
33
+ });
34
+ }
35
+ /**
36
+ * This handleRequest function is not awaiting lower level logic resulting in the actual
37
+ * response being an asynchronous side effect happening after the function has returned
38
+ */
39
+ handleRequest(req, res) {
40
+ if (!req.accountability?.user && !req.accountability?.role && req.accountability?.admin !== true) {
41
+ throw new ForbiddenError();
42
+ }
43
+ if (!req.accepts('application/json')) {
44
+ // we currently dont support "text/event-stream" requests
45
+ res.status(405).send();
46
+ return;
47
+ }
48
+ this.server.setNotificationHandler(InitializedNotificationSchema, () => {
49
+ res.status(202).send();
50
+ });
51
+ // list prompts
52
+ this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
53
+ const prompts = [];
54
+ if (!this.promptsCollection) {
55
+ throw new McpError(1001, `A prompts collection must be set in settings`);
56
+ }
57
+ const service = new ItemsService(this.promptsCollection, {
58
+ accountability: req.accountability,
59
+ schema: req.schema,
60
+ });
61
+ try {
62
+ const promptList = await service.readByQuery({
63
+ fields: ['name', 'description', 'system_prompt', 'messages'],
64
+ });
65
+ for (const prompt of promptList) {
66
+ // builds args
67
+ const args = [];
68
+ // Add system prompt as the first assistant message if it exists
69
+ if (prompt.system_prompt) {
70
+ for (const varName of tokenize(prompt.system_prompt).varNames) {
71
+ args.push({
72
+ name: varName,
73
+ description: `Value for ${varName}`,
74
+ required: false,
75
+ });
76
+ }
77
+ }
78
+ for (const message of prompt.messages || []) {
79
+ for (const varName of tokenize(message.text).varNames) {
80
+ args.push({
81
+ name: varName,
82
+ description: `Value for ${varName}`,
83
+ required: false,
84
+ });
85
+ }
86
+ }
87
+ prompts.push({
88
+ name: prompt.name,
89
+ description: prompt.description,
90
+ arguments: args,
91
+ });
92
+ }
93
+ return { prompts };
94
+ }
95
+ catch (error) {
96
+ return this.toExecutionError(error);
97
+ }
98
+ });
99
+ // get prompt
100
+ this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
101
+ if (!this.promptsCollection) {
102
+ throw new McpError(1001, `A prompts collection must be set in settings`);
103
+ }
104
+ const service = new ItemsService(this.promptsCollection, {
105
+ accountability: req.accountability,
106
+ schema: req.schema,
107
+ });
108
+ const { name: promptName, arguments: args } = request.params;
109
+ const promptCommand = await service.readByQuery({
110
+ fields: ['description', 'system_prompt', 'messages'],
111
+ filter: {
112
+ name: {
113
+ _eq: promptName,
114
+ },
115
+ },
116
+ });
117
+ const prompt = promptCommand[0];
118
+ if (!prompt) {
119
+ throw new McpError(JSONRPCErrorCode.InvalidParams, `Invalid prompt "${promptName}"`);
120
+ }
121
+ const messages = [];
122
+ // Add system prompt as the first assistant message if it exists
123
+ if (prompt.system_prompt) {
124
+ messages.push({
125
+ role: 'assistant',
126
+ content: {
127
+ type: 'text',
128
+ text: render(prompt.system_prompt, args),
129
+ },
130
+ });
131
+ }
132
+ // render any provided args
133
+ (prompt.messages || []).forEach((message) => {
134
+ // skip invalid prompts
135
+ if (!message.role || !message.text)
136
+ return;
137
+ messages.push({
138
+ role: message.role,
139
+ content: {
140
+ type: 'text',
141
+ text: render(message.text, args),
142
+ },
143
+ });
144
+ });
145
+ return this.toPromptResponse({
146
+ messages,
147
+ description: prompt.description,
148
+ });
149
+ });
150
+ // listing tools
151
+ this.server.setRequestHandler(ListToolsRequestSchema, () => {
152
+ const tools = [];
153
+ for (const tool of getAllMcpTools()) {
154
+ if (req.accountability?.admin !== true && tool.admin === true)
155
+ continue;
156
+ if (tool.name === 'system-prompt' && this.systemPromptEnabled === false)
157
+ continue;
158
+ tools.push({
159
+ name: tool.name,
160
+ description: tool.description,
161
+ inputSchema: z.toJSONSchema(tool.inputSchema),
162
+ annotations: tool.annotations,
163
+ });
164
+ }
165
+ return { tools };
166
+ });
167
+ // calling tools
168
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
169
+ const tool = findMcpTool(request.params.name);
170
+ let sanitizedQuery = {};
171
+ try {
172
+ if (!tool || (tool.name === 'system-prompt' && this.systemPromptEnabled === false)) {
173
+ throw new InvalidPayloadError({ reason: `"${request.params.name}" doesn't exist in the toolset` });
174
+ }
175
+ if (req.accountability?.admin !== true && tool.admin === true) {
176
+ throw new ForbiddenError({ reason: 'You must be an admin to access this tool' });
177
+ }
178
+ if (tool.name === 'system-prompt') {
179
+ request.params.arguments = { promptOverride: this.systemPrompt };
180
+ }
181
+ // ensure json expected fields are not stringified
182
+ if (request.params.arguments) {
183
+ for (const field of ['data', 'keys', 'query']) {
184
+ const arg = request.params.arguments[field];
185
+ if (typeof arg === 'string') {
186
+ request.params.arguments[field] = parseJSON(arg);
187
+ }
188
+ }
189
+ }
190
+ const { error, data: args } = tool.validateSchema?.safeParse(request.params.arguments) ?? {
191
+ data: request.params.arguments,
192
+ };
193
+ if (error) {
194
+ throw new InvalidPayloadError({ reason: fromZodError(error).message });
195
+ }
196
+ if (!isObject(args)) {
197
+ throw new InvalidPayloadError({ reason: '"arguments" must be an object' });
198
+ }
199
+ if ('action' in args && args['action'] === 'delete' && !this.allowDeletes) {
200
+ throw new InvalidPayloadError({ reason: 'Delete actions are disabled' });
201
+ }
202
+ if ('query' in args && args['query']) {
203
+ sanitizedQuery = await sanitizeQuery({
204
+ fields: args['query']['fields'] || '*',
205
+ ...args['query'],
206
+ }, req.schema, req.accountability || null);
207
+ }
208
+ const result = await tool.handler({
209
+ args,
210
+ sanitizedQuery,
211
+ schema: req.schema,
212
+ accountability: req.accountability,
213
+ });
214
+ // if single item and create/read/update/import add url
215
+ const data = toArray(result?.data);
216
+ if ('action' in args &&
217
+ ['create', 'update', 'read', 'import'].includes(args['action']) &&
218
+ result?.data &&
219
+ data.length === 1) {
220
+ result.url = this.buildURL(tool, args, data[0]);
221
+ }
222
+ return this.toToolResponse(result);
223
+ }
224
+ catch (error) {
225
+ return this.toExecutionError(error);
226
+ }
227
+ });
228
+ const transport = new DirectusTransport(res);
229
+ this.server.connect(transport);
230
+ try {
231
+ const parsedMessage = JSONRPCMessageSchema.parse(req.body);
232
+ transport.onmessage?.(parsedMessage);
233
+ }
234
+ catch (error) {
235
+ transport.onerror?.(error);
236
+ throw error;
237
+ }
238
+ }
239
+ buildURL(tool, input, data) {
240
+ const env = useEnv();
241
+ const publicURL = env['PUBLIC_URL'];
242
+ if (!publicURL)
243
+ return;
244
+ if (!tool.endpoint)
245
+ return;
246
+ const path = tool.endpoint({ input, data });
247
+ if (!path)
248
+ return;
249
+ return new Url(env['PUBLIC_URL']).addPath('admin', ...path).toString();
250
+ }
251
+ toPromptResponse(result) {
252
+ const response = {
253
+ messages: result.messages,
254
+ };
255
+ if (result.description) {
256
+ response.description = result.description;
257
+ }
258
+ return response;
259
+ }
260
+ toToolResponse(result) {
261
+ const response = {
262
+ content: [],
263
+ };
264
+ if (!result || typeof result.data === 'undefined' || result.data === null)
265
+ return response;
266
+ if (result.type === 'text') {
267
+ response.content.push({
268
+ type: 'text',
269
+ text: JSON.stringify({ raw: result.data, url: result.url }),
270
+ });
271
+ }
272
+ else {
273
+ response.content.push(result);
274
+ }
275
+ return response;
276
+ }
277
+ toExecutionError(err) {
278
+ const errors = [];
279
+ const receivedErrors = Array.isArray(err) ? err : [err];
280
+ for (const error of receivedErrors) {
281
+ if (isDirectusError(error)) {
282
+ errors.push({
283
+ error: error.message || 'Unknown error',
284
+ code: error.code,
285
+ });
286
+ }
287
+ else {
288
+ // Handle generic errors
289
+ let message = 'An unknown error occurred.';
290
+ let code;
291
+ if (error instanceof Error) {
292
+ message = error.message;
293
+ code = 'code' in error ? String(error.code) : undefined;
294
+ }
295
+ else if (typeof error === 'object' && error !== null) {
296
+ message = 'message' in error ? String(error.message) : message;
297
+ code = 'code' in error ? String(error.code) : undefined;
298
+ }
299
+ else if (typeof error === 'string') {
300
+ message = error;
301
+ }
302
+ errors.push({ error: message, ...(code && { code }) });
303
+ }
304
+ }
305
+ return {
306
+ isError: true,
307
+ content: [{ type: 'text', text: JSON.stringify(errors) }],
308
+ };
309
+ }
310
+ }
@@ -0,0 +1,3 @@
1
+ export declare const assets: import("../types.js").ToolConfig<{
2
+ id: string;
3
+ }>;
@@ -0,0 +1,54 @@
1
+ import { UnsupportedMediaTypeError } from '@directus/errors';
2
+ import { z } from 'zod';
3
+ import { AssetsService } from '../../services/assets.js';
4
+ import { FilesService } from '../../services/files.js';
5
+ import { defineTool } from '../define.js';
6
+ import prompts from './prompts/index.js';
7
+ const AssetsValidateSchema = z.strictObject({
8
+ id: z.string(),
9
+ });
10
+ const AssetsInputSchema = z.object({
11
+ id: z.string(),
12
+ });
13
+ export const assets = defineTool({
14
+ name: 'assets',
15
+ description: prompts.assets,
16
+ annotations: {
17
+ title: 'Directus - Assets',
18
+ },
19
+ inputSchema: AssetsInputSchema,
20
+ validateSchema: AssetsValidateSchema,
21
+ async handler({ args, schema, accountability }) {
22
+ const serviceOptions = {
23
+ accountability,
24
+ schema,
25
+ };
26
+ const filesService = new FilesService(serviceOptions);
27
+ const file = await filesService.readOne(args.id, { limit: 1 });
28
+ if (!file.type || !['image', 'audio'].some((t) => file.type?.startsWith(t))) {
29
+ throw new UnsupportedMediaTypeError({ mediaType: file.type ?? 'unknown', where: 'asset tool' });
30
+ }
31
+ let transformation = undefined;
32
+ // ensure image dimensions are within allowable LLM limits
33
+ if (file.type.startsWith('image') && file.width && file.height && (file.width > 1200 || file.height > 1200)) {
34
+ transformation = {
35
+ transformationParams: {
36
+ transforms: file.width > file.height
37
+ ? [['resize', { width: 800, fit: 'contain' }]]
38
+ : [['resize', { height: 800, fit: 'contain' }]],
39
+ },
40
+ };
41
+ }
42
+ const assetsService = new AssetsService(serviceOptions);
43
+ const asset = await assetsService.getAsset(args.id, transformation);
44
+ const chunks = [];
45
+ for await (const chunk of asset.stream) {
46
+ chunks.push(Buffer.from(chunk));
47
+ }
48
+ return {
49
+ type: file.type.startsWith('image') ? 'image' : 'audio',
50
+ data: Buffer.concat(chunks).toString('base64'),
51
+ mimeType: file.type,
52
+ };
53
+ },
54
+ });