@youdotcom-oss/mcp 2.1.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -72
- package/bin/stdio.js +6699 -28740
- package/package.json +10 -22
- package/server.json +51 -0
- package/src/contents/contents.schemas.ts +0 -30
- package/src/contents/contents.utils.ts +0 -84
- package/src/contents/register-contents-tool.ts +0 -94
- package/src/contents/tests/contents.utils.spec.ts +0 -123
- package/src/get-mcp-server.ts +0 -17
- package/src/http.ts +0 -94
- package/src/research/register-research-tool.ts +0 -69
- package/src/research/research.schemas.ts +0 -19
- package/src/research/research.utils.ts +0 -30
- package/src/search/register-search-tool.ts +0 -87
- package/src/search/search.schema.ts +0 -38
- package/src/search/search.utils.ts +0 -70
- package/src/search/tests/search.utils.spec.ts +0 -156
- package/src/shared/format-search-results-text.ts +0 -49
- package/src/shared/get-logger.ts +0 -10
- package/src/shared/tests/shared.utils.spec.ts +0 -160
- package/src/shared/use-client-version.ts +0 -21
- package/src/stdio.ts +0 -24
- package/src/tests/http.spec.ts +0 -583
- package/src/tests/tool.spec.ts +0 -649
package/src/tests/tool.spec.ts
DELETED
|
@@ -1,649 +0,0 @@
|
|
|
1
|
-
import { afterAll, beforeAll, describe, expect, test } from 'bun:test'
|
|
2
|
-
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
3
|
-
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
|
4
|
-
import { $ } from 'bun'
|
|
5
|
-
import type { ContentsStructuredContent } from '../contents/contents.schemas.ts'
|
|
6
|
-
import type { ResearchStructuredContent } from '../research/research.schemas.ts'
|
|
7
|
-
import type { SearchStructuredContent } from '../search/search.schema.ts'
|
|
8
|
-
|
|
9
|
-
let client: Client
|
|
10
|
-
|
|
11
|
-
beforeAll(async () => {
|
|
12
|
-
await $`bun run build`
|
|
13
|
-
const transport = new StdioClientTransport({
|
|
14
|
-
command: 'npx',
|
|
15
|
-
args: [Bun.resolveSync('../../bin/stdio', import.meta.dir)],
|
|
16
|
-
env: {
|
|
17
|
-
YDC_API_KEY: process.env.YDC_API_KEY ?? '',
|
|
18
|
-
},
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
client = new Client({
|
|
22
|
-
name: 'test-client',
|
|
23
|
-
version: '0.0.1',
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
await client.connect(transport)
|
|
27
|
-
}, 30_000)
|
|
28
|
-
|
|
29
|
-
afterAll(async () => {
|
|
30
|
-
await client.close()
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
describe('registerSearchTool', () => {
|
|
34
|
-
test('tool is registered and available', async () => {
|
|
35
|
-
const tools = await client.listTools()
|
|
36
|
-
|
|
37
|
-
const searchTool = tools.tools.find((t) => t.name === 'you-search')
|
|
38
|
-
|
|
39
|
-
expect(searchTool).toBeDefined()
|
|
40
|
-
expect(searchTool?.title).toBe('Web Search')
|
|
41
|
-
expect(searchTool?.description).toContain('Web and news search')
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
test(
|
|
45
|
-
'performs basic search successfully',
|
|
46
|
-
async () => {
|
|
47
|
-
const result = await client.callTool({
|
|
48
|
-
name: 'you-search',
|
|
49
|
-
arguments: {
|
|
50
|
-
query: 'javascript tutorial',
|
|
51
|
-
count: 3,
|
|
52
|
-
},
|
|
53
|
-
})
|
|
54
|
-
const content = result.content as { type: string; text: string }[]
|
|
55
|
-
expect(result).toHaveProperty('content')
|
|
56
|
-
expect(Array.isArray(content)).toBe(true)
|
|
57
|
-
expect(content[0]).toHaveProperty('type', 'text')
|
|
58
|
-
expect(content[0]).toHaveProperty('text')
|
|
59
|
-
|
|
60
|
-
const text = content[0]?.text
|
|
61
|
-
expect(text).toContain('Search Results for')
|
|
62
|
-
expect(text).toContain('javascript tutorial')
|
|
63
|
-
const structuredContent = result.structuredContent as SearchStructuredContent
|
|
64
|
-
// Should have structured content with minimal format
|
|
65
|
-
expect(result).toHaveProperty('structuredContent')
|
|
66
|
-
expect(structuredContent).toHaveProperty('resultCounts')
|
|
67
|
-
expect(structuredContent.resultCounts).toHaveProperty('web')
|
|
68
|
-
expect(structuredContent.resultCounts).toHaveProperty('news')
|
|
69
|
-
expect(structuredContent.resultCounts).toHaveProperty('total')
|
|
70
|
-
},
|
|
71
|
-
{ retry: 2 },
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
test(
|
|
75
|
-
'handles search with web results formatting',
|
|
76
|
-
async () => {
|
|
77
|
-
const result = await client.callTool({
|
|
78
|
-
name: 'you-search',
|
|
79
|
-
arguments: {
|
|
80
|
-
query: 'react components',
|
|
81
|
-
count: 2,
|
|
82
|
-
},
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
const content = result.content as { type: string; text: string }[]
|
|
86
|
-
const text = content[0]?.text
|
|
87
|
-
expect(text).toContain('WEB RESULTS:')
|
|
88
|
-
expect(text).toContain('Title:')
|
|
89
|
-
// URL should be in text content
|
|
90
|
-
expect(text).toContain('URL:')
|
|
91
|
-
expect(text).toContain('Description:')
|
|
92
|
-
expect(text).toContain('Snippets:')
|
|
93
|
-
|
|
94
|
-
// Verify structured data has result counts
|
|
95
|
-
const structuredContent = result.structuredContent as SearchStructuredContent
|
|
96
|
-
expect(structuredContent.resultCounts.web).toBeGreaterThan(0)
|
|
97
|
-
expect(structuredContent.resultCounts.total).toBeGreaterThan(0)
|
|
98
|
-
|
|
99
|
-
// URLs should be in structuredContent.results
|
|
100
|
-
expect(structuredContent.results).toBeDefined()
|
|
101
|
-
expect(structuredContent.results?.web).toBeDefined()
|
|
102
|
-
expect(structuredContent.results?.web?.length).toBeGreaterThan(0)
|
|
103
|
-
expect(structuredContent.results?.web?.[0]).toHaveProperty('url')
|
|
104
|
-
expect(structuredContent.results?.web?.[0]).toHaveProperty('title')
|
|
105
|
-
},
|
|
106
|
-
{ retry: 2 },
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
test(
|
|
110
|
-
'handles search with news results',
|
|
111
|
-
async () => {
|
|
112
|
-
const result = await client.callTool({
|
|
113
|
-
name: 'you-search',
|
|
114
|
-
arguments: {
|
|
115
|
-
query: 'technology news',
|
|
116
|
-
count: 2,
|
|
117
|
-
},
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
const content = result.content as { type: string; text: string }[]
|
|
121
|
-
const text = content[0]?.text
|
|
122
|
-
|
|
123
|
-
const structuredContent = result.structuredContent as SearchStructuredContent
|
|
124
|
-
// Check if news results are included
|
|
125
|
-
if (structuredContent.resultCounts.news > 0) {
|
|
126
|
-
expect(text).toContain('NEWS RESULTS:')
|
|
127
|
-
expect(text).toContain('Published:')
|
|
128
|
-
expect(structuredContent.resultCounts.news).toBeGreaterThan(0)
|
|
129
|
-
}
|
|
130
|
-
},
|
|
131
|
-
{ retry: 2 },
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
test(
|
|
135
|
-
'handles mixed web and news results with proper separation',
|
|
136
|
-
async () => {
|
|
137
|
-
const result = await client.callTool({
|
|
138
|
-
name: 'you-search',
|
|
139
|
-
arguments: {
|
|
140
|
-
query: 'artificial intelligence',
|
|
141
|
-
count: 3,
|
|
142
|
-
},
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
const content = result.content as { type: string; text: string }[]
|
|
146
|
-
const text = content[0]?.text
|
|
147
|
-
|
|
148
|
-
// Should have web results
|
|
149
|
-
expect(text).toContain('WEB RESULTS:')
|
|
150
|
-
|
|
151
|
-
const structuredContent = result.structuredContent as SearchStructuredContent
|
|
152
|
-
// If both web and news results exist, check for separator
|
|
153
|
-
if (structuredContent.resultCounts.news > 0) {
|
|
154
|
-
expect(text).toContain('NEWS RESULTS:')
|
|
155
|
-
expect(text).toContain('='.repeat(50))
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
{ retry: 2 },
|
|
159
|
-
)
|
|
160
|
-
|
|
161
|
-
test(
|
|
162
|
-
'handles freshness parameter',
|
|
163
|
-
async () => {
|
|
164
|
-
const result = await client.callTool({
|
|
165
|
-
name: 'you-search',
|
|
166
|
-
arguments: {
|
|
167
|
-
query: 'recent news',
|
|
168
|
-
freshness: 'week',
|
|
169
|
-
},
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
const content = result.content as { type: string; text: string }[]
|
|
173
|
-
expect(content[0]).toHaveProperty('text')
|
|
174
|
-
expect(content[0]?.text).toContain('recent news')
|
|
175
|
-
},
|
|
176
|
-
{ retry: 2 },
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
test(
|
|
180
|
-
'handles country parameter',
|
|
181
|
-
async () => {
|
|
182
|
-
const result = await client.callTool({
|
|
183
|
-
name: 'you-search',
|
|
184
|
-
arguments: {
|
|
185
|
-
query: 'local news',
|
|
186
|
-
country: 'US',
|
|
187
|
-
},
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
const content = result.content as { type: string; text: string }[]
|
|
191
|
-
expect(content[0]).toHaveProperty('text')
|
|
192
|
-
expect(content[0]?.text).toContain('local news')
|
|
193
|
-
},
|
|
194
|
-
{ retry: 2 },
|
|
195
|
-
)
|
|
196
|
-
|
|
197
|
-
test(
|
|
198
|
-
'handles safesearch parameter',
|
|
199
|
-
async () => {
|
|
200
|
-
const result = await client.callTool({
|
|
201
|
-
name: 'you-search',
|
|
202
|
-
arguments: {
|
|
203
|
-
query: 'educational content',
|
|
204
|
-
safesearch: 'strict',
|
|
205
|
-
},
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
const content = result.content as { type: string; text: string }[]
|
|
209
|
-
expect(content[0]).toHaveProperty('text')
|
|
210
|
-
expect(content[0]?.text).toContain('educational content')
|
|
211
|
-
},
|
|
212
|
-
{ retry: 2 },
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
test(
|
|
216
|
-
'handles site: operator in query',
|
|
217
|
-
async () => {
|
|
218
|
-
const result = await client.callTool({
|
|
219
|
-
name: 'you-search',
|
|
220
|
-
arguments: {
|
|
221
|
-
query: 'react components site:github.com',
|
|
222
|
-
},
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
const content = result.content as { type: string; text: string }[]
|
|
226
|
-
expect(content[0]?.text).toContain('react components')
|
|
227
|
-
},
|
|
228
|
-
{ retry: 2 },
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
test(
|
|
232
|
-
'handles filetype: operator in query',
|
|
233
|
-
async () => {
|
|
234
|
-
const result = await client.callTool({
|
|
235
|
-
name: 'you-search',
|
|
236
|
-
arguments: {
|
|
237
|
-
query: 'documentation filetype:pdf',
|
|
238
|
-
},
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
const content = result.content as { type: string; text: string }[]
|
|
242
|
-
expect(content[0]?.text).toContain('documentation')
|
|
243
|
-
},
|
|
244
|
-
{ retry: 2 },
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
test(
|
|
248
|
-
'handles lang: operator in query',
|
|
249
|
-
async () => {
|
|
250
|
-
const result = await client.callTool({
|
|
251
|
-
name: 'you-search',
|
|
252
|
-
arguments: {
|
|
253
|
-
query: 'tutorial lang:es',
|
|
254
|
-
},
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
const content = result.content as { type: string; text: string }[]
|
|
258
|
-
expect(content[0]?.text).toContain('tutorial')
|
|
259
|
-
},
|
|
260
|
-
{ retry: 2 },
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
test(
|
|
264
|
-
'handles + operator for required terms in query',
|
|
265
|
-
async () => {
|
|
266
|
-
const result = await client.callTool({
|
|
267
|
-
name: 'you-search',
|
|
268
|
-
arguments: {
|
|
269
|
-
query: 'programming +javascript +typescript',
|
|
270
|
-
},
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
const content = result.content as { type: string; text: string }[]
|
|
274
|
-
expect(content[0]?.text).toContain('programming')
|
|
275
|
-
},
|
|
276
|
-
{ retry: 2 },
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
test(
|
|
280
|
-
'handles - operator for excluded terms in query',
|
|
281
|
-
async () => {
|
|
282
|
-
const result = await client.callTool({
|
|
283
|
-
name: 'you-search',
|
|
284
|
-
arguments: {
|
|
285
|
-
query: 'tutorial -beginner -basic',
|
|
286
|
-
},
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
const content = result.content as { type: string; text: string }[]
|
|
290
|
-
expect(content[0]?.text).toContain('tutorial')
|
|
291
|
-
},
|
|
292
|
-
{ retry: 2 },
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
test(
|
|
296
|
-
'handles complex search with multiple operators in query',
|
|
297
|
-
async () => {
|
|
298
|
-
const result = await client.callTool({
|
|
299
|
-
name: 'you-search',
|
|
300
|
-
arguments: {
|
|
301
|
-
query: 'machine learning tutorial site:github.com filetype:md lang:en',
|
|
302
|
-
count: 5,
|
|
303
|
-
offset: 1,
|
|
304
|
-
freshness: 'month',
|
|
305
|
-
country: 'US',
|
|
306
|
-
safesearch: 'moderate',
|
|
307
|
-
},
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
const content = result.content as { type: string; text: string }[]
|
|
311
|
-
expect(content[0]).toHaveProperty('text')
|
|
312
|
-
// Test should pass even if no results (very specific query might have no results)
|
|
313
|
-
|
|
314
|
-
// Verify results are limited by count if there are results
|
|
315
|
-
const structuredContent = result.structuredContent as SearchStructuredContent
|
|
316
|
-
if (structuredContent.resultCounts.web > 0) {
|
|
317
|
-
expect(structuredContent.resultCounts.web).toBeLessThanOrEqual(5)
|
|
318
|
-
}
|
|
319
|
-
},
|
|
320
|
-
{ retry: 2 },
|
|
321
|
-
)
|
|
322
|
-
|
|
323
|
-
test(
|
|
324
|
-
'handles special characters in query',
|
|
325
|
-
async () => {
|
|
326
|
-
const result = await client.callTool({
|
|
327
|
-
name: 'you-search',
|
|
328
|
-
arguments: {
|
|
329
|
-
query: 'C++ programming "hello world"',
|
|
330
|
-
},
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
const content = result.content as { type: string; text: string }[]
|
|
334
|
-
expect(content[0]).toHaveProperty('text')
|
|
335
|
-
expect(content[0]?.text).toContain('C++')
|
|
336
|
-
},
|
|
337
|
-
{ retry: 2 },
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
test(
|
|
341
|
-
'handles empty search results gracefully',
|
|
342
|
-
async () => {
|
|
343
|
-
const result = await client.callTool({
|
|
344
|
-
name: 'you-search',
|
|
345
|
-
arguments: {
|
|
346
|
-
query: '_',
|
|
347
|
-
},
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
const content = result.content as { type: string; text: string }[]
|
|
351
|
-
|
|
352
|
-
// Should still have content even if no results
|
|
353
|
-
expect(content[0]).toHaveProperty('text')
|
|
354
|
-
|
|
355
|
-
const text = content[0]?.text
|
|
356
|
-
const structuredContent = result.structuredContent as SearchStructuredContent
|
|
357
|
-
expect(structuredContent.resultCounts.web).toBe(0)
|
|
358
|
-
expect(structuredContent.resultCounts.news).toBe(0)
|
|
359
|
-
expect(structuredContent.resultCounts.total).toBe(0)
|
|
360
|
-
expect(text).toContain('No results found')
|
|
361
|
-
},
|
|
362
|
-
{ retry: 2 },
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
test(
|
|
366
|
-
'validates structured response format',
|
|
367
|
-
async () => {
|
|
368
|
-
const result = await client.callTool({
|
|
369
|
-
name: 'you-search',
|
|
370
|
-
arguments: {
|
|
371
|
-
query: `what's the latest tech news`,
|
|
372
|
-
count: 2,
|
|
373
|
-
},
|
|
374
|
-
})
|
|
375
|
-
|
|
376
|
-
const structuredContent = result.structuredContent as SearchStructuredContent
|
|
377
|
-
// Validate minimal structured content schema
|
|
378
|
-
expect(structuredContent).toHaveProperty('resultCounts')
|
|
379
|
-
|
|
380
|
-
// Check result counts structure
|
|
381
|
-
const resultCounts = structuredContent.resultCounts
|
|
382
|
-
expect(resultCounts).toHaveProperty('web')
|
|
383
|
-
expect(resultCounts).toHaveProperty('news')
|
|
384
|
-
expect(resultCounts).toHaveProperty('total')
|
|
385
|
-
expect(typeof resultCounts.web).toBe('number')
|
|
386
|
-
expect(typeof resultCounts.news).toBe('number')
|
|
387
|
-
expect(typeof resultCounts.total).toBe('number')
|
|
388
|
-
expect(resultCounts.total).toBe(resultCounts.web + resultCounts.news)
|
|
389
|
-
},
|
|
390
|
-
{ retry: 2 },
|
|
391
|
-
)
|
|
392
|
-
|
|
393
|
-
test('handles API errors gracefully', async () => {
|
|
394
|
-
try {
|
|
395
|
-
await client.callTool({
|
|
396
|
-
name: 'you-search',
|
|
397
|
-
arguments: {
|
|
398
|
-
query: undefined,
|
|
399
|
-
},
|
|
400
|
-
})
|
|
401
|
-
} catch (error) {
|
|
402
|
-
// If it errors, that's also acceptable behavior
|
|
403
|
-
expect(error).toBeDefined()
|
|
404
|
-
}
|
|
405
|
-
})
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
// NOTE: The following tests require a You.com API key with access to the Contents API
|
|
409
|
-
// Using example.com and Wikipedia URLs that work with the Contents API
|
|
410
|
-
describe('registerContentsTool', () => {
|
|
411
|
-
test('tool is registered and available', async () => {
|
|
412
|
-
const tools = await client.listTools()
|
|
413
|
-
|
|
414
|
-
const contentsTool = tools.tools.find((t) => t.name === 'you-contents')
|
|
415
|
-
|
|
416
|
-
expect(contentsTool).toBeDefined()
|
|
417
|
-
expect(contentsTool?.title).toBe('Extract Web Page Contents')
|
|
418
|
-
expect(contentsTool?.description).toContain('Extract page content')
|
|
419
|
-
})
|
|
420
|
-
|
|
421
|
-
test(
|
|
422
|
-
'extracts content from a single URL',
|
|
423
|
-
async () => {
|
|
424
|
-
const result = await client.callTool({
|
|
425
|
-
name: 'you-contents',
|
|
426
|
-
arguments: {
|
|
427
|
-
urls: ['https://documentation.you.com/developer-resources/mcp-server'],
|
|
428
|
-
format: 'markdown',
|
|
429
|
-
},
|
|
430
|
-
})
|
|
431
|
-
|
|
432
|
-
expect(result).toHaveProperty('content')
|
|
433
|
-
expect(result).toHaveProperty('structuredContent')
|
|
434
|
-
|
|
435
|
-
const content = result.content as { type: string; text: string }[]
|
|
436
|
-
expect(Array.isArray(content)).toBe(true)
|
|
437
|
-
expect(content[0]).toHaveProperty('type', 'text')
|
|
438
|
-
expect(content[0]).toHaveProperty('text')
|
|
439
|
-
|
|
440
|
-
const text = content[0]?.text
|
|
441
|
-
expect(text).toContain('Successfully extracted content')
|
|
442
|
-
expect(text).toContain('https://documentation.you.com/developer-resources/mcp-server')
|
|
443
|
-
expect(text).toContain('Formats: markdown')
|
|
444
|
-
|
|
445
|
-
const structuredContent = result.structuredContent as ContentsStructuredContent
|
|
446
|
-
expect(structuredContent).toHaveProperty('count', 1)
|
|
447
|
-
expect(structuredContent).toHaveProperty('formats')
|
|
448
|
-
expect(structuredContent.formats).toEqual(['markdown'])
|
|
449
|
-
expect(structuredContent).toHaveProperty('items')
|
|
450
|
-
expect(structuredContent.items).toHaveLength(1)
|
|
451
|
-
|
|
452
|
-
const item = structuredContent.items[0]
|
|
453
|
-
expect(item).toBeDefined()
|
|
454
|
-
|
|
455
|
-
expect(item).toHaveProperty('url', 'https://documentation.you.com/developer-resources/mcp-server')
|
|
456
|
-
expect(item).toHaveProperty('markdown')
|
|
457
|
-
expect(typeof item?.markdown).toBe('string')
|
|
458
|
-
expect(item?.markdown?.length).toBeGreaterThan(0)
|
|
459
|
-
},
|
|
460
|
-
{ retry: 2 },
|
|
461
|
-
)
|
|
462
|
-
|
|
463
|
-
test(
|
|
464
|
-
'extracts content from multiple URLs',
|
|
465
|
-
async () => {
|
|
466
|
-
const result = await client.callTool({
|
|
467
|
-
name: 'you-contents',
|
|
468
|
-
arguments: {
|
|
469
|
-
urls: [
|
|
470
|
-
'https://documentation.you.com/developer-resources/mcp-server',
|
|
471
|
-
'https://documentation.you.com/developer-resources/python-sdk',
|
|
472
|
-
],
|
|
473
|
-
format: 'markdown',
|
|
474
|
-
},
|
|
475
|
-
})
|
|
476
|
-
|
|
477
|
-
const structuredContent = result.structuredContent as ContentsStructuredContent
|
|
478
|
-
expect(structuredContent.count).toBe(2)
|
|
479
|
-
expect(structuredContent.items).toHaveLength(2)
|
|
480
|
-
|
|
481
|
-
const content = result.content as { type: string; text: string }[]
|
|
482
|
-
const text = content[0]?.text
|
|
483
|
-
expect(text).toContain('Successfully extracted content from 2 URL(s)')
|
|
484
|
-
},
|
|
485
|
-
{ retry: 2 },
|
|
486
|
-
)
|
|
487
|
-
|
|
488
|
-
test(
|
|
489
|
-
'handles html format',
|
|
490
|
-
async () => {
|
|
491
|
-
const result = await client.callTool({
|
|
492
|
-
name: 'you-contents',
|
|
493
|
-
arguments: {
|
|
494
|
-
urls: ['https://documentation.you.com/developer-resources/mcp-server'],
|
|
495
|
-
format: 'html',
|
|
496
|
-
},
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
const structuredContent = result.structuredContent as ContentsStructuredContent
|
|
500
|
-
expect(structuredContent.formats).toEqual(['html'])
|
|
501
|
-
|
|
502
|
-
const content = result.content as { type: string; text: string }[]
|
|
503
|
-
const text = content[0]?.text
|
|
504
|
-
expect(text).toContain('Formats: html')
|
|
505
|
-
},
|
|
506
|
-
{ retry: 2 },
|
|
507
|
-
)
|
|
508
|
-
|
|
509
|
-
test(
|
|
510
|
-
'defaults to markdown format when not specified',
|
|
511
|
-
async () => {
|
|
512
|
-
const result = await client.callTool({
|
|
513
|
-
name: 'you-contents',
|
|
514
|
-
arguments: {
|
|
515
|
-
urls: ['https://documentation.you.com/developer-resources/mcp-server'],
|
|
516
|
-
},
|
|
517
|
-
})
|
|
518
|
-
|
|
519
|
-
const structuredContent = result.structuredContent as ContentsStructuredContent
|
|
520
|
-
expect(structuredContent.formats).toEqual(['markdown'])
|
|
521
|
-
},
|
|
522
|
-
{ retry: 2 },
|
|
523
|
-
)
|
|
524
|
-
})
|
|
525
|
-
|
|
526
|
-
describe('registerResearchTool', () => {
|
|
527
|
-
test('tool is registered and available', async () => {
|
|
528
|
-
const tools = await client.listTools()
|
|
529
|
-
|
|
530
|
-
const researchTool = tools.tools.find((t) => t.name === 'you-research')
|
|
531
|
-
|
|
532
|
-
expect(researchTool).toBeDefined()
|
|
533
|
-
expect(researchTool?.title).toBe('Research')
|
|
534
|
-
expect(researchTool?.description).toContain('Research a topic')
|
|
535
|
-
})
|
|
536
|
-
|
|
537
|
-
test(
|
|
538
|
-
'performs basic research successfully',
|
|
539
|
-
async () => {
|
|
540
|
-
const result = await client.callTool({
|
|
541
|
-
name: 'you-research',
|
|
542
|
-
arguments: {
|
|
543
|
-
input: 'What is TypeScript?',
|
|
544
|
-
research_effort: 'lite',
|
|
545
|
-
},
|
|
546
|
-
})
|
|
547
|
-
|
|
548
|
-
expect(result).toHaveProperty('content')
|
|
549
|
-
expect(result).toHaveProperty('structuredContent')
|
|
550
|
-
|
|
551
|
-
const content = result.content as { type: string; text: string }[]
|
|
552
|
-
expect(Array.isArray(content)).toBe(true)
|
|
553
|
-
expect(content[0]).toHaveProperty('type', 'text')
|
|
554
|
-
expect(content[0]).toHaveProperty('text')
|
|
555
|
-
|
|
556
|
-
const text = content[0]?.text
|
|
557
|
-
expect(typeof text).toBe('string')
|
|
558
|
-
expect(text?.length).toBeGreaterThan(0)
|
|
559
|
-
},
|
|
560
|
-
{ retry: 2 },
|
|
561
|
-
)
|
|
562
|
-
|
|
563
|
-
test(
|
|
564
|
-
'returns structured content with source information',
|
|
565
|
-
async () => {
|
|
566
|
-
const result = await client.callTool({
|
|
567
|
-
name: 'you-research',
|
|
568
|
-
arguments: {
|
|
569
|
-
input: 'What are the benefits of REST APIs?',
|
|
570
|
-
research_effort: 'lite',
|
|
571
|
-
},
|
|
572
|
-
})
|
|
573
|
-
|
|
574
|
-
const structuredContent = result.structuredContent as ResearchStructuredContent
|
|
575
|
-
expect(structuredContent).toHaveProperty('content_type', 'text')
|
|
576
|
-
expect(structuredContent).toHaveProperty('sourceCount')
|
|
577
|
-
expect(typeof structuredContent.sourceCount).toBe('number')
|
|
578
|
-
expect(structuredContent.sourceCount).toBeGreaterThan(0)
|
|
579
|
-
|
|
580
|
-
expect(structuredContent).toHaveProperty('sources')
|
|
581
|
-
expect(Array.isArray(structuredContent.sources)).toBe(true)
|
|
582
|
-
expect(structuredContent.sources.length).toBeGreaterThan(0)
|
|
583
|
-
|
|
584
|
-
const source = structuredContent.sources[0]
|
|
585
|
-
expect(source).toBeDefined()
|
|
586
|
-
expect(source).toHaveProperty('url')
|
|
587
|
-
expect(typeof source?.url).toBe('string')
|
|
588
|
-
expect(source).toHaveProperty('snippetCount')
|
|
589
|
-
expect(typeof source?.snippetCount).toBe('number')
|
|
590
|
-
},
|
|
591
|
-
{ retry: 2 },
|
|
592
|
-
)
|
|
593
|
-
|
|
594
|
-
test(
|
|
595
|
-
'text content contains markdown with answer and sources',
|
|
596
|
-
async () => {
|
|
597
|
-
const result = await client.callTool({
|
|
598
|
-
name: 'you-research',
|
|
599
|
-
arguments: {
|
|
600
|
-
input: 'What is JWT authentication?',
|
|
601
|
-
research_effort: 'lite',
|
|
602
|
-
},
|
|
603
|
-
})
|
|
604
|
-
|
|
605
|
-
const content = result.content as { type: string; text: string }[]
|
|
606
|
-
const text = content[0]?.text
|
|
607
|
-
expect(text).toContain('# Answer')
|
|
608
|
-
expect(text).toContain('## Sources')
|
|
609
|
-
},
|
|
610
|
-
{ retry: 2 },
|
|
611
|
-
)
|
|
612
|
-
|
|
613
|
-
test(
|
|
614
|
-
'handles research_effort parameter',
|
|
615
|
-
async () => {
|
|
616
|
-
const transport = new StdioClientTransport({
|
|
617
|
-
command: 'npx',
|
|
618
|
-
args: [Bun.resolveSync('../../bin/stdio', import.meta.dir)],
|
|
619
|
-
env: {
|
|
620
|
-
YDC_API_KEY: process.env.YDC_API_KEY ?? '',
|
|
621
|
-
},
|
|
622
|
-
})
|
|
623
|
-
|
|
624
|
-
const dedicatedClient = new Client({
|
|
625
|
-
name: 'test-client-research',
|
|
626
|
-
version: '0.0.1',
|
|
627
|
-
})
|
|
628
|
-
|
|
629
|
-
await dedicatedClient.connect(transport)
|
|
630
|
-
|
|
631
|
-
try {
|
|
632
|
-
const result = await dedicatedClient.callTool({
|
|
633
|
-
name: 'you-research',
|
|
634
|
-
arguments: {
|
|
635
|
-
input: 'Explain microservices architecture',
|
|
636
|
-
research_effort: 'standard',
|
|
637
|
-
},
|
|
638
|
-
})
|
|
639
|
-
|
|
640
|
-
const content = result.content as { type: string; text: string }[]
|
|
641
|
-
expect(content[0]).toHaveProperty('text')
|
|
642
|
-
expect(content[0]?.text?.length).toBeGreaterThan(0)
|
|
643
|
-
} finally {
|
|
644
|
-
await dedicatedClient.close()
|
|
645
|
-
}
|
|
646
|
-
},
|
|
647
|
-
{ retry: 2, timeout: 120_000 },
|
|
648
|
-
)
|
|
649
|
-
})
|