timeline_add_track
Create a new track to organize timeline events for social media campaigns. Use this tool to structure content across platforms like X/Twitter, LinkedIn, and Instagram by grouping related posts together.
Instructions
Create a new track for organizing timeline events. Check existing tracks with timeline_list_tracks first to avoid duplicates.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| name | Yes | ||
| type | No | planned | |
| order | No | Optional order position. If not provided, will be added at the end. |
Implementation Reference
- timeline-fastmcp.ts:325-445 (handler)The complete handler implementation and registration for the 'timeline_add_track' MCP tool. Includes input validation schema, logic to check for duplicates, create DB record using Drizzle ORM, generate UUID, create disk folder structure with metadata JSON, and return validated response using trackResponseSchema.mcp.addTool({ name: 'timeline_add_track', description: 'Create a new track for organizing timeline events. Check existing tracks with timeline_list_tracks first to avoid duplicates.', parameters: z.object({ name: z.string().min(1, 'Track name cannot be empty').max(100, 'Track name too long'), type: z.enum(['planned', 'automation']).optional().default('planned'), order: z.number().int().optional().describe('Optional order position. If not provided, will be added at the end.') }), execute: async (params) => { console.error('[Timeline MCP] Add track called with params:', params); try { const db = await getDb(); // Check if track with same name already exists const existingTrack = await db.select().from(tracks) .where(and( eq(tracks.name, params.name), eq(tracks.type, params.type) )) .limit(1); if (existingTrack.length > 0) { return JSON.stringify({ success: false, error: `Track "${params.name}" with type "${params.type}" already exists`, existingTrack: trackResponseSchema.parse({ id: existingTrack[0].id, name: existingTrack[0].name, type: params.type === 'automation' ? 'automation' : 'schedule', order: existingTrack[0].order, createdAt: existingTrack[0].createdAt }) }, null, 2); } // Determine order let order = params.order; if (order === undefined) { // Get the maximum order and add 1 const maxOrder = await db.select({ maxOrder: tracks.order }) .from(tracks) .orderBy(desc(tracks.order)) .limit(1); order = (maxOrder[0]?.maxOrder || 0) + 1; } // Create new track const trackId = uuidv4(); const now = new Date().toISOString(); const postyAccountId = await getDefaultPostyAccountId(); await db.insert(tracks).values({ id: trackId, postyAccountId, name: params.name, type: params.type, order: order, createdAt: now, updatedAt: now }); // Fetch the created track const [newTrack] = await db.select().from(tracks).where(eq(tracks.id, trackId)); if (!newTrack) { throw new Error('Failed to create track'); } // Create track folder on disk const workspacePath = getWorkspacePath(); const trackFolderName = sanitizeFileName(params.name); const trackFolderPath = path.join(workspacePath, 'tracks', trackFolderName); try { await fs.mkdir(trackFolderPath, { recursive: true }); console.error('[Timeline MCP] Created track folder:', trackFolderPath); // Create a track info file const trackInfoFile = path.join(trackFolderPath, '.track-info.json'); const trackInfo = { id: trackId, name: params.name, type: params.type, order: order, createdAt: now, folderName: trackFolderName }; await fs.writeFile(trackInfoFile, JSON.stringify(trackInfo, null, 2)); } catch (folderError) { console.error('[Timeline MCP] Warning: Could not create track folder:', folderError); // Continue anyway - folder creation is not critical } const response = { success: true, track: trackResponseSchema.parse({ id: newTrack.id, name: newTrack.name, type: params.type === 'automation' ? 'automation' : 'schedule', order: newTrack.order, createdAt: newTrack.createdAt }), message: `Track "${params.name}" created successfully` }; console.error('[Timeline MCP] Track created successfully:', response); return JSON.stringify(response, null, 2); } catch (error) { console.error('[Timeline MCP] Error in add_track:', error); return JSON.stringify({ success: false, error: error instanceof Error ? error.message : 'Unknown error occurred', stack: error instanceof Error ? error.stack : undefined }, null, 2); } } });
- timeline-fastmcp.ts:328-332 (schema)Zod schema defining the input parameters for the timeline_add_track tool: track name, type, and optional order.parameters: z.object({ name: z.string().min(1, 'Track name cannot be empty').max(100, 'Track name too long'), type: z.enum(['planned', 'automation']).optional().default('planned'), order: z.number().int().optional().describe('Optional order position. If not provided, will be added at the end.') }),
- schemas/validation.ts:66-71 (schema)trackResponseSchema used to parse and validate the track object in the tool's response.export const trackResponseSchema = z.object({ id: z.string(), name: z.string(), type: z.enum(['schedule']), order: z.number(), createdAt: z.string().optional()
- schema-sqlite.ts:27-38 (schema)SQLite database schema definition for the 'timeline_tracks' table, directly used by the handler for inserting new tracks.export const tracks = sqliteTable('timeline_tracks', { id: text('id').primaryKey().$defaultFn(uuid.defaultFn), postyAccountId: text('posty_account_id').references(() => postyAccounts.id, { onDelete: 'cascade' }), name: text('name').notNull(), type: text('type', { enum: ['planned', 'automation'] }).notNull(), order: integer('order').notNull().default(0), createdAt: text('created_at').notNull().$defaultFn(timestamp.defaultNow), updatedAt: text('updated_at').notNull().$defaultFn(timestamp.defaultNow), }, (table) => ({ accountIdx: index('timeline_tracks_account_idx').on(table.postyAccountId), orderIdx: index('timeline_tracks_order_idx').on(table.order), }));
- timeline-fastmcp.ts:102-107 (helper)sanitizeFileName helper function used to create safe folder names for the new track directory on disk.function sanitizeFileName(name: string): string { return name .replace(/[<>:"/\\|?*]/g, '-') .replace(/\s+/g, '_') .replace(/^\.+/, '') .slice(0, 100);