@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.
@@ -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
- })