From d67c0363db6134770859fa76b6e1620b05a0bcfb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 24 Sep 2025 12:52:33 +0000 Subject: [PATCH] feat: add choose-your-own-adventure game tool to everything server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adds new adventureGame tool with elicitation for theme selection - Implements story continuation using MCP sampling for narrative generation - Supports three actions: start, continue (with choice), and reset - Uses elicitation at each step for narrative development preferences - Generates 3 choices at each decision point - Ends adventure after 5 chapters or when story reaches conclusion šŸ¤– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/everything/everything.ts | 229 ++++++++++++++++++++++++++++++++++- 1 file changed, 228 insertions(+), 1 deletion(-) diff --git a/src/everything/everything.ts b/src/everything/everything.ts index e8b2f672..624470fd 100644 --- a/src/everything/everything.ts +++ b/src/everything/everything.ts @@ -100,6 +100,16 @@ const GetResourceLinksSchema = z.object({ const ListRootsSchema = z.object({}); +const AdventureGameSchema = z.object({ + action: z + .enum(["start", "continue", "reset"]) + .describe("Action to take in the adventure game"), + choice: z + .number() + .optional() + .describe("Choice number (1-3) when continuing the adventure"), +}); + const StructuredContentSchema = { input: z.object({ location: z @@ -134,7 +144,8 @@ enum ToolName { ELICITATION = "startElicitation", GET_RESOURCE_LINKS = "getResourceLinks", STRUCTURED_CONTENT = "structuredContent", - LIST_ROOTS = "listRoots" + LIST_ROOTS = "listRoots", + ADVENTURE_GAME = "adventureGame" } enum PromptName { @@ -182,6 +193,20 @@ export const createServer = () => { let clientSupportsRoots = false; let sessionId: string | undefined; + // Adventure game state + interface GameState { + theme?: string; + story?: string; + choices?: string[]; + currentStep: number; + isActive: boolean; + } + + let adventureGameState: GameState = { + currentStep: 0, + isActive: false + }; + // Function to start notification intervals when a client connects const startNotificationIntervals = (sid?: string|undefined) => { sessionId = sid; @@ -525,6 +550,12 @@ export const createServer = () => { inputSchema: zodToJsonSchema(StructuredContentSchema.input) as ToolInput, outputSchema: zodToJsonSchema(StructuredContentSchema.output) as ToolOutput, }, + { + name: ToolName.ADVENTURE_GAME, + description: + "Play a choose-your-own-adventure game. Uses elicitation to get the theme initially and at each step, and sampling to generate the story", + inputSchema: zodToJsonSchema(AdventureGameSchema) as ToolInput, + }, ]; if (clientCapabilities!.roots) tools.push ({ name: ToolName.LIST_ROOTS, @@ -876,6 +907,202 @@ export const createServer = () => { }; } + if (name === ToolName.ADVENTURE_GAME) { + const validatedArgs = AdventureGameSchema.parse(args); + const content = []; + + if (validatedArgs.action === "reset") { + adventureGameState = { + currentStep: 0, + isActive: false + }; + content.push({ + type: "text", + text: "šŸŽ® Adventure game has been reset. Use 'start' action to begin a new adventure!" + }); + return { content }; + } + + if (validatedArgs.action === "start") { + // Use elicitation to get the theme + const themeElicitation = await requestElicitation( + "šŸŽ­ Let's create an adventure! What theme would you like for your story?", + { + type: "object", + properties: { + theme: { + type: "string", + enum: ["fantasy", "sci-fi", "mystery", "horror", "western", "pirates"], + description: "Choose the theme for your adventure" + }, + difficulty: { + type: "string", + enum: ["easy", "medium", "hard"], + description: "Choose the difficulty level" + } + }, + required: ["theme"] + } + ); + + if (themeElicitation.action === "decline" || themeElicitation.action === "cancel") { + content.push({ + type: "text", + text: "āŒ Adventure setup cancelled. Use 'start' action to try again when you're ready!" + }); + return { content }; + } + + if (themeElicitation.action === "accept" && themeElicitation.content) { + const { theme, difficulty } = themeElicitation.content; + adventureGameState.theme = theme || "fantasy"; + adventureGameState.isActive = true; + adventureGameState.currentStep = 1; + + // Use sampling to generate the initial story + const storyPrompt = `Create the opening scene for a ${theme} adventure story. The difficulty is ${difficulty || 'medium'}. + Write a compelling 2-3 paragraph opening that ends with the protagonist facing an important decision. + Then provide exactly 3 choices for what the protagonist should do next. + Format as: [STORY] story text [CHOICES] 1. first choice 2. second choice 3. third choice`; + + const storyResult = await requestSampling( + storyPrompt, + "adventure-game-start", + 250 + ); + + // Parse the story and choices from the result + const storyText = (storyResult.content as any).text || ""; + const storyParts = storyText.split("[CHOICES]"); + const story = storyParts[0].replace("[STORY]", "").trim(); + const choicesText = storyParts[1] || ""; + + // Extract choices + const choiceMatches = choicesText.match(/[123]\.\s*([^\n]+)/g) || []; + const choices = choiceMatches.map((c: string) => c.replace(/^[123]\.\s*/, "")); + + adventureGameState.story = story; + adventureGameState.choices = choices; + + content.push({ + type: "text", + text: `šŸŽ® **${theme.toUpperCase()} ADVENTURE** (Difficulty: ${difficulty || 'medium'})\n\n${story}\n\n**What do you do?**\n${choices.map((c: string, i: number) => `${i + 1}. ${c}`).join('\n')}\n\nUse 'continue' action with your choice number (1-3)` + }); + } + + return { content }; + } + + if (validatedArgs.action === "continue") { + if (!adventureGameState.isActive) { + content.push({ + type: "text", + text: "āŒ No active adventure. Use 'start' action to begin a new adventure!" + }); + return { content }; + } + + if (!validatedArgs.choice || validatedArgs.choice < 1 || validatedArgs.choice > 3) { + content.push({ + type: "text", + text: "āŒ Please provide a valid choice number (1-3)" + }); + return { content }; + } + + const chosenAction = adventureGameState.choices?.[validatedArgs.choice - 1]; + + // Use elicitation for narrative decision + const narrativeElicitation = await requestElicitation( + `You chose: "${chosenAction}"\n\nHow would you like the story to develop?`, + { + type: "object", + properties: { + tone: { + type: "string", + enum: ["dramatic", "humorous", "suspenseful", "action-packed", "thoughtful"], + description: "Choose the tone for the next part" + }, + outcome: { + type: "string", + enum: ["success", "partial-success", "setback", "discovery"], + description: "Choose the general outcome" + } + }, + required: ["outcome"] + } + ); + + if (narrativeElicitation.action === "decline" || narrativeElicitation.action === "cancel") { + content.push({ + type: "text", + text: "āš ļø Story development cancelled. Use 'continue' with a choice to try again, or 'reset' to start over." + }); + return { content }; + } + + if (narrativeElicitation.action === "accept" && narrativeElicitation.content) { + const { tone, outcome } = narrativeElicitation.content; + adventureGameState.currentStep++; + + // Use sampling to generate the next part of the story + const continuePrompt = `Continue the ${adventureGameState.theme} adventure story. + Previous action: "${chosenAction}" + Desired tone: ${tone || 'neutral'} + Outcome type: ${outcome} + + Write 2-3 paragraphs continuing the story with the specified outcome and tone. + If this is step ${adventureGameState.currentStep}, ${adventureGameState.currentStep >= 5 ? 'provide a satisfying conclusion to the adventure' : 'end with another decision point and provide exactly 3 new choices'}. + ${adventureGameState.currentStep < 5 ? 'Format as: [STORY] story text [CHOICES] 1. first choice 2. second choice 3. third choice' : 'Format as: [STORY] story text [END]'}`; + + const storyResult = await requestSampling( + continuePrompt, + "adventure-game-continue", + 300 + ); + + const storyText = (storyResult.content as any).text || ""; + + if (adventureGameState.currentStep >= 5 || storyText.includes("[END]")) { + // End the adventure + const finalStory = storyText.replace("[STORY]", "").replace("[END]", "").trim(); + adventureGameState.isActive = false; + + content.push({ + type: "text", + text: `šŸ“– **CHAPTER ${adventureGameState.currentStep}**\n\n${finalStory}\n\nšŸŽŠ **THE END**\n\nThanks for playing! Use 'start' action to begin a new adventure!` + }); + } else { + // Continue the adventure + const storyParts = storyText.split("[CHOICES]"); + const story = storyParts[0].replace("[STORY]", "").trim(); + const choicesText = storyParts[1] || ""; + + // Extract choices + const choiceMatches = choicesText.match(/[123]\.\s*([^\n]+)/g) || []; + const choices = choiceMatches.map((c: string) => c.replace(/^[123]\.\s*/, "")); + + adventureGameState.story = story; + adventureGameState.choices = choices; + + content.push({ + type: "text", + text: `šŸ“– **CHAPTER ${adventureGameState.currentStep}**\n\n${story}\n\n**What do you do next?**\n${choices.map((c: string, i: number) => `${i + 1}. ${c}`).join('\n')}\n\nUse 'continue' action with your choice number (1-3)` + }); + } + } + + return { content }; + } + + // Default response for invalid action + content.push({ + type: "text", + text: "āŒ Invalid action. Use 'start' to begin, 'continue' with a choice number to play, or 'reset' to start over." + }); + return { content }; + } + throw new Error(`Unknown tool: ${name}`); });