Skip to content

Commit 07d69b9

Browse files
Merge pull request #1 from chrisreddington/elicitation
Initial elicitation implementation and tool call consolidation.
2 parents 3c7de64 + 5454e71 commit 07d69b9

File tree

13 files changed

+518
-107
lines changed

13 files changed

+518
-107
lines changed
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
/**
2+
* MCP Elicitation handlers for user input collection
3+
* Provides structured ways to gather user preferences and decisions
4+
*/
5+
6+
export interface ElicitationResult {
7+
action: "accept" | "decline" | "cancel"
8+
content?: Record<string, any>
9+
}
10+
11+
/**
12+
* Game creation preferences elicitation
13+
*/
14+
export async function elicitGameCreationPreferences(
15+
server: any,
16+
gameType: string,
17+
existingArgs?: Record<string, any>
18+
): Promise<ElicitationResult> {
19+
const schemas = {
20+
'tic-tac-toe': {
21+
type: "object",
22+
properties: {
23+
difficulty: {
24+
type: "string",
25+
enum: ["easy", "medium", "hard"],
26+
title: "AI Difficulty Level",
27+
description: "How challenging should the AI opponent be?"
28+
},
29+
playerSymbol: {
30+
type: "string",
31+
enum: ["X", "O"],
32+
title: "Your Symbol",
33+
description: "Do you want to be X (goes first) or O (goes second)?",
34+
default: "X"
35+
},
36+
playerName: {
37+
type: "string",
38+
title: "Player Name",
39+
description: "What should we call you in the game?",
40+
default: "Player"
41+
}
42+
},
43+
required: ["difficulty"]
44+
},
45+
'rock-paper-scissors': {
46+
type: "object",
47+
properties: {
48+
difficulty: {
49+
type: "string",
50+
enum: ["easy", "medium", "hard"],
51+
title: "AI Difficulty Level",
52+
description: "How smart should the AI be at pattern recognition?"
53+
},
54+
maxRounds: {
55+
type: "number",
56+
minimum: 1,
57+
maximum: 10,
58+
title: "Number of Rounds",
59+
description: "How many rounds should we play?",
60+
default: 3
61+
},
62+
playerName: {
63+
type: "string",
64+
title: "Player Name",
65+
description: "What should we call you?",
66+
default: "Player"
67+
}
68+
},
69+
required: ["difficulty"]
70+
}
71+
}
72+
73+
const schema = schemas[gameType as keyof typeof schemas]
74+
if (!schema) {
75+
throw new Error(`No elicitation schema defined for game type: ${gameType}`)
76+
}
77+
78+
const message = `Let's set up your ${gameType.replace('-', ' ')} game! 🎮\n\nI'll need a few preferences to customize your experience:`
79+
80+
try {
81+
const result = await server.elicitInput({
82+
message,
83+
requestedSchema: schema
84+
})
85+
86+
return result
87+
} catch (error) {
88+
console.error('Elicitation failed:', error)
89+
// Return default preferences if elicitation fails
90+
return {
91+
action: "accept",
92+
content: {
93+
difficulty: existingArgs?.aiDifficulty || "medium",
94+
playerName: existingArgs?.playerName || "Player",
95+
...(gameType === 'rock-paper-scissors' && { maxRounds: 3 }),
96+
...(gameType === 'tic-tac-toe' && { playerSymbol: "X" })
97+
}
98+
}
99+
}
100+
}
101+
102+
/**
103+
* Mid-game decision elicitation
104+
*/
105+
export async function elicitMidGameDecision(
106+
server: any,
107+
context: {
108+
gameType: string
109+
gameId: string
110+
situation: string
111+
options: Array<{ value: string; label: string; description?: string }>
112+
}
113+
): Promise<ElicitationResult> {
114+
const { gameType, gameId, situation, options } = context
115+
116+
const schema = {
117+
type: "object",
118+
properties: {
119+
choice: {
120+
type: "string",
121+
enum: options.map(opt => opt.value),
122+
enumNames: options.map(opt => opt.label),
123+
title: "Your Choice",
124+
description: "What would you like to do?"
125+
},
126+
feedback: {
127+
type: "string",
128+
title: "Any feedback? (Optional)",
129+
description: "Let me know if you have any thoughts about the game so far"
130+
}
131+
},
132+
required: ["choice"]
133+
}
134+
135+
const message = `🤔 **${gameType.replace('-', ' ')} Game Decision**\n\n${situation}\n\nWhat would you like to do?`
136+
137+
try {
138+
return await server.elicitInput({
139+
message,
140+
requestedSchema: schema
141+
})
142+
} catch (error) {
143+
console.error('Mid-game elicitation failed:', error)
144+
// Return first option as default
145+
return {
146+
action: "accept",
147+
content: { choice: options[0]?.value || "continue" }
148+
}
149+
}
150+
}
151+
152+
/**
153+
* Game completion feedback elicitation
154+
*/
155+
export async function elicitGameCompletionFeedback(
156+
server: any,
157+
context: {
158+
gameType: string
159+
gameId: string
160+
result: 'win' | 'loss' | 'draw'
161+
aiDifficulty: string
162+
}
163+
): Promise<ElicitationResult> {
164+
const { gameType, result, aiDifficulty } = context
165+
166+
const resultMessages = {
167+
win: "🎉 Congratulations! You won!",
168+
loss: "😅 Good game! The AI won this time.",
169+
draw: "🤝 It's a draw! Well played by both sides."
170+
}
171+
172+
const schema = {
173+
type: "object",
174+
properties: {
175+
difficultyFeedback: {
176+
type: "string",
177+
enum: ["too_easy", "just_right", "too_hard"],
178+
enumNames: ["Too Easy", "Just Right", "Too Hard"],
179+
title: "How was the difficulty?",
180+
description: `The AI was set to ${aiDifficulty} difficulty`
181+
},
182+
playAgain: {
183+
type: "boolean",
184+
title: "Play another game?",
185+
description: "Would you like to start a new game?"
186+
},
187+
gameTypeForNext: {
188+
type: "string",
189+
enum: ["same", "tic-tac-toe", "rock-paper-scissors"],
190+
enumNames: ["Same Game", "Tic-Tac-Toe", "Rock Paper Scissors"],
191+
title: "If playing again, which game?",
192+
description: "Choose the game type for your next match"
193+
},
194+
comments: {
195+
type: "string",
196+
title: "Any comments? (Optional)",
197+
description: "Share your thoughts about the game experience"
198+
}
199+
},
200+
required: ["difficultyFeedback", "playAgain"]
201+
}
202+
203+
const message = `${resultMessages[result]}\n\n**Game Complete: ${gameType.replace('-', ' ')}**\n\nI'd love to get your feedback to improve future games:`
204+
205+
try {
206+
return await server.elicitInput({
207+
message,
208+
requestedSchema: schema
209+
})
210+
} catch (error) {
211+
console.error('Completion feedback elicitation failed:', error)
212+
return {
213+
action: "decline",
214+
content: {}
215+
}
216+
}
217+
}
218+
219+
/**
220+
* Strategy hint elicitation
221+
*/
222+
export async function elicitStrategyPreference(
223+
server: any,
224+
context: {
225+
gameType: string
226+
gameId: string
227+
availableHints: string[]
228+
currentSituation: string
229+
}
230+
): Promise<ElicitationResult> {
231+
const { gameType, availableHints, currentSituation } = context
232+
233+
const schema = {
234+
type: "object",
235+
properties: {
236+
wantHint: {
237+
type: "boolean",
238+
title: "Would you like a strategy hint?",
239+
description: "I can provide some strategic advice for this situation"
240+
},
241+
hintType: {
242+
type: "string",
243+
enum: ["beginner", "intermediate", "advanced"],
244+
enumNames: ["Basic Tips", "Strategic Insights", "Advanced Analysis"],
245+
title: "What level of hint?",
246+
description: "Choose the depth of strategic advice"
247+
},
248+
explainMoves: {
249+
type: "boolean",
250+
title: "Explain possible moves?",
251+
description: "Would you like me to analyze the available options?"
252+
}
253+
},
254+
required: ["wantHint"]
255+
}
256+
257+
const message = `🧠 **Strategy Assistance Available**\n\n**Current situation:** ${currentSituation}\n\nI can provide strategic guidance if you'd like:`
258+
259+
try {
260+
return await server.elicitInput({
261+
message,
262+
requestedSchema: schema
263+
})
264+
} catch (error) {
265+
console.error('Strategy elicitation failed:', error)
266+
return {
267+
action: "decline",
268+
content: { wantHint: false }
269+
}
270+
}
271+
}
272+
273+
/**
274+
* Error recovery elicitation
275+
*/
276+
export async function elicitErrorRecovery(
277+
server: any,
278+
context: {
279+
gameType: string
280+
gameId: string
281+
error: string
282+
recoveryOptions: Array<{ value: string; label: string; description: string }>
283+
}
284+
): Promise<ElicitationResult> {
285+
const { gameType, error, recoveryOptions } = context
286+
287+
const schema = {
288+
type: "object",
289+
properties: {
290+
action: {
291+
type: "string",
292+
enum: recoveryOptions.map(opt => opt.value),
293+
enumNames: recoveryOptions.map(opt => opt.label),
294+
title: "How should we handle this?",
295+
description: "Choose your preferred recovery option"
296+
},
297+
reportIssue: {
298+
type: "boolean",
299+
title: "Report this issue for improvement?",
300+
description: "Help us improve by reporting this problem"
301+
}
302+
},
303+
required: ["action"]
304+
}
305+
306+
const message = `⚠️ **${gameType.replace('-', ' ')} Game Issue**\n\n**Problem:** ${error}\n\nHow would you like to proceed?`
307+
308+
try {
309+
return await server.elicitInput({
310+
message,
311+
requestedSchema: schema
312+
})
313+
} catch (error) {
314+
console.error('Error recovery elicitation failed:', error)
315+
return {
316+
action: "accept",
317+
content: {
318+
action: recoveryOptions[0]?.value || "retry",
319+
reportIssue: false
320+
}
321+
}
322+
}
323+
}

mcp-server/src/handlers/game-operations.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,8 @@ export async function createGame(
318318
gameType: string,
319319
playerName: string = 'Player',
320320
gameId?: string,
321-
aiDifficulty: string = 'medium'
321+
aiDifficulty: string = 'medium',
322+
gameSpecificOptions?: Record<string, any>
322323
) {
323324
// Check if game already exists (for games that support custom IDs)
324325
if (gameId && gameType === 'tic-tac-toe') {
@@ -341,7 +342,7 @@ export async function createGame(
341342
}
342343

343344
// Create new game via API
344-
const gameSession = await createGameViaAPI(gameType, playerName, gameId, aiDifficulty)
345+
const gameSession = await createGameViaAPI(gameType, playerName, gameId, aiDifficulty, gameSpecificOptions)
345346

346347
const response: any = {
347348
gameId: gameSession.gameState.id,

mcp-server/src/handlers/prompt-handlers.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ describe('Prompt Handlers', () => {
6969
expect(message.content.type).toBe('text')
7070
expect(message.content.text).toContain('Please explain how to play Tic-Tac-Toe')
7171
expect(message.content.text).toContain('objective of the game')
72-
expect(message.content.text).toContain('create_tic_tac_toe_game')
72+
expect(message.content.text).toContain("create_game with gameType: 'tic-tac-toe'")
7373
})
7474

7575
it('should return rock-paper-scissors rules', async () => {
@@ -78,7 +78,7 @@ describe('Prompt Handlers', () => {
7878
expect(result.messages[0].role).toBe('user')
7979
expect(result.messages[0].content.text).toContain('Please explain how to play Rock Paper Scissors')
8080
expect(result.messages[0].content.text).toContain('what beats what')
81-
expect(result.messages[0].content.text).toContain('create_rock_paper_scissors_game')
81+
expect(result.messages[0].content.text).toContain("create_game with gameType: 'rock-paper-scissors'")
8282
})
8383
})
8484

@@ -248,8 +248,8 @@ describe('Prompt Handlers', () => {
248248
const content = result.messages[0].content.text
249249

250250
const gameType = gameName.replace('_rules', '').replace(/_/g, '-')
251-
expect(content).toContain(`create_${gameName.replace('_rules', '_game')}`)
252-
expect(content).toContain(`play_${gameName.replace('_rules', '')}`)
251+
expect(content).toContain(`create_game with gameType: '${gameType}'`)
252+
expect(content).toContain(`play_game with gameType: '${gameType}'`)
253253
}
254254
})
255255

mcp-server/src/handlers/prompt-handlers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export const GAME_RULES_PROMPTS: PromptDefinition[] = [
4040
3. How to make moves (using positions 1-9)
4141
4. All possible winning conditions
4242
5. Basic strategy tips for beginners
43-
6. How to use the MCP commands (create_tic_tac_toe_game, play_tic_tac_toe, wait_for_player_move)
43+
6. How to use the MCP commands (create_game with gameType: 'tic-tac-toe', play_game with gameType: 'tic-tac-toe', wait_for_player_move)
4444
7. What happens with perfect play
4545
4646
Make it comprehensive but easy to understand for someone who has never played before.`
@@ -66,7 +66,7 @@ Make it comprehensive but easy to understand for someone who has never played be
6666
3. Strategy tips for beginners and advanced players
6767
4. How psychology and pattern recognition work in this game
6868
5. What the different AI difficulty levels mean and how to counter them
69-
6. How to use the MCP commands (create_rock_paper_scissors_game, play_rock_paper_scissors, wait_for_player_move)
69+
6. How to use the MCP commands (create_game with gameType: 'rock-paper-scissors', play_game with gameType: 'rock-paper-scissors', wait_for_player_move)
7070
7. Why unpredictability is key to mastery
7171
7272
Make it comprehensive and include both basic rules and advanced psychological strategies.`

0 commit comments

Comments
 (0)