diff --git a/package.json b/package.json index 250b374..24790ec 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "description": "Model Context Protocol server for Directus projects.", "contributors": [ "Rijk van Zanten ", - "Bryant Gillespie" + "Bryant Gillespie", + "Mustafa Öztürk" ], "license": "MIT", "keywords": [ @@ -27,7 +28,6 @@ "lint": "eslint .", "lint:fix": "eslint --fix ." }, - "dependencies": { "@directus/sdk": "19.1.0", "@modelcontextprotocol/sdk": "1.10.2", diff --git a/src/directus.ts b/src/directus.ts index 6376d8b..78a4ae0 100644 --- a/src/directus.ts +++ b/src/directus.ts @@ -2,14 +2,14 @@ import type { AuthenticationClient, DirectusClient, RestClient, -} from '@directus/sdk'; -import type { Config } from './config.js'; -import type { Schema } from './types/schema.js'; +} from "@directus/sdk"; +import type { Config } from "./config.js"; +import type { Schema } from "./types/schema.js"; import { authentication, createDirectus as createSdk, rest, -} from '@directus/sdk'; +} from "@directus/sdk"; export type Directus = DirectusClient & RestClient & @@ -30,7 +30,7 @@ export const createDirectus = (config: Config) => */ export async function authenticateDirectus(directus: Directus, config: Config) { if (!directus || !config) { - throw new Error('Directus or config is not defined'); + throw new Error("Directus or config is not defined"); } // Token-based authentication @@ -44,22 +44,21 @@ export async function authenticateDirectus(directus: Directus, config: Config) { try { await directus.login( config.DIRECTUS_USER_EMAIL, - config.DIRECTUS_USER_PASSWORD, + config.DIRECTUS_USER_PASSWORD ); return; - } - catch (error) { + } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error( - `Failed to authenticate with credentials: ${errorMessage}`, + `Failed to authenticate with credentials: ${errorMessage}` ); } } // No valid authentication method throw new Error( - 'No valid authentication method provided (requires either DIRECTUS_TOKEN or both DIRECTUS_USER_EMAIL and DIRECTUS_USER_PASSWORD)', + "No valid authentication method provided (requires either DIRECTUS_TOKEN or both DIRECTUS_USER_EMAIL and DIRECTUS_USER_PASSWORD)" ); } diff --git a/src/index.ts b/src/index.ts index e91708c..316b961 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,20 +1,20 @@ #!/usr/bin/env node -import type { CallToolRequest } from '@modelcontextprotocol/sdk/types.js'; -import { Server } from '@modelcontextprotocol/sdk/server/index.js'; -import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import type { CallToolRequest } from "@modelcontextprotocol/sdk/types.js"; +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListToolsRequestSchema, -} from '@modelcontextprotocol/sdk/types.js'; -import { createConfig } from './config.js'; -import { authenticateDirectus, createDirectus } from './directus.js'; -import { getAvailablePrompts, handleGetPrompt } from './prompts/handlers.js'; -import { fetchPrompts } from './prompts/index.js'; -import { getTools } from './tools/index.js'; -import { fetchSchema } from './utils/fetch-schema.js'; -import { toMpcTools } from './utils/to-mpc-tools.js'; +} from "@modelcontextprotocol/sdk/types.js"; +import { createConfig } from "./config.js"; +import { authenticateDirectus, createDirectus } from "./directus.js"; +import { getAvailablePrompts, handleGetPrompt } from "./prompts/handlers.js"; +import { fetchPrompts } from "./prompts/index.js"; +import { getTools } from "./tools/index.js"; +import { fetchSchema } from "./utils/fetch-schema.js"; +import { toMpcTools } from "./utils/to-mpc-tools.js"; async function main() { const config = createConfig(); @@ -26,8 +26,8 @@ async function main() { const server = new Server( { - name: 'Directus MCP Server', - version: '0.0.1', + name: "Directus MCP Server", + version: "0.0.1", }, { capabilities: { @@ -35,7 +35,7 @@ async function main() { resources: {}, prompts: {}, }, - }, + } ); // Manage prompts @@ -50,12 +50,7 @@ async function main() { const promptName = request.params.name; const args = request.params.arguments || {}; - return await handleGetPrompt( - directus, - config, - promptName, - args, - ); + return await handleGetPrompt(directus, config, promptName, args); }); // Manage tool requests @@ -75,24 +70,26 @@ async function main() { // Proceed with execution if permission check passes const { inputSchema, handler } = tool; const args = inputSchema.parse(request.params.arguments); - return await handler(directus, args, { schema, baseUrl: config.DIRECTUS_URL }); - } - catch (error) { - console.error('Error executing tool:', error); + return await handler(directus, args, { + schema, + baseUrl: config.DIRECTUS_URL, + }); + } catch (error) { + console.error("Error executing tool:", error); const errorMessage = error instanceof Error ? error.message : JSON.stringify(error); return { content: [ { - type: 'text', + type: "text", text: errorMessage, }, ], isError: true, }; } - }, + } ); // Return the pre-filtered list for listing purposes @@ -107,8 +104,7 @@ async function main() { try { await main(); -} -catch (error) { - console.error('Fatal error in main():', error); +} catch (error) { + console.error("Fatal error in main():", error); process.exit(1); } diff --git a/src/tools/collections.ts b/src/tools/collections.ts new file mode 100644 index 0000000..56637fa --- /dev/null +++ b/src/tools/collections.ts @@ -0,0 +1,67 @@ +import { createCollection, updateCollection } from "@directus/sdk"; +import * as z from "zod"; +import { defineTool } from "../utils/define.js"; +import { + formatErrorResponse, + formatSuccessResponse, +} from "../utils/response.js"; + +export const createCollectionTool = defineTool("create-collection", { + description: `Create a new collection in Directus. The 'collection' and 'schema' properties are required. You can also provide an array of 'fields' to be created during the creation of the collection.`, + inputSchema: z.object({ + collection: z.string().describe("Unique name of the collection."), + schema: z + .union([z.object({}), z.null()]) + .describe("Schema definition for the collection, or null for a folder."), + fields: z + .array( + z.object({ + field: z.string(), + type: z.string(), + interface: z.string(), + }) + ) + .optional() + .describe("Fields to be created in the collection."), + meta: z + .record(z.string(), z.unknown()) + .optional() + .describe("Optional meta properties for the collection."), + }), + handler: async (directus, input) => { + try { + const { collection, schema, fields, meta } = input; + const collectionObject: any = { + collection, + schema, + }; + if (fields) collectionObject.fields = fields; + if (meta) Object.assign(collectionObject, meta); + const result = await directus.request(createCollection(collectionObject)); + return formatSuccessResponse(result); + } catch (error) { + return formatErrorResponse(error); + } + }, +}); + +export const updateCollectionTool = defineTool("update-collection", { + description: `Update the metadata for an existing collection. Only the 'meta' values of the collection object can be updated. Updating the collection name is not supported at this time.`, + inputSchema: z.object({ + collection: z.string().describe("Unique identifier of the collection."), + meta: z + .record(z.string(), z.unknown()) + .describe("Metadata of the collection."), + }), + handler: async (directus, input) => { + try { + const { collection, meta } = input; + const result = await directus.request( + updateCollection(collection, { meta }) + ); + return formatSuccessResponse(result); + } catch (error) { + return formatErrorResponse(error); + } + }, +}); diff --git a/src/tools/fields.ts b/src/tools/fields.ts index 298e2d8..5e2e045 100644 --- a/src/tools/fields.ts +++ b/src/tools/fields.ts @@ -4,27 +4,27 @@ import { readFields, readFieldsByCollection, updateField, -} from '@directus/sdk'; -import * as z from 'zod'; +} from "@directus/sdk"; +import * as z from "zod"; import { CreateFieldDataSchema, UpdateFieldDataSchema, -} from '../types/fields.js'; -import { defineTool } from '../utils/define.js'; +} from "../types/fields.js"; +import { defineTool } from "../utils/define.js"; import { formatErrorResponse, formatSuccessResponse, -} from '../utils/response.js'; +} from "../utils/response.js"; -export const readFieldsTool = defineTool('read-fields', { +export const readFieldsTool = defineTool("read-fields", { description: - 'Retrieve the field definitions for all collections or a specific collection. Note: This is lots of data and should be used sparingly. Use only if you cannot find the field information you need and you absolutely need to have the raw field definition.', + "Retrieve the field definitions for all collections or a specific collection. Note: This is lots of data and should be used sparingly. Use only if you cannot find the field information you need and you absolutely need to have the raw field definition.", inputSchema: z.object({ collection: z .string() .optional() .describe( - 'Optional: The name (ID) of the collection to retrieve fields for. If omitted, fields for all collections are returned.', + "Optional: The name (ID) of the collection to retrieve fields for. If omitted, fields for all collections are returned." ), }), handler: async (directus, { collection }) => { @@ -33,72 +33,70 @@ export const readFieldsTool = defineTool('read-fields', { ? await directus.request(readFieldsByCollection(collection)) : await directus.request(readFields()); return formatSuccessResponse(fields); - } - catch (error) { + } catch (error) { return formatErrorResponse(error); } }, }); -export const readFieldTool = defineTool('read-field', { +export const readFieldTool = defineTool("read-field", { description: - 'Retrieve the definition of a specific field within a collection.', + "Retrieve the definition of a specific field within a collection.", inputSchema: z.object({ collection: z .string() - .describe('The name (ID) of the collection the field belongs to.'), - field: z.string().describe('The name (ID) of the field to retrieve.'), + .describe("The name (ID) of the collection the field belongs to."), + field: z.string().describe("The name (ID) of the field to retrieve."), }), handler: async (directus, { collection, field }) => { try { const fieldData = await directus.request(readField(collection, field)); return formatSuccessResponse(fieldData); - } - catch (error) { + } catch (error) { return formatErrorResponse(error); } }, }); -export const createFieldTool = defineTool('create-field', { - description: 'Create a new field in a specified collection.', +export const createFieldTool = defineTool("create-field", { + description: "Create a new field in a specified collection.", inputSchema: z.object({ collection: z .string() - .describe('The name (ID) of the collection to add the field to.'), + .describe("The name (ID) of the collection to add the field to."), data: CreateFieldDataSchema.describe( - 'The data for the new field (field name, type, optional schema/meta).', + "The data for the new field (field name, type, optional schema/meta)." ), }), handler: async (directus, { collection, data }) => { try { const result = await directus.request(createField(collection, data)); return formatSuccessResponse(result); - } - catch (error) { + } catch (error) { return formatErrorResponse(error); } }, }); -export const updateFieldTool = defineTool('update-field', { - description: 'Update an existing field in a specified collection.', +export const updateFieldTool = defineTool("update-field", { + description: "Update an existing field in a specified collection.", inputSchema: z.object({ collection: z .string() - .describe('The name (ID) of the collection containing the field.'), - field: z.string().describe('The name (ID) of the field to update.'), + .describe("The name (ID) of the collection containing the field."), + field: z.string().describe("The name (ID) of the field to update."), data: UpdateFieldDataSchema.describe( - 'The partial data to update the field with (type, schema, meta).', + "The partial data to update the field with (type, schema, meta)." ), }), handler: async (directus, { collection, field, data }) => { try { - const result = await directus.request(updateField(collection, field, data)); + const result = await directus.request( + updateField(collection, field, data) + ); return formatSuccessResponse(result); - } - catch (error) { + } catch (error) { return formatErrorResponse(error); } }, diff --git a/src/tools/index.ts b/src/tools/index.ts index 829e516..e565289 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -1,32 +1,30 @@ -import type { Config } from '../config.js'; -import type { ToolDefinition } from '../types/tool.js'; -import { readCommentsTool, upsertCommentTool } from './comments.js'; +import type { Config } from "../config.js"; +import type { ToolDefinition } from "../types/tool.js"; +import { readCommentsTool, upsertCommentTool } from "./comments.js"; import { createFieldTool, readFieldsTool, readFieldTool, updateFieldTool, -} from './fields.js'; +} from "./fields.js"; import { importFileTool, readFilesTool, readFoldersTool, updateFilesTool, -} from './files.js'; -import { - readFlowsTool, - triggerFlowTool, -} from './flows.js'; +} from "./files.js"; +import { readFlowsTool, triggerFlowTool } from "./flows.js"; import { createItemTool, deleteItemTool, readItemsTool, updateItemTool, -} from './items.js'; -import { markdownTool } from './markdown.js'; -import { createSystemPrompt } from './prompts.js'; -import schemaTool from './schema.js'; -import { readUsersTool, usersMeTool } from './users.js'; +} from "./items.js"; +import { markdownTool } from "./markdown.js"; +import { createSystemPrompt } from "./prompts.js"; +import schemaTool from "./schema.js"; +import { readUsersTool, usersMeTool } from "./users.js"; +import { createCollectionTool, updateCollectionTool } from "./collections.js"; export const getTools = (config: Config) => { const toolList: ToolDefinition[] = [ @@ -35,6 +33,9 @@ export const getTools = (config: Config) => { readUsersTool, // Schema schemaTool, + // Collections + createCollectionTool, + updateCollectionTool, // Items readItemsTool, createItemTool, @@ -61,13 +62,13 @@ export const getTools = (config: Config) => { ]; // If system propmt is enabled and exists, add the system prompt tool - if (config.MCP_SYSTEM_PROMPT_ENABLED === 'true' && config.MCP_SYSTEM_PROMPT) { + if (config.MCP_SYSTEM_PROMPT_ENABLED === "true" && config.MCP_SYSTEM_PROMPT) { toolList.push(createSystemPrompt(config)); } // Filter the list of available tools based on cof const availableTools = toolList.filter( - (tool) => !config.DISABLE_TOOLS.includes(tool.name), + (tool) => !config.DISABLE_TOOLS.includes(tool.name) ); return availableTools;