@youdotcom-oss/mcp 3.2.2 → 3.3.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.
@@ -14,30 +14,16 @@ describe('formatContentsResponse', () => {
14
14
 
15
15
  const result = formatContentsResponse(mockResponse, ['markdown'])
16
16
 
17
- expect(result).toHaveProperty('content')
18
- expect(result).toHaveProperty('structuredContent')
19
- expect(Array.isArray(result.content)).toBe(true)
20
- expect(result.content[0]).toHaveProperty('type', 'text')
21
- expect(result.content[0]).toHaveProperty('text')
17
+ expect(Array.isArray(result)).toBe(true)
18
+ expect(result[0]).toHaveProperty('type', 'text')
19
+ expect(result[0]).toHaveProperty('text')
22
20
 
23
- const text = result.content[0]?.text
21
+ const text = result[0]?.text
24
22
  expect(text).toContain('Example Page')
25
23
  expect(text).toContain('https://example.com')
26
24
  expect(text).toContain('Formats: markdown')
27
25
  expect(text).toContain('# Hello')
28
26
  expect(text).toContain('This is a test page with some content.')
29
-
30
- expect(result.structuredContent).toHaveProperty('count', 1)
31
- expect(result.structuredContent).toHaveProperty('formats')
32
- expect(result.structuredContent.formats).toEqual(['markdown'])
33
- expect(result.structuredContent.items).toHaveLength(1)
34
-
35
- const item = result.structuredContent.items[0]
36
- expect(item).toBeDefined()
37
-
38
- expect(item).toHaveProperty('url', 'https://example.com')
39
- expect(item).toHaveProperty('title', 'Example Page')
40
- expect(item).toHaveProperty('markdown', '# Hello\n\nThis is a test page with some content.')
41
27
  })
42
28
 
43
29
  test('formats multiple items correctly', () => {
@@ -56,10 +42,8 @@ describe('formatContentsResponse', () => {
56
42
 
57
43
  const result = formatContentsResponse(mockResponse, ['markdown'])
58
44
 
59
- expect(result.structuredContent.count).toBe(2)
60
- expect(result.structuredContent.items).toHaveLength(2)
61
-
62
- const text = result.content[0]?.text
45
+ const text = result[0]?.text
46
+ expect(text).toContain('Successfully extracted content from 2 URL(s)')
63
47
  expect(text).toContain('Page 1')
64
48
  expect(text).toContain('Page 2')
65
49
  expect(text).toContain('https://example1.com')
@@ -77,8 +61,7 @@ describe('formatContentsResponse', () => {
77
61
 
78
62
  const result = formatContentsResponse(mockResponse, ['html'])
79
63
 
80
- expect(result.structuredContent.formats).toEqual(['html'])
81
- const text = result.content[0]?.text
64
+ const text = result[0]?.text
82
65
  expect(text).toContain('Formats: html')
83
66
  expect(text).toContain('<html>')
84
67
  })
@@ -95,13 +78,8 @@ describe('formatContentsResponse', () => {
95
78
 
96
79
  const result = formatContentsResponse(mockResponse, ['markdown'])
97
80
 
98
- const text = result.content[0]?.text
99
- // Full content should be included (not truncated)
81
+ const text = result[0]?.text
100
82
  expect(text).toContain(longContent)
101
-
102
- // Structured content should have full markdown content
103
- const item = result.structuredContent.items[0]
104
- expect(item?.markdown).toBe(longContent)
105
83
  })
106
84
 
107
85
  test('handles empty content gracefully', () => {
@@ -115,9 +93,7 @@ describe('formatContentsResponse', () => {
115
93
 
116
94
  const result = formatContentsResponse(mockResponse, ['markdown'])
117
95
 
118
- expect(result.structuredContent.items[0]?.markdown).toBe('')
119
- const text = result.content[0]?.text
96
+ const text = result[0]?.text
120
97
  expect(text).toContain('Empty Page')
121
- // Empty content should still be handled gracefully
122
98
  })
123
99
  })
package/src/main.ts CHANGED
@@ -1,8 +1,5 @@
1
- export type { ContentsStructuredContent } from './contents/contents.schemas.ts'
2
1
  export { registerContentsTool } from './contents/register-contents-tool.ts'
3
2
  export { getMcpServer } from './get-mcp-server.ts'
4
3
  export { registerResearchTool } from './research/register-research-tool.ts'
5
- export type { ResearchStructuredContent } from './research/research.schemas.ts'
6
4
  export { registerSearchTool } from './search/register-search-tool.ts'
7
- export type { SearchStructuredContent } from './search/search.schemas.ts'
8
5
  export { useGetClientVersion } from './shared/use-client-version.ts'
@@ -1,7 +1,6 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
- import { callResearch, generateErrorReportLink, ResearchQuerySchema } from '@youdotcom-oss/api'
2
+ import { callResearch, generateErrorReportLink, ResearchQuerySchema, ResearchResponseSchema } from '@youdotcom-oss/api'
3
3
  import { getLogger } from '../shared/get-logger.ts'
4
- import { ResearchStructuredContentSchema } from './research.schemas.ts'
5
4
  import { formatResearchResults } from './research.utils.ts'
6
5
 
7
6
  export const registerResearchTool = ({
@@ -19,8 +18,8 @@ export const registerResearchTool = ({
19
18
  title: 'Research',
20
19
  description:
21
20
  'Research a topic with comprehensive answers and cited sources. Configurable effort levels (lite, standard, deep, exhaustive).',
22
- inputSchema: ResearchQuerySchema.shape,
23
- outputSchema: ResearchStructuredContentSchema.shape,
21
+ inputSchema: ResearchQuerySchema,
22
+ outputSchema: ResearchResponseSchema,
24
23
  },
25
24
  async (researchQuery, { sendNotification }) => {
26
25
  const logger = getLogger(sendNotification)
@@ -38,8 +37,8 @@ export const registerResearchTool = ({
38
37
  data: `Research for "${researchQuery.input.substring(0, 100)}" complete: ${sourceCount} source(s)`,
39
38
  })
40
39
 
41
- const { content, structuredContent } = formatResearchResults(response)
42
- return { content, structuredContent }
40
+ const content = formatResearchResults(response)
41
+ return { content, structuredContent: response }
43
42
  } catch (err: unknown) {
44
43
  const errorMessage = err instanceof Error ? err.message : String(err)
45
44
  const reportLink = generateErrorReportLink({
@@ -1,30 +1,12 @@
1
1
  import type { ResearchResponse } from '@youdotcom-oss/api'
2
2
  import { formatResearchResponse } from '@youdotcom-oss/api'
3
- import type { ResearchStructuredContent } from './research.schemas.ts'
4
3
 
5
- export const formatResearchResults = (
6
- response: ResearchResponse,
7
- ): {
8
- content: Array<{ type: 'text'; text: string }>
9
- structuredContent: ResearchStructuredContent
10
- } => {
4
+ export const formatResearchResults = (response: ResearchResponse): Array<{ type: 'text'; text: string }> => {
11
5
  const text = formatResearchResponse(response)
12
-
13
- return {
14
- content: [
15
- {
16
- type: 'text',
17
- text,
18
- },
19
- ],
20
- structuredContent: {
21
- contentType: response.output.content_type,
22
- sourceCount: response.output.sources.length,
23
- sources: response.output.sources.map((source) => ({
24
- url: source.url,
25
- title: source.title,
26
- snippetCount: source.snippets?.length ?? 0,
27
- })),
6
+ return [
7
+ {
8
+ type: 'text',
9
+ text,
28
10
  },
29
- }
11
+ ]
30
12
  }
@@ -25,75 +25,14 @@ describe('formatResearchResults', () => {
25
25
 
26
26
  const result = formatResearchResults(mockResponse)
27
27
 
28
- expect(result).toHaveProperty('content')
29
- expect(result).toHaveProperty('structuredContent')
30
- expect(Array.isArray(result.content)).toBe(true)
31
- expect(result.content[0]).toHaveProperty('type', 'text')
32
- expect(result.content[0]).toHaveProperty('text')
28
+ expect(Array.isArray(result)).toBe(true)
29
+ expect(result[0]).toHaveProperty('type', 'text')
30
+ expect(result[0]).toHaveProperty('text')
33
31
 
34
- const text = result.content[0]?.text
32
+ const text = result[0]?.text
35
33
  expect(text).toContain('Research Answer')
36
34
  expect(text).toContain('Source One')
37
35
  expect(text).toContain('https://example.com/source1')
38
-
39
- expect(result.structuredContent.contentType).toBe('text')
40
- expect(result.structuredContent.sourceCount).toBe(2)
41
- expect(result.structuredContent.sources).toHaveLength(2)
42
- expect(result.structuredContent.sources[0]).toMatchObject({
43
- url: 'https://example.com/source1',
44
- title: 'Source One',
45
- snippetCount: 2,
46
- })
47
- expect(result.structuredContent.sources[1]).toMatchObject({
48
- url: 'https://example.com/source2',
49
- title: 'Source Two',
50
- snippetCount: 1,
51
- })
52
- })
53
-
54
- test('handles source with undefined title', () => {
55
- const mockResponse: ResearchResponse = {
56
- output: {
57
- content: 'Answer text',
58
- content_type: 'text',
59
- sources: [
60
- {
61
- url: 'https://example.com/no-title',
62
- snippets: ['A snippet'],
63
- },
64
- ],
65
- },
66
- }
67
-
68
- const result = formatResearchResults(mockResponse)
69
-
70
- expect(result.structuredContent.sourceCount).toBe(1)
71
- expect(result.structuredContent.sources[0]).toMatchObject({
72
- url: 'https://example.com/no-title',
73
- title: undefined,
74
- snippetCount: 1,
75
- })
76
- })
77
-
78
- test('handles source with empty snippets array', () => {
79
- const mockResponse: ResearchResponse = {
80
- output: {
81
- content: 'Answer with no-snippet source',
82
- content_type: 'text',
83
- sources: [
84
- {
85
- url: 'https://example.com/empty-snippets',
86
- title: 'Empty Snippets Source',
87
- snippets: [],
88
- },
89
- ],
90
- },
91
- }
92
-
93
- const result = formatResearchResults(mockResponse)
94
-
95
- expect(result.structuredContent.sourceCount).toBe(1)
96
- expect(result.structuredContent.sources[0]?.snippetCount).toBe(0)
97
36
  })
98
37
 
99
38
  test('handles response with zero sources', () => {
@@ -107,8 +46,6 @@ describe('formatResearchResults', () => {
107
46
 
108
47
  const result = formatResearchResults(mockResponse)
109
48
 
110
- expect(result.structuredContent.sourceCount).toBe(0)
111
- expect(result.structuredContent.sources).toHaveLength(0)
112
- expect(result.content[0]?.text).toContain('An answer with no cited sources.')
49
+ expect(result[0]?.text).toContain('An answer with no cited sources.')
113
50
  })
114
51
  })
@@ -1,7 +1,11 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
- import { fetchSearchResults, generateErrorReportLink, SearchQuerySchema } from '@youdotcom-oss/api'
2
+ import {
3
+ fetchSearchResults,
4
+ generateErrorReportLink,
5
+ SearchQuerySchema,
6
+ SearchResponseSchema,
7
+ } from '@youdotcom-oss/api'
3
8
  import { getLogger } from '../shared/get-logger.ts'
4
- import { SearchStructuredContentSchema } from './search.schemas.ts'
5
9
  import { formatSearchResults } from './search.utils.ts'
6
10
 
7
11
  export const registerSearchTool = ({
@@ -17,9 +21,10 @@ export const registerSearchTool = ({
17
21
  'you-search',
18
22
  {
19
23
  title: 'Web Search',
20
- description: 'Web and news search via You.com',
21
- inputSchema: SearchQuerySchema.shape,
22
- outputSchema: SearchStructuredContentSchema.shape,
24
+ description:
25
+ 'Web and news search via You.com. Supports domain filtering, language selection, livecrawl for full page content, and date freshness controls.',
26
+ inputSchema: SearchQuerySchema,
27
+ outputSchema: SearchResponseSchema,
23
28
  },
24
29
  async (searchQuery, { sendNotification }) => {
25
30
  const logger = getLogger(sendNotification)
@@ -41,13 +46,7 @@ export const registerSearchTool = ({
41
46
 
42
47
  return {
43
48
  content: [{ type: 'text' as const, text: 'No results found.' }],
44
- structuredContent: {
45
- resultCounts: {
46
- web: 0,
47
- news: 0,
48
- total: 0,
49
- },
50
- },
49
+ structuredContent: response,
51
50
  }
52
51
  }
53
52
 
@@ -56,8 +55,8 @@ export const registerSearchTool = ({
56
55
  data: `Search successful for query: "${searchQuery.query}" - ${webCount} web results, ${newsCount} news results (${webCount + newsCount} total)`,
57
56
  })
58
57
 
59
- const { content, structuredContent } = formatSearchResults(response)
60
- return { content, structuredContent }
58
+ const content = formatSearchResults(response)
59
+ return { content, structuredContent: response }
61
60
  } catch (err: unknown) {
62
61
  const errorMessage = err instanceof Error ? err.message : String(err)
63
62
  const reportLink = generateErrorReportLink({
@@ -1,23 +1,16 @@
1
- import type { NewsResult, SearchResponse } from '@youdotcom-oss/api'
1
+ import type { SearchResponse } from '@youdotcom-oss/api'
2
2
  import { formatSearchResultsText } from '../shared/format-search-results-text.ts'
3
3
 
4
4
  export const formatSearchResults = (response: SearchResponse) => {
5
5
  let formattedResults = ''
6
6
 
7
- // Format web results using shared utility
8
7
  if (response.results.web?.length) {
9
8
  const webResults = formatSearchResultsText(response.results.web)
10
9
  formattedResults += `WEB RESULTS:\n\n${webResults}`
11
10
  }
12
11
 
13
- // Format news results
14
12
  if (response.results.news?.length) {
15
- const newsResults = response.results.news
16
- .map(
17
- (article: NewsResult) =>
18
- `Title: ${article.title}\nURL: ${article.url}\nDescription: ${article.description}\nPublished: ${article.page_age}`,
19
- )
20
- .join('\n\n---\n\n')
13
+ const newsResults = formatSearchResultsText(response.results.news)
21
14
 
22
15
  if (formattedResults) {
23
16
  formattedResults += `\n\n${'='.repeat(50)}\n\n`
@@ -25,46 +18,10 @@ export const formatSearchResults = (response: SearchResponse) => {
25
18
  formattedResults += `NEWS RESULTS:\n\n${newsResults}`
26
19
  }
27
20
 
28
- // Extract fields for structuredContent
29
- const structuredResults: {
30
- web?: Array<{ url: string; title: string; page_age?: string }>
31
- news?: Array<{ url: string; title: string; page_age: string }>
32
- } = {}
33
-
34
- if (response.results.web?.length) {
35
- structuredResults.web = response.results.web.map((result) => {
36
- const item: { url: string; title: string; page_age?: string } = {
37
- url: result.url,
38
- title: result.title,
39
- }
40
- if (result.page_age) item.page_age = result.page_age
41
- return item
42
- })
43
- }
44
-
45
- if (response.results.news?.length) {
46
- structuredResults.news = response.results.news.map((article) => ({
47
- url: article.url,
48
- title: article.title,
49
- page_age: article.page_age,
50
- }))
51
- }
52
-
53
- return {
54
- content: [
55
- {
56
- type: 'text' as const,
57
- text: `Search Results for "${response.metadata.query}":\n\n${formattedResults}`,
58
- },
59
- ],
60
- structuredContent: {
61
- resultCounts: {
62
- web: response.results.web?.length || 0,
63
- news: response.results.news?.length || 0,
64
- total: (response.results.web?.length || 0) + (response.results.news?.length || 0),
65
- },
66
- results: Object.keys(structuredResults).length > 0 ? structuredResults : undefined,
21
+ return [
22
+ {
23
+ type: 'text' as const,
24
+ text: `Search Results for "${response.metadata.query}":\n\n${formattedResults}`,
67
25
  },
68
- fullResponse: response,
69
- }
26
+ ]
70
27
  }
@@ -0,0 +1,119 @@
1
+ import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test'
2
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
3
+ import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
4
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
5
+ import type { SearchResponse } from '@youdotcom-oss/api'
6
+ import * as api from '@youdotcom-oss/api'
7
+ import { registerSearchTool } from '../register-search-tool.ts'
8
+
9
+ const emptyResponse: SearchResponse = {
10
+ results: { web: [], news: [] },
11
+ metadata: { search_uuid: 'test', query: 'test', latency: 0 },
12
+ }
13
+
14
+ const oneResultResponse: SearchResponse = {
15
+ results: {
16
+ web: [
17
+ {
18
+ url: 'https://example.com',
19
+ title: 'Example',
20
+ description: 'A test result',
21
+ snippets: ['snippet'],
22
+ page_age: '2025-01-01T00:00:00',
23
+ authors: [],
24
+ },
25
+ ],
26
+ news: [],
27
+ },
28
+ metadata: { search_uuid: 'test', query: 'test', latency: 0.1 },
29
+ }
30
+
31
+ let mockFetchResponse: SearchResponse | Error = emptyResponse
32
+ let fetchSearchResultsSpy: ReturnType<typeof spyOn<typeof api, 'fetchSearchResults'>> | undefined
33
+ let generateErrorReportLinkSpy: ReturnType<typeof spyOn<typeof api, 'generateErrorReportLink'>> | undefined
34
+
35
+ type Cleanup = () => Promise<void>
36
+
37
+ const setupMcpClient = async (): Promise<{ client: Client; cleanup: Cleanup }> => {
38
+ const server = new McpServer({ name: 'test', version: '0.0.0' }, { capabilities: { logging: {}, tools: {} } })
39
+ registerSearchTool({
40
+ mcp: server,
41
+ YDC_API_KEY: 'test-key',
42
+ getUserAgent: () => 'test-agent',
43
+ })
44
+
45
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
46
+ await server.connect(serverTransport)
47
+
48
+ const client = new Client({ name: 'test-client', version: '0.0.0' })
49
+ await client.connect(clientTransport)
50
+
51
+ const cleanup = async () => {
52
+ await client.close()
53
+ await server.close()
54
+ }
55
+
56
+ return { client, cleanup }
57
+ }
58
+
59
+ describe('registerSearchTool', () => {
60
+ let cleanup: Cleanup | undefined
61
+
62
+ beforeEach(() => {
63
+ mockFetchResponse = emptyResponse
64
+ fetchSearchResultsSpy = spyOn(api, 'fetchSearchResults').mockImplementation(async () => {
65
+ if (mockFetchResponse instanceof Error) throw mockFetchResponse
66
+ return mockFetchResponse
67
+ })
68
+ generateErrorReportLinkSpy = spyOn(api, 'generateErrorReportLink').mockImplementation(
69
+ () => 'https://example.com/report',
70
+ )
71
+ })
72
+
73
+ afterEach(async () => {
74
+ if (cleanup) {
75
+ await cleanup()
76
+ cleanup = undefined
77
+ }
78
+ fetchSearchResultsSpy?.mockRestore()
79
+ fetchSearchResultsSpy = undefined
80
+ generateErrorReportLinkSpy?.mockRestore()
81
+ generateErrorReportLinkSpy = undefined
82
+ })
83
+
84
+ test('handles empty search results gracefully', async () => {
85
+ const result = await setupMcpClient()
86
+ cleanup = result.cleanup
87
+
88
+ const toolResult = await result.client.callTool({ name: 'you-search', arguments: { query: 'nonexistent' } })
89
+
90
+ expect(toolResult.content).toEqual([{ type: 'text', text: 'No results found.' }])
91
+ expect(toolResult.structuredContent).toEqual(emptyResponse)
92
+ })
93
+
94
+ test('returns formatted results for successful search', async () => {
95
+ mockFetchResponse = oneResultResponse
96
+ const result = await setupMcpClient()
97
+ cleanup = result.cleanup
98
+
99
+ const toolResult = await result.client.callTool({ name: 'you-search', arguments: { query: 'example' } })
100
+
101
+ const text = (toolResult.content as Array<{ type: string; text: string }>)[0]?.text
102
+ expect(text).toContain('Example')
103
+ expect(text).toContain('https://example.com')
104
+
105
+ expect(toolResult.structuredContent).toEqual(oneResultResponse)
106
+ })
107
+
108
+ test('returns error when API call fails', async () => {
109
+ mockFetchResponse = new Error('API rate limit exceeded')
110
+ const result = await setupMcpClient()
111
+ cleanup = result.cleanup
112
+
113
+ const toolResult = await result.client.callTool({ name: 'you-search', arguments: { query: 'test' } })
114
+
115
+ expect(toolResult.isError).toBe(true)
116
+ const text = (toolResult.content as Array<{ type: string; text: string }>)[0]?.text
117
+ expect(text).toContain('API rate limit exceeded')
118
+ })
119
+ })