Skip to main content
Glama
quest-tools.ts14.2 kB
import { z } from 'zod'; import { randomUUID } from 'crypto'; import { QuestRepository } from '../storage/repos/quest.repo.js'; import { CharacterRepository } from '../storage/repos/character.repo.js'; import { InventoryRepository } from '../storage/repos/inventory.repo.js'; import { ItemRepository } from '../storage/repos/item.repo.js'; import { QuestSchema } from '../schema/quest.js'; import { getDb } from '../storage/index.js'; import { SessionContext } from './types.js'; function ensureDb() { const dbPath = process.env.NODE_ENV === 'test' ? ':memory:' : process.env.RPG_DATA_DIR ? `${process.env.RPG_DATA_DIR}/rpg.db` : 'rpg.db'; const db = getDb(dbPath); const questRepo = new QuestRepository(db); const characterRepo = new CharacterRepository(db); const inventoryRepo = new InventoryRepository(db); const itemRepo = new ItemRepository(db); return { questRepo, characterRepo, inventoryRepo, itemRepo }; } export const QuestTools = { CREATE_QUEST: { name: 'create_quest', description: 'Define a new quest in the world.', inputSchema: QuestSchema.omit({ id: true, createdAt: true, updatedAt: true }) }, GET_QUEST: { name: 'get_quest', description: 'Get a single quest by ID with full details.', inputSchema: z.object({ questId: z.string() }) }, LIST_QUESTS: { name: 'list_quests', description: 'List all quests, optionally filtered by world.', inputSchema: z.object({ worldId: z.string().optional() }) }, ASSIGN_QUEST: { name: 'assign_quest', description: 'Assign a quest to a character.', inputSchema: z.object({ characterId: z.string(), questId: z.string() }) }, UPDATE_OBJECTIVE: { name: 'update_objective', description: 'Update progress on a quest objective.', inputSchema: z.object({ characterId: z.string(), questId: z.string(), objectiveId: z.string(), progress: z.number().int().min(1).default(1) }) }, COMPLETE_OBJECTIVE: { name: 'complete_objective', description: 'Mark an objective as fully completed.', inputSchema: z.object({ questId: z.string(), objectiveId: z.string() }) }, COMPLETE_QUEST: { name: 'complete_quest', description: 'Mark a quest as completed and grant rewards.', inputSchema: z.object({ characterId: z.string(), questId: z.string() }) }, GET_QUEST_LOG: { name: 'get_quest_log', description: 'Get the quest log for a character.', inputSchema: z.object({ characterId: z.string() }) } } as const; export async function handleCreateQuest(args: unknown, _ctx: SessionContext) { const { questRepo } = ensureDb(); const parsed = QuestTools.CREATE_QUEST.inputSchema.parse(args); const now = new Date().toISOString(); // Ensure all objectives have IDs const objectives = parsed.objectives.map(obj => ({ ...obj, id: obj.id || randomUUID(), current: obj.current ?? 0, completed: obj.completed ?? false })); const quest = { ...parsed, objectives, id: randomUUID(), createdAt: now, updatedAt: now }; questRepo.create(quest); return { content: [{ type: 'text' as const, text: JSON.stringify(quest, null, 2) }] }; } export async function handleGetQuest(args: unknown, _ctx: SessionContext) { const { questRepo } = ensureDb(); const parsed = QuestTools.GET_QUEST.inputSchema.parse(args); const quest = questRepo.findById(parsed.questId); if (!quest) { throw new Error(`Quest ${parsed.questId} not found`); } return { content: [{ type: 'text' as const, text: JSON.stringify(quest, null, 2) }] }; } export async function handleListQuests(args: unknown, _ctx: SessionContext) { const { questRepo } = ensureDb(); const parsed = QuestTools.LIST_QUESTS.inputSchema.parse(args); const quests = questRepo.findAll(parsed.worldId); return { content: [{ type: 'text' as const, text: JSON.stringify({ quests, count: quests.length }, null, 2) }] }; } export async function handleAssignQuest(args: unknown, _ctx: SessionContext) { const { questRepo, characterRepo } = ensureDb(); const parsed = QuestTools.ASSIGN_QUEST.inputSchema.parse(args); const character = characterRepo.findById(parsed.characterId); if (!character) throw new Error(`Character ${parsed.characterId} not found`); const quest = questRepo.findById(parsed.questId); if (!quest) throw new Error(`Quest ${parsed.questId} not found`); let log = questRepo.getLog(parsed.characterId); if (!log) { log = { characterId: parsed.characterId, activeQuests: [], completedQuests: [], failedQuests: [] }; } if (log.activeQuests.includes(parsed.questId)) { throw new Error(`Quest ${parsed.questId} is already active for character ${parsed.characterId}`); } if (log.completedQuests.includes(parsed.questId)) { throw new Error(`Quest ${parsed.questId} is already completed by character ${parsed.characterId}`); } // Check prerequisites for (const prereqId of quest.prerequisites) { if (!log.completedQuests.includes(prereqId)) { const prereqQuest = questRepo.findById(prereqId); const prereqName = prereqQuest?.name || prereqId; throw new Error(`Prerequisite quest "${prereqName}" not completed`); } } log.activeQuests.push(parsed.questId); questRepo.updateLog(log); return { content: [{ type: 'text' as const, text: JSON.stringify({ message: `Assigned quest "${quest.name}" to ${character.name}`, quest: quest }, null, 2) }] }; } export async function handleUpdateObjective(args: unknown, _ctx: SessionContext) { const { questRepo, characterRepo } = ensureDb(); const parsed = QuestTools.UPDATE_OBJECTIVE.inputSchema.parse(args); // Verify character exists and has this quest const character = characterRepo.findById(parsed.characterId); if (!character) throw new Error(`Character ${parsed.characterId} not found`); const log = questRepo.getLog(parsed.characterId); if (!log || !log.activeQuests.includes(parsed.questId)) { throw new Error(`Quest ${parsed.questId} is not active for character ${parsed.characterId}`); } const quest = questRepo.findById(parsed.questId); if (!quest) throw new Error(`Quest ${parsed.questId} not found`); const objectiveIndex = quest.objectives.findIndex(o => o.id === parsed.objectiveId); if (objectiveIndex === -1) throw new Error(`Objective ${parsed.objectiveId} not found in quest`); // Update progress const updatedQuest = questRepo.updateObjectiveProgress( parsed.questId, parsed.objectiveId, parsed.progress ); if (!updatedQuest) { throw new Error('Failed to update objective progress'); } const objective = updatedQuest.objectives[objectiveIndex]; // Check if all objectives are now complete const allComplete = questRepo.areAllObjectivesComplete(parsed.questId); return { content: [{ type: 'text' as const, text: JSON.stringify({ message: `Updated objective: ${objective.description}`, objective: { id: objective.id, description: objective.description, progress: `${objective.current}/${objective.required}`, completed: objective.completed }, questComplete: allComplete, quest: updatedQuest }, null, 2) }] }; } export async function handleCompleteObjective(args: unknown, _ctx: SessionContext) { const { questRepo } = ensureDb(); const parsed = QuestTools.COMPLETE_OBJECTIVE.inputSchema.parse(args); const quest = questRepo.findById(parsed.questId); if (!quest) throw new Error(`Quest ${parsed.questId} not found`); const objectiveIndex = quest.objectives.findIndex(o => o.id === parsed.objectiveId); if (objectiveIndex === -1) throw new Error(`Objective ${parsed.objectiveId} not found`); const updatedQuest = questRepo.completeObjective(parsed.questId, parsed.objectiveId); if (!updatedQuest) { throw new Error('Failed to complete objective'); } const objective = updatedQuest.objectives[objectiveIndex]; const allComplete = questRepo.areAllObjectivesComplete(parsed.questId); return { content: [{ type: 'text' as const, text: JSON.stringify({ message: `Completed objective: ${objective.description}`, objective: { id: objective.id, description: objective.description, completed: true }, questComplete: allComplete, quest: updatedQuest }, null, 2) }] }; } export async function handleCompleteQuest(args: unknown, _ctx: SessionContext) { const { questRepo, characterRepo, inventoryRepo, itemRepo } = ensureDb(); const parsed = QuestTools.COMPLETE_QUEST.inputSchema.parse(args); const character = characterRepo.findById(parsed.characterId); if (!character) throw new Error(`Character ${parsed.characterId} not found`); const quest = questRepo.findById(parsed.questId); if (!quest) throw new Error(`Quest ${parsed.questId} not found`); let log = questRepo.getLog(parsed.characterId); if (!log || !log.activeQuests.includes(parsed.questId)) { throw new Error(`Quest "${quest.name}" is not active for character ${character.name}`); } // Verify all objectives are completed const allCompleted = quest.objectives.every(o => o.completed); if (!allCompleted) { const incomplete = quest.objectives.filter(o => !o.completed); throw new Error(`Not all objectives completed. Remaining: ${incomplete.map(o => o.description).join(', ')}`); } // Grant rewards const rewardsGranted: { xp?: number; gold?: number; items: string[] } = { items: [] }; // Grant XP (update character - need to check if character schema supports xp) if (quest.rewards.experience > 0) { rewardsGranted.xp = quest.rewards.experience; // Note: Character XP tracking would need to be added to character schema // For now, we just report it } // Grant gold if (quest.rewards.gold > 0) { rewardsGranted.gold = quest.rewards.gold; // Note: Gold tracking would need to be added to character or inventory system // For now, we just report it } // Grant items for (const itemId of quest.rewards.items) { try { inventoryRepo.addItem(parsed.characterId, itemId, 1); const item = itemRepo.findById(itemId); rewardsGranted.items.push(item?.name || itemId); } catch (err) { // Item may not exist, still complete the quest rewardsGranted.items.push(`${itemId} (item not found)`); } } // Update quest log log.activeQuests = log.activeQuests.filter(id => id !== parsed.questId); log.completedQuests.push(parsed.questId); questRepo.updateLog(log); // Update quest status questRepo.update(parsed.questId, { status: 'completed' }); return { content: [{ type: 'text' as const, text: JSON.stringify({ message: `Completed quest: "${quest.name}"!`, character: character.name, rewards: { xp: rewardsGranted.xp || 0, gold: rewardsGranted.gold || 0, items: rewardsGranted.items }, quest: { id: quest.id, name: quest.name, status: 'completed' } }, null, 2) }] }; } export async function handleGetQuestLog(args: unknown, _ctx: SessionContext) { const { questRepo, characterRepo } = ensureDb(); const parsed = QuestTools.GET_QUEST_LOG.inputSchema.parse(args); // Verify character exists const character = characterRepo.findById(parsed.characterId); if (!character) { throw new Error(`Character ${parsed.characterId} not found`); } // Get full quest log with complete quest data const fullLog = questRepo.getFullQuestLog(parsed.characterId); // Transform to frontend-friendly format const quests = fullLog.quests.map(quest => ({ id: quest.id, title: quest.name, name: quest.name, description: quest.description, status: quest.logStatus, questGiver: quest.giver, objectives: quest.objectives.map(obj => ({ id: obj.id, description: obj.description, type: obj.type, target: obj.target, current: obj.current, required: obj.required, completed: obj.completed, progress: `${obj.current}/${obj.required}` })), rewards: { experience: quest.rewards.experience, gold: quest.rewards.gold, items: quest.rewards.items }, prerequisites: quest.prerequisites })); return { content: [{ type: 'text' as const, text: JSON.stringify({ characterId: parsed.characterId, characterName: character.name, quests, summary: fullLog.summary }, null, 2) }] }; }

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/Mnehmos/rpg-mcp'

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