From 458224cb61f00ad3b5ba06a2da2bbfd1108c6ef1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 2 Jul 2025 14:32:06 +0000 Subject: [PATCH 1/6] Update test files with proactive checks and expanded test scenarios Co-authored-by: me --- .../01.problem.simple/src/index.test.ts | 13 +--- .../02.problem.args/src/index.test.ts | 36 ++++++++++ .../02.solution.args/src/index.test.ts | 36 ++++++++++ .../03.problem.errors/src/index.test.ts | 60 +++++++++++++++- .../03.solution.errors/src/index.test.ts | 60 +++++++++++++++- .../01.problem.simple/src/index.test.ts | 59 ++++++++++++++++ .../01.solution.simple/src/index.test.ts | 59 ++++++++++++++++ .../01.problem.prompts/src/index.test.ts | 68 +++++++++++++++++++ .../01.solution.prompts/src/index.test.ts | 68 +++++++++++++++++++ .../02.problem.advanced/src/index.test.ts | 26 +++++++ .../02.solution.advanced/src/index.test.ts | 26 +++++++ 11 files changed, 498 insertions(+), 13 deletions(-) diff --git a/exercises/02.tools/01.problem.simple/src/index.test.ts b/exercises/02.tools/01.problem.simple/src/index.test.ts index 31b23ca..8361c5c 100644 --- a/exercises/02.tools/01.problem.simple/src/index.test.ts +++ b/exercises/02.tools/01.problem.simple/src/index.test.ts @@ -29,15 +29,9 @@ test('Tool Definition', async () => { expect(firstTool).toEqual( expect.objectContaining({ name: expect.stringMatching(/^add$/i), - description: expect.stringMatching(/^add two numbers$/i), + description: expect.stringMatching(/add/i), inputSchema: expect.objectContaining({ type: 'object', - properties: expect.objectContaining({ - firstNumber: expect.objectContaining({ - type: 'number', - description: expect.stringMatching(/first/i), - }), - }), }), }), ) @@ -46,10 +40,7 @@ test('Tool Definition', async () => { test('Tool Call', async () => { const result = await client.callTool({ name: 'add', - arguments: { - firstNumber: 1, - secondNumber: 2, - }, + arguments: {}, }) expect(result).toEqual( diff --git a/exercises/02.tools/02.problem.args/src/index.test.ts b/exercises/02.tools/02.problem.args/src/index.test.ts index 31b23ca..46185ae 100644 --- a/exercises/02.tools/02.problem.args/src/index.test.ts +++ b/exercises/02.tools/02.problem.args/src/index.test.ts @@ -37,10 +37,25 @@ test('Tool Definition', async () => { type: 'number', description: expect.stringMatching(/first/i), }), + secondNumber: expect.objectContaining({ + type: 'number', + description: expect.stringMatching(/second/i), + }), }), + required: expect.arrayContaining(['firstNumber', 'secondNumber']), }), }), ) + + // 🚨 Proactive check: Ensure the tool schema includes both required arguments + invariant( + firstTool.inputSchema?.properties?.firstNumber, + '🚨 Tool must have firstNumber parameter defined' + ) + invariant( + firstTool.inputSchema?.properties?.secondNumber, + '🚨 Tool must have secondNumber parameter defined' + ) }) test('Tool Call', async () => { @@ -63,3 +78,24 @@ test('Tool Call', async () => { }), ) }) + +test('Tool Call with Different Numbers', async () => { + const result = await client.callTool({ + name: 'add', + arguments: { + firstNumber: 5, + secondNumber: 7, + }, + }) + + expect(result).toEqual( + expect.objectContaining({ + content: expect.arrayContaining([ + expect.objectContaining({ + type: 'text', + text: expect.stringMatching(/12/), + }), + ]), + }), + ) +}) diff --git a/exercises/02.tools/02.solution.args/src/index.test.ts b/exercises/02.tools/02.solution.args/src/index.test.ts index 31b23ca..46185ae 100644 --- a/exercises/02.tools/02.solution.args/src/index.test.ts +++ b/exercises/02.tools/02.solution.args/src/index.test.ts @@ -37,10 +37,25 @@ test('Tool Definition', async () => { type: 'number', description: expect.stringMatching(/first/i), }), + secondNumber: expect.objectContaining({ + type: 'number', + description: expect.stringMatching(/second/i), + }), }), + required: expect.arrayContaining(['firstNumber', 'secondNumber']), }), }), ) + + // 🚨 Proactive check: Ensure the tool schema includes both required arguments + invariant( + firstTool.inputSchema?.properties?.firstNumber, + '🚨 Tool must have firstNumber parameter defined' + ) + invariant( + firstTool.inputSchema?.properties?.secondNumber, + '🚨 Tool must have secondNumber parameter defined' + ) }) test('Tool Call', async () => { @@ -63,3 +78,24 @@ test('Tool Call', async () => { }), ) }) + +test('Tool Call with Different Numbers', async () => { + const result = await client.callTool({ + name: 'add', + arguments: { + firstNumber: 5, + secondNumber: 7, + }, + }) + + expect(result).toEqual( + expect.objectContaining({ + content: expect.arrayContaining([ + expect.objectContaining({ + type: 'text', + text: expect.stringMatching(/12/), + }), + ]), + }), + ) +}) diff --git a/exercises/02.tools/03.problem.errors/src/index.test.ts b/exercises/02.tools/03.problem.errors/src/index.test.ts index 31b23ca..7f0decd 100644 --- a/exercises/02.tools/03.problem.errors/src/index.test.ts +++ b/exercises/02.tools/03.problem.errors/src/index.test.ts @@ -37,13 +37,28 @@ test('Tool Definition', async () => { type: 'number', description: expect.stringMatching(/first/i), }), + secondNumber: expect.objectContaining({ + type: 'number', + description: expect.stringMatching(/second/i), + }), }), + required: expect.arrayContaining(['firstNumber', 'secondNumber']), }), }), ) + + // 🚨 Proactive check: Ensure the tool schema includes both required arguments + invariant( + firstTool.inputSchema?.properties?.firstNumber, + '🚨 Tool must have firstNumber parameter defined' + ) + invariant( + firstTool.inputSchema?.properties?.secondNumber, + '🚨 Tool must have secondNumber parameter defined' + ) }) -test('Tool Call', async () => { +test('Tool Call - Successful Addition', async () => { const result = await client.callTool({ name: 'add', arguments: { @@ -63,3 +78,46 @@ test('Tool Call', async () => { }), ) }) + +test('Tool Call - Error with Negative Second Number', async () => { + const result = await client.callTool({ + name: 'add', + arguments: { + firstNumber: 5, + secondNumber: -3, + }, + }) + + expect(result).toEqual( + expect.objectContaining({ + content: expect.arrayContaining([ + expect.objectContaining({ + type: 'text', + text: expect.stringMatching(/negative/i), + }), + ]), + isError: true, + }), + ) +}) + +test('Tool Call - Another Successful Addition', async () => { + const result = await client.callTool({ + name: 'add', + arguments: { + firstNumber: 10, + secondNumber: 5, + }, + }) + + expect(result).toEqual( + expect.objectContaining({ + content: expect.arrayContaining([ + expect.objectContaining({ + type: 'text', + text: expect.stringMatching(/15/), + }), + ]), + }), + ) +}) diff --git a/exercises/02.tools/03.solution.errors/src/index.test.ts b/exercises/02.tools/03.solution.errors/src/index.test.ts index 31b23ca..7f0decd 100644 --- a/exercises/02.tools/03.solution.errors/src/index.test.ts +++ b/exercises/02.tools/03.solution.errors/src/index.test.ts @@ -37,13 +37,28 @@ test('Tool Definition', async () => { type: 'number', description: expect.stringMatching(/first/i), }), + secondNumber: expect.objectContaining({ + type: 'number', + description: expect.stringMatching(/second/i), + }), }), + required: expect.arrayContaining(['firstNumber', 'secondNumber']), }), }), ) + + // 🚨 Proactive check: Ensure the tool schema includes both required arguments + invariant( + firstTool.inputSchema?.properties?.firstNumber, + '🚨 Tool must have firstNumber parameter defined' + ) + invariant( + firstTool.inputSchema?.properties?.secondNumber, + '🚨 Tool must have secondNumber parameter defined' + ) }) -test('Tool Call', async () => { +test('Tool Call - Successful Addition', async () => { const result = await client.callTool({ name: 'add', arguments: { @@ -63,3 +78,46 @@ test('Tool Call', async () => { }), ) }) + +test('Tool Call - Error with Negative Second Number', async () => { + const result = await client.callTool({ + name: 'add', + arguments: { + firstNumber: 5, + secondNumber: -3, + }, + }) + + expect(result).toEqual( + expect.objectContaining({ + content: expect.arrayContaining([ + expect.objectContaining({ + type: 'text', + text: expect.stringMatching(/negative/i), + }), + ]), + isError: true, + }), + ) +}) + +test('Tool Call - Another Successful Addition', async () => { + const result = await client.callTool({ + name: 'add', + arguments: { + firstNumber: 10, + secondNumber: 5, + }, + }) + + expect(result).toEqual( + expect.objectContaining({ + content: expect.arrayContaining([ + expect.objectContaining({ + type: 'text', + text: expect.stringMatching(/15/), + }), + ]), + }), + ) +}) diff --git a/exercises/03.resources/01.problem.simple/src/index.test.ts b/exercises/03.resources/01.problem.simple/src/index.test.ts index 2c0c5b4..fbd5768 100644 --- a/exercises/03.resources/01.problem.simple/src/index.test.ts +++ b/exercises/03.resources/01.problem.simple/src/index.test.ts @@ -1,11 +1,16 @@ +import fs from 'node:fs/promises' +import path from 'node:path' import { invariant } from '@epic-web/invariant' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { test, beforeAll, afterAll, expect } from 'vitest' let client: Client +const EPIC_ME_DB_PATH = `./test.ignored/db.${process.env.VITEST_WORKER_ID}.sqlite` beforeAll(async () => { + const dir = path.dirname(EPIC_ME_DB_PATH) + await fs.mkdir(dir, { recursive: true }) client = new Client({ name: 'EpicMeTester', version: '1.0.0', @@ -13,12 +18,17 @@ beforeAll(async () => { const transport = new StdioClientTransport({ command: 'tsx', args: ['src/index.ts'], + env: { + ...process.env, + EPIC_ME_DB_PATH, + }, }) await client.connect(transport) }) afterAll(async () => { await client.transport?.close() + await fs.unlink(EPIC_ME_DB_PATH) }) test('Tool Definition', async () => { @@ -69,3 +79,52 @@ test('Tool Call', async () => { }), ) }) + +test('Resource List', async () => { + const list = await client.listResources() + const tagsResource = list.resources.find(r => r.name === 'tags') + + // 🚨 Proactive check: Ensure the tags resource is registered + invariant(tagsResource, '🚨 No "tags" resource found - make sure to register the tags resource') + + expect(tagsResource).toEqual( + expect.objectContaining({ + name: 'tags', + uri: expect.stringMatching(/^epicme:\/\/tags$/i), + description: expect.stringMatching(/tags/i), + }), + ) +}) + +test('Tags Resource Read', async () => { + const result = await client.readResource({ + uri: 'epicme://tags', + }) + + expect(result).toEqual( + expect.objectContaining({ + contents: expect.arrayContaining([ + expect.objectContaining({ + mimeType: 'application/json', + uri: 'epicme://tags', + text: expect.any(String), + }), + ]), + }), + ) + + // 🚨 Proactive check: Ensure the resource content is valid JSON + const content = result.contents[0] + invariant(content && 'text' in content, '🚨 Resource content must have text field') + invariant(typeof content.text === 'string', '🚨 Resource content text must be a string') + + let tags: unknown + try { + tags = JSON.parse(content.text) + } catch (error) { + throw new Error('🚨 Resource content must be valid JSON') + } + + // 🚨 Proactive check: Ensure tags is an array + invariant(Array.isArray(tags), '🚨 Tags resource should return an array of tags') +}) diff --git a/exercises/03.resources/01.solution.simple/src/index.test.ts b/exercises/03.resources/01.solution.simple/src/index.test.ts index 2c0c5b4..fbd5768 100644 --- a/exercises/03.resources/01.solution.simple/src/index.test.ts +++ b/exercises/03.resources/01.solution.simple/src/index.test.ts @@ -1,11 +1,16 @@ +import fs from 'node:fs/promises' +import path from 'node:path' import { invariant } from '@epic-web/invariant' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { test, beforeAll, afterAll, expect } from 'vitest' let client: Client +const EPIC_ME_DB_PATH = `./test.ignored/db.${process.env.VITEST_WORKER_ID}.sqlite` beforeAll(async () => { + const dir = path.dirname(EPIC_ME_DB_PATH) + await fs.mkdir(dir, { recursive: true }) client = new Client({ name: 'EpicMeTester', version: '1.0.0', @@ -13,12 +18,17 @@ beforeAll(async () => { const transport = new StdioClientTransport({ command: 'tsx', args: ['src/index.ts'], + env: { + ...process.env, + EPIC_ME_DB_PATH, + }, }) await client.connect(transport) }) afterAll(async () => { await client.transport?.close() + await fs.unlink(EPIC_ME_DB_PATH) }) test('Tool Definition', async () => { @@ -69,3 +79,52 @@ test('Tool Call', async () => { }), ) }) + +test('Resource List', async () => { + const list = await client.listResources() + const tagsResource = list.resources.find(r => r.name === 'tags') + + // 🚨 Proactive check: Ensure the tags resource is registered + invariant(tagsResource, '🚨 No "tags" resource found - make sure to register the tags resource') + + expect(tagsResource).toEqual( + expect.objectContaining({ + name: 'tags', + uri: expect.stringMatching(/^epicme:\/\/tags$/i), + description: expect.stringMatching(/tags/i), + }), + ) +}) + +test('Tags Resource Read', async () => { + const result = await client.readResource({ + uri: 'epicme://tags', + }) + + expect(result).toEqual( + expect.objectContaining({ + contents: expect.arrayContaining([ + expect.objectContaining({ + mimeType: 'application/json', + uri: 'epicme://tags', + text: expect.any(String), + }), + ]), + }), + ) + + // 🚨 Proactive check: Ensure the resource content is valid JSON + const content = result.contents[0] + invariant(content && 'text' in content, '🚨 Resource content must have text field') + invariant(typeof content.text === 'string', '🚨 Resource content text must be a string') + + let tags: unknown + try { + tags = JSON.parse(content.text) + } catch (error) { + throw new Error('🚨 Resource content must be valid JSON') + } + + // 🚨 Proactive check: Ensure tags is an array + invariant(Array.isArray(tags), '🚨 Tags resource should return an array of tags') +}) diff --git a/exercises/04.prompts/01.problem.prompts/src/index.test.ts b/exercises/04.prompts/01.problem.prompts/src/index.test.ts index 2c0c5b4..5ad2ae7 100644 --- a/exercises/04.prompts/01.problem.prompts/src/index.test.ts +++ b/exercises/04.prompts/01.problem.prompts/src/index.test.ts @@ -1,11 +1,16 @@ +import fs from 'node:fs/promises' +import path from 'node:path' import { invariant } from '@epic-web/invariant' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { test, beforeAll, afterAll, expect } from 'vitest' let client: Client +const EPIC_ME_DB_PATH = `./test.ignored/db.${process.env.VITEST_WORKER_ID}.sqlite` beforeAll(async () => { + const dir = path.dirname(EPIC_ME_DB_PATH) + await fs.mkdir(dir, { recursive: true }) client = new Client({ name: 'EpicMeTester', version: '1.0.0', @@ -13,12 +18,17 @@ beforeAll(async () => { const transport = new StdioClientTransport({ command: 'tsx', args: ['src/index.ts'], + env: { + ...process.env, + EPIC_ME_DB_PATH, + }, }) await client.connect(transport) }) afterAll(async () => { await client.transport?.close() + await fs.unlink(EPIC_ME_DB_PATH) }) test('Tool Definition', async () => { @@ -69,3 +79,61 @@ test('Tool Call', async () => { }), ) }) + +test('Prompts List', async () => { + const list = await client.listPrompts() + + // 🚨 Proactive check: Ensure prompts are registered + invariant(list.prompts.length > 0, '🚨 No prompts found - make sure to register prompts with the prompts capability') + + const tagSuggestionsPrompt = list.prompts.find(p => p.name.includes('tag') || p.name.includes('suggest')) + invariant(tagSuggestionsPrompt, '🚨 No tag suggestions prompt found - should include a prompt for suggesting tags') + + expect(tagSuggestionsPrompt).toEqual( + expect.objectContaining({ + name: expect.any(String), + description: expect.stringMatching(/tag|suggest/i), + arguments: expect.arrayContaining([ + expect.objectContaining({ + name: expect.stringMatching(/entry|id/i), + description: expect.any(String), + required: true, + }), + ]), + }), + ) +}) + +test('Prompt Get', async () => { + const list = await client.listPrompts() + const firstPrompt = list.prompts[0] + invariant(firstPrompt, '🚨 No prompts available to test') + + const result = await client.getPrompt({ + name: firstPrompt.name, + arguments: { + entryId: '1', + }, + }) + + expect(result).toEqual( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: expect.stringMatching(/user|system/), + content: expect.objectContaining({ + type: 'text', + text: expect.any(String), + }), + }), + ]), + }), + ) + + // 🚨 Proactive check: Ensure prompt contains meaningful content + invariant(result.messages.length > 0, '🚨 Prompt should contain at least one message') + const firstMessage = result.messages[0] + invariant(firstMessage, '🚨 First message should exist') + invariant(typeof firstMessage.content.text === 'string', '🚨 Message content text should be a string') + invariant(firstMessage.content.text.length > 10, '🚨 Prompt message should be more than just a placeholder') +}) diff --git a/exercises/04.prompts/01.solution.prompts/src/index.test.ts b/exercises/04.prompts/01.solution.prompts/src/index.test.ts index 2c0c5b4..5ad2ae7 100644 --- a/exercises/04.prompts/01.solution.prompts/src/index.test.ts +++ b/exercises/04.prompts/01.solution.prompts/src/index.test.ts @@ -1,11 +1,16 @@ +import fs from 'node:fs/promises' +import path from 'node:path' import { invariant } from '@epic-web/invariant' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { test, beforeAll, afterAll, expect } from 'vitest' let client: Client +const EPIC_ME_DB_PATH = `./test.ignored/db.${process.env.VITEST_WORKER_ID}.sqlite` beforeAll(async () => { + const dir = path.dirname(EPIC_ME_DB_PATH) + await fs.mkdir(dir, { recursive: true }) client = new Client({ name: 'EpicMeTester', version: '1.0.0', @@ -13,12 +18,17 @@ beforeAll(async () => { const transport = new StdioClientTransport({ command: 'tsx', args: ['src/index.ts'], + env: { + ...process.env, + EPIC_ME_DB_PATH, + }, }) await client.connect(transport) }) afterAll(async () => { await client.transport?.close() + await fs.unlink(EPIC_ME_DB_PATH) }) test('Tool Definition', async () => { @@ -69,3 +79,61 @@ test('Tool Call', async () => { }), ) }) + +test('Prompts List', async () => { + const list = await client.listPrompts() + + // 🚨 Proactive check: Ensure prompts are registered + invariant(list.prompts.length > 0, '🚨 No prompts found - make sure to register prompts with the prompts capability') + + const tagSuggestionsPrompt = list.prompts.find(p => p.name.includes('tag') || p.name.includes('suggest')) + invariant(tagSuggestionsPrompt, '🚨 No tag suggestions prompt found - should include a prompt for suggesting tags') + + expect(tagSuggestionsPrompt).toEqual( + expect.objectContaining({ + name: expect.any(String), + description: expect.stringMatching(/tag|suggest/i), + arguments: expect.arrayContaining([ + expect.objectContaining({ + name: expect.stringMatching(/entry|id/i), + description: expect.any(String), + required: true, + }), + ]), + }), + ) +}) + +test('Prompt Get', async () => { + const list = await client.listPrompts() + const firstPrompt = list.prompts[0] + invariant(firstPrompt, '🚨 No prompts available to test') + + const result = await client.getPrompt({ + name: firstPrompt.name, + arguments: { + entryId: '1', + }, + }) + + expect(result).toEqual( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: expect.stringMatching(/user|system/), + content: expect.objectContaining({ + type: 'text', + text: expect.any(String), + }), + }), + ]), + }), + ) + + // 🚨 Proactive check: Ensure prompt contains meaningful content + invariant(result.messages.length > 0, '🚨 Prompt should contain at least one message') + const firstMessage = result.messages[0] + invariant(firstMessage, '🚨 First message should exist') + invariant(typeof firstMessage.content.text === 'string', '🚨 Message content text should be a string') + invariant(firstMessage.content.text.length > 10, '🚨 Prompt message should be more than just a placeholder') +}) diff --git a/exercises/05.sampling/02.problem.advanced/src/index.test.ts b/exercises/05.sampling/02.problem.advanced/src/index.test.ts index d63c950..0853f05 100644 --- a/exercises/05.sampling/02.problem.advanced/src/index.test.ts +++ b/exercises/05.sampling/02.problem.advanced/src/index.test.ts @@ -152,6 +152,32 @@ test('Sampling', async () => { }), ) + // 🚨 Proactive checks for advanced sampling requirements + const params = request.params + invariant(params && 'maxTokens' in params, '🚨 maxTokens parameter is required') + invariant(params.maxTokens > 50, '🚨 maxTokens should be increased for longer responses (>50)') + + invariant(params && 'systemPrompt' in params, '🚨 systemPrompt is required') + invariant(typeof params.systemPrompt === 'string', '🚨 systemPrompt must be a string') + + invariant(params && 'messages' in params && Array.isArray(params.messages), '🚨 messages array is required') + const userMessage = params.messages.find(m => m.role === 'user') + invariant(userMessage, '🚨 User message is required') + invariant(userMessage.content.mimeType === 'application/json', '🚨 Content should be JSON for structured data') + + // 🚨 Validate the JSON structure contains required fields + invariant(typeof userMessage.content.text === 'string', '🚨 User message content text must be a string') + let messageData: any + try { + messageData = JSON.parse(userMessage.content.text) + } catch (error) { + throw new Error('🚨 User message content must be valid JSON') + } + + invariant(messageData.entry, '🚨 JSON should contain entry data') + invariant(messageData.existingTags, '🚨 JSON should contain existingTags for context') + invariant(Array.isArray(messageData.existingTags), '🚨 existingTags should be an array') + messageResultDeferred.resolve({ model: 'stub-model', stopReason: 'endTurn', diff --git a/exercises/05.sampling/02.solution.advanced/src/index.test.ts b/exercises/05.sampling/02.solution.advanced/src/index.test.ts index d63c950..0853f05 100644 --- a/exercises/05.sampling/02.solution.advanced/src/index.test.ts +++ b/exercises/05.sampling/02.solution.advanced/src/index.test.ts @@ -152,6 +152,32 @@ test('Sampling', async () => { }), ) + // 🚨 Proactive checks for advanced sampling requirements + const params = request.params + invariant(params && 'maxTokens' in params, '🚨 maxTokens parameter is required') + invariant(params.maxTokens > 50, '🚨 maxTokens should be increased for longer responses (>50)') + + invariant(params && 'systemPrompt' in params, '🚨 systemPrompt is required') + invariant(typeof params.systemPrompt === 'string', '🚨 systemPrompt must be a string') + + invariant(params && 'messages' in params && Array.isArray(params.messages), '🚨 messages array is required') + const userMessage = params.messages.find(m => m.role === 'user') + invariant(userMessage, '🚨 User message is required') + invariant(userMessage.content.mimeType === 'application/json', '🚨 Content should be JSON for structured data') + + // 🚨 Validate the JSON structure contains required fields + invariant(typeof userMessage.content.text === 'string', '🚨 User message content text must be a string') + let messageData: any + try { + messageData = JSON.parse(userMessage.content.text) + } catch (error) { + throw new Error('🚨 User message content must be valid JSON') + } + + invariant(messageData.entry, '🚨 JSON should contain entry data') + invariant(messageData.existingTags, '🚨 JSON should contain existingTags for context') + invariant(Array.isArray(messageData.existingTags), '🚨 existingTags should be an array') + messageResultDeferred.resolve({ model: 'stub-model', stopReason: 'endTurn', From b29fddaed2e25d7a94c184fb558ff975c86a7604 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 2 Jul 2025 14:51:16 +0000 Subject: [PATCH 2/6] Add test infrastructure for temporary SQLite database in test files Co-authored-by: me --- .../02.problem.template/src/index.test.ts | 88 +++++++++++++++ .../02.solution.template/src/index.test.ts | 88 +++++++++++++++ .../03.problem.list/src/index.test.ts | 101 +++++++++++++++++ .../03.solution.list/src/index.test.ts | 101 +++++++++++++++++ .../04.problem.completion/src/index.test.ts | 45 ++++++++ .../04.solution.completion/src/index.test.ts | 45 ++++++++ .../src/index.test.ts | 105 ++++++++++++++++++ .../src/index.test.ts | 105 ++++++++++++++++++ 8 files changed, 678 insertions(+) diff --git a/exercises/03.resources/02.problem.template/src/index.test.ts b/exercises/03.resources/02.problem.template/src/index.test.ts index 2c0c5b4..9d23f38 100644 --- a/exercises/03.resources/02.problem.template/src/index.test.ts +++ b/exercises/03.resources/02.problem.template/src/index.test.ts @@ -1,11 +1,16 @@ +import fs from 'node:fs/promises' +import path from 'node:path' import { invariant } from '@epic-web/invariant' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { test, beforeAll, afterAll, expect } from 'vitest' let client: Client +const EPIC_ME_DB_PATH = `./test.ignored/db.${process.env.VITEST_WORKER_ID}.sqlite` beforeAll(async () => { + const dir = path.dirname(EPIC_ME_DB_PATH) + await fs.mkdir(dir, { recursive: true }) client = new Client({ name: 'EpicMeTester', version: '1.0.0', @@ -13,12 +18,17 @@ beforeAll(async () => { const transport = new StdioClientTransport({ command: 'tsx', args: ['src/index.ts'], + env: { + ...process.env, + EPIC_ME_DB_PATH, + }, }) await client.connect(transport) }) afterAll(async () => { await client.transport?.close() + await fs.unlink(EPIC_ME_DB_PATH) }) test('Tool Definition', async () => { @@ -69,3 +79,81 @@ test('Tool Call', async () => { }), ) }) + +test('Resource Templates List', async () => { + const list = await client.listResourceTemplates() + + // 🚨 Proactive check: Ensure resource templates are registered + invariant(list.resourceTemplates.length > 0, '🚨 No resource templates found - this exercise requires implementing parameterized resources like epicme://entries/{id}') + + const entriesTemplate = list.resourceTemplates.find(rt => + rt.uriTemplate.includes('entries') && rt.uriTemplate.includes('{') + ) + const tagsTemplate = list.resourceTemplates.find(rt => + rt.uriTemplate.includes('tags') && rt.uriTemplate.includes('{') + ) + + // 🚨 Proactive checks for specific templates + invariant(entriesTemplate, '🚨 No entries resource template found - should implement epicme://entries/{id} template') + invariant(tagsTemplate, '🚨 No tags resource template found - should implement epicme://tags/{id} template') + + expect(entriesTemplate).toEqual( + expect.objectContaining({ + name: expect.any(String), + uriTemplate: expect.stringMatching(/entries.*\{.*\}/), + description: expect.stringMatching(/entry|entries/i), + }), + ) + + expect(tagsTemplate).toEqual( + expect.objectContaining({ + name: expect.any(String), + uriTemplate: expect.stringMatching(/tags.*\{.*\}/), + description: expect.stringMatching(/tag|tags/i), + }), + ) +}) + +test('Resource Template Read - Entry', async () => { + // First create an entry to test against + await client.callTool({ + name: 'create_entry', + arguments: { + title: 'Template Test Entry', + content: 'This entry is for testing templates', + }, + }) + + const result = await client.readResource({ + uri: 'epicme://entries/1', + }) + + expect(result).toEqual( + expect.objectContaining({ + contents: expect.arrayContaining([ + expect.objectContaining({ + mimeType: 'application/json', + uri: 'epicme://entries/1', + text: expect.any(String), + }), + ]), + }), + ) + + // 🚨 Proactive check: Ensure the resource content is valid JSON and contains entry data + const content = result.contents[0] + invariant(content && 'text' in content, '🚨 Resource content must have text field') + invariant(typeof content.text === 'string', '🚨 Resource content text must be a string') + + let entryData: any + try { + entryData = JSON.parse(content.text) + } catch (error) { + throw new Error('🚨 Resource content must be valid JSON') + } + + // 🚨 Proactive check: Ensure entry data contains expected fields + invariant(entryData.id, '🚨 Entry resource should contain id field') + invariant(entryData.title, '🚨 Entry resource should contain title field') + invariant(entryData.content, '🚨 Entry resource should contain content field') +}) diff --git a/exercises/03.resources/02.solution.template/src/index.test.ts b/exercises/03.resources/02.solution.template/src/index.test.ts index 2c0c5b4..9d23f38 100644 --- a/exercises/03.resources/02.solution.template/src/index.test.ts +++ b/exercises/03.resources/02.solution.template/src/index.test.ts @@ -1,11 +1,16 @@ +import fs from 'node:fs/promises' +import path from 'node:path' import { invariant } from '@epic-web/invariant' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { test, beforeAll, afterAll, expect } from 'vitest' let client: Client +const EPIC_ME_DB_PATH = `./test.ignored/db.${process.env.VITEST_WORKER_ID}.sqlite` beforeAll(async () => { + const dir = path.dirname(EPIC_ME_DB_PATH) + await fs.mkdir(dir, { recursive: true }) client = new Client({ name: 'EpicMeTester', version: '1.0.0', @@ -13,12 +18,17 @@ beforeAll(async () => { const transport = new StdioClientTransport({ command: 'tsx', args: ['src/index.ts'], + env: { + ...process.env, + EPIC_ME_DB_PATH, + }, }) await client.connect(transport) }) afterAll(async () => { await client.transport?.close() + await fs.unlink(EPIC_ME_DB_PATH) }) test('Tool Definition', async () => { @@ -69,3 +79,81 @@ test('Tool Call', async () => { }), ) }) + +test('Resource Templates List', async () => { + const list = await client.listResourceTemplates() + + // 🚨 Proactive check: Ensure resource templates are registered + invariant(list.resourceTemplates.length > 0, '🚨 No resource templates found - this exercise requires implementing parameterized resources like epicme://entries/{id}') + + const entriesTemplate = list.resourceTemplates.find(rt => + rt.uriTemplate.includes('entries') && rt.uriTemplate.includes('{') + ) + const tagsTemplate = list.resourceTemplates.find(rt => + rt.uriTemplate.includes('tags') && rt.uriTemplate.includes('{') + ) + + // 🚨 Proactive checks for specific templates + invariant(entriesTemplate, '🚨 No entries resource template found - should implement epicme://entries/{id} template') + invariant(tagsTemplate, '🚨 No tags resource template found - should implement epicme://tags/{id} template') + + expect(entriesTemplate).toEqual( + expect.objectContaining({ + name: expect.any(String), + uriTemplate: expect.stringMatching(/entries.*\{.*\}/), + description: expect.stringMatching(/entry|entries/i), + }), + ) + + expect(tagsTemplate).toEqual( + expect.objectContaining({ + name: expect.any(String), + uriTemplate: expect.stringMatching(/tags.*\{.*\}/), + description: expect.stringMatching(/tag|tags/i), + }), + ) +}) + +test('Resource Template Read - Entry', async () => { + // First create an entry to test against + await client.callTool({ + name: 'create_entry', + arguments: { + title: 'Template Test Entry', + content: 'This entry is for testing templates', + }, + }) + + const result = await client.readResource({ + uri: 'epicme://entries/1', + }) + + expect(result).toEqual( + expect.objectContaining({ + contents: expect.arrayContaining([ + expect.objectContaining({ + mimeType: 'application/json', + uri: 'epicme://entries/1', + text: expect.any(String), + }), + ]), + }), + ) + + // 🚨 Proactive check: Ensure the resource content is valid JSON and contains entry data + const content = result.contents[0] + invariant(content && 'text' in content, '🚨 Resource content must have text field') + invariant(typeof content.text === 'string', '🚨 Resource content text must be a string') + + let entryData: any + try { + entryData = JSON.parse(content.text) + } catch (error) { + throw new Error('🚨 Resource content must be valid JSON') + } + + // 🚨 Proactive check: Ensure entry data contains expected fields + invariant(entryData.id, '🚨 Entry resource should contain id field') + invariant(entryData.title, '🚨 Entry resource should contain title field') + invariant(entryData.content, '🚨 Entry resource should contain content field') +}) diff --git a/exercises/03.resources/03.problem.list/src/index.test.ts b/exercises/03.resources/03.problem.list/src/index.test.ts index 2c0c5b4..a0a725a 100644 --- a/exercises/03.resources/03.problem.list/src/index.test.ts +++ b/exercises/03.resources/03.problem.list/src/index.test.ts @@ -1,11 +1,16 @@ +import fs from 'node:fs/promises' +import path from 'node:path' import { invariant } from '@epic-web/invariant' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { test, beforeAll, afterAll, expect } from 'vitest' let client: Client +const EPIC_ME_DB_PATH = `./test.ignored/db.${process.env.VITEST_WORKER_ID}.sqlite` beforeAll(async () => { + const dir = path.dirname(EPIC_ME_DB_PATH) + await fs.mkdir(dir, { recursive: true }) client = new Client({ name: 'EpicMeTester', version: '1.0.0', @@ -13,12 +18,17 @@ beforeAll(async () => { const transport = new StdioClientTransport({ command: 'tsx', args: ['src/index.ts'], + env: { + ...process.env, + EPIC_ME_DB_PATH, + }, }) await client.connect(transport) }) afterAll(async () => { await client.transport?.close() + await fs.unlink(EPIC_ME_DB_PATH) }) test('Tool Definition', async () => { @@ -69,3 +79,94 @@ test('Tool Call', async () => { }), ) }) + +test('Resource Templates List', async () => { + const list = await client.listResourceTemplates() + + // 🚨 Proactive check: Ensure resource templates are registered + invariant(list.resourceTemplates.length > 0, '🚨 No resource templates found - this exercise requires implementing parameterized resources with list callbacks') + + const entriesTemplate = list.resourceTemplates.find(rt => + rt.uriTemplate.includes('entries') && rt.uriTemplate.includes('{') + ) + const tagsTemplate = list.resourceTemplates.find(rt => + rt.uriTemplate.includes('tags') && rt.uriTemplate.includes('{') + ) + + // 🚨 Proactive checks for specific templates + invariant(entriesTemplate, '🚨 No entries resource template found - should implement epicme://entries/{id} template') + invariant(tagsTemplate, '🚨 No tags resource template found - should implement epicme://tags/{id} template') +}) + +test('Resource List - Entries', async () => { + // First create some entries to test against + await client.callTool({ + name: 'create_entry', + arguments: { + title: 'List Test Entry 1', + content: 'This is test entry 1', + }, + }) + + await client.callTool({ + name: 'create_entry', + arguments: { + title: 'List Test Entry 2', + content: 'This is test entry 2', + }, + }) + + const list = await client.listResources() + + // 🚨 Proactive check: Ensure list callback returns actual entries + const entryResources = list.resources.filter(r => r.uri.includes('entries')) + invariant(entryResources.length > 0, '🚨 No entry resources found in list - the list callback should return actual entries from the database') + + // Check that we have at least the entries we created + const foundEntries = entryResources.filter(r => + r.uri.includes('entries/1') || r.uri.includes('entries/2') + ) + invariant(foundEntries.length >= 2, '🚨 List should return the entries that were created') + + // Validate the structure of listed resources + entryResources.forEach(resource => { + expect(resource).toEqual( + expect.objectContaining({ + name: expect.any(String), + uri: expect.stringMatching(/epicme:\/\/entries\/\d+/), + mimeType: 'application/json', + }), + ) + + // 🚨 Proactive check: List should not include content (only metadata) + invariant(!('text' in resource), '🚨 Resource list should only contain metadata, not the full content - use readResource to get content') + }) +}) + +test('Resource List - Tags', async () => { + // Create a tag to test against + await client.callTool({ + name: 'create_tag', + arguments: { + name: 'List Test Tag', + description: 'This is a test tag for listing', + }, + }) + + const list = await client.listResources() + + // 🚨 Proactive check: Ensure list callback returns actual tags + const tagResources = list.resources.filter(r => r.uri.includes('tags')) + invariant(tagResources.length > 0, '🚨 No tag resources found in list - the list callback should return actual tags from the database') + + // Validate the structure of listed tag resources + tagResources.forEach(resource => { + expect(resource).toEqual( + expect.objectContaining({ + name: expect.any(String), + uri: expect.stringMatching(/epicme:\/\/tags\/\d+/), + mimeType: 'application/json', + }), + ) + }) +}) diff --git a/exercises/03.resources/03.solution.list/src/index.test.ts b/exercises/03.resources/03.solution.list/src/index.test.ts index 2c0c5b4..a0a725a 100644 --- a/exercises/03.resources/03.solution.list/src/index.test.ts +++ b/exercises/03.resources/03.solution.list/src/index.test.ts @@ -1,11 +1,16 @@ +import fs from 'node:fs/promises' +import path from 'node:path' import { invariant } from '@epic-web/invariant' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { test, beforeAll, afterAll, expect } from 'vitest' let client: Client +const EPIC_ME_DB_PATH = `./test.ignored/db.${process.env.VITEST_WORKER_ID}.sqlite` beforeAll(async () => { + const dir = path.dirname(EPIC_ME_DB_PATH) + await fs.mkdir(dir, { recursive: true }) client = new Client({ name: 'EpicMeTester', version: '1.0.0', @@ -13,12 +18,17 @@ beforeAll(async () => { const transport = new StdioClientTransport({ command: 'tsx', args: ['src/index.ts'], + env: { + ...process.env, + EPIC_ME_DB_PATH, + }, }) await client.connect(transport) }) afterAll(async () => { await client.transport?.close() + await fs.unlink(EPIC_ME_DB_PATH) }) test('Tool Definition', async () => { @@ -69,3 +79,94 @@ test('Tool Call', async () => { }), ) }) + +test('Resource Templates List', async () => { + const list = await client.listResourceTemplates() + + // 🚨 Proactive check: Ensure resource templates are registered + invariant(list.resourceTemplates.length > 0, '🚨 No resource templates found - this exercise requires implementing parameterized resources with list callbacks') + + const entriesTemplate = list.resourceTemplates.find(rt => + rt.uriTemplate.includes('entries') && rt.uriTemplate.includes('{') + ) + const tagsTemplate = list.resourceTemplates.find(rt => + rt.uriTemplate.includes('tags') && rt.uriTemplate.includes('{') + ) + + // 🚨 Proactive checks for specific templates + invariant(entriesTemplate, '🚨 No entries resource template found - should implement epicme://entries/{id} template') + invariant(tagsTemplate, '🚨 No tags resource template found - should implement epicme://tags/{id} template') +}) + +test('Resource List - Entries', async () => { + // First create some entries to test against + await client.callTool({ + name: 'create_entry', + arguments: { + title: 'List Test Entry 1', + content: 'This is test entry 1', + }, + }) + + await client.callTool({ + name: 'create_entry', + arguments: { + title: 'List Test Entry 2', + content: 'This is test entry 2', + }, + }) + + const list = await client.listResources() + + // 🚨 Proactive check: Ensure list callback returns actual entries + const entryResources = list.resources.filter(r => r.uri.includes('entries')) + invariant(entryResources.length > 0, '🚨 No entry resources found in list - the list callback should return actual entries from the database') + + // Check that we have at least the entries we created + const foundEntries = entryResources.filter(r => + r.uri.includes('entries/1') || r.uri.includes('entries/2') + ) + invariant(foundEntries.length >= 2, '🚨 List should return the entries that were created') + + // Validate the structure of listed resources + entryResources.forEach(resource => { + expect(resource).toEqual( + expect.objectContaining({ + name: expect.any(String), + uri: expect.stringMatching(/epicme:\/\/entries\/\d+/), + mimeType: 'application/json', + }), + ) + + // 🚨 Proactive check: List should not include content (only metadata) + invariant(!('text' in resource), '🚨 Resource list should only contain metadata, not the full content - use readResource to get content') + }) +}) + +test('Resource List - Tags', async () => { + // Create a tag to test against + await client.callTool({ + name: 'create_tag', + arguments: { + name: 'List Test Tag', + description: 'This is a test tag for listing', + }, + }) + + const list = await client.listResources() + + // 🚨 Proactive check: Ensure list callback returns actual tags + const tagResources = list.resources.filter(r => r.uri.includes('tags')) + invariant(tagResources.length > 0, '🚨 No tag resources found in list - the list callback should return actual tags from the database') + + // Validate the structure of listed tag resources + tagResources.forEach(resource => { + expect(resource).toEqual( + expect.objectContaining({ + name: expect.any(String), + uri: expect.stringMatching(/epicme:\/\/tags\/\d+/), + mimeType: 'application/json', + }), + ) + }) +}) diff --git a/exercises/03.resources/04.problem.completion/src/index.test.ts b/exercises/03.resources/04.problem.completion/src/index.test.ts index 2c0c5b4..a627488 100644 --- a/exercises/03.resources/04.problem.completion/src/index.test.ts +++ b/exercises/03.resources/04.problem.completion/src/index.test.ts @@ -1,11 +1,16 @@ +import fs from 'node:fs/promises' +import path from 'node:path' import { invariant } from '@epic-web/invariant' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { test, beforeAll, afterAll, expect } from 'vitest' let client: Client +const EPIC_ME_DB_PATH = `./test.ignored/db.${process.env.VITEST_WORKER_ID}.sqlite` beforeAll(async () => { + const dir = path.dirname(EPIC_ME_DB_PATH) + await fs.mkdir(dir, { recursive: true }) client = new Client({ name: 'EpicMeTester', version: '1.0.0', @@ -13,12 +18,17 @@ beforeAll(async () => { const transport = new StdioClientTransport({ command: 'tsx', args: ['src/index.ts'], + env: { + ...process.env, + EPIC_ME_DB_PATH, + }, }) await client.connect(transport) }) afterAll(async () => { await client.transport?.close() + await fs.unlink(EPIC_ME_DB_PATH) }) test('Tool Definition', async () => { @@ -69,3 +79,38 @@ test('Tool Call', async () => { }), ) }) + +test('Resource Template Completions', async () => { + // First create some entries to have data for completion + await client.callTool({ + name: 'create_entry', + arguments: { + title: 'Completion Test Entry 1', + content: 'This is for testing completions', + }, + }) + + await client.callTool({ + name: 'create_entry', + arguments: { + title: 'Completion Test Entry 2', + content: 'This is another completion test', + }, + }) + + // Test that resource templates exist and support completion + const templates = await client.listResourceTemplates() + + // 🚨 Proactive check: Ensure resource templates are registered + invariant(templates.resourceTemplates.length > 0, '🚨 No resource templates found - this exercise requires implementing resource templates with completion callbacks') + + const entriesTemplate = templates.resourceTemplates.find(rt => + rt.uriTemplate.includes('entries') && rt.uriTemplate.includes('{') + ) + invariant(entriesTemplate, '🚨 No entries resource template found - should implement epicme://entries/{id} template with completion support') + + // 🚨 Additional proactive check: This exercise specifically requires completion callbacks + invariant(entriesTemplate.description && entriesTemplate.description.toLowerCase().includes('completion') || + entriesTemplate.name.toLowerCase().includes('completion'), + '🚨 Resource template should indicate completion support in its description or name for this exercise') +}) diff --git a/exercises/03.resources/04.solution.completion/src/index.test.ts b/exercises/03.resources/04.solution.completion/src/index.test.ts index 2c0c5b4..a627488 100644 --- a/exercises/03.resources/04.solution.completion/src/index.test.ts +++ b/exercises/03.resources/04.solution.completion/src/index.test.ts @@ -1,11 +1,16 @@ +import fs from 'node:fs/promises' +import path from 'node:path' import { invariant } from '@epic-web/invariant' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { test, beforeAll, afterAll, expect } from 'vitest' let client: Client +const EPIC_ME_DB_PATH = `./test.ignored/db.${process.env.VITEST_WORKER_ID}.sqlite` beforeAll(async () => { + const dir = path.dirname(EPIC_ME_DB_PATH) + await fs.mkdir(dir, { recursive: true }) client = new Client({ name: 'EpicMeTester', version: '1.0.0', @@ -13,12 +18,17 @@ beforeAll(async () => { const transport = new StdioClientTransport({ command: 'tsx', args: ['src/index.ts'], + env: { + ...process.env, + EPIC_ME_DB_PATH, + }, }) await client.connect(transport) }) afterAll(async () => { await client.transport?.close() + await fs.unlink(EPIC_ME_DB_PATH) }) test('Tool Definition', async () => { @@ -69,3 +79,38 @@ test('Tool Call', async () => { }), ) }) + +test('Resource Template Completions', async () => { + // First create some entries to have data for completion + await client.callTool({ + name: 'create_entry', + arguments: { + title: 'Completion Test Entry 1', + content: 'This is for testing completions', + }, + }) + + await client.callTool({ + name: 'create_entry', + arguments: { + title: 'Completion Test Entry 2', + content: 'This is another completion test', + }, + }) + + // Test that resource templates exist and support completion + const templates = await client.listResourceTemplates() + + // 🚨 Proactive check: Ensure resource templates are registered + invariant(templates.resourceTemplates.length > 0, '🚨 No resource templates found - this exercise requires implementing resource templates with completion callbacks') + + const entriesTemplate = templates.resourceTemplates.find(rt => + rt.uriTemplate.includes('entries') && rt.uriTemplate.includes('{') + ) + invariant(entriesTemplate, '🚨 No entries resource template found - should implement epicme://entries/{id} template with completion support') + + // 🚨 Additional proactive check: This exercise specifically requires completion callbacks + invariant(entriesTemplate.description && entriesTemplate.description.toLowerCase().includes('completion') || + entriesTemplate.name.toLowerCase().includes('completion'), + '🚨 Resource template should indicate completion support in its description or name for this exercise') +}) diff --git a/exercises/04.prompts/02.problem.optimized-prompt/src/index.test.ts b/exercises/04.prompts/02.problem.optimized-prompt/src/index.test.ts index 2c0c5b4..bc00b9b 100644 --- a/exercises/04.prompts/02.problem.optimized-prompt/src/index.test.ts +++ b/exercises/04.prompts/02.problem.optimized-prompt/src/index.test.ts @@ -1,11 +1,16 @@ +import fs from 'node:fs/promises' +import path from 'node:path' import { invariant } from '@epic-web/invariant' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { test, beforeAll, afterAll, expect } from 'vitest' let client: Client +const EPIC_ME_DB_PATH = `./test.ignored/db.${process.env.VITEST_WORKER_ID}.sqlite` beforeAll(async () => { + const dir = path.dirname(EPIC_ME_DB_PATH) + await fs.mkdir(dir, { recursive: true }) client = new Client({ name: 'EpicMeTester', version: '1.0.0', @@ -13,12 +18,17 @@ beforeAll(async () => { const transport = new StdioClientTransport({ command: 'tsx', args: ['src/index.ts'], + env: { + ...process.env, + EPIC_ME_DB_PATH, + }, }) await client.connect(transport) }) afterAll(async () => { await client.transport?.close() + await fs.unlink(EPIC_ME_DB_PATH) }) test('Tool Definition', async () => { @@ -69,3 +79,98 @@ test('Tool Call', async () => { }), ) }) + +test('Prompts List', async () => { + const list = await client.listPrompts() + + // 🚨 Proactive check: Ensure prompts are registered + invariant(list.prompts.length > 0, '🚨 No prompts found - make sure to register prompts with the prompts capability') + + const tagSuggestionsPrompt = list.prompts.find(p => p.name.includes('tag') || p.name.includes('suggest')) + invariant(tagSuggestionsPrompt, '🚨 No tag suggestions prompt found - should include a prompt for suggesting tags') +}) + +test('Optimized Prompt with Embedded Resources', async () => { + // First create an entry and tag for testing + await client.callTool({ + name: 'create_entry', + arguments: { + title: 'Optimized Test Entry', + content: 'This entry is for testing optimized prompts', + }, + }) + + await client.callTool({ + name: 'create_tag', + arguments: { + name: 'Optimization', + description: 'Tag for optimization testing', + }, + }) + + const list = await client.listPrompts() + const firstPrompt = list.prompts[0] + invariant(firstPrompt, '🚨 No prompts available to test') + + const result = await client.getPrompt({ + name: firstPrompt.name, + arguments: { + entryId: '1', + }, + }) + + expect(result).toEqual( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: expect.stringMatching(/user|system/), + content: expect.objectContaining({ + type: expect.stringMatching(/text|resource/), + }), + }), + ]), + }), + ) + + // 🚨 Proactive check: Ensure prompt has multiple messages (optimization means embedding data) + invariant(result.messages.length > 1, '🚨 Optimized prompt should have multiple messages - instructions plus embedded data') + + // 🚨 Proactive check: Ensure at least one message is a resource (embedded data) + const resourceMessages = result.messages.filter(m => m.content.type === 'resource') + invariant(resourceMessages.length > 0, '🚨 Optimized prompt should embed resource data directly instead of instructing LLM to run tools') + + // 🚨 Proactive check: Ensure prompt doesn't tell LLM to run tools (that's what we're optimizing away) + const textMessages = result.messages.filter(m => m.content.type === 'text') + const hasToolInstructions = textMessages.some(m => + typeof m.content.text === 'string' && + (m.content.text.toLowerCase().includes('get_entry') || + m.content.text.toLowerCase().includes('list_tags') || + m.content.text.toLowerCase().includes('create_tag')) + ) + invariant(!hasToolInstructions, '🚨 Optimized prompt should NOT instruct LLM to run tools - data should be embedded directly') + + // Validate structure of resource messages + resourceMessages.forEach(resMsg => { + expect(resMsg.content).toEqual( + expect.objectContaining({ + type: 'resource', + resource: expect.objectContaining({ + uri: expect.any(String), + mimeType: 'application/json', + text: expect.any(String), + }), + }), + ) + + // 🚨 Proactive check: Ensure embedded resource contains valid JSON + invariant('resource' in resMsg.content, '🚨 Resource message must have resource field') + invariant(typeof resMsg.content.resource === 'object' && resMsg.content.resource !== null, '🚨 Resource must be an object') + invariant('text' in resMsg.content.resource, '🚨 Resource must have text field') + invariant(typeof resMsg.content.resource.text === 'string', '🚨 Resource text must be a string') + try { + JSON.parse(resMsg.content.resource.text) + } catch (error) { + throw new Error('🚨 Embedded resource data must be valid JSON') + } + }) +}) diff --git a/exercises/04.prompts/02.solution.optimized-prompt/src/index.test.ts b/exercises/04.prompts/02.solution.optimized-prompt/src/index.test.ts index 2c0c5b4..bc00b9b 100644 --- a/exercises/04.prompts/02.solution.optimized-prompt/src/index.test.ts +++ b/exercises/04.prompts/02.solution.optimized-prompt/src/index.test.ts @@ -1,11 +1,16 @@ +import fs from 'node:fs/promises' +import path from 'node:path' import { invariant } from '@epic-web/invariant' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js' import { test, beforeAll, afterAll, expect } from 'vitest' let client: Client +const EPIC_ME_DB_PATH = `./test.ignored/db.${process.env.VITEST_WORKER_ID}.sqlite` beforeAll(async () => { + const dir = path.dirname(EPIC_ME_DB_PATH) + await fs.mkdir(dir, { recursive: true }) client = new Client({ name: 'EpicMeTester', version: '1.0.0', @@ -13,12 +18,17 @@ beforeAll(async () => { const transport = new StdioClientTransport({ command: 'tsx', args: ['src/index.ts'], + env: { + ...process.env, + EPIC_ME_DB_PATH, + }, }) await client.connect(transport) }) afterAll(async () => { await client.transport?.close() + await fs.unlink(EPIC_ME_DB_PATH) }) test('Tool Definition', async () => { @@ -69,3 +79,98 @@ test('Tool Call', async () => { }), ) }) + +test('Prompts List', async () => { + const list = await client.listPrompts() + + // 🚨 Proactive check: Ensure prompts are registered + invariant(list.prompts.length > 0, '🚨 No prompts found - make sure to register prompts with the prompts capability') + + const tagSuggestionsPrompt = list.prompts.find(p => p.name.includes('tag') || p.name.includes('suggest')) + invariant(tagSuggestionsPrompt, '🚨 No tag suggestions prompt found - should include a prompt for suggesting tags') +}) + +test('Optimized Prompt with Embedded Resources', async () => { + // First create an entry and tag for testing + await client.callTool({ + name: 'create_entry', + arguments: { + title: 'Optimized Test Entry', + content: 'This entry is for testing optimized prompts', + }, + }) + + await client.callTool({ + name: 'create_tag', + arguments: { + name: 'Optimization', + description: 'Tag for optimization testing', + }, + }) + + const list = await client.listPrompts() + const firstPrompt = list.prompts[0] + invariant(firstPrompt, '🚨 No prompts available to test') + + const result = await client.getPrompt({ + name: firstPrompt.name, + arguments: { + entryId: '1', + }, + }) + + expect(result).toEqual( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: expect.stringMatching(/user|system/), + content: expect.objectContaining({ + type: expect.stringMatching(/text|resource/), + }), + }), + ]), + }), + ) + + // 🚨 Proactive check: Ensure prompt has multiple messages (optimization means embedding data) + invariant(result.messages.length > 1, '🚨 Optimized prompt should have multiple messages - instructions plus embedded data') + + // 🚨 Proactive check: Ensure at least one message is a resource (embedded data) + const resourceMessages = result.messages.filter(m => m.content.type === 'resource') + invariant(resourceMessages.length > 0, '🚨 Optimized prompt should embed resource data directly instead of instructing LLM to run tools') + + // 🚨 Proactive check: Ensure prompt doesn't tell LLM to run tools (that's what we're optimizing away) + const textMessages = result.messages.filter(m => m.content.type === 'text') + const hasToolInstructions = textMessages.some(m => + typeof m.content.text === 'string' && + (m.content.text.toLowerCase().includes('get_entry') || + m.content.text.toLowerCase().includes('list_tags') || + m.content.text.toLowerCase().includes('create_tag')) + ) + invariant(!hasToolInstructions, '🚨 Optimized prompt should NOT instruct LLM to run tools - data should be embedded directly') + + // Validate structure of resource messages + resourceMessages.forEach(resMsg => { + expect(resMsg.content).toEqual( + expect.objectContaining({ + type: 'resource', + resource: expect.objectContaining({ + uri: expect.any(String), + mimeType: 'application/json', + text: expect.any(String), + }), + }), + ) + + // 🚨 Proactive check: Ensure embedded resource contains valid JSON + invariant('resource' in resMsg.content, '🚨 Resource message must have resource field') + invariant(typeof resMsg.content.resource === 'object' && resMsg.content.resource !== null, '🚨 Resource must be an object') + invariant('text' in resMsg.content.resource, '🚨 Resource must have text field') + invariant(typeof resMsg.content.resource.text === 'string', '🚨 Resource text must be a string') + try { + JSON.parse(resMsg.content.resource.text) + } catch (error) { + throw new Error('🚨 Embedded resource data must be valid JSON') + } + }) +}) From 8006e34fb0a04534f85cfaf63e8bc13a83c3aa6f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 2 Jul 2025 15:10:28 +0000 Subject: [PATCH 3/6] Refactor tests to improve resource template and prompt validation checks Co-authored-by: me --- .../03.problem.list/src/index.test.ts | 11 ++++- .../03.solution.list/src/index.test.ts | 11 ++++- .../04.problem.completion/src/index.test.ts | 44 ++++++++++++++++--- .../04.solution.completion/src/index.test.ts | 44 ++++++++++++++++--- .../src/index.test.ts | 10 +++-- .../src/index.test.ts | 10 +++-- 6 files changed, 104 insertions(+), 26 deletions(-) diff --git a/exercises/03.resources/03.problem.list/src/index.test.ts b/exercises/03.resources/03.problem.list/src/index.test.ts index a0a725a..cf37205 100644 --- a/exercises/03.resources/03.problem.list/src/index.test.ts +++ b/exercises/03.resources/03.problem.list/src/index.test.ts @@ -159,8 +159,15 @@ test('Resource List - Tags', async () => { const tagResources = list.resources.filter(r => r.uri.includes('tags')) invariant(tagResources.length > 0, '🚨 No tag resources found in list - the list callback should return actual tags from the database') - // Validate the structure of listed tag resources - tagResources.forEach(resource => { + // Should have both static resource and parameterized resources from list callback + const staticTagsResource = tagResources.find(r => r.uri === 'epicme://tags') + const parameterizedTagResources = tagResources.filter(r => r.uri.match(/epicme:\/\/tags\/\d+/)) + + // 🚨 Proactive check: List should include resources from template list callback + invariant(parameterizedTagResources.length > 0, '🚨 No parameterized tag resources found - the resource template list callback should return individual tags') + + // Validate the structure of parameterized tag resources (from list callback) + parameterizedTagResources.forEach(resource => { expect(resource).toEqual( expect.objectContaining({ name: expect.any(String), diff --git a/exercises/03.resources/03.solution.list/src/index.test.ts b/exercises/03.resources/03.solution.list/src/index.test.ts index a0a725a..cf37205 100644 --- a/exercises/03.resources/03.solution.list/src/index.test.ts +++ b/exercises/03.resources/03.solution.list/src/index.test.ts @@ -159,8 +159,15 @@ test('Resource List - Tags', async () => { const tagResources = list.resources.filter(r => r.uri.includes('tags')) invariant(tagResources.length > 0, '🚨 No tag resources found in list - the list callback should return actual tags from the database') - // Validate the structure of listed tag resources - tagResources.forEach(resource => { + // Should have both static resource and parameterized resources from list callback + const staticTagsResource = tagResources.find(r => r.uri === 'epicme://tags') + const parameterizedTagResources = tagResources.filter(r => r.uri.match(/epicme:\/\/tags\/\d+/)) + + // 🚨 Proactive check: List should include resources from template list callback + invariant(parameterizedTagResources.length > 0, '🚨 No parameterized tag resources found - the resource template list callback should return individual tags') + + // Validate the structure of parameterized tag resources (from list callback) + parameterizedTagResources.forEach(resource => { expect(resource).toEqual( expect.objectContaining({ name: expect.any(String), diff --git a/exercises/03.resources/04.problem.completion/src/index.test.ts b/exercises/03.resources/04.problem.completion/src/index.test.ts index a627488..e9c525c 100644 --- a/exercises/03.resources/04.problem.completion/src/index.test.ts +++ b/exercises/03.resources/04.problem.completion/src/index.test.ts @@ -98,19 +98,49 @@ test('Resource Template Completions', async () => { }, }) - // Test that resource templates exist and support completion + // Test that resource templates exist const templates = await client.listResourceTemplates() // 🚨 Proactive check: Ensure resource templates are registered - invariant(templates.resourceTemplates.length > 0, '🚨 No resource templates found - this exercise requires implementing resource templates with completion callbacks') + invariant(templates.resourceTemplates.length > 0, '🚨 No resource templates found - this exercise requires implementing resource templates') const entriesTemplate = templates.resourceTemplates.find(rt => rt.uriTemplate.includes('entries') && rt.uriTemplate.includes('{') ) - invariant(entriesTemplate, '🚨 No entries resource template found - should implement epicme://entries/{id} template with completion support') + invariant(entriesTemplate, '🚨 No entries resource template found - should implement epicme://entries/{id} template') - // 🚨 Additional proactive check: This exercise specifically requires completion callbacks - invariant(entriesTemplate.description && entriesTemplate.description.toLowerCase().includes('completion') || - entriesTemplate.name.toLowerCase().includes('completion'), - '🚨 Resource template should indicate completion support in its description or name for this exercise') + // 🚨 The key learning objective for this exercise is adding completion support + // This requires BOTH declaring completions capability AND implementing complete callbacks + + // Test if completion capability is properly declared by trying to use completion API + let completionSupported = false + try { + // This should work if server declares completion capability and implements complete callbacks + await (client as any)._client.request({ + method: 'completion/complete', + params: { + ref: { + type: 'resource', + uri: 'epicme://entries/{id}', + }, + argument: { + name: 'id', + value: '1', + }, + }, + }) + completionSupported = true + } catch (error: any) { + // -32601 = Method not found (missing completion capability) + // -32602 = Invalid params (missing complete callbacks) + if (error?.code === -32601 || error?.code === -32602) { + completionSupported = false + } else { + // Other errors might be acceptable (like no matches found) + completionSupported = true + } + } + + // 🚨 Proactive check: Completion functionality must be fully implemented + invariant(completionSupported, '🚨 Resource template completion requires both declaring completions capability in server AND implementing complete callbacks for template parameters') }) diff --git a/exercises/03.resources/04.solution.completion/src/index.test.ts b/exercises/03.resources/04.solution.completion/src/index.test.ts index a627488..e9c525c 100644 --- a/exercises/03.resources/04.solution.completion/src/index.test.ts +++ b/exercises/03.resources/04.solution.completion/src/index.test.ts @@ -98,19 +98,49 @@ test('Resource Template Completions', async () => { }, }) - // Test that resource templates exist and support completion + // Test that resource templates exist const templates = await client.listResourceTemplates() // 🚨 Proactive check: Ensure resource templates are registered - invariant(templates.resourceTemplates.length > 0, '🚨 No resource templates found - this exercise requires implementing resource templates with completion callbacks') + invariant(templates.resourceTemplates.length > 0, '🚨 No resource templates found - this exercise requires implementing resource templates') const entriesTemplate = templates.resourceTemplates.find(rt => rt.uriTemplate.includes('entries') && rt.uriTemplate.includes('{') ) - invariant(entriesTemplate, '🚨 No entries resource template found - should implement epicme://entries/{id} template with completion support') + invariant(entriesTemplate, '🚨 No entries resource template found - should implement epicme://entries/{id} template') - // 🚨 Additional proactive check: This exercise specifically requires completion callbacks - invariant(entriesTemplate.description && entriesTemplate.description.toLowerCase().includes('completion') || - entriesTemplate.name.toLowerCase().includes('completion'), - '🚨 Resource template should indicate completion support in its description or name for this exercise') + // 🚨 The key learning objective for this exercise is adding completion support + // This requires BOTH declaring completions capability AND implementing complete callbacks + + // Test if completion capability is properly declared by trying to use completion API + let completionSupported = false + try { + // This should work if server declares completion capability and implements complete callbacks + await (client as any)._client.request({ + method: 'completion/complete', + params: { + ref: { + type: 'resource', + uri: 'epicme://entries/{id}', + }, + argument: { + name: 'id', + value: '1', + }, + }, + }) + completionSupported = true + } catch (error: any) { + // -32601 = Method not found (missing completion capability) + // -32602 = Invalid params (missing complete callbacks) + if (error?.code === -32601 || error?.code === -32602) { + completionSupported = false + } else { + // Other errors might be acceptable (like no matches found) + completionSupported = true + } + } + + // 🚨 Proactive check: Completion functionality must be fully implemented + invariant(completionSupported, '🚨 Resource template completion requires both declaring completions capability in server AND implementing complete callbacks for template parameters') }) diff --git a/exercises/04.prompts/02.problem.optimized-prompt/src/index.test.ts b/exercises/04.prompts/02.problem.optimized-prompt/src/index.test.ts index bc00b9b..324a048 100644 --- a/exercises/04.prompts/02.problem.optimized-prompt/src/index.test.ts +++ b/exercises/04.prompts/02.problem.optimized-prompt/src/index.test.ts @@ -139,15 +139,17 @@ test('Optimized Prompt with Embedded Resources', async () => { const resourceMessages = result.messages.filter(m => m.content.type === 'resource') invariant(resourceMessages.length > 0, '🚨 Optimized prompt should embed resource data directly instead of instructing LLM to run tools') - // 🚨 Proactive check: Ensure prompt doesn't tell LLM to run tools (that's what we're optimizing away) + // 🚨 Proactive check: Ensure prompt doesn't tell LLM to run data retrieval tools (that's what we're optimizing away) const textMessages = result.messages.filter(m => m.content.type === 'text') - const hasToolInstructions = textMessages.some(m => + const hasDataRetrievalInstructions = textMessages.some(m => typeof m.content.text === 'string' && (m.content.text.toLowerCase().includes('get_entry') || m.content.text.toLowerCase().includes('list_tags') || - m.content.text.toLowerCase().includes('create_tag')) + m.content.text.toLowerCase().includes('look up')) ) - invariant(!hasToolInstructions, '🚨 Optimized prompt should NOT instruct LLM to run tools - data should be embedded directly') + invariant(!hasDataRetrievalInstructions, '🚨 Optimized prompt should NOT instruct LLM to run data retrieval tools like get_entry or list_tags - data should be embedded directly') + + // Note: The prompt can still instruct the LLM to use action tools like create_tag or add_tag_to_entry // Validate structure of resource messages resourceMessages.forEach(resMsg => { diff --git a/exercises/04.prompts/02.solution.optimized-prompt/src/index.test.ts b/exercises/04.prompts/02.solution.optimized-prompt/src/index.test.ts index bc00b9b..324a048 100644 --- a/exercises/04.prompts/02.solution.optimized-prompt/src/index.test.ts +++ b/exercises/04.prompts/02.solution.optimized-prompt/src/index.test.ts @@ -139,15 +139,17 @@ test('Optimized Prompt with Embedded Resources', async () => { const resourceMessages = result.messages.filter(m => m.content.type === 'resource') invariant(resourceMessages.length > 0, '🚨 Optimized prompt should embed resource data directly instead of instructing LLM to run tools') - // 🚨 Proactive check: Ensure prompt doesn't tell LLM to run tools (that's what we're optimizing away) + // 🚨 Proactive check: Ensure prompt doesn't tell LLM to run data retrieval tools (that's what we're optimizing away) const textMessages = result.messages.filter(m => m.content.type === 'text') - const hasToolInstructions = textMessages.some(m => + const hasDataRetrievalInstructions = textMessages.some(m => typeof m.content.text === 'string' && (m.content.text.toLowerCase().includes('get_entry') || m.content.text.toLowerCase().includes('list_tags') || - m.content.text.toLowerCase().includes('create_tag')) + m.content.text.toLowerCase().includes('look up')) ) - invariant(!hasToolInstructions, '🚨 Optimized prompt should NOT instruct LLM to run tools - data should be embedded directly') + invariant(!hasDataRetrievalInstructions, '🚨 Optimized prompt should NOT instruct LLM to run data retrieval tools like get_entry or list_tags - data should be embedded directly') + + // Note: The prompt can still instruct the LLM to use action tools like create_tag or add_tag_to_entry // Validate structure of resource messages resourceMessages.forEach(resMsg => { From b9f0350b17b675cbfe8a8ad358b6f2fb58466f5a Mon Sep 17 00:00:00 2001 From: "Kent C. Dodds" Date: Wed, 2 Jul 2025 17:24:10 +0200 Subject: [PATCH 4/6] some updates --- exercises/01.ping/01.problem.connect/src/index.test.ts | 9 ++++++++- exercises/01.ping/01.solution.connect/src/index.test.ts | 9 ++++++++- exercises/05.sampling/01.problem.simple/package.json | 5 ----- exercises/05.sampling/01.solution.simple/package.json | 5 ----- exercises/05.sampling/02.problem.advanced/package.json | 5 ----- exercises/05.sampling/02.solution.advanced/package.json | 5 ----- package.json | 3 --- 7 files changed, 16 insertions(+), 25 deletions(-) diff --git a/exercises/01.ping/01.problem.connect/src/index.test.ts b/exercises/01.ping/01.problem.connect/src/index.test.ts index 3c04cf4..3b53c95 100644 --- a/exercises/01.ping/01.problem.connect/src/index.test.ts +++ b/exercises/01.ping/01.problem.connect/src/index.test.ts @@ -13,7 +13,14 @@ beforeAll(async () => { command: 'tsx', args: ['src/index.ts'], }) - await client.connect(transport) + try { + await client.connect(transport) + } catch (error) { + console.error( + '🚨 Could not connect to MCP server. Have you created the server and connected it to the STDIO transport yet?', + ) + throw error + } }) afterAll(async () => { diff --git a/exercises/01.ping/01.solution.connect/src/index.test.ts b/exercises/01.ping/01.solution.connect/src/index.test.ts index 3c04cf4..3b53c95 100644 --- a/exercises/01.ping/01.solution.connect/src/index.test.ts +++ b/exercises/01.ping/01.solution.connect/src/index.test.ts @@ -13,7 +13,14 @@ beforeAll(async () => { command: 'tsx', args: ['src/index.ts'], }) - await client.connect(transport) + try { + await client.connect(transport) + } catch (error) { + console.error( + '🚨 Could not connect to MCP server. Have you created the server and connected it to the STDIO transport yet?', + ) + throw error + } }) afterAll(async () => { diff --git a/exercises/05.sampling/01.problem.simple/package.json b/exercises/05.sampling/01.problem.simple/package.json index 6d99d0b..17d5d68 100644 --- a/exercises/05.sampling/01.problem.simple/package.json +++ b/exercises/05.sampling/01.problem.simple/package.json @@ -2,11 +2,6 @@ "name": "exercises_05.sampling_01.problem.simple", "private": true, "type": "module", - "epicshop": { - "testTab": { - "enabled": true - } - }, "scripts": { "dev": "mcp-dev", "dev:mcp": "tsx src/index.ts", diff --git a/exercises/05.sampling/01.solution.simple/package.json b/exercises/05.sampling/01.solution.simple/package.json index fe05d05..c4cb4fa 100644 --- a/exercises/05.sampling/01.solution.simple/package.json +++ b/exercises/05.sampling/01.solution.simple/package.json @@ -2,11 +2,6 @@ "name": "exercises_05.sampling_01.solution.simple", "private": true, "type": "module", - "epicshop": { - "testTab": { - "enabled": true - } - }, "scripts": { "dev": "mcp-dev", "dev:mcp": "tsx src/index.ts", diff --git a/exercises/05.sampling/02.problem.advanced/package.json b/exercises/05.sampling/02.problem.advanced/package.json index 125001d..a4d7b2f 100644 --- a/exercises/05.sampling/02.problem.advanced/package.json +++ b/exercises/05.sampling/02.problem.advanced/package.json @@ -2,11 +2,6 @@ "name": "exercises_05.sampling_02.problem.advanced", "private": true, "type": "module", - "epicshop": { - "testTab": { - "enabled": true - } - }, "scripts": { "dev": "mcp-dev", "dev:mcp": "tsx src/index.ts", diff --git a/exercises/05.sampling/02.solution.advanced/package.json b/exercises/05.sampling/02.solution.advanced/package.json index 9e2a1f4..76c27a0 100644 --- a/exercises/05.sampling/02.solution.advanced/package.json +++ b/exercises/05.sampling/02.solution.advanced/package.json @@ -2,11 +2,6 @@ "name": "exercises_05.sampling_02.solution.advanced", "private": true, "type": "module", - "epicshop": { - "testTab": { - "enabled": true - } - }, "scripts": { "dev": "mcp-dev", "dev:mcp": "tsx src/index.ts", diff --git a/package.json b/package.json index de449b6..3055c8e 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,6 @@ "host": "www.epicai.pro", "displayName": "EpicAI.pro", "displayNameShort": "Epic AI" - }, - "testTab": { - "enabled": false } }, "type": "module", From 913719bf6811dcb7eb434b5db36bb3098cf63884 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 2 Jul 2025 16:09:12 +0000 Subject: [PATCH 5/6] Enhance test error handling with detailed diagnostic messages Co-authored-by: me --- .../01.problem.connect/src/index.test.ts | 27 +++-- .../01.solution.connect/src/index.test.ts | 27 +++-- .../01.problem.simple/src/index.test.ts | 76 ++++++++---- .../01.solution.simple/src/index.test.ts | 76 ++++++++---- .../02.problem.args/src/index.test.ts | 91 +++++++++------ .../02.solution.args/src/index.test.ts | 91 +++++++++------ .../01.problem.simple/src/index.test.ts | 108 +++++++++++------- .../01.solution.simple/src/index.test.ts | 108 +++++++++++------- .../01.problem.simple/src/index.test.ts | 101 ++++++++++------ 9 files changed, 453 insertions(+), 252 deletions(-) diff --git a/exercises/01.ping/01.problem.connect/src/index.test.ts b/exercises/01.ping/01.problem.connect/src/index.test.ts index 3b53c95..573414d 100644 --- a/exercises/01.ping/01.problem.connect/src/index.test.ts +++ b/exercises/01.ping/01.problem.connect/src/index.test.ts @@ -13,12 +13,14 @@ beforeAll(async () => { command: 'tsx', args: ['src/index.ts'], }) + try { await client.connect(transport) - } catch (error) { - console.error( - '🚨 Could not connect to MCP server. Have you created the server and connected it to the STDIO transport yet?', - ) + } catch (error: any) { + console.error('🚨 Connection failed! This exercise requires implementing the main function in src/index.ts') + console.error('🚨 Replace the "throw new Error(\'Not implemented\')" with the actual MCP server setup') + console.error('🚨 You need to: 1) Create an EpicMeMCP instance, 2) Initialize it, 3) Connect to stdio transport') + console.error('Original error:', error.message || error) throw error } }) @@ -28,7 +30,18 @@ afterAll(async () => { }) test('Ping', async () => { - const result = await client.ping() - - expect(result).toEqual({}) + try { + const result = await client.ping() + expect(result).toEqual({}) + } catch (error: any) { + if (error.message?.includes('Connection closed') || error.code === -32000) { + console.error('🚨 Ping failed because the MCP server crashed!') + console.error('🚨 This means the main() function in src/index.ts is not properly implemented') + console.error('🚨 Check that you\'ve replaced the "Not implemented" error with actual server setup code') + const enhancedError = new Error('🚨 MCP server implementation required in main() function. ' + (error.message || error)) + enhancedError.stack = error.stack + throw enhancedError + } + throw error + } }) diff --git a/exercises/01.ping/01.solution.connect/src/index.test.ts b/exercises/01.ping/01.solution.connect/src/index.test.ts index 3b53c95..573414d 100644 --- a/exercises/01.ping/01.solution.connect/src/index.test.ts +++ b/exercises/01.ping/01.solution.connect/src/index.test.ts @@ -13,12 +13,14 @@ beforeAll(async () => { command: 'tsx', args: ['src/index.ts'], }) + try { await client.connect(transport) - } catch (error) { - console.error( - '🚨 Could not connect to MCP server. Have you created the server and connected it to the STDIO transport yet?', - ) + } catch (error: any) { + console.error('🚨 Connection failed! This exercise requires implementing the main function in src/index.ts') + console.error('🚨 Replace the "throw new Error(\'Not implemented\')" with the actual MCP server setup') + console.error('🚨 You need to: 1) Create an EpicMeMCP instance, 2) Initialize it, 3) Connect to stdio transport') + console.error('Original error:', error.message || error) throw error } }) @@ -28,7 +30,18 @@ afterAll(async () => { }) test('Ping', async () => { - const result = await client.ping() - - expect(result).toEqual({}) + try { + const result = await client.ping() + expect(result).toEqual({}) + } catch (error: any) { + if (error.message?.includes('Connection closed') || error.code === -32000) { + console.error('🚨 Ping failed because the MCP server crashed!') + console.error('🚨 This means the main() function in src/index.ts is not properly implemented') + console.error('🚨 Check that you\'ve replaced the "Not implemented" error with actual server setup code') + const enhancedError = new Error('🚨 MCP server implementation required in main() function. ' + (error.message || error)) + enhancedError.stack = error.stack + throw enhancedError + } + throw error + } }) diff --git a/exercises/02.tools/01.problem.simple/src/index.test.ts b/exercises/02.tools/01.problem.simple/src/index.test.ts index 8361c5c..0ec7839 100644 --- a/exercises/02.tools/01.problem.simple/src/index.test.ts +++ b/exercises/02.tools/01.problem.simple/src/index.test.ts @@ -22,35 +22,61 @@ afterAll(async () => { }) test('Tool Definition', async () => { - const list = await client.listTools() - const [firstTool] = list.tools - invariant(firstTool, '🚨 No tools found') + try { + const list = await client.listTools() + const [firstTool] = list.tools + invariant(firstTool, '🚨 No tools found') - expect(firstTool).toEqual( - expect.objectContaining({ - name: expect.stringMatching(/^add$/i), - description: expect.stringMatching(/add/i), - inputSchema: expect.objectContaining({ - type: 'object', + expect(firstTool).toEqual( + expect.objectContaining({ + name: expect.stringMatching(/^add$/i), + description: expect.stringMatching(/add/i), + inputSchema: expect.objectContaining({ + type: 'object', + }), }), - }), - ) + ) + } catch (error: any) { + if (error.code === -32601) { + console.error('🚨 Tools capability not implemented!') + console.error('🚨 This exercise requires registering tools with the MCP server') + console.error('🚨 You need to: 1) Add tools: {} to server capabilities, 2) Register an "add" tool in initializeTools()') + console.error('🚨 Check src/tools.ts and make sure you implement the "add" tool') + const enhancedError = new Error('🚨 Tools capability required. Register an "add" tool that hardcodes 1 + 2 = 3. ' + (error.message || error)) + enhancedError.stack = error.stack + throw enhancedError + } + throw error + } }) test('Tool Call', async () => { - const result = await client.callTool({ - name: 'add', - arguments: {}, - }) + try { + const result = await client.callTool({ + name: 'add', + arguments: {}, + }) - expect(result).toEqual( - expect.objectContaining({ - content: expect.arrayContaining([ - expect.objectContaining({ - type: 'text', - text: expect.stringMatching(/3/), - }), - ]), - }), - ) + expect(result).toEqual( + expect.objectContaining({ + content: expect.arrayContaining([ + expect.objectContaining({ + type: 'text', + text: expect.stringMatching(/3/), + }), + ]), + }), + ) + } catch (error: any) { + if (error.code === -32601) { + console.error('🚨 Tool call failed - tools capability not implemented!') + console.error('🚨 This means you haven\'t registered the "add" tool properly') + console.error('🚨 In src/tools.ts, use agent.server.registerTool() to create a simple "add" tool') + console.error('🚨 The tool should return "1 + 2 = 3" (hardcoded for this simple exercise)') + const enhancedError = new Error('🚨 "add" tool registration required. ' + (error.message || error)) + enhancedError.stack = error.stack + throw enhancedError + } + throw error + } }) diff --git a/exercises/02.tools/01.solution.simple/src/index.test.ts b/exercises/02.tools/01.solution.simple/src/index.test.ts index 8361c5c..0ec7839 100644 --- a/exercises/02.tools/01.solution.simple/src/index.test.ts +++ b/exercises/02.tools/01.solution.simple/src/index.test.ts @@ -22,35 +22,61 @@ afterAll(async () => { }) test('Tool Definition', async () => { - const list = await client.listTools() - const [firstTool] = list.tools - invariant(firstTool, '🚨 No tools found') + try { + const list = await client.listTools() + const [firstTool] = list.tools + invariant(firstTool, '🚨 No tools found') - expect(firstTool).toEqual( - expect.objectContaining({ - name: expect.stringMatching(/^add$/i), - description: expect.stringMatching(/add/i), - inputSchema: expect.objectContaining({ - type: 'object', + expect(firstTool).toEqual( + expect.objectContaining({ + name: expect.stringMatching(/^add$/i), + description: expect.stringMatching(/add/i), + inputSchema: expect.objectContaining({ + type: 'object', + }), }), - }), - ) + ) + } catch (error: any) { + if (error.code === -32601) { + console.error('🚨 Tools capability not implemented!') + console.error('🚨 This exercise requires registering tools with the MCP server') + console.error('🚨 You need to: 1) Add tools: {} to server capabilities, 2) Register an "add" tool in initializeTools()') + console.error('🚨 Check src/tools.ts and make sure you implement the "add" tool') + const enhancedError = new Error('🚨 Tools capability required. Register an "add" tool that hardcodes 1 + 2 = 3. ' + (error.message || error)) + enhancedError.stack = error.stack + throw enhancedError + } + throw error + } }) test('Tool Call', async () => { - const result = await client.callTool({ - name: 'add', - arguments: {}, - }) + try { + const result = await client.callTool({ + name: 'add', + arguments: {}, + }) - expect(result).toEqual( - expect.objectContaining({ - content: expect.arrayContaining([ - expect.objectContaining({ - type: 'text', - text: expect.stringMatching(/3/), - }), - ]), - }), - ) + expect(result).toEqual( + expect.objectContaining({ + content: expect.arrayContaining([ + expect.objectContaining({ + type: 'text', + text: expect.stringMatching(/3/), + }), + ]), + }), + ) + } catch (error: any) { + if (error.code === -32601) { + console.error('🚨 Tool call failed - tools capability not implemented!') + console.error('🚨 This means you haven\'t registered the "add" tool properly') + console.error('🚨 In src/tools.ts, use agent.server.registerTool() to create a simple "add" tool') + console.error('🚨 The tool should return "1 + 2 = 3" (hardcoded for this simple exercise)') + const enhancedError = new Error('🚨 "add" tool registration required. ' + (error.message || error)) + enhancedError.stack = error.stack + throw enhancedError + } + throw error + } }) diff --git a/exercises/02.tools/02.problem.args/src/index.test.ts b/exercises/02.tools/02.problem.args/src/index.test.ts index 46185ae..37e3802 100644 --- a/exercises/02.tools/02.problem.args/src/index.test.ts +++ b/exercises/02.tools/02.problem.args/src/index.test.ts @@ -26,26 +26,38 @@ test('Tool Definition', async () => { const [firstTool] = list.tools invariant(firstTool, '🚨 No tools found') - expect(firstTool).toEqual( - expect.objectContaining({ - name: expect.stringMatching(/^add$/i), - description: expect.stringMatching(/^add two numbers$/i), - inputSchema: expect.objectContaining({ - type: 'object', - properties: expect.objectContaining({ - firstNumber: expect.objectContaining({ - type: 'number', - description: expect.stringMatching(/first/i), - }), - secondNumber: expect.objectContaining({ - type: 'number', - description: expect.stringMatching(/second/i), + try { + expect(firstTool).toEqual( + expect.objectContaining({ + name: expect.stringMatching(/^add$/i), + description: expect.stringMatching(/^add two numbers$/i), + inputSchema: expect.objectContaining({ + type: 'object', + properties: expect.objectContaining({ + firstNumber: expect.objectContaining({ + type: 'number', + description: expect.stringMatching(/first/i), + }), + secondNumber: expect.objectContaining({ + type: 'number', + description: expect.stringMatching(/second/i), + }), }), + required: expect.arrayContaining(['firstNumber', 'secondNumber']), }), - required: expect.arrayContaining(['firstNumber', 'secondNumber']), }), - }), - ) + ) + } catch (error: any) { + console.error('🚨 Tool schema mismatch!') + console.error('🚨 This exercise requires updating the "add" tool to accept dynamic arguments') + console.error('🚨 Current tool schema:', JSON.stringify(firstTool, null, 2)) + console.error('🚨 You need to: 1) Add proper inputSchema with firstNumber and secondNumber parameters') + console.error('🚨 2) Update the tool description to "add two numbers"') + console.error('🚨 3) Make the tool calculate firstNumber + secondNumber instead of hardcoding 1 + 2') + const enhancedError = new Error('🚨 Tool schema update required. Add firstNumber and secondNumber parameters to the "add" tool. ' + (error.message || error)) + enhancedError.stack = error.stack + throw enhancedError + } // 🚨 Proactive check: Ensure the tool schema includes both required arguments invariant( @@ -80,22 +92,33 @@ test('Tool Call', async () => { }) test('Tool Call with Different Numbers', async () => { - const result = await client.callTool({ - name: 'add', - arguments: { - firstNumber: 5, - secondNumber: 7, - }, - }) + try { + const result = await client.callTool({ + name: 'add', + arguments: { + firstNumber: 5, + secondNumber: 7, + }, + }) - expect(result).toEqual( - expect.objectContaining({ - content: expect.arrayContaining([ - expect.objectContaining({ - type: 'text', - text: expect.stringMatching(/12/), - }), - ]), - }), - ) + expect(result).toEqual( + expect.objectContaining({ + content: expect.arrayContaining([ + expect.objectContaining({ + type: 'text', + text: expect.stringMatching(/12/), + }), + ]), + }), + ) + } catch (error: any) { + console.error('🚨 Tool call with different numbers failed!') + console.error('🚨 This suggests the tool implementation is still hardcoded') + console.error('🚨 The tool should calculate firstNumber + secondNumber = 5 + 7 = 12') + console.error('🚨 But it\'s probably still returning hardcoded "1 + 2 = 3"') + console.error('🚨 Update the tool implementation to use the dynamic arguments from the input schema') + const enhancedError = new Error('🚨 Dynamic tool calculation required. Tool should calculate arguments, not return hardcoded values. ' + (error.message || error)) + enhancedError.stack = error.stack + throw enhancedError + } }) diff --git a/exercises/02.tools/02.solution.args/src/index.test.ts b/exercises/02.tools/02.solution.args/src/index.test.ts index 46185ae..37e3802 100644 --- a/exercises/02.tools/02.solution.args/src/index.test.ts +++ b/exercises/02.tools/02.solution.args/src/index.test.ts @@ -26,26 +26,38 @@ test('Tool Definition', async () => { const [firstTool] = list.tools invariant(firstTool, '🚨 No tools found') - expect(firstTool).toEqual( - expect.objectContaining({ - name: expect.stringMatching(/^add$/i), - description: expect.stringMatching(/^add two numbers$/i), - inputSchema: expect.objectContaining({ - type: 'object', - properties: expect.objectContaining({ - firstNumber: expect.objectContaining({ - type: 'number', - description: expect.stringMatching(/first/i), - }), - secondNumber: expect.objectContaining({ - type: 'number', - description: expect.stringMatching(/second/i), + try { + expect(firstTool).toEqual( + expect.objectContaining({ + name: expect.stringMatching(/^add$/i), + description: expect.stringMatching(/^add two numbers$/i), + inputSchema: expect.objectContaining({ + type: 'object', + properties: expect.objectContaining({ + firstNumber: expect.objectContaining({ + type: 'number', + description: expect.stringMatching(/first/i), + }), + secondNumber: expect.objectContaining({ + type: 'number', + description: expect.stringMatching(/second/i), + }), }), + required: expect.arrayContaining(['firstNumber', 'secondNumber']), }), - required: expect.arrayContaining(['firstNumber', 'secondNumber']), }), - }), - ) + ) + } catch (error: any) { + console.error('🚨 Tool schema mismatch!') + console.error('🚨 This exercise requires updating the "add" tool to accept dynamic arguments') + console.error('🚨 Current tool schema:', JSON.stringify(firstTool, null, 2)) + console.error('🚨 You need to: 1) Add proper inputSchema with firstNumber and secondNumber parameters') + console.error('🚨 2) Update the tool description to "add two numbers"') + console.error('🚨 3) Make the tool calculate firstNumber + secondNumber instead of hardcoding 1 + 2') + const enhancedError = new Error('🚨 Tool schema update required. Add firstNumber and secondNumber parameters to the "add" tool. ' + (error.message || error)) + enhancedError.stack = error.stack + throw enhancedError + } // 🚨 Proactive check: Ensure the tool schema includes both required arguments invariant( @@ -80,22 +92,33 @@ test('Tool Call', async () => { }) test('Tool Call with Different Numbers', async () => { - const result = await client.callTool({ - name: 'add', - arguments: { - firstNumber: 5, - secondNumber: 7, - }, - }) + try { + const result = await client.callTool({ + name: 'add', + arguments: { + firstNumber: 5, + secondNumber: 7, + }, + }) - expect(result).toEqual( - expect.objectContaining({ - content: expect.arrayContaining([ - expect.objectContaining({ - type: 'text', - text: expect.stringMatching(/12/), - }), - ]), - }), - ) + expect(result).toEqual( + expect.objectContaining({ + content: expect.arrayContaining([ + expect.objectContaining({ + type: 'text', + text: expect.stringMatching(/12/), + }), + ]), + }), + ) + } catch (error: any) { + console.error('🚨 Tool call with different numbers failed!') + console.error('🚨 This suggests the tool implementation is still hardcoded') + console.error('🚨 The tool should calculate firstNumber + secondNumber = 5 + 7 = 12') + console.error('🚨 But it\'s probably still returning hardcoded "1 + 2 = 3"') + console.error('🚨 Update the tool implementation to use the dynamic arguments from the input schema') + const enhancedError = new Error('🚨 Dynamic tool calculation required. Tool should calculate arguments, not return hardcoded values. ' + (error.message || error)) + enhancedError.stack = error.stack + throw enhancedError + } }) diff --git a/exercises/03.resources/01.problem.simple/src/index.test.ts b/exercises/03.resources/01.problem.simple/src/index.test.ts index fbd5768..464550e 100644 --- a/exercises/03.resources/01.problem.simple/src/index.test.ts +++ b/exercises/03.resources/01.problem.simple/src/index.test.ts @@ -81,50 +81,76 @@ test('Tool Call', async () => { }) test('Resource List', async () => { - const list = await client.listResources() - const tagsResource = list.resources.find(r => r.name === 'tags') - - // 🚨 Proactive check: Ensure the tags resource is registered - invariant(tagsResource, '🚨 No "tags" resource found - make sure to register the tags resource') - - expect(tagsResource).toEqual( - expect.objectContaining({ - name: 'tags', - uri: expect.stringMatching(/^epicme:\/\/tags$/i), - description: expect.stringMatching(/tags/i), - }), - ) + try { + const list = await client.listResources() + const tagsResource = list.resources.find(r => r.name === 'tags') + + // 🚨 Proactive check: Ensure the tags resource is registered + invariant(tagsResource, '🚨 No "tags" resource found - make sure to register the tags resource') + + expect(tagsResource).toEqual( + expect.objectContaining({ + name: 'tags', + uri: expect.stringMatching(/^epicme:\/\/tags$/i), + description: expect.stringMatching(/tags/i), + }), + ) + } catch (error: any) { + if (error.code === -32601) { + console.error('🚨 Resources capability not implemented!') + console.error('🚨 This exercise requires implementing resources with the MCP server') + console.error('🚨 You need to: 1) Add resources: {} to server capabilities, 2) Register a "tags" resource in initializeResources()') + console.error('🚨 Check src/resources.ts and implement a static resource for "epicme://tags"') + const enhancedError = new Error('🚨 Resources capability required. Register a "tags" resource that returns all tags from the database. ' + (error.message || error)) + enhancedError.stack = error.stack + throw enhancedError + } + throw error + } }) test('Tags Resource Read', async () => { - const result = await client.readResource({ - uri: 'epicme://tags', - }) - - expect(result).toEqual( - expect.objectContaining({ - contents: expect.arrayContaining([ - expect.objectContaining({ - mimeType: 'application/json', - uri: 'epicme://tags', - text: expect.any(String), - }), - ]), - }), - ) - - // 🚨 Proactive check: Ensure the resource content is valid JSON - const content = result.contents[0] - invariant(content && 'text' in content, '🚨 Resource content must have text field') - invariant(typeof content.text === 'string', '🚨 Resource content text must be a string') - - let tags: unknown try { - tags = JSON.parse(content.text) - } catch (error) { - throw new Error('🚨 Resource content must be valid JSON') + const result = await client.readResource({ + uri: 'epicme://tags', + }) + + expect(result).toEqual( + expect.objectContaining({ + contents: expect.arrayContaining([ + expect.objectContaining({ + mimeType: 'application/json', + uri: 'epicme://tags', + text: expect.any(String), + }), + ]), + }), + ) + + // 🚨 Proactive check: Ensure the resource content is valid JSON + const content = result.contents[0] + invariant(content && 'text' in content, '🚨 Resource content must have text field') + invariant(typeof content.text === 'string', '🚨 Resource content text must be a string') + + let tags: unknown + try { + tags = JSON.parse(content.text) + } catch (error) { + throw new Error('🚨 Resource content must be valid JSON') + } + + // 🚨 Proactive check: Ensure tags is an array + invariant(Array.isArray(tags), '🚨 Tags resource should return an array of tags') + } catch (error: any) { + if (error.code === -32601) { + console.error('🚨 Resource read failed - resources capability not implemented!') + console.error('🚨 This means you haven\'t registered the "tags" resource properly') + console.error('🚨 In src/resources.ts, use agent.server.registerResource() to create a "tags" resource') + console.error('🚨 The resource should return JSON array of all tags from agent.db.getTags()') + const enhancedError = new Error('🚨 "tags" resource registration required. ' + (error.message || error)) + enhancedError.stack = error.stack + throw enhancedError + } + throw error } - - // 🚨 Proactive check: Ensure tags is an array - invariant(Array.isArray(tags), '🚨 Tags resource should return an array of tags') }) diff --git a/exercises/03.resources/01.solution.simple/src/index.test.ts b/exercises/03.resources/01.solution.simple/src/index.test.ts index fbd5768..464550e 100644 --- a/exercises/03.resources/01.solution.simple/src/index.test.ts +++ b/exercises/03.resources/01.solution.simple/src/index.test.ts @@ -81,50 +81,76 @@ test('Tool Call', async () => { }) test('Resource List', async () => { - const list = await client.listResources() - const tagsResource = list.resources.find(r => r.name === 'tags') - - // 🚨 Proactive check: Ensure the tags resource is registered - invariant(tagsResource, '🚨 No "tags" resource found - make sure to register the tags resource') - - expect(tagsResource).toEqual( - expect.objectContaining({ - name: 'tags', - uri: expect.stringMatching(/^epicme:\/\/tags$/i), - description: expect.stringMatching(/tags/i), - }), - ) + try { + const list = await client.listResources() + const tagsResource = list.resources.find(r => r.name === 'tags') + + // 🚨 Proactive check: Ensure the tags resource is registered + invariant(tagsResource, '🚨 No "tags" resource found - make sure to register the tags resource') + + expect(tagsResource).toEqual( + expect.objectContaining({ + name: 'tags', + uri: expect.stringMatching(/^epicme:\/\/tags$/i), + description: expect.stringMatching(/tags/i), + }), + ) + } catch (error: any) { + if (error.code === -32601) { + console.error('🚨 Resources capability not implemented!') + console.error('🚨 This exercise requires implementing resources with the MCP server') + console.error('🚨 You need to: 1) Add resources: {} to server capabilities, 2) Register a "tags" resource in initializeResources()') + console.error('🚨 Check src/resources.ts and implement a static resource for "epicme://tags"') + const enhancedError = new Error('🚨 Resources capability required. Register a "tags" resource that returns all tags from the database. ' + (error.message || error)) + enhancedError.stack = error.stack + throw enhancedError + } + throw error + } }) test('Tags Resource Read', async () => { - const result = await client.readResource({ - uri: 'epicme://tags', - }) - - expect(result).toEqual( - expect.objectContaining({ - contents: expect.arrayContaining([ - expect.objectContaining({ - mimeType: 'application/json', - uri: 'epicme://tags', - text: expect.any(String), - }), - ]), - }), - ) - - // 🚨 Proactive check: Ensure the resource content is valid JSON - const content = result.contents[0] - invariant(content && 'text' in content, '🚨 Resource content must have text field') - invariant(typeof content.text === 'string', '🚨 Resource content text must be a string') - - let tags: unknown try { - tags = JSON.parse(content.text) - } catch (error) { - throw new Error('🚨 Resource content must be valid JSON') + const result = await client.readResource({ + uri: 'epicme://tags', + }) + + expect(result).toEqual( + expect.objectContaining({ + contents: expect.arrayContaining([ + expect.objectContaining({ + mimeType: 'application/json', + uri: 'epicme://tags', + text: expect.any(String), + }), + ]), + }), + ) + + // 🚨 Proactive check: Ensure the resource content is valid JSON + const content = result.contents[0] + invariant(content && 'text' in content, '🚨 Resource content must have text field') + invariant(typeof content.text === 'string', '🚨 Resource content text must be a string') + + let tags: unknown + try { + tags = JSON.parse(content.text) + } catch (error) { + throw new Error('🚨 Resource content must be valid JSON') + } + + // 🚨 Proactive check: Ensure tags is an array + invariant(Array.isArray(tags), '🚨 Tags resource should return an array of tags') + } catch (error: any) { + if (error.code === -32601) { + console.error('🚨 Resource read failed - resources capability not implemented!') + console.error('🚨 This means you haven\'t registered the "tags" resource properly') + console.error('🚨 In src/resources.ts, use agent.server.registerResource() to create a "tags" resource') + console.error('🚨 The resource should return JSON array of all tags from agent.db.getTags()') + const enhancedError = new Error('🚨 "tags" resource registration required. ' + (error.message || error)) + enhancedError.stack = error.stack + throw enhancedError + } + throw error } - - // 🚨 Proactive check: Ensure tags is an array - invariant(Array.isArray(tags), '🚨 Tags resource should return an array of tags') }) diff --git a/exercises/05.sampling/01.problem.simple/src/index.test.ts b/exercises/05.sampling/01.problem.simple/src/index.test.ts index ab7f8ac..4085e54 100644 --- a/exercises/05.sampling/01.problem.simple/src/index.test.ts +++ b/exercises/05.sampling/01.problem.simple/src/index.test.ts @@ -102,46 +102,71 @@ test('Sampling', async () => { return messageResultDeferred.promise }) - const entry = { - title: faker.lorem.words(3), - content: faker.lorem.paragraphs(2), - } - await client.callTool({ - name: 'create_entry', - arguments: entry, - }) - const request = await messageRequestDeferred.promise + try { + const entry = { + title: faker.lorem.words(3), + content: faker.lorem.paragraphs(2), + } + await client.callTool({ + name: 'create_entry', + arguments: entry, + }) + + // Add a timeout wrapper to detect if sampling isn't working + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('🚨 Sampling timeout - server did not send a sampling request')) + }, 3000) // Shorter timeout for better UX + }) + + const request = await Promise.race([ + messageRequestDeferred.promise, + timeoutPromise + ]) - expect(request).toEqual( - expect.objectContaining({ - method: 'sampling/createMessage', - params: expect.objectContaining({ - maxTokens: expect.any(Number), - systemPrompt: expect.any(String), - messages: expect.arrayContaining([ - expect.objectContaining({ - role: 'user', - content: expect.objectContaining({ - type: 'text', - text: expect.any(String), - mimeType: 'text/plain', + expect(request).toEqual( + expect.objectContaining({ + method: 'sampling/createMessage', + params: expect.objectContaining({ + maxTokens: expect.any(Number), + systemPrompt: expect.any(String), + messages: expect.arrayContaining([ + expect.objectContaining({ + role: 'user', + content: expect.objectContaining({ + type: 'text', + text: expect.any(String), + mimeType: 'text/plain', + }), }), - }), - ]), + ]), + }), }), - }), - ) + ) - messageResultDeferred.resolve({ - model: 'stub-model', - stopReason: 'endTurn', - role: 'assistant', - content: { - type: 'text', - text: 'Congratulations!', - }, - }) + messageResultDeferred.resolve({ + model: 'stub-model', + stopReason: 'endTurn', + role: 'assistant', + content: { + type: 'text', + text: 'Congratulations!', + }, + }) - // give the server a chance to process the result - await new Promise((resolve) => setTimeout(resolve, 100)) -}) + // give the server a chance to process the result + await new Promise((resolve) => setTimeout(resolve, 100)) + } catch (error: any) { + if (error.message?.includes('Sampling timeout') || error.message?.includes('Test timed out')) { + console.error('🚨 Sampling capability not implemented!') + console.error('🚨 This exercise requires implementing sampling requests to interact with LLMs') + console.error('🚨 You need to: 1) Connect the client to your server, 2) Use client.createMessage() after tool calls') + console.error('🚨 The create_entry tool should trigger a sampling request to celebrate the user\'s accomplishment') + console.error('🚨 Check that your tool implementation includes a client.createMessage() call') + const enhancedError = new Error('🚨 Sampling capability required. Tool should send LLM requests after creating entries. ' + (error.message || error)) + enhancedError.stack = error.stack + throw enhancedError + } + throw error + } +}, 10000) // Increase overall test timeout From 43b6e374c19406bc33528365bbace72c19b8ee45 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 2 Jul 2025 16:02:04 +0000 Subject: [PATCH 6/6] Enhance test suites with detailed error messages and proactive checks Co-authored-by: me --- .../03.problem.errors/src/index.test.ts | 35 +++-- .../02.problem.template/src/index.test.ts | 73 ++++++---- .../04.problem.completion/src/index.test.ts | 58 ++++---- .../05.problem.linked/src/index.test.ts | 62 +++++++++ .../06.problem.embedded/src/index.test.ts | 113 ++++++++++++++- .../01.problem.prompts/src/index.test.ts | 131 +++++++++++------- .../src/index.test.ts | 131 ++++++++++-------- .../03.problem.completion/src/index.test.ts | 113 +++++++++++++++ .../02.problem.advanced/src/index.test.ts | 105 ++++++++------ 9 files changed, 605 insertions(+), 216 deletions(-) diff --git a/exercises/02.tools/03.problem.errors/src/index.test.ts b/exercises/02.tools/03.problem.errors/src/index.test.ts index 7f0decd..576ed7d 100644 --- a/exercises/02.tools/03.problem.errors/src/index.test.ts +++ b/exercises/02.tools/03.problem.errors/src/index.test.ts @@ -88,17 +88,30 @@ test('Tool Call - Error with Negative Second Number', async () => { }, }) - expect(result).toEqual( - expect.objectContaining({ - content: expect.arrayContaining([ - expect.objectContaining({ - type: 'text', - text: expect.stringMatching(/negative/i), - }), - ]), - isError: true, - }), - ) + try { + expect(result).toEqual( + expect.objectContaining({ + content: expect.arrayContaining([ + expect.objectContaining({ + type: 'text', + text: expect.stringMatching(/negative/i), + }), + ]), + isError: true, + }), + ) + } catch (error) { + console.error('🚨 Tool error handling not properly implemented!') + console.error('🚨 This exercise teaches you how to handle errors in MCP tools') + console.error('🚨 Expected: Tool should return isError: true with message about negative numbers') + console.error(`🚨 Actual: Tool returned normal response: ${JSON.stringify(result, null, 2)}`) + console.error('🚨 You need to:') + console.error('🚨 1. Check if secondNumber is negative in your add tool') + console.error('🚨 2. Throw an Error with message containing "negative"') + console.error('🚨 3. The MCP SDK will automatically set isError: true') + console.error('🚨 In src/index.ts, add: if (secondNumber < 0) throw new Error("Second number cannot be negative")') + throw new Error(`🚨 Tool should return error response when secondNumber is negative, but returned normal response instead. ${error}`) + } }) test('Tool Call - Another Successful Addition', async () => { diff --git a/exercises/03.resources/02.problem.template/src/index.test.ts b/exercises/03.resources/02.problem.template/src/index.test.ts index 9d23f38..008ba75 100644 --- a/exercises/03.resources/02.problem.template/src/index.test.ts +++ b/exercises/03.resources/02.problem.template/src/index.test.ts @@ -124,36 +124,51 @@ test('Resource Template Read - Entry', async () => { }, }) - const result = await client.readResource({ - uri: 'epicme://entries/1', - }) - - expect(result).toEqual( - expect.objectContaining({ - contents: expect.arrayContaining([ - expect.objectContaining({ - mimeType: 'application/json', - uri: 'epicme://entries/1', - text: expect.any(String), - }), - ]), - }), - ) - - // 🚨 Proactive check: Ensure the resource content is valid JSON and contains entry data - const content = result.contents[0] - invariant(content && 'text' in content, '🚨 Resource content must have text field') - invariant(typeof content.text === 'string', '🚨 Resource content text must be a string') - - let entryData: any try { - entryData = JSON.parse(content.text) + const result = await client.readResource({ + uri: 'epicme://entries/1', + }) + + expect(result).toEqual( + expect.objectContaining({ + contents: expect.arrayContaining([ + expect.objectContaining({ + mimeType: 'application/json', + uri: 'epicme://entries/1', + text: expect.any(String), + }), + ]), + }), + ) + + // 🚨 Proactive check: Ensure the resource content is valid JSON and contains entry data + const content = result.contents[0] + invariant(content && 'text' in content, '🚨 Resource content must have text field') + invariant(typeof content.text === 'string', '🚨 Resource content text must be a string') + + let entryData: any + try { + entryData = JSON.parse(content.text) + } catch (error) { + throw new Error('🚨 Resource content must be valid JSON') + } + + // 🚨 Proactive check: Ensure entry data contains expected fields + invariant(entryData.id, '🚨 Entry resource should contain id field') + invariant(entryData.title, '🚨 Entry resource should contain title field') + invariant(entryData.content, '🚨 Entry resource should contain content field') } catch (error) { - throw new Error('🚨 Resource content must be valid JSON') + if (error instanceof Error && error.message.includes('Resource epicme://entries/1 not found')) { + console.error('🚨 Resource template reading not implemented!') + console.error('🚨 This exercise teaches parameterized resource URIs like epicme://entries/{id}') + console.error('🚨 You need to:') + console.error('🚨 1. Register resource templates with server.setRequestHandler(ListResourceTemplatesRequestSchema, ...)') + console.error('🚨 2. Handle ReadResourceRequestSchema with URI parameter extraction') + console.error('🚨 3. Parse the {id} from the URI and query your database') + console.error('🚨 4. Return the resource content as JSON') + console.error('🚨 Check the solution to see how to extract parameters from template URIs') + throw new Error(`🚨 Resource template reading not implemented - need to handle parameterized URIs like epicme://entries/1. ${error}`) + } + throw error } - - // 🚨 Proactive check: Ensure entry data contains expected fields - invariant(entryData.id, '🚨 Entry resource should contain id field') - invariant(entryData.title, '🚨 Entry resource should contain title field') - invariant(entryData.content, '🚨 Entry resource should contain content field') }) diff --git a/exercises/03.resources/04.problem.completion/src/index.test.ts b/exercises/03.resources/04.problem.completion/src/index.test.ts index e9c525c..632939d 100644 --- a/exercises/03.resources/04.problem.completion/src/index.test.ts +++ b/exercises/03.resources/04.problem.completion/src/index.test.ts @@ -112,35 +112,45 @@ test('Resource Template Completions', async () => { // 🚨 The key learning objective for this exercise is adding completion support // This requires BOTH declaring completions capability AND implementing complete callbacks - // Test if completion capability is properly declared by trying to use completion API - let completionSupported = false try { - // This should work if server declares completion capability and implements complete callbacks - await (client as any)._client.request({ - method: 'completion/complete', - params: { - ref: { - type: 'resource', - uri: 'epicme://entries/{id}', - }, - argument: { - name: 'id', - value: '1', - }, + // Test completion functionality using the proper MCP SDK method + const completionResult = await (client as any).completeResource({ + ref: { + type: 'resource', + uri: entriesTemplate.uriTemplate, }, + argument: { + name: 'id', + value: '1', // Should match at least one of our created entries + }, + }) + + // 🚨 Proactive check: Completion should return results + invariant(Array.isArray(completionResult.completion?.values), '🚨 Completion should return an array of values') + invariant(completionResult.completion.values.length > 0, '🚨 Completion should return at least one matching result for id="1"') + + // Check that completion values are strings + completionResult.completion.values.forEach((value: any) => { + invariant(typeof value === 'string', '🚨 Completion values should be strings') }) - completionSupported = true + } catch (error: any) { - // -32601 = Method not found (missing completion capability) - // -32602 = Invalid params (missing complete callbacks) - if (error?.code === -32601 || error?.code === -32602) { - completionSupported = false + console.error('🚨 Resource template completion not fully implemented!') + console.error('🚨 This exercise teaches you how to add completion support to resource templates') + console.error('🚨 You need to:') + console.error('🚨 1. Add "completion" to your server capabilities') + console.error('🚨 2. Add complete callback to your ResourceTemplate:') + console.error('🚨 complete: { async id(value) { return ["1", "2", "3"] } }') + console.error('🚨 3. The complete callback should filter entries matching the partial value') + console.error('🚨 4. Return an array of valid completion strings') + console.error(`🚨 Error details: ${error?.message || error}`) + + if (error?.code === -32601) { + throw new Error('🚨 Completion capability not declared - add "completion" to server capabilities and implement complete callbacks') + } else if (error?.code === -32602) { + throw new Error('🚨 Complete callback not implemented - add complete: { async id(value) { ... } } to your ResourceTemplate') } else { - // Other errors might be acceptable (like no matches found) - completionSupported = true + throw new Error(`🚨 Resource template completion not working - check capability declaration and complete callback implementation. ${error}`) } } - - // 🚨 Proactive check: Completion functionality must be fully implemented - invariant(completionSupported, '🚨 Resource template completion requires both declaring completions capability in server AND implementing complete callbacks for template parameters') }) diff --git a/exercises/03.resources/05.problem.linked/src/index.test.ts b/exercises/03.resources/05.problem.linked/src/index.test.ts index 2c0c5b4..9afb6a4 100644 --- a/exercises/03.resources/05.problem.linked/src/index.test.ts +++ b/exercises/03.resources/05.problem.linked/src/index.test.ts @@ -69,3 +69,65 @@ test('Tool Call', async () => { }), ) }) + +test('Resource Link in Tool Response', async () => { + try { + const result = await client.callTool({ + name: 'create_entry', + arguments: { + title: 'Linked Entry Test', + content: 'This entry should be linked as a resource', + }, + }) + + // 🚨 The key learning objective: Tool responses should include resource_link content + // when creating resources, not just text confirmations + + // Type guard for content array + const content = result.content as Array + invariant(Array.isArray(content), '🚨 Tool response content must be an array') + + // Check if response includes resource_link content type + const hasResourceLink = content.some((item: any) => + item.type === 'resource_link' + ) + + if (!hasResourceLink) { + throw new Error('Tool response should include resource_link content type') + } + + // Find the resource_link content + const resourceLink = content.find((item: any) => + item.type === 'resource_link' + ) as any + + // 🚨 Proactive checks: Resource link should have proper structure + invariant(resourceLink, '🚨 Tool response should include resource_link content type') + invariant(resourceLink.uri, '🚨 Resource link must have uri field') + invariant(resourceLink.name, '🚨 Resource link must have name field') + invariant(typeof resourceLink.uri === 'string', '🚨 Resource link uri must be a string') + invariant(typeof resourceLink.name === 'string', '🚨 Resource link name must be a string') + invariant(resourceLink.uri.includes('entries'), '🚨 Resource link URI should reference the created entry') + + expect(resourceLink).toEqual( + expect.objectContaining({ + type: 'resource_link', + uri: expect.stringMatching(/epicme:\/\/entries\/\d+/), + name: expect.stringMatching(/Linked Entry Test/), + description: expect.any(String), + mimeType: expect.stringMatching(/application\/json/), + }), + ) + + } catch (error) { + console.error('🚨 Resource linking not implemented in tool responses!') + console.error('🚨 This exercise teaches you how to include resource links in tool responses') + console.error('🚨 You need to:') + console.error('🚨 1. When your tool creates a resource, include a resource_link content item') + console.error('🚨 2. Set type: "resource_link" in the response content') + console.error('🚨 3. Include uri, name, description, and mimeType fields') + console.error('🚨 4. The URI should point to the created resource (e.g., epicme://entries/1)') + console.error('🚨 Example: { type: "resource_link", uri: "epicme://entries/1", name: "My Entry", description: "...", mimeType: "application/json" }') + throw new Error(`🚨 Tool should include resource_link content type when creating resources. ${error}`) + } +}) diff --git a/exercises/03.resources/06.problem.embedded/src/index.test.ts b/exercises/03.resources/06.problem.embedded/src/index.test.ts index 2c0c5b4..a8c93ef 100644 --- a/exercises/03.resources/06.problem.embedded/src/index.test.ts +++ b/exercises/03.resources/06.problem.embedded/src/index.test.ts @@ -23,10 +23,17 @@ afterAll(async () => { test('Tool Definition', async () => { const list = await client.listTools() - const [firstTool] = list.tools - invariant(firstTool, '🚨 No tools found') + + // 🚨 Proactive check: Should have both create_entry and get_entry tools + invariant(list.tools.length >= 2, '🚨 Should have both create_entry and get_entry tools for this exercise') + + const createTool = list.tools.find(tool => tool.name.toLowerCase().includes('create')) + const getTool = list.tools.find(tool => tool.name.toLowerCase().includes('get')) + + invariant(createTool, '🚨 No create_entry tool found') + invariant(getTool, '🚨 No get_entry tool found - this exercise requires implementing get_entry tool') - expect(firstTool).toEqual( + expect(createTool).toEqual( expect.objectContaining({ name: expect.stringMatching(/^create_entry$/i), description: expect.stringMatching(/^create a new journal entry$/i), @@ -45,6 +52,22 @@ test('Tool Definition', async () => { }), }), ) + + expect(getTool).toEqual( + expect.objectContaining({ + name: expect.stringMatching(/^get_entry$/i), + description: expect.stringMatching(/^get.*entry$/i), + inputSchema: expect.objectContaining({ + type: 'object', + properties: expect.objectContaining({ + id: expect.objectContaining({ + type: 'number', + description: expect.stringMatching(/id/i), + }), + }), + }), + }), + ) }) test('Tool Call', async () => { @@ -69,3 +92,87 @@ test('Tool Call', async () => { }), ) }) + +test('Embedded Resource in Tool Response', async () => { + // First create an entry to get + await client.callTool({ + name: 'create_entry', + arguments: { + title: 'Embedded Resource Test', + content: 'This entry should be returned as an embedded resource', + }, + }) + + try { + const result = await client.callTool({ + name: 'get_entry', + arguments: { + id: 1, + }, + }) + + // 🚨 The key learning objective: Tool responses should include embedded resources + // with type: 'resource' instead of just text content + + // Type guard for content array + const content = result.content as Array + invariant(Array.isArray(content), '🚨 Tool response content must be an array') + + // Check if response includes embedded resource content type + const hasEmbeddedResource = content.some((item: any) => + item.type === 'resource' + ) + + if (!hasEmbeddedResource) { + throw new Error('Tool response should include embedded resource content type') + } + + // Find the embedded resource content + const embeddedResource = content.find((item: any) => + item.type === 'resource' + ) as any + + // 🚨 Proactive checks: Embedded resource should have proper structure + invariant(embeddedResource, '🚨 Tool response should include embedded resource content type') + invariant(embeddedResource.resource, '🚨 Embedded resource must have resource field') + invariant(embeddedResource.resource.uri, '🚨 Embedded resource must have uri field') + invariant(embeddedResource.resource.mimeType, '🚨 Embedded resource must have mimeType field') + invariant(embeddedResource.resource.text, '🚨 Embedded resource must have text field') + invariant(typeof embeddedResource.resource.uri === 'string', '🚨 Embedded resource uri must be a string') + invariant(embeddedResource.resource.uri.includes('entries'), '🚨 Embedded resource URI should reference an entry') + + expect(embeddedResource).toEqual( + expect.objectContaining({ + type: 'resource', + resource: expect.objectContaining({ + uri: expect.stringMatching(/epicme:\/\/entries\/\d+/), + mimeType: 'application/json', + text: expect.any(String), + }), + }), + ) + + // 🚨 Proactive check: Embedded resource text should be valid JSON with entry data + let entryData: any + try { + entryData = JSON.parse(embeddedResource.resource.text) + } catch (error) { + throw new Error('🚨 Embedded resource text must be valid JSON') + } + + invariant(entryData.id, '🚨 Embedded entry resource should contain id field') + invariant(entryData.title, '🚨 Embedded entry resource should contain title field') + invariant(entryData.content, '🚨 Embedded entry resource should contain content field') + + } catch (error) { + console.error('🚨 Embedded resources not implemented in get_entry tool!') + console.error('🚨 This exercise teaches you how to embed resources in tool responses') + console.error('🚨 You need to:') + console.error('🚨 1. Implement a get_entry tool that takes an id parameter') + console.error('🚨 2. Instead of returning just text, return content with type: "resource"') + console.error('🚨 3. Include resource object with uri, mimeType, and text fields') + console.error('🚨 4. The text field should contain the JSON representation of the entry') + console.error('🚨 Example: { type: "resource", resource: { uri: "epicme://entries/1", mimeType: "application/json", text: "{\\"id\\": 1, ...}" } }') + throw new Error(`🚨 get_entry tool should return embedded resource content type. ${error}`) + } +}) diff --git a/exercises/04.prompts/01.problem.prompts/src/index.test.ts b/exercises/04.prompts/01.problem.prompts/src/index.test.ts index 5ad2ae7..9514637 100644 --- a/exercises/04.prompts/01.problem.prompts/src/index.test.ts +++ b/exercises/04.prompts/01.problem.prompts/src/index.test.ts @@ -81,59 +81,90 @@ test('Tool Call', async () => { }) test('Prompts List', async () => { - const list = await client.listPrompts() - - // 🚨 Proactive check: Ensure prompts are registered - invariant(list.prompts.length > 0, '🚨 No prompts found - make sure to register prompts with the prompts capability') - - const tagSuggestionsPrompt = list.prompts.find(p => p.name.includes('tag') || p.name.includes('suggest')) - invariant(tagSuggestionsPrompt, '🚨 No tag suggestions prompt found - should include a prompt for suggesting tags') - - expect(tagSuggestionsPrompt).toEqual( - expect.objectContaining({ - name: expect.any(String), - description: expect.stringMatching(/tag|suggest/i), - arguments: expect.arrayContaining([ - expect.objectContaining({ - name: expect.stringMatching(/entry|id/i), - description: expect.any(String), - required: true, - }), - ]), - }), - ) + try { + const list = await client.listPrompts() + + // 🚨 Proactive check: Ensure prompts are registered + invariant(list.prompts.length > 0, '🚨 No prompts found - make sure to register prompts with the prompts capability') + + const tagSuggestionsPrompt = list.prompts.find(p => p.name.includes('tag') || p.name.includes('suggest')) + invariant(tagSuggestionsPrompt, '🚨 No tag suggestions prompt found - should include a prompt for suggesting tags') + + expect(tagSuggestionsPrompt).toEqual( + expect.objectContaining({ + name: expect.any(String), + description: expect.stringMatching(/tag|suggest/i), + arguments: expect.arrayContaining([ + expect.objectContaining({ + name: expect.stringMatching(/entry|id/i), + description: expect.any(String), + required: true, + }), + ]), + }), + ) + } catch (error: any) { + if (error?.code === -32601 || error?.message?.includes('Method not found')) { + console.error('🚨 Prompts capability not implemented!') + console.error('🚨 This exercise teaches you how to add prompts to your MCP server') + console.error('🚨 You need to:') + console.error('🚨 1. Add "prompts" to your server capabilities') + console.error('🚨 2. Import ListPromptsRequestSchema and GetPromptRequestSchema') + console.error('🚨 3. Set up handlers: server.setRequestHandler(ListPromptsRequestSchema, ...)') + console.error('🚨 4. Set up handlers: server.setRequestHandler(GetPromptRequestSchema, ...)') + console.error('🚨 5. Register prompts that can help users analyze their journal entries') + console.error('🚨 In src/index.ts, add prompts capability and request handlers') + throw new Error(`🚨 Prompts capability not declared - add "prompts" to server capabilities and implement prompt handlers. ${error}`) + } + throw error + } }) test('Prompt Get', async () => { - const list = await client.listPrompts() - const firstPrompt = list.prompts[0] - invariant(firstPrompt, '🚨 No prompts available to test') - - const result = await client.getPrompt({ - name: firstPrompt.name, - arguments: { - entryId: '1', - }, - }) + try { + const list = await client.listPrompts() + const firstPrompt = list.prompts[0] + invariant(firstPrompt, '🚨 No prompts available to test') + + const result = await client.getPrompt({ + name: firstPrompt.name, + arguments: { + entryId: '1', + }, + }) - expect(result).toEqual( - expect.objectContaining({ - messages: expect.arrayContaining([ - expect.objectContaining({ - role: expect.stringMatching(/user|system/), - content: expect.objectContaining({ - type: 'text', - text: expect.any(String), + expect(result).toEqual( + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + role: expect.stringMatching(/user|system/), + content: expect.objectContaining({ + type: 'text', + text: expect.any(String), + }), }), - }), - ]), - }), - ) - - // 🚨 Proactive check: Ensure prompt contains meaningful content - invariant(result.messages.length > 0, '🚨 Prompt should contain at least one message') - const firstMessage = result.messages[0] - invariant(firstMessage, '🚨 First message should exist') - invariant(typeof firstMessage.content.text === 'string', '🚨 Message content text should be a string') - invariant(firstMessage.content.text.length > 10, '🚨 Prompt message should be more than just a placeholder') + ]), + }), + ) + + // 🚨 Proactive check: Ensure prompt contains meaningful content + invariant(result.messages.length > 0, '🚨 Prompt should contain at least one message') + const firstMessage = result.messages[0] + invariant(firstMessage, '🚨 First message should exist') + invariant(typeof firstMessage.content.text === 'string', '🚨 Message content text should be a string') + invariant(firstMessage.content.text.length > 10, '🚨 Prompt message should be more than just a placeholder') + } catch (error: any) { + if (error?.code === -32601 || error?.message?.includes('Method not found')) { + console.error('🚨 Prompts capability not implemented!') + console.error('🚨 This exercise teaches you how to create and serve prompts via MCP') + console.error('🚨 You need to:') + console.error('🚨 1. Add "prompts" to your server capabilities') + console.error('🚨 2. Handle GetPromptRequestSchema requests') + console.error('🚨 3. Create prompt templates that help analyze journal entries') + console.error('🚨 4. Return prompt messages with proper role and content') + console.error('🚨 In src/index.ts, implement GetPromptRequestSchema handler to return formatted prompts') + throw new Error(`🚨 Prompt get functionality not implemented - add prompts capability and GetPromptRequestSchema handler. ${error}`) + } + throw error + } }) diff --git a/exercises/04.prompts/02.problem.optimized-prompt/src/index.test.ts b/exercises/04.prompts/02.problem.optimized-prompt/src/index.test.ts index 324a048..f8eccc9 100644 --- a/exercises/04.prompts/02.problem.optimized-prompt/src/index.test.ts +++ b/exercises/04.prompts/02.problem.optimized-prompt/src/index.test.ts @@ -112,67 +112,82 @@ test('Optimized Prompt with Embedded Resources', async () => { const firstPrompt = list.prompts[0] invariant(firstPrompt, '🚨 No prompts available to test') - const result = await client.getPrompt({ - name: firstPrompt.name, - arguments: { - entryId: '1', - }, - }) + try { + const result = await client.getPrompt({ + name: firstPrompt.name, + arguments: { + entryId: '1', + }, + }) - expect(result).toEqual( - expect.objectContaining({ - messages: expect.arrayContaining([ - expect.objectContaining({ - role: expect.stringMatching(/user|system/), - content: expect.objectContaining({ - type: expect.stringMatching(/text|resource/), - }), - }), - ]), - }), - ) - - // 🚨 Proactive check: Ensure prompt has multiple messages (optimization means embedding data) - invariant(result.messages.length > 1, '🚨 Optimized prompt should have multiple messages - instructions plus embedded data') - - // 🚨 Proactive check: Ensure at least one message is a resource (embedded data) - const resourceMessages = result.messages.filter(m => m.content.type === 'resource') - invariant(resourceMessages.length > 0, '🚨 Optimized prompt should embed resource data directly instead of instructing LLM to run tools') - - // 🚨 Proactive check: Ensure prompt doesn't tell LLM to run data retrieval tools (that's what we're optimizing away) - const textMessages = result.messages.filter(m => m.content.type === 'text') - const hasDataRetrievalInstructions = textMessages.some(m => - typeof m.content.text === 'string' && - (m.content.text.toLowerCase().includes('get_entry') || - m.content.text.toLowerCase().includes('list_tags') || - m.content.text.toLowerCase().includes('look up')) - ) - invariant(!hasDataRetrievalInstructions, '🚨 Optimized prompt should NOT instruct LLM to run data retrieval tools like get_entry or list_tags - data should be embedded directly') - - // Note: The prompt can still instruct the LLM to use action tools like create_tag or add_tag_to_entry - - // Validate structure of resource messages - resourceMessages.forEach(resMsg => { - expect(resMsg.content).toEqual( + expect(result).toEqual( expect.objectContaining({ - type: 'resource', - resource: expect.objectContaining({ - uri: expect.any(String), - mimeType: 'application/json', - text: expect.any(String), - }), + messages: expect.arrayContaining([ + expect.objectContaining({ + role: expect.stringMatching(/user|system/), + content: expect.objectContaining({ + type: expect.stringMatching(/text|resource/), + }), + }), + ]), }), ) - // 🚨 Proactive check: Ensure embedded resource contains valid JSON - invariant('resource' in resMsg.content, '🚨 Resource message must have resource field') - invariant(typeof resMsg.content.resource === 'object' && resMsg.content.resource !== null, '🚨 Resource must be an object') - invariant('text' in resMsg.content.resource, '🚨 Resource must have text field') - invariant(typeof resMsg.content.resource.text === 'string', '🚨 Resource text must be a string') - try { - JSON.parse(resMsg.content.resource.text) - } catch (error) { - throw new Error('🚨 Embedded resource data must be valid JSON') - } - }) + // 🚨 Proactive check: Ensure prompt has multiple messages (optimization means embedding data) + invariant(result.messages.length > 1, '🚨 Optimized prompt should have multiple messages - instructions plus embedded data') + + // 🚨 Proactive check: Ensure at least one message is a resource (embedded data) + const resourceMessages = result.messages.filter(m => m.content.type === 'resource') + invariant(resourceMessages.length > 0, '🚨 Optimized prompt should embed resource data directly instead of instructing LLM to run tools') + + // 🚨 Proactive check: Ensure prompt doesn't tell LLM to run data retrieval tools (that's what we're optimizing away) + const textMessages = result.messages.filter(m => m.content.type === 'text') + const hasDataRetrievalInstructions = textMessages.some(m => + typeof m.content.text === 'string' && + (m.content.text.toLowerCase().includes('get_entry') || + m.content.text.toLowerCase().includes('list_tags') || + m.content.text.toLowerCase().includes('look up')) + ) + invariant(!hasDataRetrievalInstructions, '🚨 Optimized prompt should NOT instruct LLM to run data retrieval tools like get_entry or list_tags - data should be embedded directly') + + // Note: The prompt can still instruct the LLM to use action tools like create_tag or add_tag_to_entry + + // Validate structure of resource messages + resourceMessages.forEach(resMsg => { + expect(resMsg.content).toEqual( + expect.objectContaining({ + type: 'resource', + resource: expect.objectContaining({ + uri: expect.any(String), + mimeType: 'application/json', + text: expect.any(String), + }), + }), + ) + + // 🚨 Proactive check: Ensure embedded resource contains valid JSON + invariant('resource' in resMsg.content, '🚨 Resource message must have resource field') + invariant(typeof resMsg.content.resource === 'object' && resMsg.content.resource !== null, '🚨 Resource must be an object') + invariant('text' in resMsg.content.resource, '🚨 Resource must have text field') + invariant(typeof resMsg.content.resource.text === 'string', '🚨 Resource text must be a string') + try { + JSON.parse(resMsg.content.resource.text) + } catch (error) { + throw new Error('🚨 Embedded resource data must be valid JSON') + } + }) + + } catch (error) { + console.error('🚨 Prompt optimization not properly implemented!') + console.error('🚨 This exercise teaches you how to optimize prompts by embedding resources') + console.error('🚨 OPTIMIZATION CONCEPT: Instead of telling the LLM to call get_entry/list_tags,') + console.error('🚨 embed the data directly in the prompt as resource content') + console.error('🚨 You need to:') + console.error('🚨 1. Fetch the entry and tag data in your GetPromptRequestSchema handler') + console.error('🚨 2. Create multiple messages: text instructions + resource content') + console.error('🚨 3. Use content.type = "resource" with embedded data') + console.error('🚨 4. DO NOT tell LLM to call get_entry - provide the data directly') + console.error('🚨 This reduces LLM tool calls and improves performance!') + throw new Error(`🚨 Optimized prompt should embed resource data directly, not instruct LLM to fetch it. ${error}`) + } }) diff --git a/exercises/04.prompts/03.problem.completion/src/index.test.ts b/exercises/04.prompts/03.problem.completion/src/index.test.ts index 2c0c5b4..1d3f90b 100644 --- a/exercises/04.prompts/03.problem.completion/src/index.test.ts +++ b/exercises/04.prompts/03.problem.completion/src/index.test.ts @@ -69,3 +69,116 @@ test('Tool Call', async () => { }), ) }) + +test('Prompts List', async () => { + try { + const list = await client.listPrompts() + + // 🚨 Proactive check: Ensure prompts are registered + invariant(list.prompts.length > 0, '🚨 No prompts found - make sure to register prompts with the prompts capability') + + const tagSuggestionsPrompt = list.prompts.find(p => p.name.includes('tag') || p.name.includes('suggest')) + invariant(tagSuggestionsPrompt, '🚨 No tag suggestions prompt found - should include a prompt for suggesting tags') + + expect(tagSuggestionsPrompt).toEqual( + expect.objectContaining({ + name: expect.any(String), + description: expect.stringMatching(/tag|suggest/i), + arguments: expect.arrayContaining([ + expect.objectContaining({ + name: expect.stringMatching(/entry|id/i), + description: expect.any(String), + required: true, + }), + ]), + }), + ) + } catch (error: any) { + if (error?.code === -32601 || error?.message?.includes('Method not found')) { + console.error('🚨 Prompts capability not implemented!') + console.error('🚨 This exercise teaches you how to add prompts to your MCP server') + console.error('🚨 You need to:') + console.error('🚨 1. Add "prompts" to your server capabilities') + console.error('🚨 2. Import ListPromptsRequestSchema and GetPromptRequestSchema') + console.error('🚨 3. Set up handlers: server.setRequestHandler(ListPromptsRequestSchema, ...)') + console.error('🚨 4. Set up handlers: server.setRequestHandler(GetPromptRequestSchema, ...)') + console.error('🚨 5. Register prompts that can help users analyze their journal entries') + console.error('🚨 In src/index.ts, add prompts capability and request handlers') + throw new Error(`🚨 Prompts capability not declared - add "prompts" to server capabilities and implement prompt handlers. ${error}`) + } + throw error + } +}) + +test('Prompt Argument Completion', async () => { + // First create some entries to have data for completion + await client.callTool({ + name: 'create_entry', + arguments: { + title: 'Completion Test Entry 1', + content: 'This is for testing prompt completions', + }, + }) + + await client.callTool({ + name: 'create_entry', + arguments: { + title: 'Completion Test Entry 2', + content: 'This is another prompt completion test', + }, + }) + + try { + // Test that prompt completion functionality works + const list = await client.listPrompts() + invariant(list.prompts.length > 0, '🚨 No prompts found - need prompts to test completion') + + const firstPrompt = list.prompts[0] + invariant(firstPrompt, '🚨 No prompts available to test completion') + invariant(firstPrompt.arguments && firstPrompt.arguments.length > 0, '🚨 Prompt should have completable arguments') + + const firstArg = firstPrompt.arguments[0] + invariant(firstArg, '🚨 First prompt argument should exist') + + // Test completion functionality using the proper MCP SDK method + const completionResult = await (client as any).completePrompt({ + ref: { + type: 'prompt', + name: firstPrompt.name, + }, + argument: { + name: firstArg.name, + value: '1', // Should match at least one of our created entries + }, + }) + + // 🚨 Proactive check: Completion should return results + invariant(Array.isArray(completionResult.completion?.values), '🚨 Prompt completion should return an array of values') + invariant(completionResult.completion.values.length > 0, '🚨 Prompt completion should return at least one matching result for value="1"') + + // Check that completion values are strings + completionResult.completion.values.forEach((value: any) => { + invariant(typeof value === 'string', '🚨 Completion values should be strings') + }) + + } catch (error: any) { + console.error('🚨 Prompt argument completion not fully implemented!') + console.error('🚨 This exercise teaches you how to add completion support to prompt arguments') + console.error('🚨 You need to:') + console.error('🚨 1. Add "completion" to your server capabilities') + console.error('🚨 2. Import completable from @modelcontextprotocol/sdk/server/completable.js') + console.error('🚨 3. Wrap your prompt argument schema with completable():') + console.error('🚨 entryId: completable(z.string(), async (value) => { return ["1", "2", "3"] })') + console.error('🚨 4. The completion callback should filter entries matching the partial value') + console.error('🚨 5. Return an array of valid completion strings') + console.error(`🚨 Error details: ${error?.message || error}`) + + if (error?.code === -32601) { + throw new Error('🚨 Completion capability not declared - add "completion" to server capabilities and use completable() for prompt arguments') + } else if (error?.code === -32602) { + throw new Error('🚨 Completable arguments not implemented - wrap prompt arguments with completable() function') + } else { + throw new Error(`🚨 Prompt argument completion not working - check capability declaration and completable() usage. ${error}`) + } + } +}) diff --git a/exercises/05.sampling/02.problem.advanced/src/index.test.ts b/exercises/05.sampling/02.problem.advanced/src/index.test.ts index 0853f05..8c89b7f 100644 --- a/exercises/05.sampling/02.problem.advanced/src/index.test.ts +++ b/exercises/05.sampling/02.problem.advanced/src/index.test.ts @@ -132,51 +132,74 @@ test('Sampling', async () => { }) const request = await messageRequestDeferred.promise - expect(request).toEqual( - expect.objectContaining({ - method: 'sampling/createMessage', - params: expect.objectContaining({ - maxTokens: expect.any(Number), - systemPrompt: expect.stringMatching(/example/i), - messages: expect.arrayContaining([ - expect.objectContaining({ - role: 'user', - content: expect.objectContaining({ - type: 'text', - text: expect.stringMatching(/entry/i), - mimeType: 'application/json', + try { + expect(request).toEqual( + expect.objectContaining({ + method: 'sampling/createMessage', + params: expect.objectContaining({ + maxTokens: expect.any(Number), + systemPrompt: expect.stringMatching(/example/i), + messages: expect.arrayContaining([ + expect.objectContaining({ + role: 'user', + content: expect.objectContaining({ + type: 'text', + text: expect.stringMatching(/entry/i), + mimeType: 'application/json', + }), }), - }), - ]), + ]), + }), }), - }), - ) - - // 🚨 Proactive checks for advanced sampling requirements - const params = request.params - invariant(params && 'maxTokens' in params, '🚨 maxTokens parameter is required') - invariant(params.maxTokens > 50, '🚨 maxTokens should be increased for longer responses (>50)') - - invariant(params && 'systemPrompt' in params, '🚨 systemPrompt is required') - invariant(typeof params.systemPrompt === 'string', '🚨 systemPrompt must be a string') - - invariant(params && 'messages' in params && Array.isArray(params.messages), '🚨 messages array is required') - const userMessage = params.messages.find(m => m.role === 'user') - invariant(userMessage, '🚨 User message is required') - invariant(userMessage.content.mimeType === 'application/json', '🚨 Content should be JSON for structured data') - - // 🚨 Validate the JSON structure contains required fields - invariant(typeof userMessage.content.text === 'string', '🚨 User message content text must be a string') - let messageData: any - try { - messageData = JSON.parse(userMessage.content.text) + ) + + // 🚨 Proactive checks for advanced sampling requirements + const params = request.params + invariant(params && 'maxTokens' in params, '🚨 maxTokens parameter is required') + invariant(params.maxTokens > 50, '🚨 maxTokens should be increased for longer responses (>50)') + + invariant(params && 'systemPrompt' in params, '🚨 systemPrompt is required') + invariant(typeof params.systemPrompt === 'string', '🚨 systemPrompt must be a string') + + invariant(params && 'messages' in params && Array.isArray(params.messages), '🚨 messages array is required') + const userMessage = params.messages.find(m => m.role === 'user') + invariant(userMessage, '🚨 User message is required') + invariant(userMessage.content.mimeType === 'application/json', '🚨 Content should be JSON for structured data') + + // 🚨 Validate the JSON structure contains required fields + invariant(typeof userMessage.content.text === 'string', '🚨 User message content text must be a string') + let messageData: any + try { + messageData = JSON.parse(userMessage.content.text) + } catch (error) { + throw new Error('🚨 User message content must be valid JSON') + } + + invariant(messageData.entry, '🚨 JSON should contain entry data') + invariant(messageData.existingTags, '🚨 JSON should contain existingTags for context') + invariant(Array.isArray(messageData.existingTags), '🚨 existingTags should be an array') + } catch (error) { - throw new Error('🚨 User message content must be valid JSON') + console.error('🚨 Advanced sampling features not properly implemented!') + console.error('🚨 This exercise teaches you advanced sampling with structured data and proper configuration') + console.error('🚨 You need to:') + console.error('🚨 1. Increase maxTokens to a reasonable value (e.g., 150) for longer responses') + console.error('🚨 2. Create a meaningful systemPrompt with examples of expected output format') + console.error('🚨 3. Structure the user message as JSON with mimeType: "application/json"') + console.error('🚨 4. Include both entry data AND existingTags context in the JSON') + console.error('🚨 5. Use structured data format: { entry: {...}, existingTags: [...] }') + console.error('🚨 EXAMPLE: systemPrompt should include examples of expected tag suggestions') + console.error('🚨 EXAMPLE: user message should be structured JSON, not plain text') + + const params = request.params + if (params) { + console.error(`🚨 Current maxTokens: ${params.maxTokens} (should be >50)`) + console.error(`🚨 Current mimeType: ${params.messages?.[0]?.content?.mimeType} (should be "application/json")`) + console.error(`🚨 SystemPrompt contains "example": ${typeof params.systemPrompt === 'string' && params.systemPrompt.toLowerCase().includes('example')}`) + } + + throw new Error(`🚨 Advanced sampling not configured properly - need structured JSON messages, higher maxTokens, and example-rich system prompt. ${error}`) } - - invariant(messageData.entry, '🚨 JSON should contain entry data') - invariant(messageData.existingTags, '🚨 JSON should contain existingTags for context') - invariant(Array.isArray(messageData.existingTags), '🚨 existingTags should be an array') messageResultDeferred.resolve({ model: 'stub-model',