@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.
- package/bin/stdio.js +70 -165
- package/package.json +4 -3
- package/server.json +3 -3
- package/src/contents/contents.utils.ts +8 -34
- package/src/contents/register-contents-tool.ts +15 -13
- package/src/contents/tests/contents.utils.spec.ts +9 -33
- package/src/main.ts +0 -3
- package/src/research/register-research-tool.ts +5 -6
- package/src/research/research.utils.ts +6 -24
- package/src/research/tests/research.utils.spec.ts +5 -68
- package/src/search/register-search-tool.ts +13 -14
- package/src/search/search.utils.ts +7 -50
- package/src/search/tests/register-search-tool.spec.ts +119 -0
- package/src/search/tests/search.utils.spec.ts +104 -68
- package/src/shared/format-search-results-text.ts +20 -0
- package/src/shared/tests/format-search-results-text.spec.ts +95 -0
- package/src/contents/contents.schemas.ts +0 -30
- package/src/research/research.schemas.ts +0 -19
- package/src/search/search.schemas.ts +0 -38
|
@@ -14,30 +14,16 @@ describe('formatContentsResponse', () => {
|
|
|
14
14
|
|
|
15
15
|
const result = formatContentsResponse(mockResponse, ['markdown'])
|
|
16
16
|
|
|
17
|
-
expect(result).
|
|
18
|
-
expect(result).toHaveProperty('
|
|
19
|
-
expect(
|
|
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
|
|
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
|
-
|
|
60
|
-
expect(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
23
|
-
outputSchema:
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
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).
|
|
29
|
-
expect(result).toHaveProperty('
|
|
30
|
-
expect(
|
|
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
|
|
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
|
|
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 {
|
|
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:
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
+
})
|