@youdotcom-oss/mcp 1.3.8 → 1.3.9-next.1

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.
@@ -7,63 +7,75 @@ const getUserAgent = () => 'MCP/test (You.com; test-client)';
7
7
  // NOTE: The following tests require a You.com API key with access to the Contents API
8
8
  // Using example.com/example.org as test URLs since You.com blocks self-scraping
9
9
  describe('fetchContents', () => {
10
- test('returns valid response structure for single URL', async () => {
11
- const result = await fetchContents({
12
- contentsQuery: {
13
- urls: ['https://documentation.you.com/developer-resources/mcp-server'],
14
- format: 'markdown',
15
- },
16
- getUserAgent,
17
- });
18
-
19
- expect(Array.isArray(result)).toBe(true);
20
- expect(result.length).toBeGreaterThan(0);
21
-
22
- const firstItem = result[0];
23
- expect(firstItem).toBeDefined();
24
-
25
- // Should have markdown content
26
- expect(firstItem?.markdown).toBeDefined();
27
- expect(typeof firstItem?.markdown).toBe('string');
28
- });
29
-
30
- test('handles multiple URLs', async () => {
31
- const result = await fetchContents({
32
- contentsQuery: {
33
- urls: [
34
- 'https://documentation.you.com/developer-resources/mcp-server',
35
- 'https://documentation.you.com/developer-resources/python-sdk',
36
- ],
37
- format: 'markdown',
38
- },
39
- getUserAgent,
40
- });
41
-
42
- expect(Array.isArray(result)).toBe(true);
43
- expect(result.length).toBe(2);
44
-
45
- for (const item of result) {
46
- expect(item).toHaveProperty('url');
47
- expect(item.markdown).toBeDefined();
48
- }
49
- });
50
-
51
- test('handles html format', async () => {
52
- const result = await fetchContents({
53
- contentsQuery: {
54
- urls: ['https://documentation.you.com/developer-resources/mcp-server'],
55
- format: 'html',
56
- },
57
- getUserAgent,
58
- });
59
-
60
- expect(Array.isArray(result)).toBe(true);
61
- const firstItem = result[0];
62
- expect(firstItem).toBeDefined();
63
-
64
- expect(firstItem?.html).toBeDefined();
65
- expect(typeof firstItem?.html).toBe('string');
66
- });
10
+ test(
11
+ 'returns valid response structure for single URL',
12
+ async () => {
13
+ const result = await fetchContents({
14
+ contentsQuery: {
15
+ urls: ['https://documentation.you.com/developer-resources/mcp-server'],
16
+ format: 'markdown',
17
+ },
18
+ getUserAgent,
19
+ });
20
+
21
+ expect(Array.isArray(result)).toBe(true);
22
+ expect(result.length).toBeGreaterThan(0);
23
+
24
+ const firstItem = result[0];
25
+ expect(firstItem).toBeDefined();
26
+
27
+ // Should have markdown content
28
+ expect(firstItem?.markdown).toBeDefined();
29
+ expect(typeof firstItem?.markdown).toBe('string');
30
+ },
31
+ { retry: 2 },
32
+ );
33
+
34
+ test(
35
+ 'handles multiple URLs',
36
+ async () => {
37
+ const result = await fetchContents({
38
+ contentsQuery: {
39
+ urls: [
40
+ 'https://documentation.you.com/developer-resources/mcp-server',
41
+ 'https://documentation.you.com/developer-resources/python-sdk',
42
+ ],
43
+ format: 'markdown',
44
+ },
45
+ getUserAgent,
46
+ });
47
+
48
+ expect(Array.isArray(result)).toBe(true);
49
+ expect(result.length).toBe(2);
50
+
51
+ for (const item of result) {
52
+ expect(item).toHaveProperty('url');
53
+ expect(item.markdown).toBeDefined();
54
+ }
55
+ },
56
+ { retry: 2 },
57
+ );
58
+
59
+ test(
60
+ 'handles html format',
61
+ async () => {
62
+ const result = await fetchContents({
63
+ contentsQuery: {
64
+ urls: ['https://documentation.you.com/developer-resources/mcp-server'],
65
+ format: 'html',
66
+ },
67
+ getUserAgent,
68
+ });
69
+
70
+ expect(Array.isArray(result)).toBe(true);
71
+ const firstItem = result[0];
72
+ expect(firstItem).toBeDefined();
73
+
74
+ expect(firstItem?.html).toBeDefined();
75
+ expect(typeof firstItem?.html).toBe('string');
76
+ },
77
+ { retry: 2 },
78
+ );
67
79
  });
68
80
 
69
81
  describe('formatContentsResponse', () => {
@@ -1,3 +1,4 @@
1
+ import { EXPRESS_API_URL } from '../shared/api-constants.ts';
1
2
  import { checkResponseForErrors } from '../shared/check-response-for-errors.ts';
2
3
  import { formatSearchResultsText } from '../shared/format-search-results-text.ts';
3
4
  import {
@@ -7,9 +8,6 @@ import {
7
8
  type ExpressAgentMcpResponse,
8
9
  } from './express.schemas.ts';
9
10
 
10
- // Express Agent Constants
11
- const AGENTS_RUN_URL = 'https://api.you.com/v1/agents/runs';
12
-
13
11
  /**
14
12
  * Checks response status and throws appropriate errors for agent API calls
15
13
  */
@@ -70,7 +68,7 @@ export const callExpressAgent = async ({
70
68
  body: JSON.stringify(requestBody),
71
69
  };
72
70
 
73
- const response = await fetch(AGENTS_RUN_URL, options);
71
+ const response = await fetch(EXPRESS_API_URL, options);
74
72
 
75
73
  if (!response.ok) {
76
74
  await agentThrowOnFailedStatus(response);
@@ -7,68 +7,80 @@ const getUserAgent = () => 'MCP/test (You.com; test-client)';
7
7
  setDefaultTimeout(20_000);
8
8
 
9
9
  describe('callExpressAgent', () => {
10
- test('returns answer only (WITHOUT web_search tools)', async () => {
11
- const result = await callExpressAgent({
12
- agentInput: { input: 'What is machine learning?' },
13
- getUserAgent,
14
- });
15
-
16
- // Verify MCP response structure
17
- expect(result).toHaveProperty('answer');
18
- expect(typeof result.answer).toBe('string');
19
- expect(result.answer.length).toBeGreaterThan(0);
20
-
21
- // Should NOT have results when web_search is not used
22
- expect(result.results).toBeUndefined();
23
-
24
- expect(result.agent).toBe('express');
25
- });
26
-
27
- test('returns answer and search results (WITH web_search tools)', async () => {
28
- const result = await callExpressAgent({
29
- agentInput: {
30
- input: 'Latest developments in quantum computing',
31
- tools: [{ type: 'web_search' }],
32
- },
33
- getUserAgent,
34
- });
35
-
36
- // Verify MCP response has both answer and results
37
- expect(result).toHaveProperty('answer');
38
- expect(typeof result.answer).toBe('string');
39
- expect(result.answer.length).toBeGreaterThan(0);
40
-
41
- expect(result).toHaveProperty('results');
42
- expect(result.results).toHaveProperty('web');
43
- expect(Array.isArray(result.results?.web)).toBe(true);
44
- expect(result.results?.web.length).toBeGreaterThan(0);
45
-
46
- // Verify each search result has required fields
47
- const firstResult = result.results?.web[0];
48
- expect(firstResult).toHaveProperty('url');
49
- expect(firstResult).toHaveProperty('title');
50
- expect(firstResult).toHaveProperty('snippet');
51
- expect(typeof firstResult?.url).toBe('string');
52
- expect(typeof firstResult?.title).toBe('string');
53
- expect(typeof firstResult?.snippet).toBe('string');
54
- expect(firstResult?.url.length).toBeGreaterThan(0);
55
- expect(firstResult?.title.length).toBeGreaterThan(0);
56
-
57
- expect(result.agent).toBe('express');
58
- }, 30000);
59
-
60
- test('works without optional parameters', async () => {
61
- const result = await callExpressAgent({
62
- agentInput: { input: 'What is the capital of France?' },
63
- getUserAgent,
64
- // No progressToken or sendProgress provided
65
- });
66
-
67
- // Should work normally without progress tracking
68
- expect(result).toHaveProperty('answer');
69
- expect(result.answer.length).toBeGreaterThan(0);
70
- expect(result.agent).toBe('express');
71
- });
10
+ test(
11
+ 'returns answer only (WITHOUT web_search tools)',
12
+ async () => {
13
+ const result = await callExpressAgent({
14
+ agentInput: { input: 'What is machine learning?' },
15
+ getUserAgent,
16
+ });
17
+
18
+ // Verify MCP response structure
19
+ expect(result).toHaveProperty('answer');
20
+ expect(typeof result.answer).toBe('string');
21
+ expect(result.answer.length).toBeGreaterThan(0);
22
+
23
+ // Should NOT have results when web_search is not used
24
+ expect(result.results).toBeUndefined();
25
+
26
+ expect(result.agent).toBe('express');
27
+ },
28
+ { retry: 2 },
29
+ );
30
+
31
+ test(
32
+ 'returns answer and search results (WITH web_search tools)',
33
+ async () => {
34
+ const result = await callExpressAgent({
35
+ agentInput: {
36
+ input: 'Latest developments in quantum computing',
37
+ tools: [{ type: 'web_search' }],
38
+ },
39
+ getUserAgent,
40
+ });
41
+
42
+ // Verify MCP response has both answer and results
43
+ expect(result).toHaveProperty('answer');
44
+ expect(typeof result.answer).toBe('string');
45
+ expect(result.answer.length).toBeGreaterThan(0);
46
+
47
+ expect(result).toHaveProperty('results');
48
+ expect(result.results).toHaveProperty('web');
49
+ expect(Array.isArray(result.results?.web)).toBe(true);
50
+ expect(result.results?.web.length).toBeGreaterThan(0);
51
+
52
+ // Verify each search result has required fields
53
+ const firstResult = result.results?.web[0];
54
+ expect(firstResult).toHaveProperty('url');
55
+ expect(firstResult).toHaveProperty('title');
56
+ expect(firstResult).toHaveProperty('snippet');
57
+ expect(typeof firstResult?.url).toBe('string');
58
+ expect(typeof firstResult?.title).toBe('string');
59
+ expect(typeof firstResult?.snippet).toBe('string');
60
+ expect(firstResult?.url.length).toBeGreaterThan(0);
61
+ expect(firstResult?.title.length).toBeGreaterThan(0);
62
+
63
+ expect(result.agent).toBe('express');
64
+ },
65
+ { timeout: 30_000, retry: 2 },
66
+ );
67
+
68
+ test(
69
+ 'works without optional parameters',
70
+ async () => {
71
+ const result = await callExpressAgent({
72
+ agentInput: { input: 'What is the capital of France?' },
73
+ getUserAgent,
74
+ // No progressToken or sendProgress provided
75
+ });
76
+
77
+ // Should work normally without progress tracking
78
+ expect(result).toHaveProperty('answer');
79
+ expect(result.answer.length).toBeGreaterThan(0);
80
+ expect(result.agent).toBe('express');
81
+ },
82
+ { retry: 2 },
83
+ );
72
84
  });
73
85
 
74
86
  describe('formatExpressAgentResponse', () => {
package/src/main.ts ADDED
@@ -0,0 +1,9 @@
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/api-constants.ts';
8
+ export * from './shared/check-response-for-errors.ts';
9
+ export * from './shared/format-search-results-text.ts';
@@ -1,3 +1,4 @@
1
+ import { SEARCH_API_URL } from '../shared/api-constants.ts';
1
2
  import { checkResponseForErrors } from '../shared/check-response-for-errors.ts';
2
3
  import { formatSearchResultsText } from '../shared/format-search-results-text.ts';
3
4
  import { type NewsResult, type SearchQuery, type SearchResponse, SearchResponseSchema } from './search.schemas.ts';
@@ -11,7 +12,7 @@ export const fetchSearchResults = async ({
11
12
  YDC_API_KEY?: string;
12
13
  getUserAgent: () => string;
13
14
  }) => {
14
- const url = new URL('https://ydc-index.io/v1/search');
15
+ const url = new URL(SEARCH_API_URL);
15
16
 
16
17
  const searchParams = new URLSearchParams();
17
18
 
@@ -5,113 +5,135 @@ import { fetchSearchResults, formatSearchResults } from '../search.utils.ts';
5
5
  const getUserAgent = () => 'MCP/test (You.com; test-client)';
6
6
 
7
7
  describe('fetchSearchResults', () => {
8
- test('returns valid response structure for basic query', async () => {
9
- const result = await fetchSearchResults({
10
- searchQuery: { query: 'latest stock news' },
11
- getUserAgent,
12
- });
8
+ test(
9
+ 'returns valid response structure for basic query',
10
+ async () => {
11
+ const result = await fetchSearchResults({
12
+ searchQuery: { query: 'latest stock news' },
13
+ getUserAgent,
14
+ });
13
15
 
14
- expect(result).toHaveProperty('results');
15
- expect(result).toHaveProperty('metadata');
16
- expect(result.results).toHaveProperty('web');
17
- expect(result.results).toHaveProperty('news');
18
- expect(Array.isArray(result.results.web)).toBe(true);
19
- expect(Array.isArray(result.results.news)).toBe(true);
16
+ expect(result).toHaveProperty('results');
17
+ expect(result).toHaveProperty('metadata');
18
+ expect(result.results).toHaveProperty('web');
19
+ expect(result.results).toHaveProperty('news');
20
+ expect(Array.isArray(result.results.web)).toBe(true);
21
+ expect(Array.isArray(result.results.news)).toBe(true);
20
22
 
21
- // Assert required metadata fields
22
- expect(typeof result.metadata?.query).toBe('string');
23
+ // Assert required metadata fields
24
+ expect(typeof result.metadata?.query).toBe('string');
23
25
 
24
- // Optional fields: only assert type if present
25
- if (result.metadata?.search_uuid !== undefined) {
26
- expect(typeof result.metadata.search_uuid).toBe('string');
27
- }
28
- });
26
+ // search_uuid is optional but should be string if present
27
+ expect(result.metadata?.search_uuid).toBeDefined();
28
+ expect(typeof result.metadata?.search_uuid).toBe('string');
29
+ },
30
+ { retry: 2 },
31
+ );
29
32
 
30
- test('handles search with filters', async () => {
31
- const result = await fetchSearchResults({
32
- searchQuery: {
33
- query: 'javascript tutorial',
34
- count: 3,
35
- freshness: 'week',
36
- country: 'US',
37
- },
38
- getUserAgent,
39
- });
33
+ test(
34
+ 'handles search with filters',
35
+ async () => {
36
+ const result = await fetchSearchResults({
37
+ searchQuery: {
38
+ query: 'javascript tutorial',
39
+ count: 3,
40
+ freshness: 'week',
41
+ country: 'US',
42
+ },
43
+ getUserAgent,
44
+ });
40
45
 
41
- expect(result.results.web?.length).toBeLessThanOrEqual(3);
42
- expect(result.metadata?.query).toContain('javascript tutorial');
43
- });
46
+ expect(result.results.web?.length).toBeLessThanOrEqual(3);
47
+ expect(result.metadata?.query).toContain('javascript tutorial');
48
+ },
49
+ { retry: 2 },
50
+ );
44
51
 
45
- test('validates response schema', async () => {
46
- const result = await fetchSearchResults({
47
- searchQuery: { query: 'latest technology news' },
48
- getUserAgent,
49
- });
52
+ test(
53
+ 'validates response schema',
54
+ async () => {
55
+ const result = await fetchSearchResults({
56
+ searchQuery: { query: 'latest technology news' },
57
+ getUserAgent,
58
+ });
50
59
 
51
- // Test that web results have required properties
52
- // biome-ignore lint/style/noNonNullAssertion: Test
53
- const webResult = result.results.web![0];
60
+ // Test that web results have required properties
61
+ // biome-ignore lint/style/noNonNullAssertion: Test
62
+ const webResult = result.results.web![0];
54
63
 
55
- expect(webResult).toHaveProperty('url');
56
- expect(webResult).toHaveProperty('title');
57
- expect(webResult).toHaveProperty('description');
58
- expect(webResult).toHaveProperty('snippets');
59
- expect(Array.isArray(webResult?.snippets)).toBe(true);
64
+ expect(webResult).toHaveProperty('url');
65
+ expect(webResult).toHaveProperty('title');
66
+ expect(webResult).toHaveProperty('description');
67
+ expect(webResult).toHaveProperty('snippets');
68
+ expect(Array.isArray(webResult?.snippets)).toBe(true);
60
69
 
61
- // Test that news results have required properties
62
- // biome-ignore lint/style/noNonNullAssertion: Test
63
- const newsResult = result.results.news![0];
64
- expect(newsResult).toHaveProperty('url');
65
- expect(newsResult).toHaveProperty('title');
66
- expect(newsResult).toHaveProperty('description');
67
- expect(newsResult).toHaveProperty('page_age');
68
- });
70
+ // Test that news results have required properties
71
+ // biome-ignore lint/style/noNonNullAssertion: Test
72
+ const newsResult = result.results.news![0];
73
+ expect(newsResult).toHaveProperty('url');
74
+ expect(newsResult).toHaveProperty('title');
75
+ expect(newsResult).toHaveProperty('description');
76
+ expect(newsResult).toHaveProperty('page_age');
77
+ },
78
+ { retry: 2 },
79
+ );
69
80
 
70
- test('handles livecrawl parameters', async () => {
71
- const result = await fetchSearchResults({
72
- searchQuery: {
73
- query: 'python tutorial',
74
- count: 2,
75
- livecrawl: 'web',
76
- livecrawl_formats: 'markdown',
77
- },
78
- getUserAgent,
79
- });
81
+ test(
82
+ 'handles livecrawl parameters',
83
+ async () => {
84
+ const result = await fetchSearchResults({
85
+ searchQuery: {
86
+ query: 'python tutorial',
87
+ count: 2,
88
+ livecrawl: 'web',
89
+ livecrawl_formats: 'markdown',
90
+ },
91
+ getUserAgent,
92
+ });
80
93
 
81
- expect(result.results.web?.length).toBeLessThanOrEqual(2);
82
- // If livecrawl worked, results should have contents field
83
- if (result.results.web?.[0]?.contents) {
84
- expect(result.results.web[0].contents).toHaveProperty('markdown');
85
- expect(typeof result.results.web[0].contents.markdown).toBe('string');
86
- }
87
- });
94
+ expect(result.results.web?.length).toBeLessThanOrEqual(2);
95
+ // Livecrawl should return contents field (fails naturally if not present)
96
+ expect(result.results.web?.[0]).toHaveProperty('contents');
97
+ expect(result.results.web?.[0]?.contents).toHaveProperty('markdown');
98
+ expect(typeof result.results.web?.[0]?.contents?.markdown).toBe('string');
99
+ },
100
+ { retry: 2 },
101
+ );
88
102
 
89
- test('handles freshness date ranges', async () => {
90
- const result = await fetchSearchResults({
91
- searchQuery: {
92
- query: 'AI news',
93
- freshness: '2024-01-01to2024-12-31',
94
- count: 3,
95
- },
96
- getUserAgent,
97
- });
103
+ test(
104
+ 'handles freshness date ranges',
105
+ async () => {
106
+ const result = await fetchSearchResults({
107
+ searchQuery: {
108
+ query: 'AI news',
109
+ freshness: '2024-01-01to2024-12-31',
110
+ count: 3,
111
+ },
112
+ getUserAgent,
113
+ });
98
114
 
99
- expect(result).toHaveProperty('results');
100
- expect(result.metadata?.query).toContain('AI news');
101
- });
115
+ expect(result).toHaveProperty('results');
116
+ expect(result.metadata?.query).toContain('AI news');
117
+ },
118
+ { retry: 2 },
119
+ );
102
120
 
103
- test('handles count greater than 20', async () => {
104
- const result = await fetchSearchResults({
105
- searchQuery: {
106
- query: 'machine learning',
107
- count: 50,
108
- },
109
- getUserAgent,
110
- });
121
+ test(
122
+ 'handles count greater than 20',
123
+ async () => {
124
+ const result = await fetchSearchResults({
125
+ searchQuery: {
126
+ query: 'machine learning',
127
+ count: 50,
128
+ },
129
+ getUserAgent,
130
+ });
111
131
 
112
- expect(result.results.web?.length).toBeGreaterThan(0);
113
- expect(result.results.web?.length).toBeLessThanOrEqual(50);
114
- });
132
+ expect(result.results.web?.length).toBeGreaterThan(0);
133
+ expect(result.results.web?.length).toBeLessThanOrEqual(50);
134
+ },
135
+ { retry: 2 },
136
+ );
115
137
  });
116
138
 
117
139
  describe('formatSearchResults', () => {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * You.com API endpoints
3
+ *
4
+ * These constants define the base URLs for You.com's APIs.
5
+ * Exported for use in tests and external packages.
6
+ */
7
+
8
+ export const SEARCH_API_URL = 'https://ydc-index.io/v1/search';
9
+ export const EXPRESS_API_URL = 'https://api.you.com/v1/agents/runs';
10
+ export const CONTENTS_API_URL = 'https://ydc-index.io/v1/contents';
@@ -283,36 +283,40 @@ describe('HTTP MCP Endpoint Basic Functionality', () => {
283
283
  expect(text).toContain('42');
284
284
  });
285
285
 
286
- test('mcp server handles search tool request for latest tech news', async () => {
287
- const response = await fetch(`${baseUrl}/mcp`, {
288
- method: 'POST',
289
- headers: {
290
- 'Content-Type': 'application/json',
291
- Accept: 'application/json, text/event-stream',
292
- Authorization: `Bearer ${testApiKey}`,
293
- },
294
- body: JSON.stringify({
295
- jsonrpc: '2.0',
296
- method: 'tools/call',
297
- id: 100,
298
- params: {
299
- name: 'you-search',
300
- arguments: {
301
- query: 'latest tech news',
302
- count: 3,
303
- },
286
+ test(
287
+ 'mcp server handles search tool request for latest tech news',
288
+ async () => {
289
+ const response = await fetch(`${baseUrl}/mcp`, {
290
+ method: 'POST',
291
+ headers: {
292
+ 'Content-Type': 'application/json',
293
+ Accept: 'application/json, text/event-stream',
294
+ Authorization: `Bearer ${testApiKey}`,
304
295
  },
305
- }),
306
- });
307
-
308
- expect(response.status).toBe(200);
309
- expect(response.headers.get('content-type')).toContain('text/event-stream');
310
-
311
- const text = await response.text();
312
- expect(text).toContain('data:');
313
- expect(text).toContain('jsonrpc');
314
- expect(text).toContain('result');
315
- expect(text).toContain('latest tech news');
316
- expect(text).toContain('Search Results for');
317
- });
296
+ body: JSON.stringify({
297
+ jsonrpc: '2.0',
298
+ method: 'tools/call',
299
+ id: 100,
300
+ params: {
301
+ name: 'you-search',
302
+ arguments: {
303
+ query: 'latest tech news',
304
+ count: 3,
305
+ },
306
+ },
307
+ }),
308
+ });
309
+
310
+ expect(response.status).toBe(200);
311
+ expect(response.headers.get('content-type')).toContain('text/event-stream');
312
+
313
+ const text = await response.text();
314
+ expect(text).toContain('data:');
315
+ expect(text).toContain('jsonrpc');
316
+ expect(text).toContain('result');
317
+ expect(text).toContain('latest tech news');
318
+ expect(text).toContain('Search Results for');
319
+ },
320
+ { retry: 2 },
321
+ );
318
322
  });