Skip to main content
Glama
storyline-tools.js22 kB
// MCP Tools for Storyline Database Management with Notion Integration import { z } from 'zod'; import { StorylineNotionSync } from './storyline-notion-sync.js'; // Helper function to execute SQL queries async function executeQuery(query, params = []) { try { const { Pool } = await import('pg'); const pool = new Pool({ connectionString: process.env.DATABASE_URL }); const result = await pool.query(query, params); await pool.end(); return result; } catch (error) { console.error('Database query error:', error); throw error; } } // Initialize Notion sync const notionSync = new StorylineNotionSync(); // Storyline management tools const storylineTools = { // Tool 1: Create new storyline create_storyline: { name: "create_storyline", description: "Create a new storyline/project in the database with title, description, and genre. Automatically syncs to Notion.", inputSchema: { type: "object", properties: { title: { type: "string", description: "Title of the storyline" }, description: { type: "string", description: "Detailed description of the storyline" }, genre: { type: "string", description: "Genre of the storyline (e.g., comedy, drama, adventure)" } }, required: ["title", "description", "genre"] }, handler: async (args) => { try { const query = ` INSERT INTO storylines (title, description, genre, status) VALUES ($1, $2, $3, 'draft') RETURNING * `; const result = await executeQuery(query, [args.title, args.description, args.genre]); const storyline = result.rows[0]; // Sync to Notion if available let notionResult = null; try { await notionSync.ensureStorylineDatabases(); notionResult = await notionSync.syncStorylineToNotion(storyline); if (notionResult.success) { // Update database with Notion page ID await executeQuery( `UPDATE storylines SET metadata = jsonb_set(COALESCE(metadata, '{}'), '{notion_page_id}', $1) WHERE id = $2`, [`"${notionResult.notion_page_id}"`, storyline.id] ); } } catch (notionError) { console.log('⚠️ Notion sync failed, continuing without it:', notionError.message); } return { success: true, storyline: storyline, notion_synced: notionResult?.success || false, notion_page_id: notionResult?.notion_page_id || null, message: `✅ Created storyline "${args.title}" with ID ${storyline.id}${notionResult?.success ? ' (synced to Notion)' : ''}` }; } catch (error) { return { success: false, error: error.message, message: `❌ Failed to create storyline: ${error.message}` }; } } }, // Tool 2: Add scene to storyline add_scene_to_storyline: { name: "add_scene_to_storyline", description: "Add a new scene to an existing storyline with proper scene numbering. Syncs to Notion if connected.", inputSchema: { type: "object", properties: { storyline_id: { type: "integer", description: "ID of the storyline to add scene to" }, title: { type: "string", description: "Title of the scene" }, description: { type: "string", description: "Description of what happens in the scene" }, dialogue: { type: "string", description: "Dialogue content for the scene" }, visual_direction: { type: "string", description: "Visual direction and camera notes" }, scene_type: { type: "string", description: "Type of scene (dialogue, action, montage, etc.)", default: "dialogue" }, characters: { type: "array", items: { type: "string" }, description: "List of character names appearing in this scene" } }, required: ["storyline_id", "title", "description"] }, handler: async (args) => { try { // Get next scene number for this storyline const sceneCountQuery = `SELECT COALESCE(MAX(scene_number), 0) + 1 as next_number FROM scenes WHERE storyline_id = $1`; const countResult = await executeQuery(sceneCountQuery, [args.storyline_id]); const nextSceneNumber = countResult.rows[0].next_number; const insertQuery = ` INSERT INTO scenes (storyline_id, scene_number, title, description, dialogue, visual_direction, scene_type) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING * `; const result = await executeQuery(insertQuery, [ args.storyline_id, nextSceneNumber, args.title, args.description, args.dialogue || '', args.visual_direction || '', args.scene_type || 'dialogue' ]); const scene = result.rows[0]; // Add characters to scene if provided if (args.characters && args.characters.length > 0) { for (const characterName of args.characters) { try { const charQuery = `SELECT id FROM characters WHERE name = $1`; const charResult = await executeQuery(charQuery, [characterName]); if (charResult.rows.length > 0) { await executeQuery( `INSERT INTO character_scenes (character_id, scene_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, [charResult.rows[0].id, scene.id] ); } } catch (charError) { console.log(`⚠️ Could not add character "${characterName}" to scene:`, charError.message); } } } // Update storyline scene count const updateQuery = `UPDATE storylines SET total_scenes = total_scenes + 1, updated_at = CURRENT_TIMESTAMP WHERE id = $1`; await executeQuery(updateQuery, [args.storyline_id]); // Sync to Notion if available let notionResult = null; try { // Get storyline's Notion page ID const storylineQuery = `SELECT metadata->>'notion_page_id' as notion_page_id FROM storylines WHERE id = $1`; const storylineResult = await executeQuery(storylineQuery, [args.storyline_id]); const storylineNotionId = storylineResult.rows[0]?.notion_page_id; if (storylineNotionId) { notionResult = await notionSync.syncSceneToNotion(scene, storylineNotionId); if (notionResult.success) { // Update scene with Notion page ID await executeQuery( `UPDATE scenes SET updated_at = CURRENT_TIMESTAMP WHERE id = $1`, [scene.id] ); } } } catch (notionError) { console.log('⚠️ Notion sync failed, continuing without it:', notionError.message); } return { success: true, scene: scene, scene_number: nextSceneNumber, characters_added: args.characters?.length || 0, notion_synced: notionResult?.success || false, message: `✅ Added scene "${args.title}" as scene #${nextSceneNumber} to storyline${notionResult?.success ? ' (synced to Notion)' : ''}` }; } catch (error) { return { success: false, error: error.message, message: `❌ Failed to add scene: ${error.message}` }; } } }, // Tool 3: Link character to storyline link_character_to_storyline: { name: "link_character_to_storyline", description: "Connect a character to a storyline with role and importance level. Links with existing Notion character data.", inputSchema: { type: "object", properties: { character_name: { type: "string", description: "Name of the character" }, storyline_id: { type: "integer", description: "ID of the storyline" }, role: { type: "string", description: "Character's role in the storyline" }, importance_level: { type: "string", description: "Importance level: main, supporting, background", default: "supporting" }, character_arc: { type: "string", description: "Description of character's development arc in this storyline" } }, required: ["character_name", "storyline_id", "role"] }, handler: async (args) => { try { // Get character ID const charQuery = `SELECT id FROM characters WHERE name = $1`; const charResult = await executeQuery(charQuery, [args.character_name]); if (charResult.rows.length === 0) { return { success: false, error: "Character not found", message: `❌ Character "${args.character_name}" not found in database. Create the character first using process_character_image tool.` }; } const characterId = charResult.rows[0].id; const insertQuery = ` INSERT INTO character_storylines (character_id, storyline_id, role, importance_level, character_arc) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (character_id, storyline_id) DO UPDATE SET role = $3, importance_level = $4, character_arc = $5 RETURNING * `; const result = await executeQuery(insertQuery, [ characterId, args.storyline_id, args.role, args.importance_level || 'supporting', args.character_arc || '' ]); return { success: true, character_storyline: result.rows[0], message: `✅ Linked "${args.character_name}" to storyline as ${args.role} (${args.importance_level} character)` }; } catch (error) { return { success: false, error: error.message, message: `❌ Failed to link character: ${error.message}` }; } } }, // Tool 4: Get storyline overview get_storyline_overview: { name: "get_storyline_overview", description: "Get comprehensive overview of a storyline including scenes, characters, and development tracking.", inputSchema: { type: "object", properties: { storyline_id: { type: "integer", description: "ID of the storyline to get overview for" } }, required: ["storyline_id"] }, handler: async (args) => { try { // Get storyline details const storylineQuery = `SELECT * FROM storylines WHERE id = $1`; const storylineResult = await executeQuery(storylineQuery, [args.storyline_id]); if (storylineResult.rows.length === 0) { return { success: false, error: "Storyline not found", message: `❌ Storyline with ID ${args.storyline_id} not found` }; } const storyline = storylineResult.rows[0]; // Get scenes const scenesQuery = ` SELECT s.*, COUNT(cs.character_id) as character_count FROM scenes s LEFT JOIN character_scenes cs ON s.id = cs.scene_id WHERE s.storyline_id = $1 GROUP BY s.id ORDER BY s.scene_number `; const scenesResult = await executeQuery(scenesQuery, [args.storyline_id]); // Get characters const charactersQuery = ` SELECT c.name, c.description, cs.role, cs.importance_level, cs.character_arc, COUNT(chsc.scene_id) as scene_appearances FROM characters c JOIN character_storylines cs ON c.id = cs.character_id LEFT JOIN character_scenes chsc ON c.id = chsc.character_id WHERE cs.storyline_id = $1 GROUP BY c.id, c.name, c.description, cs.role, cs.importance_level, cs.character_arc ORDER BY cs.importance_level, c.name `; const charactersResult = await executeQuery(charactersQuery, [args.storyline_id]); // Get character development events const developmentQuery = ` SELECT cd.*, c.name as character_name, s.title as scene_title, s.scene_number FROM character_development cd JOIN characters c ON cd.character_id = c.id JOIN scenes s ON cd.scene_id = s.id WHERE cd.storyline_id = $1 ORDER BY s.scene_number, cd.created_at `; const developmentResult = await executeQuery(developmentQuery, [args.storyline_id]); return { success: true, storyline: { ...storyline, scenes: scenesResult.rows, characters: charactersResult.rows, character_development: developmentResult.rows, statistics: { total_scenes: scenesResult.rows.length, total_characters: charactersResult.rows.length, total_development_events: developmentResult.rows.length, main_characters: charactersResult.rows.filter(c => c.importance_level === 'main').length, supporting_characters: charactersResult.rows.filter(c => c.importance_level === 'supporting').length } }, message: `✅ Retrieved complete overview for "${storyline.title}"` }; } catch (error) { return { success: false, error: error.message, message: `❌ Failed to get storyline overview: ${error.message}` }; } } }, // Tool 5: Get character development tracking get_character_development: { name: "get_character_development", description: "Get detailed character development tracking across storylines and scenes.", inputSchema: { type: "object", properties: { character_name: { type: "string", description: "Name of the character to track development for" }, storyline_id: { type: "integer", description: "Optional: specific storyline ID to focus on" } }, required: ["character_name"] }, handler: async (args) => { try { // Get character ID const charQuery = `SELECT id, name, description FROM characters WHERE name = $1`; const charResult = await executeQuery(charQuery, [args.character_name]); if (charResult.rows.length === 0) { return { success: false, error: "Character not found", message: `❌ Character "${args.character_name}" not found in database` }; } const character = charResult.rows[0]; let storylineFilter = ''; let queryParams = [character.id]; if (args.storyline_id) { storylineFilter = 'AND cd.storyline_id = $2'; queryParams.push(args.storyline_id); } // Get development events const developmentQuery = ` SELECT cd.*, st.title as storyline_title, s.title as scene_title, s.scene_number, cs.dialogue, cs.emotional_state, cs.actions FROM character_development cd JOIN storylines st ON cd.storyline_id = st.id JOIN scenes s ON cd.scene_id = s.id LEFT JOIN character_scenes cs ON cd.character_id = cs.character_id AND cd.scene_id = cs.scene_id WHERE cd.character_id = $1 ${storylineFilter} ORDER BY st.id, s.scene_number, cd.created_at `; const developmentResult = await executeQuery(developmentQuery, queryParams); // Get storyline roles const rolesQuery = ` SELECT st.title as storyline_title, cs.role, cs.importance_level, cs.character_arc FROM character_storylines cs JOIN storylines st ON cs.storyline_id = st.id WHERE cs.character_id = $1 ${args.storyline_id ? 'AND cs.storyline_id = $2' : ''} `; const rolesResult = await executeQuery(rolesQuery, queryParams); return { success: true, character: { ...character, development_timeline: developmentResult.rows, storyline_roles: rolesResult.rows, development_summary: { total_development_events: developmentResult.rows.length, storylines_involved: rolesResult.rows.length, pivotal_moments: developmentResult.rows.filter(d => d.significance === 'pivotal').length, major_moments: developmentResult.rows.filter(d => d.significance === 'major').length, average_emotional_impact: developmentResult.rows.length > 0 ? (developmentResult.rows.reduce((sum, d) => sum + d.emotional_impact, 0) / developmentResult.rows.length).toFixed(1) : 0 } }, message: `✅ Retrieved character development for "${args.character_name}"` }; } catch (error) { return { success: false, error: error.message, message: `❌ Failed to get character development: ${error.message}` }; } } } }; // Export tool definitions for MCP server registration export const getStorylineToolDefinitions = () => { return Object.values(storylineTools).map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })); }; // Export tool handlers for MCP server execution export const getStorylineToolHandlers = () => { const handlers = {}; Object.values(storylineTools).forEach(tool => { handlers[tool.name] = tool.handler; }); return handlers; }; export default storylineTools;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bermingham85/mcp-puppet-pipeline'

If you have feedback or need assistance with the MCP directory API, please join our Discord server