Skip to content

Commit 3979b80

Browse files
committed
some test improvements
1 parent 947a27f commit 3979b80

File tree

5 files changed

+251
-75
lines changed

5 files changed

+251
-75
lines changed

β€Žexercises/02.tools/03.solution.errors/src/index.test.tsβ€Ž

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,40 @@ test('Tool Call - Error with Negative Second Number', async () => {
8888
},
8989
})
9090

91-
expect(result).toEqual(
92-
expect.objectContaining({
93-
content: expect.arrayContaining([
94-
expect.objectContaining({
95-
type: 'text',
96-
text: expect.stringMatching(/negative/i),
97-
}),
98-
]),
99-
isError: true,
100-
}),
101-
)
91+
try {
92+
expect(result).toEqual(
93+
expect.objectContaining({
94+
content: expect.arrayContaining([
95+
expect.objectContaining({
96+
type: 'text',
97+
text: expect.stringMatching(/negative/i),
98+
}),
99+
]),
100+
isError: true,
101+
}),
102+
)
103+
} catch (error) {
104+
console.error('🚨 Tool error handling not properly implemented!')
105+
console.error(
106+
'🚨 This exercise teaches you how to handle errors in MCP tools',
107+
)
108+
console.error(
109+
'🚨 Expected: Tool should return isError: true with message about negative numbers',
110+
)
111+
console.error(
112+
`🚨 Actual: Tool returned normal response: ${JSON.stringify(result, null, 2)}`,
113+
)
114+
console.error('🚨 You need to:')
115+
console.error('🚨 1. Check if secondNumber is negative in your add tool')
116+
console.error('🚨 2. Throw an Error with message containing "negative"')
117+
console.error('🚨 3. The MCP SDK will automatically set isError: true')
118+
console.error(
119+
'🚨 In src/index.ts, add: if (secondNumber < 0) throw new Error("Second number cannot be negative")',
120+
)
121+
throw new Error(
122+
`🚨 Tool should return error response when secondNumber is negative, but returned normal response instead. ${error}`,
123+
)
124+
}
102125
})
103126

104127
test('Tool Call - Another Successful Addition', async () => {

β€Žexercises/03.resources/02.solution.template/src/index.test.tsβ€Ž

Lines changed: 68 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -133,42 +133,75 @@ test('Resource Template Read - Entry', async () => {
133133
},
134134
})
135135

136-
const result = await client.readResource({
137-
uri: 'epicme://entries/1',
138-
})
139-
140-
expect(result).toEqual(
141-
expect.objectContaining({
142-
contents: expect.arrayContaining([
143-
expect.objectContaining({
144-
mimeType: 'application/json',
145-
uri: 'epicme://entries/1',
146-
text: expect.any(String),
147-
}),
148-
]),
149-
}),
150-
)
151-
152-
// 🚨 Proactive check: Ensure the resource content is valid JSON and contains entry data
153-
const content = result.contents[0]
154-
invariant(
155-
content && 'text' in content,
156-
'🚨 Resource content must have text field',
157-
)
158-
invariant(
159-
typeof content.text === 'string',
160-
'🚨 Resource content text must be a string',
161-
)
162-
163-
let entryData: any
164136
try {
165-
entryData = JSON.parse(content.text)
137+
const result = await client.readResource({
138+
uri: 'epicme://entries/1',
139+
})
140+
141+
expect(result).toEqual(
142+
expect.objectContaining({
143+
contents: expect.arrayContaining([
144+
expect.objectContaining({
145+
mimeType: 'application/json',
146+
uri: 'epicme://entries/1',
147+
text: expect.any(String),
148+
}),
149+
]),
150+
}),
151+
)
152+
153+
// 🚨 Proactive check: Ensure the resource content is valid JSON and contains entry data
154+
const content = result.contents[0]
155+
invariant(
156+
content && 'text' in content,
157+
'🚨 Resource content must have text field',
158+
)
159+
invariant(
160+
typeof content.text === 'string',
161+
'🚨 Resource content text must be a string',
162+
)
163+
164+
let entryData: any
165+
try {
166+
entryData = JSON.parse(content.text)
167+
} catch (error) {
168+
throw new Error('🚨 Resource content must be valid JSON')
169+
}
170+
171+
// 🚨 Proactive check: Ensure entry data contains expected fields
172+
invariant(entryData.id, '🚨 Entry resource should contain id field')
173+
invariant(entryData.title, '🚨 Entry resource should contain title field')
174+
invariant(
175+
entryData.content,
176+
'🚨 Entry resource should contain content field',
177+
)
166178
} catch (error) {
167-
throw new Error('🚨 Resource content must be valid JSON')
179+
if (
180+
error instanceof Error &&
181+
error.message.includes('Resource epicme://entries/1 not found')
182+
) {
183+
console.error('🚨 Resource template reading not implemented!')
184+
console.error(
185+
'🚨 This exercise teaches parameterized resource URIs like epicme://entries/{id}',
186+
)
187+
console.error('🚨 You need to:')
188+
console.error(
189+
'🚨 1. Register resource templates with server.registerResource() using ResourceTemplate',
190+
)
191+
console.error(
192+
'🚨 2. Use ResourceTemplate to define parameterized URIs like epicme://entries/{id}',
193+
)
194+
console.error(
195+
'🚨 3. The callback function will receive extracted parameters like { id }',
196+
)
197+
console.error('🚨 4. Return the resource content as JSON')
198+
console.error(
199+
'🚨 Check the solution to see how to extract parameters from template URIs',
200+
)
201+
throw new Error(
202+
`🚨 Resource template reading not implemented - need to handle parameterized URIs like epicme://entries/1. ${error}`,
203+
)
204+
}
205+
throw error
168206
}
169-
170-
// 🚨 Proactive check: Ensure entry data contains expected fields
171-
invariant(entryData.id, '🚨 Entry resource should contain id field')
172-
invariant(entryData.title, '🚨 Entry resource should contain title field')
173-
invariant(entryData.content, '🚨 Entry resource should contain content field')
174207
})

β€Žexercises/03.resources/04.problem.completion/src/index.test.tsβ€Ž

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ test('Resource Template Completions', async () => {
120120

121121
try {
122122
// Test completion functionality using the proper MCP SDK method
123-
const completionResult = await (client as any).completeResource({
123+
const completionResult = await client.complete({
124124
ref: {
125-
type: 'resource',
125+
type: 'ref/resource',
126126
uri: entriesTemplate.uriTemplate,
127127
},
128128
argument: {

β€Žexercises/03.resources/04.solution.completion/src/index.test.tsβ€Ž

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -118,38 +118,65 @@ test('Resource Template Completions', async () => {
118118
// 🚨 The key learning objective for this exercise is adding completion support
119119
// This requires BOTH declaring completions capability AND implementing complete callbacks
120120

121-
// Test if completion capability is properly declared by trying to use completion API
122-
let completionSupported = false
123121
try {
124-
// This should work if server declares completion capability and implements complete callbacks
125-
await (client as any)._client.request({
126-
method: 'completion/complete',
127-
params: {
128-
ref: {
129-
type: 'resource',
130-
uri: 'epicme://entries/{id}',
131-
},
132-
argument: {
133-
name: 'id',
134-
value: '1',
135-
},
122+
// Test completion functionality using the proper MCP SDK method
123+
const completionResult = await client.complete({
124+
ref: {
125+
type: 'ref/resource',
126+
uri: entriesTemplate.uriTemplate,
136127
},
128+
argument: {
129+
name: 'id',
130+
value: '1', // Should match at least one of our created entries
131+
},
132+
})
133+
134+
// 🚨 Proactive check: Completion should return results
135+
invariant(
136+
Array.isArray(completionResult.completion?.values),
137+
'🚨 Completion should return an array of values',
138+
)
139+
invariant(
140+
completionResult.completion.values.length > 0,
141+
'🚨 Completion should return at least one matching result for id="1"',
142+
)
143+
144+
// Check that completion values are strings
145+
completionResult.completion.values.forEach((value: any) => {
146+
invariant(
147+
typeof value === 'string',
148+
'🚨 Completion values should be strings',
149+
)
137150
})
138-
completionSupported = true
139151
} catch (error: any) {
140-
// -32601 = Method not found (missing completion capability)
141-
// -32602 = Invalid params (missing complete callbacks)
142-
if (error?.code === -32601 || error?.code === -32602) {
143-
completionSupported = false
152+
console.error('🚨 Resource template completion not fully implemented!')
153+
console.error(
154+
'🚨 This exercise teaches you how to add completion support to resource templates',
155+
)
156+
console.error('🚨 You need to:')
157+
console.error('🚨 1. Add "completion" to your server capabilities')
158+
console.error('🚨 2. Add complete callback to your ResourceTemplate:')
159+
console.error(
160+
'🚨 complete: { async id(value) { return ["1", "2", "3"] } }',
161+
)
162+
console.error(
163+
'🚨 3. The complete callback should filter entries matching the partial value',
164+
)
165+
console.error('🚨 4. Return an array of valid completion strings')
166+
console.error(`🚨 Error details: ${error?.message || error}`)
167+
168+
if (error?.code === -32601) {
169+
throw new Error(
170+
'🚨 Completion capability not declared - add "completion" to server capabilities and implement complete callbacks',
171+
)
172+
} else if (error?.code === -32602) {
173+
throw new Error(
174+
'🚨 Complete callback not implemented - add complete: { async id(value) { ... } } to your ResourceTemplate',
175+
)
144176
} else {
145-
// Other errors might be acceptable (like no matches found)
146-
completionSupported = true
177+
throw new Error(
178+
`🚨 Resource template completion not working - check capability declaration and complete callback implementation. ${error}`,
179+
)
147180
}
148181
}
149-
150-
// 🚨 Proactive check: Completion functionality must be fully implemented
151-
invariant(
152-
completionSupported,
153-
'🚨 Resource template completion requires both declaring completions capability in server AND implementing complete callbacks for template parameters',
154-
)
155182
})

β€Žexercises/03.resources/05.solution.linked/src/index.test.tsβ€Ž

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,96 @@ test('Tool Call', async () => {
6969
}),
7070
)
7171
})
72+
73+
test('Resource Link in Tool Response', async () => {
74+
let result: any
75+
try {
76+
result = await client.callTool({
77+
name: 'create_tag',
78+
arguments: {
79+
name: 'Linked Tag Test',
80+
description: 'This tag should be linked as a resource',
81+
},
82+
})
83+
84+
// 🚨 The key learning objective: Tool responses should include resource_link content
85+
// when creating resources, not just text confirmations
86+
87+
// Type guard for content array
88+
const content = result.content as Array<any>
89+
invariant(
90+
Array.isArray(content),
91+
'🚨 Tool response content must be an array',
92+
)
93+
94+
// Check if response includes resource_link content type
95+
const hasResourceLink = content.some(
96+
(item: any) => item.type === 'resource_link',
97+
)
98+
99+
if (!hasResourceLink) {
100+
throw new Error('Tool response should include resource_link content type')
101+
}
102+
103+
// Find the resource_link content
104+
const resourceLink = content.find(
105+
(item: any) => item.type === 'resource_link',
106+
) as any
107+
108+
// 🚨 Proactive checks: Resource link should have proper structure
109+
invariant(
110+
resourceLink,
111+
'🚨 Tool response should include resource_link content type',
112+
)
113+
invariant(resourceLink.uri, '🚨 Resource link must have uri field')
114+
invariant(resourceLink.name, '🚨 Resource link must have name field')
115+
invariant(
116+
typeof resourceLink.uri === 'string',
117+
'🚨 Resource link uri must be a string',
118+
)
119+
invariant(
120+
typeof resourceLink.name === 'string',
121+
'🚨 Resource link name must be a string',
122+
)
123+
invariant(
124+
resourceLink.uri.includes('tags'),
125+
'🚨 Resource link URI should reference the created tag',
126+
)
127+
128+
expect(resourceLink).toEqual(
129+
expect.objectContaining({
130+
type: 'resource_link',
131+
uri: expect.stringMatching(/epicme:\/\/tags\/\d+/),
132+
name: expect.stringMatching(/Linked Tag Test/),
133+
description: expect.any(String),
134+
mimeType: expect.stringMatching(/application\/json/),
135+
}),
136+
)
137+
} catch (error) {
138+
if (typeof result !== 'undefined' && result.content) {
139+
console.error(
140+
'🚨 Actual content:',
141+
JSON.stringify(result.content, null, 2),
142+
)
143+
}
144+
console.error('🚨 Resource linking not implemented in tool responses!')
145+
console.error(
146+
'🚨 This exercise teaches you how to include resource links in tool responses',
147+
)
148+
console.error('🚨 You need to:')
149+
console.error(
150+
'🚨 1. When your tool creates a resource, include a resource_link content item',
151+
)
152+
console.error('🚨 2. Set type: "resource_link" in the response content')
153+
console.error('🚨 3. Include uri, name, description, and mimeType fields')
154+
console.error(
155+
'🚨 4. The URI should point to the created resource (e.g., epicme://tags/1)',
156+
)
157+
console.error(
158+
'🚨 Example: { type: "resource_link", uri: "epicme://tags/1", name: "My Tag", description: "...", mimeType: "application/json" }',
159+
)
160+
throw new Error(
161+
`🚨 Tool should include resource_link content type when creating resources. ${error}`,
162+
)
163+
}
164+
})

0 commit comments

Comments
Β (0)