Skip to content

Commit 6644741

Browse files
Fix "smart" elicitation
1 parent de6ddda commit 6644741

File tree

14 files changed

+96
-40
lines changed

14 files changed

+96
-40
lines changed

.vscode/mcp.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"servers": {
33
"turn-based-games": {
44
"command": "node",
5-
"args": ["dist/index.js"],
5+
"args": ["dist/server.js"],
66
"cwd": "./mcp-server"
77
},
88
"playwright": {

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

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,30 +72,65 @@ export async function elicitGameCreationPreferences(
7272
}
7373
}
7474

75-
const schema = schemas[gameType as keyof typeof schemas]
76-
if (!schema) {
75+
const baseSchema = schemas[gameType as keyof typeof schemas]
76+
if (!baseSchema) {
7777
throw new Error(`No elicitation schema defined for game type: ${gameType}`)
7878
}
7979

80+
// Filter out properties that are already provided
81+
const filteredSchema: any = { ...baseSchema }
82+
const filteredProperties: Record<string, any> = { ...baseSchema.properties }
83+
const filteredRequired = [...(baseSchema.required || [])]
84+
85+
// Remove properties that already have values
86+
if (existingArgs) {
87+
Object.keys(existingArgs).forEach(key => {
88+
if (existingArgs[key] !== undefined && existingArgs[key] !== null && existingArgs[key] !== '') {
89+
delete filteredProperties[key]
90+
// Remove from required array if present
91+
const requiredIndex = filteredRequired.indexOf(key)
92+
if (requiredIndex > -1) {
93+
filteredRequired.splice(requiredIndex, 1)
94+
}
95+
}
96+
})
97+
}
98+
99+
filteredSchema.properties = filteredProperties
100+
filteredSchema.required = filteredRequired
101+
102+
// If no properties remain to be elicited, skip elicitation
103+
if (Object.keys(filteredProperties).length === 0) {
104+
return {
105+
action: "accept",
106+
content: existingArgs || {}
107+
}
108+
}
109+
80110
const message = `Let's set up your ${gameType.replace('-', ' ')} game! 🎮\n\nI'll need a few preferences to customize your experience:`
81111

82112
try {
83113
const result = await server.elicitInput({
84114
message,
85-
requestedSchema: schema
115+
requestedSchema: filteredSchema
86116
})
87117

118+
// Merge the elicitation result with existing arguments
119+
if (result.action === 'accept' && result.content) {
120+
result.content = { ...existingArgs, ...result.content }
121+
}
122+
88123
return result
89124
} catch (error) {
90125
console.error('Elicitation failed:', error)
91126
// Return default preferences if elicitation fails
92127
return {
93128
action: "accept",
94129
content: {
95-
difficulty: existingArgs?.aiDifficulty || DEFAULT_AI_DIFFICULTY,
130+
difficulty: existingArgs?.difficulty || DEFAULT_AI_DIFFICULTY,
96131
playerName: existingArgs?.playerName || DEFAULT_PLAYER_NAME,
97-
...(gameType === 'rock-paper-scissors' && { maxRounds: 3 }),
98-
...(gameType === 'tic-tac-toe' && { playerSymbol: "X" })
132+
...(gameType === 'rock-paper-scissors' && { maxRounds: existingArgs?.maxRounds || 3 }),
133+
...(gameType === 'tic-tac-toe' && { playerSymbol: existingArgs?.playerSymbol || "X" })
99134
}
100135
}
101136
}
@@ -160,10 +195,10 @@ export async function elicitGameCompletionFeedback(
160195
gameType: string
161196
gameId: string
162197
result: 'win' | 'loss' | 'draw'
163-
aiDifficulty: string
198+
difficulty: string
164199
}
165200
): Promise<ElicitationResult> {
166-
const { gameType, result, aiDifficulty } = context
201+
const { gameType, result, difficulty } = context
167202

168203
const resultMessages = {
169204
win: "🎉 Congratulations! You won!",
@@ -179,7 +214,7 @@ export async function elicitGameCompletionFeedback(
179214
enum: ["too_easy", "just_right", "too_hard"],
180215
enumNames: ["Too Easy", "Just Right", "Too Hard"],
181216
title: "How was the difficulty?",
182-
description: `The AI was set to ${aiDifficulty} difficulty`
217+
description: `The AI was set to ${difficulty} difficulty`
183218
},
184219
playAgain: {
185220
type: "boolean",

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export async function playGame(gameType: string, gameId: string) {
3838
const gameSession = await readGameResource(gameType, gameId)
3939

4040
// Use the difficulty stored in the game session, or fall back to medium
41-
const difficulty = gameSession.aiDifficulty || 'medium'
41+
const difficulty = gameSession.difficulty || 'medium'
4242

4343
// Check if it's AI's turn
4444
if (gameSession.gameState.currentPlayerId !== 'ai') {
@@ -319,7 +319,7 @@ export async function createGame(
319319
gameType: string,
320320
playerName: string = DEFAULT_PLAYER_NAME,
321321
gameId?: string,
322-
aiDifficulty: string = DEFAULT_AI_DIFFICULTY,
322+
difficulty: string = DEFAULT_AI_DIFFICULTY,
323323
gameSpecificOptions?: Record<string, any>
324324
) {
325325
// Check if game already exists (for games that support custom IDs)
@@ -343,7 +343,7 @@ export async function createGame(
343343
}
344344

345345
// Create new game via API
346-
const gameSession = await createGameViaAPI(gameType, playerName, gameId, aiDifficulty, gameSpecificOptions)
346+
const gameSession = await createGameViaAPI(gameType, playerName, gameId, difficulty, gameSpecificOptions)
347347

348348
const response: any = {
349349
gameId: gameSession.gameState.id,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export async function readResource(uri: string) {
109109
createdAt: game.gameState?.createdAt,
110110
updatedAt: game.gameState?.updatedAt,
111111
playerCount: Object.keys(game.gameState?.players || {}).length,
112-
aiDifficulty: game.aiDifficulty
112+
difficulty: game.difficulty
113113
})),
114114
totalGames: games.length,
115115
timestamp: new Date().toISOString()

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

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,26 @@ export const TOOL_DEFINITIONS = [
8888
gameId: {
8989
type: 'string',
9090
description: 'Optional custom game ID. If not provided, a random UUID will be generated.'
91+
},
92+
difficulty: {
93+
type: 'string',
94+
enum: DIFFICULTIES,
95+
description: 'AI difficulty level (easy, medium, hard). If not provided, will be asked during setup.'
96+
},
97+
playerName: {
98+
type: 'string',
99+
description: 'Your name in the game. If not provided, will be asked during setup.'
100+
},
101+
playerSymbol: {
102+
type: 'string',
103+
enum: ['X', 'O'],
104+
description: 'For tic-tac-toe: your symbol (X goes first, O goes second). If not provided, will be asked during setup.'
105+
},
106+
maxRounds: {
107+
type: 'number',
108+
minimum: 1,
109+
maximum: 10,
110+
description: 'For rock-paper-scissors: number of rounds to play. If not provided, will be asked during setup.'
91111
}
92112
},
93113
required: ['gameType']
@@ -147,7 +167,7 @@ export async function handleToolCall(name: string, args: any, server?: any) {
147167
if (!isSupportedGameType(genericGameType)) {
148168
throw new Error(`Unsupported game type: ${genericGameType}`)
149169
}
150-
return await createGameWithElicitation(genericGameType, genericGameId, server)
170+
return await createGameWithElicitation(genericGameType, genericGameId, server, args)
151171

152172
default:
153173
throw new Error(`Unknown tool: ${name}`)
@@ -160,7 +180,7 @@ export async function handleToolCall(name: string, args: any, server?: any) {
160180
/**
161181
* Create game with interactive elicitation
162182
*/
163-
async function createGameWithElicitation(gameType: string, gameId?: string, server?: any) {
183+
async function createGameWithElicitation(gameType: string, gameId?: string, server?: any, toolArgs?: any) {
164184
if (!server) {
165185
// Fallback to regular creation if no server for elicitation
166186
return await createGame(gameType, DEFAULT_PLAYER_NAME, gameId, DEFAULT_AI_DIFFICULTY)
@@ -170,8 +190,10 @@ async function createGameWithElicitation(gameType: string, gameId?: string, serv
170190
// Elicit user preferences
171191
const elicitationResult = await elicitGameCreationPreferences(server, gameType, {
172192
gameId,
173-
playerName: 'Player',
174-
aiDifficulty: 'medium'
193+
playerName: toolArgs?.playerName,
194+
difficulty: toolArgs?.difficulty,
195+
playerSymbol: toolArgs?.playerSymbol,
196+
maxRounds: toolArgs?.maxRounds
175197
})
176198

177199
if (elicitationResult.action === 'decline' || elicitationResult.action === 'cancel') {

mcp-server/src/integration/integration.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describe('MCP Server Integration', () => {
8888
currentPlayerId: 'player1',
8989
players: { player1: 'Human', ai: 'AI' }
9090
},
91-
aiDifficulty: 'medium'
91+
difficulty: 'medium'
9292
}
9393
])
9494

@@ -154,7 +154,7 @@ describe('MCP Server Integration', () => {
154154
status: 'playing',
155155
currentPlayerId: 'ai'
156156
},
157-
aiDifficulty: 'medium'
157+
difficulty: 'medium'
158158
})
159159

160160
mockSubmitMoveViaAPI.mockResolvedValue({

mcp-server/src/utils/http-client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,16 @@ export async function createGameViaAPI(
2727
gameType: string,
2828
playerName: string,
2929
gameId?: string,
30-
aiDifficulty?: string,
30+
difficulty?: string,
3131
gameSpecificOptions?: Record<string, any>
3232
) {
3333
try {
3434
const data: any = { playerName }
3535
if (gameId) {
3636
data.gameId = gameId
3737
}
38-
if (aiDifficulty) {
39-
data.aiDifficulty = aiDifficulty
38+
if (difficulty) {
39+
data.difficulty = difficulty
4040
}
4141
// Merge any game-specific options
4242
if (gameSpecificOptions) {

shared/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,4 @@ export type * from './types/games';
33
export * from './games/index';
44
export * from './utils/index';
55
export * from './storage/index';
6-
export * from './testing/index';
76
export * from './constants/index';

shared/src/testing/api-test-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ export function createMockGameSession<T extends BaseGameState>(gameState: T, gam
8787
gameState,
8888
gameType,
8989
history: [],
90-
aiDifficulty: 'medium'
90+
difficulty: 'medium'
9191
}
9292
}
9393

shared/src/types/game.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,5 @@ export interface GameSession<TGameState extends BaseGameState = BaseGameState> {
4444
gameState: TGameState;
4545
gameType: GameType;
4646
history: GameMove[];
47-
aiDifficulty?: Difficulty;
47+
difficulty?: Difficulty;
4848
}

0 commit comments

Comments
 (0)