backlog_create
Create new tasks, epics, or other backlog items with titles, descriptions, and organizational links to manage project work effectively.
Instructions
Create a new item in the backlog.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| title | Yes | Task title | |
| description | No | Task description in markdown | |
| source_path | No | Local file path to read as description. Mutually exclusive with description — provide one or the other. Server reads the file directly. | |
| type | No | Type: task (default) or epic | |
| epic_id | No | Parent epic ID to link this task to | |
| parent_id | No | Parent ID (any entity). Supports subtasks (task→task), epic membership, folder organization, milestone grouping. | |
| references | No | Reference links. Formats: external URLs (https://...), task refs (mcp://backlog/tasks/TASK-XXXX.md), resources (mcp://backlog/resources/{path}). Local files must include extension (file:///path/to/file.md) |
Implementation Reference
- Main handler function that processes backlog_create requests: resolves source_path if provided, generates entity ID, creates the task entity using createTask factory, persists to storage, and returns the created entity ID.
async ({ title, description, source_path, type, epic_id, parent_id, references }) => { let resolvedDescription = description; if (source_path) { try { resolvedDescription = resolveSourcePath(source_path); } catch (error) { return { content: [{ type: 'text' as const, text: `Error reading source_path: ${error instanceof Error ? error.message : String(error)}` }] }; } } // parent_id takes precedence; epic_id is alias for backward compat const resolvedParent = parent_id ?? epic_id; const id = nextEntityId(storage.getMaxId(type), type); const task = createTask({ id, title, description: resolvedDescription, type, parent_id: resolvedParent, references }); // Write epic_id too for backward compat when caller used epic_id if (epic_id && !parent_id) task.epic_id = epic_id; storage.add(task); return { content: [{ type: 'text', text: `Created ${task.id}` }] }; } - Zod input schema defining the tool's parameters: title (required), description (optional markdown), source_path (optional file path), type (task/epic), epic_id/parent_id for hierarchy, and references array. Includes validation that description and source_path are mutually exclusive.
inputSchema: z.object({ title: z.string().describe('Task title'), description: z.string().optional().describe('Task description in markdown'), source_path: z.string().optional().describe('Local file path to read as description. Mutually exclusive with description — provide one or the other. Server reads the file directly.'), type: z.enum(ENTITY_TYPES).optional().describe('Type: task (default) or epic'), epic_id: z.string().optional().describe('Parent epic ID to link this task to'), parent_id: z.string().optional().describe('Parent ID (any entity). Supports subtasks (task→task), epic membership, folder organization, milestone grouping.'), references: z.array(z.object({ url: z.string(), title: z.string().optional() })).optional().describe('Reference links. Formats: external URLs (https://...), task refs (mcp://backlog/tasks/TASK-XXXX.md), resources (mcp://backlog/resources/{path}). Local files must include extension (file:///path/to/file.md)'), }).refine( (data) => !(data.description && data.source_path), { message: 'Cannot provide both description and source_path — use one or the other' }, ), - packages/server/src/tools/backlog-create.ts:19-56 (registration)Registration function that registers the backlog_create tool with the MCP server, including its description, input schema validation, and the async handler implementation.
export function registerBacklogCreateTool(server: McpServer) { server.registerTool( 'backlog_create', { description: 'Create a new item in the backlog.', inputSchema: z.object({ title: z.string().describe('Task title'), description: z.string().optional().describe('Task description in markdown'), source_path: z.string().optional().describe('Local file path to read as description. Mutually exclusive with description — provide one or the other. Server reads the file directly.'), type: z.enum(ENTITY_TYPES).optional().describe('Type: task (default) or epic'), epic_id: z.string().optional().describe('Parent epic ID to link this task to'), parent_id: z.string().optional().describe('Parent ID (any entity). Supports subtasks (task→task), epic membership, folder organization, milestone grouping.'), references: z.array(z.object({ url: z.string(), title: z.string().optional() })).optional().describe('Reference links. Formats: external URLs (https://...), task refs (mcp://backlog/tasks/TASK-XXXX.md), resources (mcp://backlog/resources/{path}). Local files must include extension (file:///path/to/file.md)'), }).refine( (data) => !(data.description && data.source_path), { message: 'Cannot provide both description and source_path — use one or the other' }, ), }, async ({ title, description, source_path, type, epic_id, parent_id, references }) => { let resolvedDescription = description; if (source_path) { try { resolvedDescription = resolveSourcePath(source_path); } catch (error) { return { content: [{ type: 'text' as const, text: `Error reading source_path: ${error instanceof Error ? error.message : String(error)}` }] }; } } // parent_id takes precedence; epic_id is alias for backward compat const resolvedParent = parent_id ?? epic_id; const id = nextEntityId(storage.getMaxId(type), type); const task = createTask({ id, title, description: resolvedDescription, type, parent_id: resolvedParent, references }); // Write epic_id too for backward compat when caller used epic_id if (epic_id && !parent_id) task.epic_id = epic_id; storage.add(task); return { content: [{ type: 'text', text: `Created ${task.id}` }] }; } ); - Helper function resolveSourcePath that expands ~ to home directory, resolves the file path, validates it exists and is a file, then reads and returns the file contents as a UTF-8 string.
export function resolveSourcePath(sourcePath: string): string { const expanded = sourcePath.startsWith('~') ? sourcePath.replace('~', homedir()) : sourcePath; const resolved = resolve(expanded); const stat = statSync(resolved, { throwIfNoEntry: false }); if (!stat) throw new Error(`File not found: ${sourcePath}`); if (!stat.isFile()) throw new Error(`Not a file: ${sourcePath}`); return readFileSync(resolved, 'utf-8'); } - Factory function createTask that constructs an Entity object with proper defaults (status='open', created_at/updated_at timestamps) and conditionally sets optional fields like description, type, parent_id, epic_id, references, due_date, content_type, and path.
export function createTask(input: CreateTaskInput): Entity { const now = new Date().toISOString(); const task: Entity = { id: input.id, title: input.title, status: 'open', created_at: now, updated_at: now, }; if (input.description) task.description = input.description; if (input.type) task.type = input.type; if (input.epic_id) task.epic_id = input.epic_id; if (input.parent_id) task.parent_id = input.parent_id; if (input.references?.length) task.references = input.references; if (input.due_date) task.due_date = input.due_date; if (input.content_type) task.content_type = input.content_type; if (input.path) task.path = input.path; return task; }