@youdotcom-oss/mcp 1.3.5-next.5 → 1.3.6

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.
@@ -0,0 +1,496 @@
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 { SearchStructuredContent } from '../search/search.schemas.ts';
7
+
8
+ let client: Client;
9
+
10
+ beforeAll(async () => {
11
+ await $`bun run build`; // 1256
12
+ const transport = new StdioClientTransport({
13
+ command: 'npx',
14
+ args: [Bun.resolveSync('../../bin/stdio', import.meta.dir)],
15
+ env: {
16
+ YDC_API_KEY: process.env.YDC_API_KEY ?? '',
17
+ },
18
+ });
19
+
20
+ client = new Client({
21
+ name: 'test-client',
22
+ version: '0.0.1',
23
+ });
24
+
25
+ await client.connect(transport);
26
+ });
27
+
28
+ afterAll(async () => {
29
+ await client.close();
30
+ });
31
+
32
+ describe('registerSearchTool', () => {
33
+ test('tool is registered and available', async () => {
34
+ const tools = await client.listTools();
35
+
36
+ const searchTool = tools.tools.find((t) => t.name === 'you-search');
37
+
38
+ expect(searchTool).toBeDefined();
39
+ expect(searchTool?.title).toBe('Web Search');
40
+ expect(searchTool?.description).toContain('Web and news search');
41
+ });
42
+
43
+ test('performs basic search successfully', async () => {
44
+ const result = await client.callTool({
45
+ name: 'you-search',
46
+ arguments: {
47
+ query: 'javascript tutorial',
48
+ count: 3,
49
+ },
50
+ });
51
+ const content = result.content as { type: string; text: string }[];
52
+ expect(result).toHaveProperty('content');
53
+ expect(Array.isArray(content)).toBe(true);
54
+ expect(content[0]).toHaveProperty('type', 'text');
55
+ expect(content[0]).toHaveProperty('text');
56
+
57
+ const text = content[0]?.text;
58
+ expect(text).toContain('Search Results for');
59
+ expect(text).toContain('javascript tutorial');
60
+ const structuredContent = result.structuredContent as SearchStructuredContent;
61
+ // Should have structured content with minimal format
62
+ expect(result).toHaveProperty('structuredContent');
63
+ expect(structuredContent).toHaveProperty('resultCounts');
64
+ expect(structuredContent.resultCounts).toHaveProperty('web');
65
+ expect(structuredContent.resultCounts).toHaveProperty('news');
66
+ expect(structuredContent.resultCounts).toHaveProperty('total');
67
+ });
68
+
69
+ test('handles search with web results formatting', async () => {
70
+ const result = await client.callTool({
71
+ name: 'you-search',
72
+ arguments: {
73
+ query: 'react components',
74
+ count: 2,
75
+ },
76
+ });
77
+
78
+ const content = result.content as { type: string; text: string }[];
79
+ const text = content[0]?.text;
80
+ expect(text).toContain('WEB RESULTS:');
81
+ expect(text).toContain('Title:');
82
+ // URL should NOT be in text content anymore
83
+ expect(text).not.toContain('URL:');
84
+ expect(text).toContain('Description:');
85
+ expect(text).toContain('Snippets:');
86
+
87
+ // Verify structured data has result counts
88
+ const structuredContent = result.structuredContent as SearchStructuredContent;
89
+ expect(structuredContent.resultCounts.web).toBeGreaterThan(0);
90
+ expect(structuredContent.resultCounts.total).toBeGreaterThan(0);
91
+
92
+ // URLs should be in structuredContent.results
93
+ expect(structuredContent.results).toBeDefined();
94
+ expect(structuredContent.results?.web).toBeDefined();
95
+ expect(structuredContent.results?.web?.length).toBeGreaterThan(0);
96
+ expect(structuredContent.results?.web?.[0]).toHaveProperty('url');
97
+ expect(structuredContent.results?.web?.[0]).toHaveProperty('title');
98
+ });
99
+
100
+ test('handles search with news results', async () => {
101
+ const result = await client.callTool({
102
+ name: 'you-search',
103
+ arguments: {
104
+ query: 'technology news',
105
+ count: 2,
106
+ },
107
+ });
108
+
109
+ const content = result.content as { type: string; text: string }[];
110
+ const text = content[0]?.text;
111
+
112
+ const structuredContent = result.structuredContent as SearchStructuredContent;
113
+ // Check if news results are included
114
+ if (structuredContent.resultCounts.news > 0) {
115
+ expect(text).toContain('NEWS RESULTS:');
116
+ expect(text).toContain('Published:');
117
+ expect(structuredContent.resultCounts.news).toBeGreaterThan(0);
118
+ }
119
+ });
120
+
121
+ test('handles mixed web and news results with proper separation', async () => {
122
+ const result = await client.callTool({
123
+ name: 'you-search',
124
+ arguments: {
125
+ query: 'artificial intelligence',
126
+ count: 3,
127
+ },
128
+ });
129
+
130
+ const content = result.content as { type: string; text: string }[];
131
+ const text = content[0]?.text;
132
+
133
+ // Should have web results
134
+ expect(text).toContain('WEB RESULTS:');
135
+
136
+ const structuredContent = result.structuredContent as SearchStructuredContent;
137
+ // If both web and news results exist, check for separator
138
+ if (structuredContent.resultCounts.news > 0) {
139
+ expect(text).toContain('NEWS RESULTS:');
140
+ expect(text).toContain('='.repeat(50));
141
+ }
142
+ });
143
+
144
+ test('handles freshness parameter', async () => {
145
+ const result = await client.callTool({
146
+ name: 'you-search',
147
+ arguments: {
148
+ query: 'recent news',
149
+ freshness: 'week',
150
+ },
151
+ });
152
+
153
+ const content = result.content as { type: string; text: string }[];
154
+ expect(content[0]).toHaveProperty('text');
155
+ expect(content[0]?.text).toContain('recent news');
156
+ });
157
+
158
+ test('handles country parameter', async () => {
159
+ const result = await client.callTool({
160
+ name: 'you-search',
161
+ arguments: {
162
+ query: 'local news',
163
+ country: 'US',
164
+ },
165
+ });
166
+
167
+ const content = result.content as { type: string; text: string }[];
168
+ expect(content[0]).toHaveProperty('text');
169
+ expect(content[0]?.text).toContain('local news');
170
+ });
171
+
172
+ test('handles safesearch parameter', async () => {
173
+ const result = await client.callTool({
174
+ name: 'you-search',
175
+ arguments: {
176
+ query: 'educational content',
177
+ safesearch: 'strict',
178
+ },
179
+ });
180
+
181
+ const content = result.content as { type: string; text: string }[];
182
+ expect(content[0]).toHaveProperty('text');
183
+ expect(content[0]?.text).toContain('educational content');
184
+ });
185
+
186
+ test('handles site parameter', async () => {
187
+ const result = await client.callTool({
188
+ name: 'you-search',
189
+ arguments: {
190
+ query: 'react components',
191
+ site: 'github.com',
192
+ },
193
+ });
194
+
195
+ const content = result.content as { type: string; text: string }[];
196
+ expect(content[0]?.text).toContain('react components');
197
+ });
198
+
199
+ test('handles fileType parameter', async () => {
200
+ const result = await client.callTool({
201
+ name: 'you-search',
202
+ arguments: {
203
+ query: 'documentation',
204
+ fileType: 'pdf',
205
+ },
206
+ });
207
+
208
+ const content = result.content as { type: string; text: string }[];
209
+ expect(content[0]?.text).toContain('documentation');
210
+ });
211
+
212
+ test('handles language parameter', async () => {
213
+ const result = await client.callTool({
214
+ name: 'you-search',
215
+ arguments: {
216
+ query: 'tutorial',
217
+ language: 'es',
218
+ },
219
+ });
220
+
221
+ const content = result.content as { type: string; text: string }[];
222
+ expect(content[0]?.text).toContain('tutorial');
223
+ });
224
+
225
+ test('handles exactTerms parameter', async () => {
226
+ const result = await client.callTool({
227
+ name: 'you-search',
228
+ arguments: {
229
+ query: 'programming',
230
+ exactTerms: 'javascript|typescript',
231
+ },
232
+ });
233
+
234
+ const content = result.content as { type: string; text: string }[];
235
+ expect(content[0]?.text).toContain('programming');
236
+ });
237
+
238
+ test('handles excludeTerms parameter', async () => {
239
+ const result = await client.callTool({
240
+ name: 'you-search',
241
+ arguments: {
242
+ query: 'tutorial',
243
+ excludeTerms: 'beginner|basic',
244
+ },
245
+ });
246
+
247
+ const content = result.content as { type: string; text: string }[];
248
+ expect(content[0]?.text).toContain('tutorial');
249
+ });
250
+
251
+ test('handles multi-word phrases with parentheses in exactTerms', async () => {
252
+ const result = await client.callTool({
253
+ name: 'you-search',
254
+ arguments: {
255
+ query: 'programming',
256
+ exactTerms: '(machine learning)|typescript',
257
+ },
258
+ });
259
+
260
+ const content = result.content as { type: string; text: string }[];
261
+ expect(content[0]?.text).toContain('programming');
262
+ });
263
+
264
+ test('handles multi-word phrases with parentheses in excludeTerms', async () => {
265
+ const result = await client.callTool({
266
+ name: 'you-search',
267
+ arguments: {
268
+ query: 'programming',
269
+ excludeTerms: '(social media)|ads',
270
+ },
271
+ });
272
+
273
+ const content = result.content as { type: string; text: string }[];
274
+ expect(content[0]?.text).toContain('programming');
275
+ });
276
+
277
+ test('handles complex search with multiple parameters', async () => {
278
+ const result = await client.callTool({
279
+ name: 'you-search',
280
+ arguments: {
281
+ query: 'machine learning tutorial',
282
+ count: 5,
283
+ offset: 1,
284
+ freshness: 'month',
285
+ country: 'US',
286
+ safesearch: 'moderate',
287
+ site: 'github.com',
288
+ fileType: 'md',
289
+ language: 'en',
290
+ },
291
+ });
292
+
293
+ const content = result.content as { type: string; text: string }[];
294
+ expect(content[0]).toHaveProperty('text');
295
+ // Test should pass even if no results (very specific query might have no results)
296
+
297
+ // Verify results are limited by count if there are results
298
+ const structuredContent = result.structuredContent as SearchStructuredContent;
299
+ if (structuredContent.resultCounts.web > 0) {
300
+ expect(structuredContent.resultCounts.web).toBeLessThanOrEqual(5);
301
+ }
302
+ });
303
+
304
+ test('handles special characters in query', async () => {
305
+ const result = await client.callTool({
306
+ name: 'you-search',
307
+ arguments: {
308
+ query: 'C++ programming "hello world"',
309
+ },
310
+ });
311
+
312
+ const content = result.content as { type: string; text: string }[];
313
+ expect(content[0]).toHaveProperty('text');
314
+ expect(content[0]?.text).toContain('C++');
315
+ });
316
+
317
+ test('handles empty search results gracefully', async () => {
318
+ const result = await client.callTool({
319
+ name: 'you-search',
320
+ arguments: {
321
+ query: '_',
322
+ },
323
+ });
324
+
325
+ const content = result.content as { type: string; text: string }[];
326
+
327
+ // Should still have content even if no results
328
+ expect(content[0]).toHaveProperty('text');
329
+
330
+ const text = content[0]?.text;
331
+ const structuredContent = result.structuredContent as SearchStructuredContent;
332
+ expect(structuredContent.resultCounts.web).toBe(0);
333
+ expect(structuredContent.resultCounts.news).toBe(0);
334
+ expect(structuredContent.resultCounts.total).toBe(0);
335
+ expect(text).toContain('No results found');
336
+ });
337
+
338
+ test('validates structured response format', async () => {
339
+ const result = await client.callTool({
340
+ name: 'you-search',
341
+ arguments: {
342
+ query: `what's the latest tech news`,
343
+ count: 2,
344
+ },
345
+ });
346
+
347
+ const structuredContent = result.structuredContent as SearchStructuredContent;
348
+ // Validate minimal structured content schema
349
+ expect(structuredContent).toHaveProperty('resultCounts');
350
+
351
+ // Check result counts structure
352
+ const resultCounts = structuredContent.resultCounts;
353
+ expect(resultCounts).toHaveProperty('web');
354
+ expect(resultCounts).toHaveProperty('news');
355
+ expect(resultCounts).toHaveProperty('total');
356
+ expect(typeof resultCounts.web).toBe('number');
357
+ expect(typeof resultCounts.news).toBe('number');
358
+ expect(typeof resultCounts.total).toBe('number');
359
+ expect(resultCounts.total).toBe(resultCounts.web + resultCounts.news);
360
+ });
361
+
362
+ test('returns error when both exactTerms and excludeTerms are provided', async () => {
363
+ const result = await client.callTool({
364
+ name: 'you-search',
365
+ arguments: {
366
+ query: 'programming',
367
+ exactTerms: 'javascript',
368
+ excludeTerms: 'beginner',
369
+ },
370
+ });
371
+
372
+ expect(result.isError).toBe(true);
373
+ const content = result.content as { type: string; text: string }[];
374
+ expect(content[0]?.text).toContain('Cannot specify both exactTerms and excludeTerms - please use only one');
375
+ });
376
+
377
+ test('handles API errors gracefully', async () => {
378
+ try {
379
+ await client.callTool({
380
+ name: 'you-search',
381
+ arguments: {
382
+ query: undefined,
383
+ },
384
+ });
385
+ } catch (error) {
386
+ // If it errors, that's also acceptable behavior
387
+ expect(error).toBeDefined();
388
+ }
389
+ });
390
+
391
+ test.skip('handles network timeout scenarios', async () => {
392
+ // TODO: How do we test this?
393
+ });
394
+ });
395
+
396
+ // NOTE: The following tests require a You.com API key with access to the Contents API
397
+ // Using example.com and Wikipedia URLs that work with the Contents API
398
+ describe('registerContentsTool', () => {
399
+ test('tool is registered and available', async () => {
400
+ const tools = await client.listTools();
401
+
402
+ const contentsTool = tools.tools.find((t) => t.name === 'you-contents');
403
+
404
+ expect(contentsTool).toBeDefined();
405
+ expect(contentsTool?.title).toBe('Extract Web Page Contents');
406
+ expect(contentsTool?.description).toContain('Extract page content');
407
+ });
408
+
409
+ test('extracts content from a single URL', async () => {
410
+ const result = await client.callTool({
411
+ name: 'you-contents',
412
+ arguments: {
413
+ urls: ['https://documentation.you.com/developer-resources/mcp-server'],
414
+ format: 'markdown',
415
+ },
416
+ });
417
+
418
+ expect(result).toHaveProperty('content');
419
+ expect(result).toHaveProperty('structuredContent');
420
+
421
+ const content = result.content as { type: string; text: string }[];
422
+ expect(Array.isArray(content)).toBe(true);
423
+ expect(content[0]).toHaveProperty('type', 'text');
424
+ expect(content[0]).toHaveProperty('text');
425
+
426
+ const text = content[0]?.text;
427
+ expect(text).toContain('Successfully extracted content');
428
+ expect(text).toContain('https://documentation.you.com/developer-resources/mcp-server');
429
+ expect(text).toContain('Format: markdown');
430
+
431
+ const structuredContent = result.structuredContent as ContentsStructuredContent;
432
+ expect(structuredContent).toHaveProperty('count', 1);
433
+ expect(structuredContent).toHaveProperty('format', 'markdown');
434
+ expect(structuredContent).toHaveProperty('items');
435
+ expect(structuredContent.items).toHaveLength(1);
436
+
437
+ const item = structuredContent.items[0];
438
+ expect(item).toBeDefined();
439
+
440
+ expect(item).toHaveProperty('url', 'https://documentation.you.com/developer-resources/mcp-server');
441
+ expect(item).toHaveProperty('content');
442
+ expect(item).toHaveProperty('contentLength');
443
+ expect(typeof item?.content).toBe('string');
444
+ expect(item?.content.length).toBeGreaterThan(0);
445
+ });
446
+
447
+ test('extracts content from multiple URLs', async () => {
448
+ const result = await client.callTool({
449
+ name: 'you-contents',
450
+ arguments: {
451
+ urls: [
452
+ 'https://documentation.you.com/developer-resources/mcp-server',
453
+ 'https://documentation.you.com/developer-resources/python-sdk',
454
+ ],
455
+ format: 'markdown',
456
+ },
457
+ });
458
+
459
+ const structuredContent = result.structuredContent as ContentsStructuredContent;
460
+ expect(structuredContent.count).toBe(2);
461
+ expect(structuredContent.items).toHaveLength(2);
462
+
463
+ const content = result.content as { type: string; text: string }[];
464
+ const text = content[0]?.text;
465
+ expect(text).toContain('Successfully extracted content from 2 URL(s)');
466
+ });
467
+
468
+ test('handles html format', async () => {
469
+ const result = await client.callTool({
470
+ name: 'you-contents',
471
+ arguments: {
472
+ urls: ['https://documentation.you.com/developer-resources/mcp-server'],
473
+ format: 'html',
474
+ },
475
+ });
476
+
477
+ const structuredContent = result.structuredContent as ContentsStructuredContent;
478
+ expect(structuredContent.format).toBe('html');
479
+
480
+ const content = result.content as { type: string; text: string }[];
481
+ const text = content[0]?.text;
482
+ expect(text).toContain('Format: html');
483
+ });
484
+
485
+ test('defaults to markdown format when not specified', async () => {
486
+ const result = await client.callTool({
487
+ name: 'you-contents',
488
+ arguments: {
489
+ urls: ['https://documentation.you.com/developer-resources/mcp-server'],
490
+ },
491
+ });
492
+
493
+ const structuredContent = result.structuredContent as ContentsStructuredContent;
494
+ expect(structuredContent.format).toBe('markdown');
495
+ });
496
+ });
package/src/utils.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from './contents/contents.schemas.ts';
2
+ export * from './contents/contents.utils.ts';
3
+ export * from './express/express.schemas.ts';
4
+ export * from './express/express.utils.ts';
5
+ export * from './search/search.schemas.ts';
6
+ export * from './search/search.utils.ts';
7
+ export * from './shared/check-response-for-errors.ts';
8
+ export * from './shared/format-search-results-text.ts';