@directus/api 32.1.0 → 32.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/dist/ai/chat/constants/system-prompt.d.ts +1 -0
  2. package/dist/ai/chat/constants/system-prompt.js +51 -0
  3. package/dist/ai/chat/controllers/chat.post.d.ts +2 -0
  4. package/dist/ai/chat/controllers/chat.post.js +47 -0
  5. package/dist/ai/chat/lib/create-ui-stream.d.ts +15 -0
  6. package/dist/ai/chat/lib/create-ui-stream.js +42 -0
  7. package/dist/ai/chat/middleware/load-settings.d.ts +2 -0
  8. package/dist/ai/chat/middleware/load-settings.js +18 -0
  9. package/dist/ai/chat/models/chat-request.d.ts +34 -0
  10. package/dist/ai/chat/models/chat-request.js +26 -0
  11. package/dist/ai/chat/models/providers.d.ts +9 -0
  12. package/dist/ai/chat/models/providers.js +9 -0
  13. package/dist/ai/chat/router.d.ts +1 -0
  14. package/dist/ai/chat/router.js +5 -0
  15. package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.d.ts +9 -0
  16. package/dist/ai/chat/utils/chat-request-tool-to-ai-sdk-tool.js +38 -0
  17. package/dist/ai/chat/utils/fix-error-tool-calls.d.ts +12 -0
  18. package/dist/ai/chat/utils/fix-error-tool-calls.js +30 -0
  19. package/dist/ai/chat/utils/parse-json-schema-7.d.ts +13 -0
  20. package/dist/ai/chat/utils/parse-json-schema-7.js +75 -0
  21. package/dist/{mcp → ai/mcp}/server.d.ts +13 -16
  22. package/dist/{mcp → ai/mcp}/server.js +4 -13
  23. package/dist/ai/mcp/types.d.ts +15 -0
  24. package/dist/{mcp/tools/assets.js → ai/tools/assets/index.js} +8 -5
  25. package/dist/{mcp/tools/collections.js → ai/tools/collections/index.js} +7 -4
  26. package/dist/{mcp/tools/fields.js → ai/tools/fields/index.js} +12 -9
  27. package/dist/{mcp/tools/files.js → ai/tools/files/index.js} +11 -5
  28. package/dist/{mcp/tools/flows.js → ai/tools/flows/index.js} +11 -5
  29. package/dist/{mcp/tools/folders.js → ai/tools/folders/index.js} +12 -5
  30. package/dist/ai/tools/index.d.ts +15 -0
  31. package/dist/ai/tools/index.js +29 -0
  32. package/dist/{mcp/tools/items.js → ai/tools/items/index.js} +13 -6
  33. package/dist/{mcp/tools/prompts/items.md → ai/tools/items/prompt.md} +19 -15
  34. package/dist/{mcp/tools/operations.d.ts → ai/tools/operations/index.d.ts} +46 -0
  35. package/dist/{mcp/tools/operations.js → ai/tools/operations/index.js} +12 -5
  36. package/dist/{mcp/tools/relations.js → ai/tools/relations/index.js} +7 -4
  37. package/dist/{mcp/tools/schema.d.ts → ai/tools/schema/index.d.ts} +1 -1
  38. package/dist/{mcp/tools/schema.js → ai/tools/schema/index.js} +9 -6
  39. package/dist/{mcp/tools/system.js → ai/tools/system/index.js} +7 -4
  40. package/dist/{mcp/tools/trigger-flow.js → ai/tools/trigger-flow/index.js} +8 -5
  41. package/dist/{mcp → ai/tools}/types.d.ts +1 -17
  42. package/dist/ai/tools/utils.d.ts +9 -0
  43. package/dist/ai/tools/utils.js +17 -0
  44. package/dist/app.js +5 -0
  45. package/dist/auth/drivers/oauth2.d.ts +2 -1
  46. package/dist/auth/drivers/oauth2.js +17 -22
  47. package/dist/auth/drivers/openid.d.ts +2 -1
  48. package/dist/auth/drivers/openid.js +13 -18
  49. package/dist/auth/drivers/saml.js +6 -3
  50. package/dist/controllers/assets.js +39 -2
  51. package/dist/controllers/mcp.js +1 -1
  52. package/dist/database/migrations/20240806A-permissions-policies.js +2 -2
  53. package/dist/database/migrations/20251103A-add-ai-settings.d.ts +3 -0
  54. package/dist/database/migrations/20251103A-add-ai-settings.js +14 -0
  55. package/dist/database/run-ast/run-ast.js +1 -1
  56. package/dist/extensions/lib/installation/manager.js +5 -9
  57. package/dist/extensions/lib/sync/status.d.ts +11 -0
  58. package/dist/extensions/lib/sync/status.js +34 -0
  59. package/dist/extensions/lib/sync/sync.d.ts +6 -0
  60. package/dist/extensions/lib/sync/sync.js +90 -0
  61. package/dist/extensions/lib/sync/tracker.d.ts +18 -0
  62. package/dist/extensions/lib/sync/tracker.js +71 -0
  63. package/dist/extensions/lib/sync/utils.d.ts +24 -0
  64. package/dist/extensions/lib/sync/utils.js +62 -0
  65. package/dist/extensions/manager.d.ts +8 -4
  66. package/dist/extensions/manager.js +30 -13
  67. package/dist/middleware/respond.js +2 -2
  68. package/dist/permissions/lib/fetch-policies.d.ts +1 -1
  69. package/dist/permissions/lib/fetch-roles-tree.d.ts +6 -3
  70. package/dist/permissions/lib/fetch-roles-tree.js +5 -27
  71. package/dist/permissions/modules/fetch-global-access/fetch-global-access.d.ts +9 -7
  72. package/dist/permissions/modules/fetch-global-access/fetch-global-access.js +17 -9
  73. package/dist/permissions/modules/fetch-policies-ip-access/fetch-policies-ip-access.d.ts +1 -1
  74. package/dist/permissions/utils/fetch-raw-permissions.d.ts +1 -1
  75. package/dist/permissions/utils/fetch-share-info.d.ts +1 -1
  76. package/dist/permissions/utils/fetch-share-info.js +1 -1
  77. package/dist/permissions/utils/filter-policies-by-ip.js +1 -1
  78. package/dist/permissions/utils/get-permissions-for-share.js +8 -8
  79. package/dist/permissions/utils/with-cache.d.ts +8 -6
  80. package/dist/permissions/utils/with-cache.js +12 -10
  81. package/dist/request/is-denied-ip.js +2 -2
  82. package/dist/services/assets/name-deduper.d.ts +7 -0
  83. package/dist/services/assets/name-deduper.js +23 -0
  84. package/dist/services/assets.d.ts +15 -2
  85. package/dist/services/assets.js +98 -5
  86. package/dist/services/authentication.js +4 -4
  87. package/dist/services/comments.js +2 -2
  88. package/dist/services/extensions.js +4 -0
  89. package/dist/services/folders.d.ts +27 -2
  90. package/dist/services/folders.js +75 -0
  91. package/dist/services/graphql/resolvers/query.js +1 -1
  92. package/dist/services/import-export.d.ts +1 -1
  93. package/dist/services/import-export.js +4 -5
  94. package/dist/services/notifications.js +2 -2
  95. package/dist/services/payload.js +20 -0
  96. package/dist/services/roles.js +2 -2
  97. package/dist/services/tus/server.js +3 -3
  98. package/dist/telemetry/utils/get-settings.d.ts +15 -0
  99. package/dist/telemetry/utils/get-settings.js +25 -9
  100. package/dist/test-utils/README.md +95 -24
  101. package/dist/test-utils/cache.d.ts +2 -2
  102. package/dist/test-utils/cache.js +2 -2
  103. package/dist/test-utils/{fields-service.d.ts → services/fields-service.d.ts} +1 -1
  104. package/dist/test-utils/{fields-service.js → services/fields-service.js} +3 -2
  105. package/dist/test-utils/services/files-service.d.ts +28 -0
  106. package/dist/test-utils/services/files-service.js +34 -0
  107. package/dist/test-utils/services/folders-service.d.ts +28 -0
  108. package/dist/test-utils/services/folders-service.js +33 -0
  109. package/dist/utils/encrypt.d.ts +2 -0
  110. package/dist/utils/encrypt.js +64 -0
  111. package/dist/utils/get-accountability-for-role.js +2 -2
  112. package/dist/utils/get-accountability-for-token.js +4 -4
  113. package/dist/utils/get-cache-key.js +2 -2
  114. package/dist/utils/is-login-redirect-allowed.d.ts +4 -0
  115. package/dist/{auth/utils → utils}/is-login-redirect-allowed.js +8 -16
  116. package/dist/utils/require-text.d.ts +1 -0
  117. package/dist/utils/require-text.js +4 -0
  118. package/dist/utils/require-yaml.js +2 -2
  119. package/package.json +31 -25
  120. package/dist/auth/utils/generate-callback-url.d.ts +0 -8
  121. package/dist/auth/utils/generate-callback-url.js +0 -11
  122. package/dist/auth/utils/is-login-redirect-allowed.d.ts +0 -8
  123. package/dist/extensions/lib/sync-extensions.d.ts +0 -3
  124. package/dist/extensions/lib/sync-extensions.js +0 -70
  125. package/dist/extensions/lib/sync-status.d.ts +0 -10
  126. package/dist/extensions/lib/sync-status.js +0 -27
  127. package/dist/mcp/tools/index.d.ts +0 -15
  128. package/dist/mcp/tools/index.js +0 -29
  129. package/dist/mcp/tools/prompts/index.d.ts +0 -16
  130. package/dist/mcp/tools/prompts/index.js +0 -19
  131. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.d.ts +0 -5
  132. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.js +0 -7
  133. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.d.ts +0 -5
  134. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.js +0 -10
  135. package/dist/permissions/modules/fetch-global-access/types.d.ts +0 -4
  136. package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.d.ts +0 -4
  137. package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.js +0 -27
  138. package/dist/utils/get-date-formatted.d.ts +0 -1
  139. package/dist/utils/get-date-formatted.js +0 -10
  140. package/dist/utils/ip-in-networks.d.ts +0 -6
  141. package/dist/utils/ip-in-networks.js +0 -13
  142. /package/dist/{mcp → ai/mcp}/index.d.ts +0 -0
  143. /package/dist/{mcp → ai/mcp}/index.js +0 -0
  144. /package/dist/{mcp → ai/mcp}/transport.d.ts +0 -0
  145. /package/dist/{mcp → ai/mcp}/transport.js +0 -0
  146. /package/dist/{mcp → ai/mcp}/types.js +0 -0
  147. /package/dist/{mcp/tools/assets.d.ts → ai/tools/assets/index.d.ts} +0 -0
  148. /package/dist/{mcp/tools/prompts/assets.md → ai/tools/assets/prompt.md} +0 -0
  149. /package/dist/{mcp/tools/collections.d.ts → ai/tools/collections/index.d.ts} +0 -0
  150. /package/dist/{mcp/tools/prompts/collections.md → ai/tools/collections/prompt.md} +0 -0
  151. /package/dist/{mcp/define.d.ts → ai/tools/define-tool.d.ts} +0 -0
  152. /package/dist/{mcp/define.js → ai/tools/define-tool.js} +0 -0
  153. /package/dist/{mcp/tools/fields.d.ts → ai/tools/fields/index.d.ts} +0 -0
  154. /package/dist/{mcp/tools/prompts/fields.md → ai/tools/fields/prompt.md} +0 -0
  155. /package/dist/{mcp/tools/files.d.ts → ai/tools/files/index.d.ts} +0 -0
  156. /package/dist/{mcp/tools/prompts/files.md → ai/tools/files/prompt.md} +0 -0
  157. /package/dist/{mcp/tools/flows.d.ts → ai/tools/flows/index.d.ts} +0 -0
  158. /package/dist/{mcp/tools/prompts/flows.md → ai/tools/flows/prompt.md} +0 -0
  159. /package/dist/{mcp/tools/folders.d.ts → ai/tools/folders/index.d.ts} +0 -0
  160. /package/dist/{mcp/tools/prompts/folders.md → ai/tools/folders/prompt.md} +0 -0
  161. /package/dist/{mcp/tools/items.d.ts → ai/tools/items/index.d.ts} +0 -0
  162. /package/dist/{mcp/tools/prompts/operations.md → ai/tools/operations/prompt.md} +0 -0
  163. /package/dist/{mcp/tools/relations.d.ts → ai/tools/relations/index.d.ts} +0 -0
  164. /package/dist/{mcp/tools/prompts/relations.md → ai/tools/relations/prompt.md} +0 -0
  165. /package/dist/{mcp/tools/prompts/schema.md → ai/tools/schema/prompt.md} +0 -0
  166. /package/dist/{mcp → ai/tools}/schema.d.ts +0 -0
  167. /package/dist/{mcp → ai/tools}/schema.js +0 -0
  168. /package/dist/{mcp/tools/system.d.ts → ai/tools/system/index.d.ts} +0 -0
  169. /package/dist/{mcp/tools/prompts/system-prompt-description.md → ai/tools/system/prompt-description.md} +0 -0
  170. /package/dist/{mcp/tools/prompts/system-prompt.md → ai/tools/system/prompt.md} +0 -0
  171. /package/dist/{mcp/tools/trigger-flow.d.ts → ai/tools/trigger-flow/index.d.ts} +0 -0
  172. /package/dist/{mcp/tools/prompts/trigger-flow.md → ai/tools/trigger-flow/prompt.md} +0 -0
  173. /package/dist/{permissions/modules/fetch-global-access → ai/tools}/types.js +0 -0
  174. /package/dist/test-utils/{items-service.d.ts → services/items-service.d.ts} +0 -0
  175. /package/dist/test-utils/{items-service.js → services/items-service.js} +0 -0
@@ -0,0 +1 @@
1
+ export declare const SYSTEM_PROMPT = "\n<behavior_instructions>\nYou are **Directus Assistant**, a Directus CMS expert with access to a Directus instance through specialized tools\n\n## Communication Style\n\n- **Be concise**: Users prefer short, direct responses. One-line confirmations: \"Created collection 'products'\"\n- **Match the audience**: Technical for developers, plain language for content editors\n- **NEVER guess**: If not at least 99% about field values or user intent, ask for clarification\n\n## Tool Usage Patterns\n\n### Discovery First\n\n1. Understand the user's task and what they need to achieve.\n2. Discover schema if needed for task - **schema()** with no params \u2192 lightweight collection list or **schema({ keys: [\"products\", \"categories\"] })** \u2192 full field/relation details\n3. Use other tools as needed to achieve the user's task.\n\n### Content Items\n\n- Use `fields` whenever possible to fetch only the exact fields you need\n- Use `filter` and `limit` to control the number of fetched items unless you absolutely need larger datasets\n- When presenting repeated structured data with 4+ items, use markdown tables for better readability\n\n### Schema & Data Changes\n\n- **Confirm before modifying any schema**: Collections, fields, relations always need approval from the user.\n- **Check namespace conflicts**: Collection folders and regular collections share namespace. Collection folders are\n distinct from file folders.\n\n### Safety Rules\n\n- **Deletions require confirmation**: ALWAYS ask before deleting anything\n- **Warn on bulk operations**: Alert when affecting many items (\"This updates 500 items\")\n- **Avoid duplicates**: Never create duplicates if you can't modify existing items\n- **Use semantic HTML**: No classes, IDs, or inline styles in content fields (unless explicitly asked for by the user)\n- **Permission errors**: Report immediately, don't retry\n\n### Behavior Rules\n\n- Call tools immediately without explanatory text\n- Use parallel tool calls when possible\n- If you don't have access to a certain tool, ask the user to grant you access to the tool from the chat settings.\n- If there are unused tools in context but task is simple, suggest disabling unused tools (once per conversation)\n\n## Error Handling\n\n- Auto-retry once for clear errors (\"field X required\")\n- Stop after 2 failures, consult user\n- If tool unavailable, suggest enabling in chat settings\n</behavior_instructions>";
@@ -0,0 +1,51 @@
1
+ export const SYSTEM_PROMPT = `
2
+ <behavior_instructions>
3
+ You are **Directus Assistant**, a Directus CMS expert with access to a Directus instance through specialized tools
4
+
5
+ ## Communication Style
6
+
7
+ - **Be concise**: Users prefer short, direct responses. One-line confirmations: "Created collection 'products'"
8
+ - **Match the audience**: Technical for developers, plain language for content editors
9
+ - **NEVER guess**: If not at least 99% about field values or user intent, ask for clarification
10
+
11
+ ## Tool Usage Patterns
12
+
13
+ ### Discovery First
14
+
15
+ 1. Understand the user's task and what they need to achieve.
16
+ 2. Discover schema if needed for task - **schema()** with no params → lightweight collection list or **schema({ keys: ["products", "categories"] })** → full field/relation details
17
+ 3. Use other tools as needed to achieve the user's task.
18
+
19
+ ### Content Items
20
+
21
+ - Use \`fields\` whenever possible to fetch only the exact fields you need
22
+ - Use \`filter\` and \`limit\` to control the number of fetched items unless you absolutely need larger datasets
23
+ - When presenting repeated structured data with 4+ items, use markdown tables for better readability
24
+
25
+ ### Schema & Data Changes
26
+
27
+ - **Confirm before modifying any schema**: Collections, fields, relations always need approval from the user.
28
+ - **Check namespace conflicts**: Collection folders and regular collections share namespace. Collection folders are
29
+ distinct from file folders.
30
+
31
+ ### Safety Rules
32
+
33
+ - **Deletions require confirmation**: ALWAYS ask before deleting anything
34
+ - **Warn on bulk operations**: Alert when affecting many items ("This updates 500 items")
35
+ - **Avoid duplicates**: Never create duplicates if you can't modify existing items
36
+ - **Use semantic HTML**: No classes, IDs, or inline styles in content fields (unless explicitly asked for by the user)
37
+ - **Permission errors**: Report immediately, don't retry
38
+
39
+ ### Behavior Rules
40
+
41
+ - Call tools immediately without explanatory text
42
+ - Use parallel tool calls when possible
43
+ - If you don't have access to a certain tool, ask the user to grant you access to the tool from the chat settings.
44
+ - If there are unused tools in context but task is simple, suggest disabling unused tools (once per conversation)
45
+
46
+ ## Error Handling
47
+
48
+ - Auto-retry once for clear errors ("field X required")
49
+ - Stop after 2 failures, consult user
50
+ - If tool unavailable, suggest enabling in chat settings
51
+ </behavior_instructions>`;
@@ -0,0 +1,2 @@
1
+ import type { RequestHandler } from 'express';
2
+ export declare const aiChatPostHandler: RequestHandler;
@@ -0,0 +1,47 @@
1
+ import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
2
+ import { safeValidateUIMessages } from 'ai';
3
+ import { fromZodError } from 'zod-validation-error';
4
+ import { createUiStream } from '../lib/create-ui-stream.js';
5
+ import { ChatRequest } from '../models/chat-request.js';
6
+ import { chatRequestToolToAiSdkTool } from '../utils/chat-request-tool-to-ai-sdk-tool.js';
7
+ import { fixErrorToolCalls } from '../utils/fix-error-tool-calls.js';
8
+ export const aiChatPostHandler = async (req, res, _next) => {
9
+ if (!req.accountability?.app) {
10
+ throw new ForbiddenError();
11
+ }
12
+ const parseResult = ChatRequest.safeParse(req.body);
13
+ if (!parseResult.success) {
14
+ throw new InvalidPayloadError({ reason: fromZodError(parseResult.error).message });
15
+ }
16
+ const { provider, model, messages: rawMessages, tools: requestedTools, toolApprovals } = parseResult.data;
17
+ if (rawMessages.length === 0) {
18
+ throw new InvalidPayloadError({ reason: `"messages" must not be empty` });
19
+ }
20
+ const tools = requestedTools.reduce((acc, t) => {
21
+ const name = typeof t === 'string' ? t : t.name;
22
+ const aiTool = chatRequestToolToAiSdkTool({
23
+ chatRequestTool: t,
24
+ accountability: req.accountability,
25
+ schema: req.schema,
26
+ ...(toolApprovals && { toolApprovals }),
27
+ });
28
+ acc[name] = aiTool;
29
+ return acc;
30
+ }, {});
31
+ const fixedMessages = fixErrorToolCalls(rawMessages);
32
+ const validationResult = await safeValidateUIMessages({ messages: fixedMessages });
33
+ if (validationResult.success === false) {
34
+ throw new InvalidPayloadError({ reason: validationResult.error.message });
35
+ }
36
+ const stream = createUiStream(validationResult.data, {
37
+ provider,
38
+ model,
39
+ tools: tools,
40
+ apiKeys: res.locals['ai'].apiKeys,
41
+ systemPrompt: res.locals['ai'].systemPrompt,
42
+ onUsage: (usage) => {
43
+ res.write(`data: ${JSON.stringify({ type: 'data-usage', data: usage })}\n\n`);
44
+ },
45
+ });
46
+ stream.pipeUIMessageStreamToResponse(res);
47
+ };
@@ -0,0 +1,15 @@
1
+ import { type LanguageModelUsage, type Tool, type UIMessage, type StreamTextResult } from 'ai';
2
+ export interface CreateUiStreamOptions {
3
+ provider: 'openai' | 'anthropic';
4
+ model: string;
5
+ tools: {
6
+ [x: string]: Tool;
7
+ };
8
+ apiKeys: {
9
+ openai: string | null;
10
+ anthropic: string | null;
11
+ };
12
+ systemPrompt?: string;
13
+ onUsage?: (usage: Pick<LanguageModelUsage, 'inputTokens' | 'outputTokens' | 'totalTokens'>) => void | Promise<void>;
14
+ }
15
+ export declare const createUiStream: (messages: UIMessage[], { provider, model, tools, apiKeys, systemPrompt, onUsage }: CreateUiStreamOptions) => StreamTextResult<Record<string, Tool<any, any>>, any>;
@@ -0,0 +1,42 @@
1
+ import { createAnthropic } from '@ai-sdk/anthropic';
2
+ import { createOpenAI } from '@ai-sdk/openai';
3
+ import { ServiceUnavailableError } from '@directus/errors';
4
+ import { convertToModelMessages, stepCountIs, streamText, } from 'ai';
5
+ import { SYSTEM_PROMPT } from '../constants/system-prompt.js';
6
+ export const createUiStream = (messages, { provider, model, tools, apiKeys, systemPrompt, onUsage }) => {
7
+ if (apiKeys[provider] === null) {
8
+ throw new ServiceUnavailableError({ service: provider, reason: 'No API key configured for LLM provider' });
9
+ }
10
+ let modelProvider;
11
+ if (provider === 'openai') {
12
+ modelProvider = createOpenAI({ apiKey: apiKeys.openai });
13
+ }
14
+ else if (provider === 'anthropic') {
15
+ modelProvider = createAnthropic({ apiKey: apiKeys.anthropic });
16
+ }
17
+ else {
18
+ throw new Error(`Unexpected provider given: "${provider}"`);
19
+ }
20
+ systemPrompt ||= SYSTEM_PROMPT;
21
+ const stream = streamText({
22
+ system: systemPrompt,
23
+ model: modelProvider(model),
24
+ messages: convertToModelMessages(messages),
25
+ stopWhen: [stepCountIs(10)],
26
+ providerOptions: {
27
+ openai: {
28
+ reasoningSummary: 'auto',
29
+ store: false,
30
+ include: ['reasoning.encrypted_content'],
31
+ },
32
+ },
33
+ tools,
34
+ onFinish({ usage }) {
35
+ if (onUsage) {
36
+ const { inputTokens, outputTokens, totalTokens } = usage;
37
+ onUsage({ inputTokens, outputTokens, totalTokens });
38
+ }
39
+ },
40
+ });
41
+ return stream;
42
+ };
@@ -0,0 +1,2 @@
1
+ import type { RequestHandler } from 'express';
2
+ export declare const loadSettings: RequestHandler;
@@ -0,0 +1,18 @@
1
+ import { SettingsService } from '../../../services/settings.js';
2
+ import { getSchema } from '../../../utils/get-schema.js';
3
+ export const loadSettings = async (_req, res, next) => {
4
+ const service = new SettingsService({
5
+ schema: await getSchema(),
6
+ });
7
+ const { ai_openai_api_key, ai_anthropic_api_key, ai_system_prompt } = await service.readSingleton({
8
+ fields: ['ai_openai_api_key', 'ai_anthropic_api_key', 'ai_system_prompt'],
9
+ });
10
+ res.locals['ai'] = {
11
+ apiKeys: {
12
+ openai: ai_openai_api_key,
13
+ anthropic: ai_anthropic_api_key,
14
+ },
15
+ systemPrompt: ai_system_prompt,
16
+ };
17
+ return next();
18
+ };
@@ -0,0 +1,34 @@
1
+ import { type JSONSchema7 } from 'ai';
2
+ import { z } from 'zod';
3
+ export declare const ChatRequestTool: z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
4
+ name: z.ZodString;
5
+ description: z.ZodString;
6
+ inputSchema: z.ZodCustom<JSONSchema7, JSONSchema7>;
7
+ }, z.core.$strip>]>;
8
+ export type ChatRequestTool = z.infer<typeof ChatRequestTool>;
9
+ export declare const ToolApprovalMode: z.ZodEnum<{
10
+ always: "always";
11
+ ask: "ask";
12
+ disabled: "disabled";
13
+ }>;
14
+ export type ToolApprovalMode = z.infer<typeof ToolApprovalMode>;
15
+ export declare const ChatRequest: z.ZodIntersection<z.ZodDiscriminatedUnion<[z.ZodObject<{
16
+ provider: z.ZodLiteral<"openai">;
17
+ model: z.ZodUnion<readonly [z.ZodLiteral<"gpt-5">, z.ZodLiteral<"gpt-5-nano">, z.ZodLiteral<"gpt-5-mini">, z.ZodLiteral<"gpt-5-pro">]>;
18
+ }, z.core.$strip>, z.ZodObject<{
19
+ provider: z.ZodLiteral<"anthropic">;
20
+ model: z.ZodUnion<readonly [z.ZodLiteral<"claude-sonnet-4-5">, z.ZodLiteral<"claude-haiku-4-5">, z.ZodLiteral<"claude-opus-4-1">]>;
21
+ }, z.core.$strip>], "provider">, z.ZodObject<{
22
+ tools: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
23
+ name: z.ZodString;
24
+ description: z.ZodString;
25
+ inputSchema: z.ZodCustom<JSONSchema7, JSONSchema7>;
26
+ }, z.core.$strip>]>>;
27
+ messages: z.ZodArray<z.ZodObject<{}, z.core.$loose>>;
28
+ toolApprovals: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
29
+ always: "always";
30
+ ask: "ask";
31
+ disabled: "disabled";
32
+ }>>>;
33
+ }, z.core.$strip>>;
34
+ export type ChatRequest = z.infer<typeof ChatRequest>;
@@ -0,0 +1,26 @@
1
+ import {} from 'ai';
2
+ import { z } from 'zod';
3
+ import { parseJsonSchema7 } from '../utils/parse-json-schema-7.js';
4
+ import { ProviderAnthropic, ProviderOpenAi } from './providers.js';
5
+ export const ChatRequestTool = z.union([
6
+ z.string(),
7
+ z.object({
8
+ name: z.string(),
9
+ description: z.string(),
10
+ inputSchema: z.custom((schema) => {
11
+ try {
12
+ parseJsonSchema7(schema);
13
+ return true;
14
+ }
15
+ catch {
16
+ return false;
17
+ }
18
+ }, { message: 'Invalid JSON schema' }),
19
+ }),
20
+ ]);
21
+ export const ToolApprovalMode = z.enum(['always', 'ask', 'disabled']);
22
+ export const ChatRequest = z.intersection(z.discriminatedUnion('provider', [ProviderOpenAi, ProviderAnthropic]), z.object({
23
+ tools: z.array(ChatRequestTool),
24
+ messages: z.array(z.looseObject({})),
25
+ toolApprovals: z.record(z.string(), ToolApprovalMode).optional(),
26
+ }));
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod';
2
+ export declare const ProviderOpenAi: z.ZodObject<{
3
+ provider: z.ZodLiteral<"openai">;
4
+ model: z.ZodUnion<readonly [z.ZodLiteral<"gpt-5">, z.ZodLiteral<"gpt-5-nano">, z.ZodLiteral<"gpt-5-mini">, z.ZodLiteral<"gpt-5-pro">]>;
5
+ }, z.core.$strip>;
6
+ export declare const ProviderAnthropic: z.ZodObject<{
7
+ provider: z.ZodLiteral<"anthropic">;
8
+ model: z.ZodUnion<readonly [z.ZodLiteral<"claude-sonnet-4-5">, z.ZodLiteral<"claude-haiku-4-5">, z.ZodLiteral<"claude-opus-4-1">]>;
9
+ }, z.core.$strip>;
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod';
2
+ export const ProviderOpenAi = z.object({
3
+ provider: z.literal('openai'),
4
+ model: z.union([z.literal('gpt-5'), z.literal('gpt-5-nano'), z.literal('gpt-5-mini'), z.literal('gpt-5-pro')]),
5
+ });
6
+ export const ProviderAnthropic = z.object({
7
+ provider: z.literal('anthropic'),
8
+ model: z.union([z.literal('claude-sonnet-4-5'), z.literal('claude-haiku-4-5'), z.literal('claude-opus-4-1')]),
9
+ });
@@ -0,0 +1 @@
1
+ export declare const aiChatRouter: import("express-serve-static-core").Router;
@@ -0,0 +1,5 @@
1
+ import { Router } from 'express';
2
+ import asyncHandler from '../../utils/async-handler.js';
3
+ import { aiChatPostHandler } from './controllers/chat.post.js';
4
+ import { loadSettings } from './middleware/load-settings.js';
5
+ export const aiChatRouter = Router().post('/', asyncHandler(loadSettings), asyncHandler(aiChatPostHandler));
@@ -0,0 +1,9 @@
1
+ import { type Accountability, type SchemaOverview } from '@directus/types';
2
+ import type { Tool } from 'ai';
3
+ import type { ChatRequestTool, ToolApprovalMode } from '../models/chat-request.js';
4
+ export declare const chatRequestToolToAiSdkTool: ({ chatRequestTool, accountability, schema, toolApprovals, }: {
5
+ chatRequestTool: ChatRequestTool;
6
+ accountability: Accountability;
7
+ schema: SchemaOverview;
8
+ toolApprovals?: Record<string, ToolApprovalMode>;
9
+ }) => Tool;
@@ -0,0 +1,38 @@
1
+ import { InvalidPayloadError } from '@directus/errors';
2
+ import {} from '@directus/types';
3
+ import { jsonSchema, tool } from 'ai';
4
+ import { fromZodError } from 'zod-validation-error';
5
+ import { ALL_TOOLS } from '../../tools/index.js';
6
+ export const chatRequestToolToAiSdkTool = ({ chatRequestTool, accountability, schema, toolApprovals, }) => {
7
+ if (typeof chatRequestTool === 'string') {
8
+ const directusTool = ALL_TOOLS.find(({ name }) => name === chatRequestTool);
9
+ if (!directusTool) {
10
+ throw new InvalidPayloadError({ reason: `Tool by name "${chatRequestTool}" does not exist` });
11
+ }
12
+ // Determine if tool needs approval based on client settings
13
+ // Default to 'ask' (needs approval) if not specified
14
+ const approvalMode = toolApprovals?.[chatRequestTool] ?? 'ask';
15
+ const needsApproval = approvalMode !== 'always';
16
+ return tool({
17
+ name: directusTool.name,
18
+ description: directusTool.description,
19
+ inputSchema: directusTool.inputSchema,
20
+ needsApproval,
21
+ execute: async (rawArgs) => {
22
+ const { error, data: args } = directusTool.validateSchema?.safeParse(rawArgs) ?? {
23
+ data: rawArgs,
24
+ };
25
+ if (error) {
26
+ throw new InvalidPayloadError({ reason: fromZodError(error).message });
27
+ }
28
+ return directusTool.handler({ args, accountability, schema });
29
+ },
30
+ });
31
+ }
32
+ // Local/client-side tool (schema only, executed on client)
33
+ return tool({
34
+ name: chatRequestTool.name,
35
+ description: chatRequestTool.description,
36
+ inputSchema: jsonSchema(chatRequestTool.inputSchema),
37
+ });
38
+ };
@@ -0,0 +1,12 @@
1
+ import type { UIMessage } from 'ai';
2
+ /**
3
+ * Fixes tool calls with error states by copying rawInput to input field.
4
+ * This is required because error tool calls from the frontend use rawInput
5
+ * but the AI SDK's convertToModelMessages expects input to generate arguments
6
+ * for the OpenAI API.
7
+ * @param messages - Array of message objects from the chat request
8
+ * @returns Messages with error tool calls fixed, typed as UIMessage[]
9
+ */
10
+ export declare const fixErrorToolCalls: (messages: {
11
+ [x: string]: unknown;
12
+ }[]) => UIMessage[];
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Fixes tool calls with error states by copying rawInput to input field.
3
+ * This is required because error tool calls from the frontend use rawInput
4
+ * but the AI SDK's convertToModelMessages expects input to generate arguments
5
+ * for the OpenAI API.
6
+ * @param messages - Array of message objects from the chat request
7
+ * @returns Messages with error tool calls fixed, typed as UIMessage[]
8
+ */
9
+ export const fixErrorToolCalls = (messages) => {
10
+ return messages.map((msg) => {
11
+ if (msg['role'] === 'assistant' && msg['parts'] && Array.isArray(msg['parts'])) {
12
+ const fixedParts = msg['parts'].map((part) => {
13
+ if (typeof part === 'object' &&
14
+ part !== null &&
15
+ 'type' in part &&
16
+ typeof part.type === 'string' &&
17
+ part.type.startsWith('tool-') &&
18
+ 'state' in part &&
19
+ part.state === 'output-error' &&
20
+ 'rawInput' in part &&
21
+ (!('input' in part) || part.input == null)) {
22
+ return { ...part, input: part.rawInput };
23
+ }
24
+ return part;
25
+ });
26
+ return { ...msg, parts: fixedParts };
27
+ }
28
+ return msg;
29
+ });
30
+ };
@@ -0,0 +1,13 @@
1
+ import type { JSONSchema7 } from 'ai';
2
+ /**
3
+ * Checks whether an input object is *likely* a JSON Schema 7 object
4
+ * Limitations:
5
+ * - Does NOT validate nested schemas (e.g., inside `properties`, `items`, `definitions`, etc.)
6
+ * - Does NOT check for correct usage of combinators (`anyOf`, `allOf`, `oneOf`, `not`)
7
+ * - Does NOT verify required/optional keyword relationships (e.g., `required` only valid for object type)
8
+ * - Does NOT validate formats, patterns, or constraints (e.g., `pattern`, `minimum`, `maximum`, etc.)
9
+ * - Accepts both `definitions` and `$defs` for tolerance, but does not enforce draft-07 strictness
10
+ * - Only checks for presence of top-level schema keywords
11
+ * This is not a perfect validator, and does not check nested properties or full JSON Schema 7 compliance
12
+ */
13
+ export declare const parseJsonSchema7: (schema: unknown) => JSONSchema7;
@@ -0,0 +1,75 @@
1
+ import { z } from 'zod';
2
+ import { fromZodError } from 'zod-validation-error';
3
+ const jsonType = z.enum(['null', 'boolean', 'object', 'array', 'number', 'integer', 'string']);
4
+ const maybeDraft7Url = z.url({ protocol: /^https?$/, hostname: /^json-schema\.org$/ }).refine((val) => {
5
+ try {
6
+ const u = new URL(val);
7
+ return u.pathname === '/draft-07/schema' && (u.hash === '' || u.hash === '#');
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }, { message: 'Must be the JSON Schema Draft-07 meta-schema URL' });
13
+ const JsonSchema7 = z
14
+ .object({
15
+ $schema: maybeDraft7Url.optional(),
16
+ $id: z.string().optional(),
17
+ $ref: z.string().optional(),
18
+ title: z.string().optional(),
19
+ description: z.string().optional(),
20
+ // common keywords
21
+ type: z.union([jsonType, z.array(jsonType).nonempty()]).optional(),
22
+ properties: z.record(z.string(), z.any()).optional(),
23
+ required: z.array(z.string()).optional(),
24
+ items: z.union([z.any(), z.array(z.any()).nonempty()]).optional(),
25
+ additionalProperties: z.union([z.boolean(), z.any()]).optional(),
26
+ patternProperties: z.record(z.string(), z.any()).optional(),
27
+ enum: z.array(z.any()).optional(),
28
+ const: z.any().optional(),
29
+ anyOf: z.array(z.any()).optional(),
30
+ allOf: z.array(z.any()).optional(),
31
+ oneOf: z.array(z.any()).optional(),
32
+ not: z.any().optional(),
33
+ // draft-07 naming; accept both for tolerance
34
+ definitions: z.record(z.string(), z.any()).optional(),
35
+ $defs: z.record(z.string(), z.any()).optional(), // not draft-07, but appears in newer drafts
36
+ })
37
+ .refine((obj) => {
38
+ const keys = new Set(Object.keys(obj));
39
+ const schemaish = [
40
+ 'type',
41
+ 'properties',
42
+ 'items',
43
+ 'required',
44
+ 'enum',
45
+ 'const',
46
+ 'anyOf',
47
+ 'allOf',
48
+ 'oneOf',
49
+ 'not',
50
+ '$ref',
51
+ 'additionalProperties',
52
+ 'patternProperties',
53
+ 'definitions',
54
+ '$defs',
55
+ ];
56
+ return schemaish.some((k) => keys.has(k));
57
+ }, { message: 'No schema keywords found' });
58
+ /**
59
+ * Checks whether an input object is *likely* a JSON Schema 7 object
60
+ * Limitations:
61
+ * - Does NOT validate nested schemas (e.g., inside `properties`, `items`, `definitions`, etc.)
62
+ * - Does NOT check for correct usage of combinators (`anyOf`, `allOf`, `oneOf`, `not`)
63
+ * - Does NOT verify required/optional keyword relationships (e.g., `required` only valid for object type)
64
+ * - Does NOT validate formats, patterns, or constraints (e.g., `pattern`, `minimum`, `maximum`, etc.)
65
+ * - Accepts both `definitions` and `$defs` for tolerance, but does not enforce draft-07 strictness
66
+ * - Only checks for presence of top-level schema keywords
67
+ * This is not a perfect validator, and does not check nested properties or full JSON Schema 7 compliance
68
+ */
69
+ export const parseJsonSchema7 = (schema) => {
70
+ const { success, error, data } = JsonSchema7.safeParse(schema);
71
+ if (!success) {
72
+ throw new Error(`Invalid JSON Schema passed: ${fromZodError(error).message}`);
73
+ }
74
+ return data;
75
+ };
@@ -1,7 +1,8 @@
1
1
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { type GetPromptResult } from '@modelcontextprotocol/sdk/types.js';
3
3
  import type { Request, Response } from 'express';
4
- import type { MCPOptions, ToolConfig, ToolResult } from './types.js';
4
+ import type { ToolConfig, ToolResult } from '../tools/types.js';
5
+ import type { MCPOptions } from './types.js';
5
6
  export declare class DirectusMCP {
6
7
  promptsCollection?: string | null;
7
8
  systemPrompt?: string | null;
@@ -22,60 +23,52 @@ export declare class DirectusMCP {
22
23
  toToolResponse(result?: ToolResult): {
23
24
  [x: string]: unknown;
24
25
  content: ({
25
- [x: string]: unknown;
26
26
  type: "text";
27
27
  text: string;
28
28
  _meta?: {
29
29
  [x: string]: unknown;
30
30
  } | undefined;
31
31
  } | {
32
- [x: string]: unknown;
33
- data: string;
34
32
  type: "image";
33
+ data: string;
35
34
  mimeType: string;
36
35
  _meta?: {
37
36
  [x: string]: unknown;
38
37
  } | undefined;
39
38
  } | {
40
- [x: string]: unknown;
41
- data: string;
42
39
  type: "audio";
40
+ data: string;
43
41
  mimeType: string;
44
42
  _meta?: {
45
43
  [x: string]: unknown;
46
44
  } | undefined;
47
45
  } | {
48
- [x: string]: unknown;
49
- type: "resource_link";
50
- name: string;
51
46
  uri: string;
47
+ name: string;
48
+ type: "resource_link";
52
49
  description?: string | undefined;
53
- title?: string | undefined;
54
50
  mimeType?: string | undefined;
55
51
  _meta?: {
56
52
  [x: string]: unknown;
57
53
  } | undefined;
58
54
  icons?: {
59
- [x: string]: unknown;
60
55
  src: string;
61
56
  mimeType?: string | undefined;
62
57
  sizes?: string[] | undefined;
63
58
  }[] | undefined;
59
+ title?: string | undefined;
64
60
  } | {
65
- [x: string]: unknown;
66
61
  type: "resource";
67
62
  resource: {
68
- [x: string]: unknown;
69
- text: string;
70
63
  uri: string;
64
+ text: string;
71
65
  mimeType?: string | undefined;
72
66
  _meta?: {
73
67
  [x: string]: unknown;
74
68
  } | undefined;
75
69
  } | {
76
- [x: string]: unknown;
77
- blob: string;
78
70
  uri: string;
71
+ blob: string;
79
72
  mimeType?: string | undefined;
80
73
  _meta?: {
81
74
  [x: string]: unknown;
@@ -87,6 +80,10 @@ export declare class DirectusMCP {
87
80
  })[];
88
81
  _meta?: {
89
82
  [x: string]: unknown;
83
+ "io.modelcontextprotocol/related-task"?: {
84
+ [x: string]: unknown;
85
+ taskId: string;
86
+ } | undefined;
90
87
  } | undefined;
91
88
  structuredContent?: {
92
89
  [x: string]: unknown;
@@ -6,10 +6,9 @@ import { CallToolRequestSchema, GetPromptRequestSchema, InitializedNotificationS
6
6
  import { render, tokenize } from 'micromustache';
7
7
  import { z } from 'zod';
8
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';
9
+ import { ItemsService } from '../../services/index.js';
10
+ import { Url } from '../../utils/url.js';
11
+ import { findMcpTool, getAllMcpTools } from '../tools/index.js';
13
12
  import { DirectusTransport } from './transport.js';
14
13
  export class DirectusMCP {
15
14
  promptsCollection;
@@ -167,7 +166,6 @@ export class DirectusMCP {
167
166
  // calling tools
168
167
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
169
168
  const tool = findMcpTool(request.params.name);
170
- let sanitizedQuery = {};
171
169
  try {
172
170
  if (!tool || (tool.name === 'system-prompt' && this.systemPromptEnabled === false)) {
173
171
  throw new InvalidPayloadError({ reason: `"${request.params.name}" doesn't exist in the toolset` });
@@ -196,18 +194,11 @@ export class DirectusMCP {
196
194
  if (!isObject(args)) {
197
195
  throw new InvalidPayloadError({ reason: '"arguments" must be an object' });
198
196
  }
199
- if ('action' in args && args['action'] === 'delete' && !this.allowDeletes) {
197
+ if (this.allowDeletes === false && 'action' in args && args['action'] === 'delete') {
200
198
  throw new InvalidPayloadError({ reason: 'Delete actions are disabled' });
201
199
  }
202
- if ('query' in args && args['query']) {
203
- sanitizedQuery = await sanitizeQuery({
204
- fields: args['query']['fields'] || '*',
205
- ...args['query'],
206
- }, req.schema, req.accountability);
207
- }
208
200
  const result = await tool.handler({
209
201
  args,
210
- sanitizedQuery,
211
202
  schema: req.schema,
212
203
  accountability: req.accountability,
213
204
  });
@@ -0,0 +1,15 @@
1
+ export interface Prompt {
2
+ name: string;
3
+ system_prompt?: string | null;
4
+ description?: string;
5
+ messages: {
6
+ role: 'user' | 'assistant';
7
+ text: string;
8
+ }[];
9
+ }
10
+ export interface MCPOptions {
11
+ promptsCollection?: string;
12
+ allowDeletes?: boolean;
13
+ systemPromptEnabled?: boolean;
14
+ systemPrompt?: string | null;
15
+ }
@@ -1,9 +1,12 @@
1
1
  import { UnsupportedMediaTypeError } from '@directus/errors';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
2
4
  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';
5
+ import { AssetsService } from '../../../services/assets.js';
6
+ import { FilesService } from '../../../services/files.js';
7
+ import { requireText } from '../../../utils/require-text.js';
8
+ import { defineTool } from '../define-tool.js';
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
10
  const AssetsValidateSchema = z.strictObject({
8
11
  id: z.string(),
9
12
  });
@@ -12,7 +15,7 @@ const AssetsInputSchema = z.object({
12
15
  });
13
16
  export const assets = defineTool({
14
17
  name: 'assets',
15
- description: prompts.assets,
18
+ description: requireText(resolve(__dirname, './prompt.md')),
16
19
  annotations: {
17
20
  title: 'Directus - Assets',
18
21
  },