@directus/api 30.0.0 → 31.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +5 -0
- package/dist/auth/drivers/oauth2.js +17 -3
- package/dist/auth/drivers/openid.js +17 -3
- package/dist/controllers/mcp.d.ts +2 -0
- package/dist/controllers/mcp.js +33 -0
- package/dist/controllers/users.js +17 -7
- package/dist/controllers/versions.js +3 -2
- package/dist/database/errors/dialects/mssql.d.ts +1 -1
- package/dist/database/errors/dialects/mssql.js +18 -10
- package/dist/database/migrations/20250813A-add-mcp.d.ts +3 -0
- package/dist/database/migrations/20250813A-add-mcp.js +18 -0
- package/dist/database/run-ast/README.md +46 -0
- package/dist/mcp/define.d.ts +2 -0
- package/dist/mcp/define.js +3 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/schema.d.ts +485 -0
- package/dist/mcp/schema.js +219 -0
- package/dist/mcp/server.d.ts +97 -0
- package/dist/mcp/server.js +310 -0
- package/dist/mcp/tools/assets.d.ts +3 -0
- package/dist/mcp/tools/assets.js +54 -0
- package/dist/mcp/tools/collections.d.ts +84 -0
- package/dist/mcp/tools/collections.js +90 -0
- package/dist/mcp/tools/fields.d.ts +101 -0
- package/dist/mcp/tools/fields.js +157 -0
- package/dist/mcp/tools/files.d.ts +235 -0
- package/dist/mcp/tools/files.js +103 -0
- package/dist/mcp/tools/flows.d.ts +323 -0
- package/dist/mcp/tools/flows.js +85 -0
- package/dist/mcp/tools/folders.d.ts +95 -0
- package/dist/mcp/tools/folders.js +96 -0
- package/dist/mcp/tools/index.d.ts +15 -0
- package/dist/mcp/tools/index.js +29 -0
- package/dist/mcp/tools/items.d.ts +87 -0
- package/dist/mcp/tools/items.js +141 -0
- package/dist/mcp/tools/operations.d.ts +171 -0
- package/dist/mcp/tools/operations.js +77 -0
- package/dist/mcp/tools/prompts/assets.md +8 -0
- package/dist/mcp/tools/prompts/collections.md +336 -0
- package/dist/mcp/tools/prompts/fields.md +521 -0
- package/dist/mcp/tools/prompts/files.md +180 -0
- package/dist/mcp/tools/prompts/flows.md +495 -0
- package/dist/mcp/tools/prompts/folders.md +34 -0
- package/dist/mcp/tools/prompts/index.d.ts +16 -0
- package/dist/mcp/tools/prompts/index.js +19 -0
- package/dist/mcp/tools/prompts/items.md +317 -0
- package/dist/mcp/tools/prompts/operations.md +721 -0
- package/dist/mcp/tools/prompts/relations.md +386 -0
- package/dist/mcp/tools/prompts/schema.md +130 -0
- package/dist/mcp/tools/prompts/system-prompt-description.md +1 -0
- package/dist/mcp/tools/prompts/system-prompt.md +44 -0
- package/dist/mcp/tools/prompts/trigger-flow.md +214 -0
- package/dist/mcp/tools/relations.d.ts +73 -0
- package/dist/mcp/tools/relations.js +93 -0
- package/dist/mcp/tools/schema.d.ts +54 -0
- package/dist/mcp/tools/schema.js +317 -0
- package/dist/mcp/tools/system.d.ts +3 -0
- package/dist/mcp/tools/system.js +22 -0
- package/dist/mcp/tools/trigger-flow.d.ts +8 -0
- package/dist/mcp/tools/trigger-flow.js +48 -0
- package/dist/mcp/transport.d.ts +13 -0
- package/dist/mcp/transport.js +18 -0
- package/dist/mcp/types.d.ts +56 -0
- package/dist/mcp/types.js +1 -0
- package/dist/services/authentication.js +36 -0
- package/dist/services/fields.js +4 -4
- package/dist/services/items.js +14 -4
- package/dist/services/payload.d.ts +7 -3
- package/dist/services/payload.js +26 -12
- package/dist/services/server.js +1 -0
- package/dist/services/tfa.d.ts +1 -1
- package/dist/services/tfa.js +20 -5
- package/dist/services/versions.d.ts +6 -4
- package/dist/services/versions.js +84 -25
- package/dist/types/auth.d.ts +2 -1
- package/dist/utils/versioning/deep-map-with-schema.d.ts +23 -0
- package/dist/utils/versioning/deep-map-with-schema.js +81 -0
- package/dist/utils/versioning/handle-version.d.ts +2 -2
- package/dist/utils/versioning/handle-version.js +47 -43
- package/dist/utils/versioning/split-recursive.d.ts +4 -0
- package/dist/utils/versioning/split-recursive.js +27 -0
- package/package.json +30 -29
|
@@ -0,0 +1,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,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
|
+
});
|