#!/usr/bin/env node
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema,
McpError,
} from '@modelcontextprotocol/sdk/types.js';
import { createDirectus, rest, readItems, createItem, updateItem, deleteItem, readMe, staticToken } from '@directus/sdk';
import { SchemaManager, DirectusConfig } from './services/SchemaManager.js';
import { Task, CreateTaskSchema, UpdateTaskSchema, TaskFieldDefinitions, CreateTaskInput, UpdateTaskInput } from './models/Task.js';
// Configuration
const DIRECTUS_URL = process.env.DIRECTUS_URL || 'https://te9-pg-api.up.railway.app/';
if (!process.env.DIRECTUS_TOKEN) {
console.error('❌ DIRECTUS_TOKEN environment variable is required');
console.error(' Please set it in your .env file');
process.exit(1);
}
const DIRECTUS_TOKEN = process.env.DIRECTUS_TOKEN as string;
const DIRECTUS_USER_EMAIL = process.env.DIRECTUS_USER_EMAIL;
const DIRECTUS_USER_PASSWORD = process.env.DIRECTUS_USER_PASSWORD;
// Collection configuration
const TASKS_COLLECTION = 'tasks';
const TASKS_COLLECTION_META = {
icon: 'checklist',
color: '#6644FF',
display_template: '{{title}} ({{status}})',
note: 'Task management collection',
sort_field: 'created_at',
};
class DirectusTaskMCPServer {
private server: Server;
private directus: any;
private schemaManager: SchemaManager;
constructor() {
this.server = new Server(
{
name: 'directus-task-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Initialize Directus client
const config: DirectusConfig = {
url: DIRECTUS_URL,
token: DIRECTUS_TOKEN,
email: DIRECTUS_USER_EMAIL,
password: DIRECTUS_USER_PASSWORD,
};
this.directus = createDirectus(DIRECTUS_URL).with(rest());
if (DIRECTUS_TOKEN) {
this.directus = this.directus.with(staticToken(DIRECTUS_TOKEN));
}
this.schemaManager = new SchemaManager(config);
this.setupToolHandlers();
}
private setupToolHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'sync-task-schema',
description: 'Synchronize the Task model schema with Directus database',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'list-tasks',
description: 'List all tasks with optional filtering',
inputSchema: {
type: 'object',
properties: {
status: {
type: 'string',
enum: ['todo', 'in_progress', 'completed'],
description: 'Filter tasks by status',
},
priority: {
type: 'string',
enum: ['low', 'medium', 'high'],
description: 'Filter tasks by priority',
},
limit: {
type: 'number',
description: 'Maximum number of tasks to return',
default: 50,
},
},
},
},
{
name: 'get-task',
description: 'Get a specific task by ID',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Task ID',
},
},
required: ['id'],
},
},
{
name: 'create-task',
description: 'Create a new task',
inputSchema: {
type: 'object',
properties: {
title: {
type: 'string',
description: 'Task title',
},
description: {
type: 'string',
description: 'Task description',
},
status: {
type: 'string',
enum: ['todo', 'in_progress', 'completed'],
description: 'Task status',
default: 'todo',
},
priority: {
type: 'string',
enum: ['low', 'medium', 'high'],
description: 'Task priority',
default: 'medium',
},
due_date: {
type: 'string',
description: 'Due date in ISO format',
},
},
required: ['title'],
},
},
{
name: 'update-task',
description: 'Update an existing task',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Task ID',
},
title: {
type: 'string',
description: 'Task title',
},
description: {
type: 'string',
description: 'Task description',
},
status: {
type: 'string',
enum: ['todo', 'in_progress', 'completed'],
description: 'Task status',
},
priority: {
type: 'string',
enum: ['low', 'medium', 'high'],
description: 'Task priority',
},
due_date: {
type: 'string',
description: 'Due date in ISO format',
},
},
required: ['id'],
},
},
{
name: 'delete-task',
description: 'Delete a task',
inputSchema: {
type: 'object',
properties: {
id: {
type: 'string',
description: 'Task ID',
},
},
required: ['id'],
},
},
{
name: 'get-user-info',
description: 'Get current user information',
inputSchema: {
type: 'object',
properties: {},
},
},
],
};
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'sync-task-schema':
return await this.syncTaskSchema();
case 'list-tasks':
return await this.listTasks(args as any);
case 'get-task':
return await this.getTask(args as { id: string });
case 'create-task':
return await this.createTask(args as CreateTaskInput);
case 'update-task':
return await this.updateTask(args as UpdateTaskInput & { id: string });
case 'delete-task':
return await this.deleteTask(args as { id: string });
case 'get-user-info':
return await this.getUserInfo();
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${name}`
);
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
throw new McpError(ErrorCode.InternalError, errorMessage);
}
});
}
private async syncTaskSchema() {
try {
await this.schemaManager.syncModelToSchema(
TASKS_COLLECTION,
TASKS_COLLECTION_META,
TaskFieldDefinitions
);
return {
content: [
{
type: 'text',
text: `Successfully synchronized Task schema with Directus database. Collection '${TASKS_COLLECTION}' is ready for use.`,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
return {
content: [
{
type: 'text',
text: `Error synchronizing schema: ${errorMessage}`,
},
],
isError: true,
};
}
}
private async listTasks(filters: { status?: string; priority?: string; limit?: number }) {
try {
const query: any = {};
if (filters.status || filters.priority) {
query.filter = {};
if (filters.status) {
query.filter.status = { _eq: filters.status };
}
if (filters.priority) {
query.filter.priority = { _eq: filters.priority };
}
}
if (filters.limit) {
query.limit = filters.limit;
}
query.sort = ['-created_at'];
const tasks = await this.directus.request(readItems(TASKS_COLLECTION, query));
return {
content: [
{
type: 'text',
text: `Found ${tasks.length} tasks:\n\n${tasks.map((task: Task) =>
`• ${task.title} (${task.status}, ${task.priority} priority)${task.due_date ? ` - Due: ${new Date(task.due_date).toLocaleDateString()}` : ''}`
).join('\n')}`,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
return {
content: [
{
type: 'text',
text: `Error listing tasks: ${errorMessage}`,
},
],
isError: true,
};
}
}
private async getTask(args: { id: string }) {
try {
const task = await this.directus.request(readItems(TASKS_COLLECTION, {
filter: { id: { _eq: args.id } },
limit: 1,
}));
if (!task || task.length === 0) {
return {
content: [
{
type: 'text',
text: `Task with ID ${args.id} not found.`,
},
],
isError: true,
};
}
const taskData = task[0] as Task;
return {
content: [
{
type: 'text',
text: `Task Details:
Title: ${taskData.title}
Description: ${taskData.description || 'No description'}
Status: ${taskData.status}
Priority: ${taskData.priority}
Due Date: ${taskData.due_date ? new Date(taskData.due_date).toLocaleDateString() : 'Not set'}
Created: ${taskData.created_at ? new Date(taskData.created_at).toLocaleDateString() : 'Unknown'}
Updated: ${taskData.updated_at ? new Date(taskData.updated_at).toLocaleDateString() : 'Unknown'}`,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
return {
content: [
{
type: 'text',
text: `Error getting task: ${errorMessage}`,
},
],
isError: true,
};
}
}
private async createTask(taskData: CreateTaskInput) {
try {
// Validate input
const validatedData = CreateTaskSchema.parse(taskData);
const newTask = await this.directus.request(createItem(TASKS_COLLECTION, validatedData));
return {
content: [
{
type: 'text',
text: `Successfully created task: "${newTask.title}" (ID: ${newTask.id})`,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
return {
content: [
{
type: 'text',
text: `Error creating task: ${errorMessage}`,
},
],
isError: true,
};
}
}
private async updateTask(args: UpdateTaskInput & { id: string }) {
try {
const { id, ...updateData } = args;
// Validate input
const validatedData = UpdateTaskSchema.parse(updateData);
const updatedTask = await this.directus.request(updateItem(TASKS_COLLECTION, id, validatedData));
return {
content: [
{
type: 'text',
text: `Successfully updated task: "${updatedTask.title}" (ID: ${updatedTask.id})`,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
return {
content: [
{
type: 'text',
text: `Error updating task: ${errorMessage}`,
},
],
isError: true,
};
}
}
private async deleteTask(args: { id: string }) {
try {
await this.directus.request(deleteItem(TASKS_COLLECTION, args.id));
return {
content: [
{
type: 'text',
text: `Successfully deleted task with ID: ${args.id}`,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
return {
content: [
{
type: 'text',
text: `Error deleting task: ${errorMessage}`,
},
],
isError: true,
};
}
}
private async getUserInfo() {
try {
const user = await this.directus.request(readMe());
return {
content: [
{
type: 'text',
text: `Current User Info:
ID: ${user.id}
Email: ${user.email}
First Name: ${user.first_name || 'Not set'}
Last Name: ${user.last_name || 'Not set'}
Role: ${user.role || 'Not set'}
Status: ${user.status}`,
},
],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
return {
content: [
{
type: 'text',
text: `Error getting user info: ${errorMessage}`,
},
],
isError: true,
};
}
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Directus Task MCP Server running on stdio');
}
}
// Start the server
const server = new DirectusTaskMCPServer();
server.run().catch(console.error);