@youdotcom-oss/mcp 3.3.0 → 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 +4886 -17930
- package/package.json +5 -11
- package/server.json +11 -4
- package/src/contents/contents.utils.ts +0 -59
- package/src/contents/register-contents-tool.ts +0 -95
- package/src/contents/tests/contents.utils.spec.ts +0 -99
- package/src/get-mcp-server.ts +0 -17
- package/src/main.ts +0 -5
- package/src/research/register-research-tool.ts +0 -68
- package/src/research/research.utils.ts +0 -12
- package/src/research/tests/research.utils.spec.ts +0 -51
- package/src/search/register-search-tool.ts +0 -86
- package/src/search/search.utils.ts +0 -27
- package/src/search/tests/register-search-tool.spec.ts +0 -119
- package/src/search/tests/search.utils.spec.ts +0 -192
- 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,119 +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(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
|
-
})
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import type { SearchResponse } from '@youdotcom-oss/api'
|
|
3
|
-
import { formatSearchResults } from '../search.utils.ts'
|
|
4
|
-
|
|
5
|
-
describe('formatSearchResults', () => {
|
|
6
|
-
test('formats web results correctly', () => {
|
|
7
|
-
const mockResponse: SearchResponse = {
|
|
8
|
-
results: {
|
|
9
|
-
web: [
|
|
10
|
-
{
|
|
11
|
-
url: 'https://example.com',
|
|
12
|
-
title: 'Test Title',
|
|
13
|
-
description: 'Test description',
|
|
14
|
-
snippets: ['snippet 1', 'snippet 2'],
|
|
15
|
-
page_age: '2023-01-01T00:00:00',
|
|
16
|
-
authors: ['Author Name'],
|
|
17
|
-
},
|
|
18
|
-
],
|
|
19
|
-
news: [],
|
|
20
|
-
},
|
|
21
|
-
metadata: {
|
|
22
|
-
search_uuid: 'test-uuid',
|
|
23
|
-
query: 'test query',
|
|
24
|
-
latency: 0.1,
|
|
25
|
-
},
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const result = formatSearchResults(mockResponse)
|
|
29
|
-
|
|
30
|
-
expect(Array.isArray(result)).toBe(true)
|
|
31
|
-
expect(result[0]).toHaveProperty('type', 'text')
|
|
32
|
-
expect(result[0]).toHaveProperty('text')
|
|
33
|
-
expect(result[0]?.text).toContain('WEB RESULTS:')
|
|
34
|
-
expect(result[0]?.text).toContain('Test Title')
|
|
35
|
-
expect(result[0]?.text).toContain('URL: https://example.com')
|
|
36
|
-
expect(result[0]?.text).toContain('Published: 2023-01-01T00:00:00')
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test('formats news results correctly', () => {
|
|
40
|
-
const mockResponse: SearchResponse = {
|
|
41
|
-
results: {
|
|
42
|
-
web: [],
|
|
43
|
-
news: [
|
|
44
|
-
{
|
|
45
|
-
title: 'News Title',
|
|
46
|
-
description: 'News description',
|
|
47
|
-
page_age: '2023-01-01T00:00:00',
|
|
48
|
-
url: 'https://news.com/article',
|
|
49
|
-
},
|
|
50
|
-
],
|
|
51
|
-
},
|
|
52
|
-
metadata: {
|
|
53
|
-
search_uuid: 'test-uuid',
|
|
54
|
-
query: 'test query',
|
|
55
|
-
latency: 0.1,
|
|
56
|
-
},
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const result = formatSearchResults(mockResponse)
|
|
60
|
-
|
|
61
|
-
expect(result[0]?.text).toContain('NEWS RESULTS:')
|
|
62
|
-
expect(result[0]?.text).toContain('News Title')
|
|
63
|
-
expect(result[0]?.text).toContain('Published: 2023-01-01T00:00:00')
|
|
64
|
-
expect(result[0]?.text).toContain('URL: https://news.com/article')
|
|
65
|
-
expect(result[0]?.text).toContain('Description: News description')
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test('formats both web and news results', () => {
|
|
69
|
-
const mockResponse: SearchResponse = {
|
|
70
|
-
results: {
|
|
71
|
-
web: [
|
|
72
|
-
{
|
|
73
|
-
url: 'https://web.com',
|
|
74
|
-
title: 'Web Title',
|
|
75
|
-
description: 'Web description',
|
|
76
|
-
snippets: ['web snippet'],
|
|
77
|
-
page_age: '2023-01-01T00:00:00',
|
|
78
|
-
authors: ['Web Author'],
|
|
79
|
-
},
|
|
80
|
-
],
|
|
81
|
-
news: [
|
|
82
|
-
{
|
|
83
|
-
title: 'News Title',
|
|
84
|
-
description: 'News description',
|
|
85
|
-
page_age: '2023-01-01T00:00:00',
|
|
86
|
-
url: 'https://news.com/article',
|
|
87
|
-
},
|
|
88
|
-
],
|
|
89
|
-
},
|
|
90
|
-
metadata: {
|
|
91
|
-
search_uuid: 'test-uuid',
|
|
92
|
-
query: 'test query',
|
|
93
|
-
latency: 0.1,
|
|
94
|
-
},
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const result = formatSearchResults(mockResponse)
|
|
98
|
-
|
|
99
|
-
expect(result[0]?.text).toContain('WEB RESULTS:')
|
|
100
|
-
expect(result[0]?.text).toContain('NEWS RESULTS:')
|
|
101
|
-
expect(result[0]?.text).toContain(`=${'='.repeat(49)}`)
|
|
102
|
-
expect(result[0]?.text).toContain('URL: https://web.com')
|
|
103
|
-
expect(result[0]?.text).toContain('URL: https://news.com/article')
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
test('includes page content indicator when livecrawl returns contents', () => {
|
|
107
|
-
const mockResponse: SearchResponse = {
|
|
108
|
-
results: {
|
|
109
|
-
web: [
|
|
110
|
-
{
|
|
111
|
-
url: 'https://example.com',
|
|
112
|
-
title: 'Livecrawl Title',
|
|
113
|
-
description: 'A page with content',
|
|
114
|
-
snippets: ['snippet'],
|
|
115
|
-
page_age: '2023-01-01T00:00:00',
|
|
116
|
-
authors: [],
|
|
117
|
-
contents: {
|
|
118
|
-
markdown: 'Full page content in markdown format.',
|
|
119
|
-
html: '<p>Full page content in HTML format.</p>',
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
],
|
|
123
|
-
news: [],
|
|
124
|
-
},
|
|
125
|
-
metadata: {
|
|
126
|
-
search_uuid: 'test-uuid',
|
|
127
|
-
query: 'livecrawl test',
|
|
128
|
-
latency: 0.5,
|
|
129
|
-
},
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const result = formatSearchResults(mockResponse)
|
|
133
|
-
|
|
134
|
-
expect(result[0]?.text).toContain('Page content available:')
|
|
135
|
-
expect(result[0]?.text).toContain('chars (markdown)')
|
|
136
|
-
expect(result[0]?.text).toContain('chars (html)')
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
test('omits content indicator when livecrawl contents absent', () => {
|
|
140
|
-
const mockResponse: SearchResponse = {
|
|
141
|
-
results: {
|
|
142
|
-
web: [
|
|
143
|
-
{
|
|
144
|
-
url: 'https://example.com',
|
|
145
|
-
title: 'No Content',
|
|
146
|
-
description: 'A page without livecrawl',
|
|
147
|
-
snippets: ['snippet'],
|
|
148
|
-
},
|
|
149
|
-
],
|
|
150
|
-
news: [],
|
|
151
|
-
},
|
|
152
|
-
metadata: {
|
|
153
|
-
search_uuid: 'test-uuid',
|
|
154
|
-
query: 'test',
|
|
155
|
-
latency: 0.1,
|
|
156
|
-
},
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const result = formatSearchResults(mockResponse)
|
|
160
|
-
|
|
161
|
-
expect(result[0]?.text).not.toContain('Page content available:')
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
test('includes content indicator for news results with livecrawl', () => {
|
|
165
|
-
const mockResponse: SearchResponse = {
|
|
166
|
-
results: {
|
|
167
|
-
web: [],
|
|
168
|
-
news: [
|
|
169
|
-
{
|
|
170
|
-
title: 'News with Content',
|
|
171
|
-
description: 'Breaking news',
|
|
172
|
-
page_age: '2023-01-01T00:00:00',
|
|
173
|
-
url: 'https://news.com/article',
|
|
174
|
-
contents: {
|
|
175
|
-
markdown: 'Full news article content in markdown.',
|
|
176
|
-
},
|
|
177
|
-
},
|
|
178
|
-
],
|
|
179
|
-
},
|
|
180
|
-
metadata: {
|
|
181
|
-
search_uuid: 'test-uuid',
|
|
182
|
-
query: 'news livecrawl test',
|
|
183
|
-
latency: 0.4,
|
|
184
|
-
},
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const result = formatSearchResults(mockResponse)
|
|
188
|
-
|
|
189
|
-
expect(result[0]?.text).toContain('Page content available:')
|
|
190
|
-
expect(result[0]?.text).toContain('chars (markdown)')
|
|
191
|
-
})
|
|
192
|
-
})
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generic search result type for Search API results
|
|
3
|
-
* Used by search.utils.ts
|
|
4
|
-
*/
|
|
5
|
-
type GenericSearchResult = {
|
|
6
|
-
url: string
|
|
7
|
-
title: string
|
|
8
|
-
description?: string
|
|
9
|
-
snippet?: string
|
|
10
|
-
snippets?: string[]
|
|
11
|
-
page_age?: string
|
|
12
|
-
contents?: { html?: string; markdown?: string }
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Format a character count with locale-aware number formatting
|
|
17
|
-
*/
|
|
18
|
-
const formatCharCount = (count: number): string => count.toLocaleString()
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Format array of search results into display text
|
|
22
|
-
* Used by search result formatting
|
|
23
|
-
* @param results - Array of search results to format
|
|
24
|
-
*/
|
|
25
|
-
export const formatSearchResultsText = (results: GenericSearchResult[]): string => {
|
|
26
|
-
return results
|
|
27
|
-
.map((result) => {
|
|
28
|
-
const parts: string[] = [`Title: ${result.title}`]
|
|
29
|
-
|
|
30
|
-
// Add URL
|
|
31
|
-
parts.push(`URL: ${result.url}`)
|
|
32
|
-
|
|
33
|
-
// Add page age if present
|
|
34
|
-
if (result.page_age) {
|
|
35
|
-
parts.push(`Published: ${result.page_age}`)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Add description if present (from Search API)
|
|
39
|
-
if (result.description) {
|
|
40
|
-
parts.push(`Description: ${result.description}`)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Handle snippets array (from Search API)
|
|
44
|
-
if (result.snippets && result.snippets.length > 0) {
|
|
45
|
-
parts.push(`Snippets:\n- ${result.snippets.join('\n- ')}`)
|
|
46
|
-
}
|
|
47
|
-
// Handle single snippet
|
|
48
|
-
else if (result.snippet) {
|
|
49
|
-
parts.push(`Snippet: ${result.snippet}`)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Add contents indicator if livecrawl returned page content
|
|
53
|
-
if (result.contents) {
|
|
54
|
-
const formats: string[] = []
|
|
55
|
-
if (result.contents.markdown) {
|
|
56
|
-
formats.push(`${formatCharCount(result.contents.markdown.length)} chars (markdown)`)
|
|
57
|
-
}
|
|
58
|
-
if (result.contents.html) {
|
|
59
|
-
formats.push(`${formatCharCount(result.contents.html.length)} chars (html)`)
|
|
60
|
-
}
|
|
61
|
-
if (formats.length > 0) {
|
|
62
|
-
parts.push(`Page content available: ${formats.join(', ')}`)
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return parts.join('\n')
|
|
67
|
-
})
|
|
68
|
-
.join('\n\n')
|
|
69
|
-
}
|
package/src/shared/get-logger.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import type { LoggingMessageNotification, ServerNotification } from '@modelcontextprotocol/sdk/types.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Creates a logger that routes notifications through the request's SSE stream
|
|
5
|
-
*
|
|
6
|
-
* @remarks
|
|
7
|
-
* Uses `extra.sendNotification` from the tool callback, which attaches `relatedRequestId`
|
|
8
|
-
* so the transport routes the message to the POST SSE stream (not the standalone GET stream).
|
|
9
|
-
*
|
|
10
|
-
* @param sendNotification - From the tool callback's `extra` parameter
|
|
11
|
-
* @returns Async function that sends logging notifications
|
|
12
|
-
*
|
|
13
|
-
* @internal
|
|
14
|
-
*/
|
|
15
|
-
export const getLogger =
|
|
16
|
-
(sendNotification: (notification: ServerNotification) => Promise<void>) =>
|
|
17
|
-
async (params: LoggingMessageNotification['params']) => {
|
|
18
|
-
await sendNotification({ method: 'notifications/message', params })
|
|
19
|
-
}
|
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { formatSearchResultsText } from '../format-search-results-text.ts'
|
|
3
|
-
|
|
4
|
-
describe('formatSearchResultsText', () => {
|
|
5
|
-
test('formats basic search results with title and URL', () => {
|
|
6
|
-
const result = formatSearchResultsText([{ url: 'https://example.com', title: 'Test' }])
|
|
7
|
-
|
|
8
|
-
expect(result).toContain('Title: Test')
|
|
9
|
-
expect(result).toContain('URL: https://example.com')
|
|
10
|
-
})
|
|
11
|
-
|
|
12
|
-
test('includes page_age when present', () => {
|
|
13
|
-
const result = formatSearchResultsText([{ url: 'https://example.com', title: 'Test', page_age: '2023-01-01' }])
|
|
14
|
-
|
|
15
|
-
expect(result).toContain('Published: 2023-01-01')
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
test('includes description when present', () => {
|
|
19
|
-
const result = formatSearchResultsText([
|
|
20
|
-
{ url: 'https://example.com', title: 'Test', description: 'A description' },
|
|
21
|
-
])
|
|
22
|
-
|
|
23
|
-
expect(result).toContain('Description: A description')
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test('includes snippets array when present', () => {
|
|
27
|
-
const result = formatSearchResultsText([{ url: 'https://example.com', title: 'Test', snippets: ['one', 'two'] }])
|
|
28
|
-
|
|
29
|
-
expect(result).toContain('Snippets:')
|
|
30
|
-
expect(result).toContain('- one')
|
|
31
|
-
expect(result).toContain('- two')
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
test('includes single snippet when present', () => {
|
|
35
|
-
const result = formatSearchResultsText([{ url: 'https://example.com', title: 'Test', snippet: 'a snippet' }])
|
|
36
|
-
|
|
37
|
-
expect(result).toContain('Snippet: a snippet')
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
test('formats multiple results with separator', () => {
|
|
41
|
-
const result = formatSearchResultsText([
|
|
42
|
-
{ url: 'https://a.com', title: 'A' },
|
|
43
|
-
{ url: 'https://b.com', title: 'B' },
|
|
44
|
-
])
|
|
45
|
-
|
|
46
|
-
expect(result).toContain('Title: A')
|
|
47
|
-
expect(result).toContain('Title: B')
|
|
48
|
-
expect(result).toContain('\n\n')
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('handles empty results array', () => {
|
|
52
|
-
const result = formatSearchResultsText([])
|
|
53
|
-
|
|
54
|
-
expect(result).toBe('')
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test('includes contents indicator when markdown content is present', () => {
|
|
58
|
-
const result = formatSearchResultsText([
|
|
59
|
-
{
|
|
60
|
-
url: 'https://example.com',
|
|
61
|
-
title: 'Test',
|
|
62
|
-
contents: { markdown: 'A'.repeat(4523) },
|
|
63
|
-
},
|
|
64
|
-
])
|
|
65
|
-
|
|
66
|
-
expect(result).toContain('Page content available:')
|
|
67
|
-
expect(result).toContain('4,523 chars (markdown)')
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
test('includes contents indicator for both markdown and html', () => {
|
|
71
|
-
const result = formatSearchResultsText([
|
|
72
|
-
{
|
|
73
|
-
url: 'https://example.com',
|
|
74
|
-
title: 'Test',
|
|
75
|
-
contents: { markdown: 'markdown content', html: '<p>html content</p>' },
|
|
76
|
-
},
|
|
77
|
-
])
|
|
78
|
-
|
|
79
|
-
expect(result).toContain('Page content available:')
|
|
80
|
-
expect(result).toContain('chars (markdown)')
|
|
81
|
-
expect(result).toContain('chars (html)')
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
test('omits contents indicator when contents object has no content', () => {
|
|
85
|
-
const result = formatSearchResultsText([{ url: 'https://example.com', title: 'Test', contents: {} }])
|
|
86
|
-
|
|
87
|
-
expect(result).not.toContain('Page content available:')
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
test('omits contents indicator when contents is not present', () => {
|
|
91
|
-
const result = formatSearchResultsText([{ url: 'https://example.com', title: 'Test' }])
|
|
92
|
-
|
|
93
|
-
expect(result).not.toContain('Page content available:')
|
|
94
|
-
})
|
|
95
|
-
})
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
3
|
-
import { useGetClientVersion } from '../use-client-version.ts'
|
|
4
|
-
|
|
5
|
-
describe('useGetClientVersion', () => {
|
|
6
|
-
test('returns formatted string with all fields present', () => {
|
|
7
|
-
const mockMcp = {
|
|
8
|
-
server: {
|
|
9
|
-
getClientVersion: () => ({
|
|
10
|
-
name: 'test-client',
|
|
11
|
-
version: '1.0.0',
|
|
12
|
-
title: 'Test Client',
|
|
13
|
-
websiteUrl: 'https://example.com',
|
|
14
|
-
}),
|
|
15
|
-
},
|
|
16
|
-
} as unknown as McpServer
|
|
17
|
-
|
|
18
|
-
const getUserAgent = useGetClientVersion(mockMcp)
|
|
19
|
-
const result = getUserAgent()
|
|
20
|
-
|
|
21
|
-
expect(result).toMatch(
|
|
22
|
-
/^MCP\/[\d.]+(-[\w.]+)? \(You\.com; test-client; 1\.0\.0; Test Client; https:\/\/example\.com\)$/,
|
|
23
|
-
)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test('returns formatted string with name and version only', () => {
|
|
27
|
-
const mockMcp = {
|
|
28
|
-
server: {
|
|
29
|
-
getClientVersion: () => ({
|
|
30
|
-
name: 'test-client',
|
|
31
|
-
version: '1.0.0',
|
|
32
|
-
}),
|
|
33
|
-
},
|
|
34
|
-
} as unknown as McpServer
|
|
35
|
-
|
|
36
|
-
const getUserAgent = useGetClientVersion(mockMcp)
|
|
37
|
-
const result = getUserAgent()
|
|
38
|
-
|
|
39
|
-
expect(result).toMatch(/^MCP\/[\d.]+(-[\w.]+)? \(You\.com; test-client; 1\.0\.0\)$/)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('returns UNKNOWN when no client version available', () => {
|
|
43
|
-
const mockMcp = {
|
|
44
|
-
server: {
|
|
45
|
-
getClientVersion: () => null,
|
|
46
|
-
},
|
|
47
|
-
} as unknown as McpServer
|
|
48
|
-
|
|
49
|
-
const getUserAgent = useGetClientVersion(mockMcp)
|
|
50
|
-
const result = getUserAgent()
|
|
51
|
-
|
|
52
|
-
expect(result).toMatch(/^MCP\/[\d.]+(-[\w.]+)? \(You\.com; UNKNOWN\)$/)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
test('returns UNKNOWN when getClientVersion returns undefined', () => {
|
|
56
|
-
const mockMcp = {
|
|
57
|
-
server: {
|
|
58
|
-
getClientVersion: () => undefined,
|
|
59
|
-
},
|
|
60
|
-
} as unknown as McpServer
|
|
61
|
-
|
|
62
|
-
const getUserAgent = useGetClientVersion(mockMcp)
|
|
63
|
-
const result = getUserAgent()
|
|
64
|
-
|
|
65
|
-
expect(result).toMatch(/^MCP\/[\d.]+(-[\w.]+)? \(You\.com; UNKNOWN\)$/)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
test('filters out empty strings from fields', () => {
|
|
69
|
-
const mockMcp = {
|
|
70
|
-
server: {
|
|
71
|
-
getClientVersion: () => ({
|
|
72
|
-
name: 'test-client',
|
|
73
|
-
version: '1.0.0',
|
|
74
|
-
title: '', // Empty string should be filtered out
|
|
75
|
-
websiteUrl: 'https://example.com',
|
|
76
|
-
}),
|
|
77
|
-
},
|
|
78
|
-
} as unknown as McpServer
|
|
79
|
-
|
|
80
|
-
const getUserAgent = useGetClientVersion(mockMcp)
|
|
81
|
-
const result = getUserAgent()
|
|
82
|
-
|
|
83
|
-
expect(result).toMatch(/^MCP\/[\d.]+(-[\w.]+)? \(You\.com; test-client; 1\.0\.0; https:\/\/example\.com\)$/)
|
|
84
|
-
expect(result).not.toContain(';;') // No double semicolons
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
test('filters out null values from fields', () => {
|
|
88
|
-
const mockMcp = {
|
|
89
|
-
server: {
|
|
90
|
-
getClientVersion: () => ({
|
|
91
|
-
name: 'test-client',
|
|
92
|
-
version: '1.0.0',
|
|
93
|
-
title: null, // Null should be filtered out
|
|
94
|
-
websiteUrl: 'https://example.com',
|
|
95
|
-
}),
|
|
96
|
-
},
|
|
97
|
-
} as unknown as McpServer
|
|
98
|
-
|
|
99
|
-
const getUserAgent = useGetClientVersion(mockMcp)
|
|
100
|
-
const result = getUserAgent()
|
|
101
|
-
|
|
102
|
-
expect(result).toMatch(/^MCP\/[\d.]+(-[\w.]+)? \(You\.com; test-client; 1\.0\.0; https:\/\/example\.com\)$/)
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
test('handles partial fields - name, version, and title only', () => {
|
|
106
|
-
const mockMcp = {
|
|
107
|
-
server: {
|
|
108
|
-
getClientVersion: () => ({
|
|
109
|
-
name: 'Claude Desktop',
|
|
110
|
-
version: '0.7.6',
|
|
111
|
-
title: 'Claude Desktop App',
|
|
112
|
-
}),
|
|
113
|
-
},
|
|
114
|
-
} as unknown as McpServer
|
|
115
|
-
|
|
116
|
-
const getUserAgent = useGetClientVersion(mockMcp)
|
|
117
|
-
const result = getUserAgent()
|
|
118
|
-
|
|
119
|
-
expect(result).toMatch(/^MCP\/[\d.]+(-[\w.]+)? \(You\.com; Claude Desktop; 0\.7\.6; Claude Desktop App\)$/)
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
test('handles Claude Desktop client info format', () => {
|
|
123
|
-
const mockMcp = {
|
|
124
|
-
server: {
|
|
125
|
-
getClientVersion: () => ({
|
|
126
|
-
name: 'Claude Desktop',
|
|
127
|
-
version: '0.7.6',
|
|
128
|
-
}),
|
|
129
|
-
},
|
|
130
|
-
} as unknown as McpServer
|
|
131
|
-
|
|
132
|
-
const getUserAgent = useGetClientVersion(mockMcp)
|
|
133
|
-
const result = getUserAgent()
|
|
134
|
-
|
|
135
|
-
expect(result).toMatch(/^MCP\/[\d.]+(-[\w.]+)? \(You\.com; Claude Desktop; 0\.7\.6\)$/)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
test('returns a function that can be called multiple times', () => {
|
|
139
|
-
const mockMcp = {
|
|
140
|
-
server: {
|
|
141
|
-
getClientVersion: () => ({
|
|
142
|
-
name: 'test-client',
|
|
143
|
-
version: '1.0.0',
|
|
144
|
-
}),
|
|
145
|
-
},
|
|
146
|
-
} as unknown as McpServer
|
|
147
|
-
|
|
148
|
-
const getUserAgent = useGetClientVersion(mockMcp)
|
|
149
|
-
|
|
150
|
-
// Call multiple times to ensure consistent results
|
|
151
|
-
const result1 = getUserAgent()
|
|
152
|
-
const result2 = getUserAgent()
|
|
153
|
-
const result3 = getUserAgent()
|
|
154
|
-
|
|
155
|
-
const pattern = /^MCP\/[\d.]+(-[\w.]+)? \(You\.com; test-client; 1\.0\.0\)$/
|
|
156
|
-
expect(result1).toMatch(pattern)
|
|
157
|
-
expect(result2).toMatch(pattern)
|
|
158
|
-
expect(result3).toMatch(pattern)
|
|
159
|
-
})
|
|
160
|
-
})
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
-
import packageJson from '../../package.json' with { type: 'json' }
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Creates User-Agent string for API requests
|
|
6
|
-
* Used by search and contents API calls
|
|
7
|
-
*/
|
|
8
|
-
const setUserAgent = (client: string) => `MCP/${packageJson.version} (You.com; ${client})`
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Get's function that returns a formatted client version information into a string
|
|
12
|
-
* Used by stdio.ts and http.ts for logging/debugging
|
|
13
|
-
*/
|
|
14
|
-
export const useGetClientVersion = (mcp: McpServer) => () => {
|
|
15
|
-
const clientVersion = mcp.server.getClientVersion()
|
|
16
|
-
if (clientVersion) {
|
|
17
|
-
const { name, version, title, websiteUrl } = clientVersion
|
|
18
|
-
return setUserAgent([name, version, title, websiteUrl].filter(Boolean).join('; '))
|
|
19
|
-
}
|
|
20
|
-
return setUserAgent('UNKNOWN')
|
|
21
|
-
}
|