@youdotcom-oss/mcp 3.2.3 → 3.4.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/README.md +49 -240
- package/bin/stdio.js +4877 -18044
- package/package.json +7 -12
- package/server.json +11 -4
- package/src/contents/contents.schemas.ts +0 -30
- package/src/contents/contents.utils.ts +0 -85
- package/src/contents/register-contents-tool.ts +0 -93
- package/src/contents/tests/contents.utils.spec.ts +0 -123
- package/src/get-mcp-server.ts +0 -17
- package/src/main.ts +0 -8
- package/src/research/register-research-tool.ts +0 -69
- package/src/research/research.schemas.ts +0 -19
- package/src/research/research.utils.ts +0 -30
- package/src/research/tests/research.utils.spec.ts +0 -128
- package/src/search/register-search-tool.ts +0 -88
- package/src/search/search.schemas.ts +0 -53
- package/src/search/search.utils.ts +0 -83
- package/src/search/tests/register-search-tool.spec.ts +0 -123
- package/src/search/tests/search.utils.spec.ts +0 -291
- package/src/shared/format-search-results-text.ts +0 -69
- package/src/shared/get-logger.ts +0 -19
- package/src/shared/tests/format-search-results-text.spec.ts +0 -95
- package/src/shared/tests/shared.utils.spec.ts +0 -160
- package/src/shared/use-client-version.ts +0 -21
- package/src/stdio-server.ts +0 -24
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import type { ResearchResponse } from '@youdotcom-oss/api'
|
|
3
|
-
import { formatResearchResults } from '../research.utils.ts'
|
|
4
|
-
|
|
5
|
-
describe('formatResearchResults', () => {
|
|
6
|
-
test('formats research response with sources correctly', () => {
|
|
7
|
-
const mockResponse: ResearchResponse = {
|
|
8
|
-
output: {
|
|
9
|
-
content: '# Research Answer\n\nThis is a comprehensive answer about the topic.',
|
|
10
|
-
content_type: 'text',
|
|
11
|
-
sources: [
|
|
12
|
-
{
|
|
13
|
-
url: 'https://example.com/source1',
|
|
14
|
-
title: 'Source One',
|
|
15
|
-
snippets: ['First snippet', 'Second snippet'],
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
url: 'https://example.com/source2',
|
|
19
|
-
title: 'Source Two',
|
|
20
|
-
snippets: ['Another snippet'],
|
|
21
|
-
},
|
|
22
|
-
],
|
|
23
|
-
},
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const result = formatResearchResults(mockResponse)
|
|
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')
|
|
33
|
-
|
|
34
|
-
const text = result.content[0]?.text
|
|
35
|
-
expect(text).toContain('Research Answer')
|
|
36
|
-
expect(text).toContain('Source One')
|
|
37
|
-
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
|
-
})
|
|
98
|
-
|
|
99
|
-
test('handles source with undefined snippets', () => {
|
|
100
|
-
const mockResponse: ResearchResponse = {
|
|
101
|
-
output: {
|
|
102
|
-
content: 'Answer',
|
|
103
|
-
content_type: 'text',
|
|
104
|
-
sources: [{ url: 'https://example.com/no-snippets', title: 'No Snippets' }],
|
|
105
|
-
},
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const result = formatResearchResults(mockResponse)
|
|
109
|
-
|
|
110
|
-
expect(result.structuredContent.sources[0]?.snippetCount).toBe(0)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
test('handles response with zero sources', () => {
|
|
114
|
-
const mockResponse: ResearchResponse = {
|
|
115
|
-
output: {
|
|
116
|
-
content: 'An answer with no cited sources.',
|
|
117
|
-
content_type: 'text',
|
|
118
|
-
sources: [],
|
|
119
|
-
},
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const result = formatResearchResults(mockResponse)
|
|
123
|
-
|
|
124
|
-
expect(result.structuredContent.sourceCount).toBe(0)
|
|
125
|
-
expect(result.structuredContent.sources).toHaveLength(0)
|
|
126
|
-
expect(result.content[0]?.text).toContain('An answer with no cited sources.')
|
|
127
|
-
})
|
|
128
|
-
})
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
-
import { fetchSearchResults, generateErrorReportLink, SearchQuerySchema } from '@youdotcom-oss/api'
|
|
3
|
-
import { getLogger } from '../shared/get-logger.ts'
|
|
4
|
-
import { SearchStructuredContentSchema } from './search.schemas.ts'
|
|
5
|
-
import { formatSearchResults } from './search.utils.ts'
|
|
6
|
-
|
|
7
|
-
export const registerSearchTool = ({
|
|
8
|
-
mcp,
|
|
9
|
-
YDC_API_KEY,
|
|
10
|
-
getUserAgent,
|
|
11
|
-
}: {
|
|
12
|
-
mcp: McpServer
|
|
13
|
-
YDC_API_KEY?: string
|
|
14
|
-
getUserAgent: () => string
|
|
15
|
-
}) => {
|
|
16
|
-
mcp.registerTool(
|
|
17
|
-
'you-search',
|
|
18
|
-
{
|
|
19
|
-
title: 'Web Search',
|
|
20
|
-
description:
|
|
21
|
-
'Web and news search via You.com. Supports domain filtering, language selection, livecrawl for full page content, and date freshness controls.',
|
|
22
|
-
inputSchema: SearchQuerySchema.shape,
|
|
23
|
-
outputSchema: SearchStructuredContentSchema.shape,
|
|
24
|
-
},
|
|
25
|
-
async (searchQuery, { sendNotification }) => {
|
|
26
|
-
const logger = getLogger(sendNotification)
|
|
27
|
-
try {
|
|
28
|
-
const response = await fetchSearchResults({
|
|
29
|
-
searchQuery,
|
|
30
|
-
YDC_API_KEY,
|
|
31
|
-
getUserAgent,
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const webCount = response.results.web?.length ?? 0
|
|
35
|
-
const newsCount = response.results.news?.length ?? 0
|
|
36
|
-
|
|
37
|
-
if (!webCount && !newsCount) {
|
|
38
|
-
await logger({
|
|
39
|
-
level: 'info',
|
|
40
|
-
data: `No results found for query: "${searchQuery.query}"`,
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
return {
|
|
44
|
-
content: [{ type: 'text' as const, text: 'No results found.' }],
|
|
45
|
-
structuredContent: {
|
|
46
|
-
resultCounts: {
|
|
47
|
-
web: 0,
|
|
48
|
-
news: 0,
|
|
49
|
-
total: 0,
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
await logger({
|
|
56
|
-
level: 'info',
|
|
57
|
-
data: `Search successful for query: "${searchQuery.query}" - ${webCount} web results, ${newsCount} news results (${webCount + newsCount} total)`,
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
const { content, structuredContent } = formatSearchResults(response)
|
|
61
|
-
return { content, structuredContent }
|
|
62
|
-
} catch (err: unknown) {
|
|
63
|
-
const errorMessage = err instanceof Error ? err.message : String(err)
|
|
64
|
-
const reportLink = generateErrorReportLink({
|
|
65
|
-
errorMessage,
|
|
66
|
-
tool: 'you-search',
|
|
67
|
-
clientInfo: getUserAgent(),
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
await logger({
|
|
71
|
-
level: 'error',
|
|
72
|
-
data: `Search API call failed: ${errorMessage}\n\nReport this issue: ${reportLink}`,
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
content: [
|
|
77
|
-
{
|
|
78
|
-
type: 'text' as const,
|
|
79
|
-
text: `Error: ${errorMessage}`,
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
structuredContent: undefined,
|
|
83
|
-
isError: true,
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
},
|
|
87
|
-
)
|
|
88
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import * as z from 'zod'
|
|
2
|
-
|
|
3
|
-
// Minimal schema for structuredContent (reduces payload duplication)
|
|
4
|
-
// Excludes metadata (query, search_uuid, latency) as these are not actionable by LLM
|
|
5
|
-
export const SearchStructuredContentSchema = z.object({
|
|
6
|
-
resultCounts: z.object({
|
|
7
|
-
web: z.number().describe('Web results'),
|
|
8
|
-
news: z.number().describe('News results'),
|
|
9
|
-
total: z.number().describe('Total results'),
|
|
10
|
-
}),
|
|
11
|
-
results: z
|
|
12
|
-
.object({
|
|
13
|
-
web: z
|
|
14
|
-
.array(
|
|
15
|
-
z.object({
|
|
16
|
-
url: z.string().describe('URL'),
|
|
17
|
-
title: z.string().describe('Title'),
|
|
18
|
-
page_age: z.string().optional().describe('Publication timestamp'),
|
|
19
|
-
snippets: z.array(z.string()).optional().describe('Content snippets'),
|
|
20
|
-
contents: z
|
|
21
|
-
.object({
|
|
22
|
-
html: z.string().optional().describe('Full HTML content'),
|
|
23
|
-
markdown: z.string().optional().describe('Full Markdown content'),
|
|
24
|
-
})
|
|
25
|
-
.optional()
|
|
26
|
-
.describe('Livecrawled page content'),
|
|
27
|
-
}),
|
|
28
|
-
)
|
|
29
|
-
.optional()
|
|
30
|
-
.describe('Web results'),
|
|
31
|
-
news: z
|
|
32
|
-
.array(
|
|
33
|
-
z.object({
|
|
34
|
-
url: z.string().describe('URL'),
|
|
35
|
-
title: z.string().describe('Title'),
|
|
36
|
-
page_age: z.string().describe('Publication timestamp'),
|
|
37
|
-
contents: z
|
|
38
|
-
.object({
|
|
39
|
-
html: z.string().optional().describe('Full HTML content'),
|
|
40
|
-
markdown: z.string().optional().describe('Full Markdown content'),
|
|
41
|
-
})
|
|
42
|
-
.optional()
|
|
43
|
-
.describe('Livecrawled page content'),
|
|
44
|
-
}),
|
|
45
|
-
)
|
|
46
|
-
.optional()
|
|
47
|
-
.describe('News results'),
|
|
48
|
-
})
|
|
49
|
-
.optional()
|
|
50
|
-
.describe('Search results'),
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
export type SearchStructuredContent = z.infer<typeof SearchStructuredContentSchema>
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type { SearchResponse } from '@youdotcom-oss/api'
|
|
2
|
-
import { formatSearchResultsText } from '../shared/format-search-results-text.ts'
|
|
3
|
-
|
|
4
|
-
export const formatSearchResults = (response: SearchResponse) => {
|
|
5
|
-
let formattedResults = ''
|
|
6
|
-
|
|
7
|
-
// Format web results using shared utility
|
|
8
|
-
if (response.results.web?.length) {
|
|
9
|
-
const webResults = formatSearchResultsText(response.results.web)
|
|
10
|
-
formattedResults += `WEB RESULTS:\n\n${webResults}`
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Format news results using shared utility (consistent with web formatting)
|
|
14
|
-
if (response.results.news?.length) {
|
|
15
|
-
const newsResults = formatSearchResultsText(response.results.news)
|
|
16
|
-
|
|
17
|
-
if (formattedResults) {
|
|
18
|
-
formattedResults += `\n\n${'='.repeat(50)}\n\n`
|
|
19
|
-
}
|
|
20
|
-
formattedResults += `NEWS RESULTS:\n\n${newsResults}`
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Extract fields for structuredContent
|
|
24
|
-
const structuredResults: {
|
|
25
|
-
web?: Array<{
|
|
26
|
-
url: string
|
|
27
|
-
title: string
|
|
28
|
-
page_age?: string
|
|
29
|
-
snippets?: string[]
|
|
30
|
-
contents?: { html?: string; markdown?: string }
|
|
31
|
-
}>
|
|
32
|
-
news?: Array<{ url: string; title: string; page_age: string; contents?: { html?: string; markdown?: string } }>
|
|
33
|
-
} = {}
|
|
34
|
-
|
|
35
|
-
if (response.results.web?.length) {
|
|
36
|
-
structuredResults.web = response.results.web.map((result) => {
|
|
37
|
-
const item: {
|
|
38
|
-
url: string
|
|
39
|
-
title: string
|
|
40
|
-
page_age?: string
|
|
41
|
-
snippets?: string[]
|
|
42
|
-
contents?: { html?: string; markdown?: string }
|
|
43
|
-
} = {
|
|
44
|
-
url: result.url,
|
|
45
|
-
title: result.title,
|
|
46
|
-
}
|
|
47
|
-
if (result.page_age) item.page_age = result.page_age
|
|
48
|
-
if (result.snippets?.length) item.snippets = result.snippets
|
|
49
|
-
if (result.contents) item.contents = result.contents ?? undefined
|
|
50
|
-
return item
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (response.results.news?.length) {
|
|
55
|
-
structuredResults.news = response.results.news.map((article) => {
|
|
56
|
-
const item: { url: string; title: string; page_age: string; contents?: { html?: string; markdown?: string } } = {
|
|
57
|
-
url: article.url,
|
|
58
|
-
title: article.title,
|
|
59
|
-
page_age: article.page_age,
|
|
60
|
-
}
|
|
61
|
-
if (article.contents) item.contents = article.contents ?? undefined
|
|
62
|
-
return item
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
content: [
|
|
68
|
-
{
|
|
69
|
-
type: 'text' as const,
|
|
70
|
-
text: `Search Results for "${response.metadata.query}":\n\n${formattedResults}`,
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
structuredContent: {
|
|
74
|
-
resultCounts: {
|
|
75
|
-
web: response.results.web?.length || 0,
|
|
76
|
-
news: response.results.news?.length || 0,
|
|
77
|
-
total: (response.results.web?.length || 0) + (response.results.news?.length || 0),
|
|
78
|
-
},
|
|
79
|
-
results: Object.keys(structuredResults).length > 0 ? structuredResults : undefined,
|
|
80
|
-
},
|
|
81
|
-
fullResponse: response,
|
|
82
|
-
}
|
|
83
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
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({
|
|
92
|
-
resultCounts: { web: 0, news: 0, total: 0 },
|
|
93
|
-
})
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
test('returns formatted results for successful search', async () => {
|
|
97
|
-
mockFetchResponse = oneResultResponse
|
|
98
|
-
const result = await setupMcpClient()
|
|
99
|
-
cleanup = result.cleanup
|
|
100
|
-
|
|
101
|
-
const toolResult = await result.client.callTool({ name: 'you-search', arguments: { query: 'example' } })
|
|
102
|
-
|
|
103
|
-
const text = (toolResult.content as Array<{ type: string; text: string }>)[0]?.text
|
|
104
|
-
expect(text).toContain('Example')
|
|
105
|
-
expect(text).toContain('https://example.com')
|
|
106
|
-
|
|
107
|
-
const structured = toolResult.structuredContent as Record<string, unknown>
|
|
108
|
-
expect(structured).toHaveProperty('resultCounts')
|
|
109
|
-
expect((structured as { resultCounts: { total: number } }).resultCounts.total).toBe(1)
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
test('returns error when API call fails', async () => {
|
|
113
|
-
mockFetchResponse = new Error('API rate limit exceeded')
|
|
114
|
-
const result = await setupMcpClient()
|
|
115
|
-
cleanup = result.cleanup
|
|
116
|
-
|
|
117
|
-
const toolResult = await result.client.callTool({ name: 'you-search', arguments: { query: 'test' } })
|
|
118
|
-
|
|
119
|
-
expect(toolResult.isError).toBe(true)
|
|
120
|
-
const text = (toolResult.content as Array<{ type: string; text: string }>)[0]?.text
|
|
121
|
-
expect(text).toContain('API rate limit exceeded')
|
|
122
|
-
})
|
|
123
|
-
})
|