// 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;