@youdotcom-oss/mcp 2.1.0 → 3.1.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 +27 -72
- package/bin/stdio.js +6699 -28740
- package/package.json +10 -22
- package/server.json +51 -0
- package/src/contents/contents.schemas.ts +0 -30
- package/src/contents/contents.utils.ts +0 -84
- package/src/contents/register-contents-tool.ts +0 -94
- package/src/contents/tests/contents.utils.spec.ts +0 -123
- package/src/get-mcp-server.ts +0 -17
- package/src/http.ts +0 -94
- 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/search/register-search-tool.ts +0 -87
- package/src/search/search.schema.ts +0 -38
- package/src/search/search.utils.ts +0 -70
- package/src/search/tests/search.utils.spec.ts +0 -156
- package/src/shared/format-search-results-text.ts +0 -49
- package/src/shared/get-logger.ts +0 -10
- package/src/shared/tests/shared.utils.spec.ts +0 -160
- package/src/shared/use-client-version.ts +0 -21
- package/src/stdio.ts +0 -24
- package/src/tests/http.spec.ts +0 -583
- package/src/tests/tool.spec.ts +0 -649
|
@@ -1,87 +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.schema.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: 'Web and news search via You.com',
|
|
21
|
-
inputSchema: SearchQuerySchema.shape,
|
|
22
|
-
outputSchema: SearchStructuredContentSchema.shape,
|
|
23
|
-
},
|
|
24
|
-
async (searchQuery) => {
|
|
25
|
-
const logger = getLogger(mcp)
|
|
26
|
-
try {
|
|
27
|
-
const response = await fetchSearchResults({
|
|
28
|
-
searchQuery,
|
|
29
|
-
YDC_API_KEY,
|
|
30
|
-
getUserAgent,
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
const webCount = response.results.web?.length ?? 0
|
|
34
|
-
const newsCount = response.results.news?.length ?? 0
|
|
35
|
-
|
|
36
|
-
if (!webCount && !newsCount) {
|
|
37
|
-
await logger({
|
|
38
|
-
level: 'info',
|
|
39
|
-
data: `No results found for query: "${searchQuery.query}"`,
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
content: [{ type: 'text' as const, text: 'No results found.' }],
|
|
44
|
-
structuredContent: {
|
|
45
|
-
resultCounts: {
|
|
46
|
-
web: 0,
|
|
47
|
-
news: 0,
|
|
48
|
-
total: 0,
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
await logger({
|
|
55
|
-
level: 'info',
|
|
56
|
-
data: `Search successful for query: "${searchQuery.query}" - ${webCount} web results, ${newsCount} news results (${webCount + newsCount} total)`,
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
const { content, structuredContent } = formatSearchResults(response)
|
|
60
|
-
return { content, structuredContent }
|
|
61
|
-
} catch (err: unknown) {
|
|
62
|
-
const errorMessage = err instanceof Error ? err.message : String(err)
|
|
63
|
-
const reportLink = generateErrorReportLink({
|
|
64
|
-
errorMessage,
|
|
65
|
-
tool: 'you-search',
|
|
66
|
-
clientInfo: getUserAgent(),
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
await logger({
|
|
70
|
-
level: 'error',
|
|
71
|
-
data: `Search API call failed: ${errorMessage}\n\nReport this issue: ${reportLink}`,
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
content: [
|
|
76
|
-
{
|
|
77
|
-
type: 'text' as const,
|
|
78
|
-
text: `Error: ${errorMessage}`,
|
|
79
|
-
},
|
|
80
|
-
],
|
|
81
|
-
structuredContent: undefined,
|
|
82
|
-
isError: true,
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
)
|
|
87
|
-
}
|
|
@@ -1,38 +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
|
-
}),
|
|
20
|
-
)
|
|
21
|
-
.optional()
|
|
22
|
-
.describe('Web results'),
|
|
23
|
-
news: z
|
|
24
|
-
.array(
|
|
25
|
-
z.object({
|
|
26
|
-
url: z.string().describe('URL'),
|
|
27
|
-
title: z.string().describe('Title'),
|
|
28
|
-
page_age: z.string().describe('Publication timestamp'),
|
|
29
|
-
}),
|
|
30
|
-
)
|
|
31
|
-
.optional()
|
|
32
|
-
.describe('News results'),
|
|
33
|
-
})
|
|
34
|
-
.optional()
|
|
35
|
-
.describe('Search results'),
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
export type SearchStructuredContent = z.infer<typeof SearchStructuredContentSchema>
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import type { NewsResult, 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
|
|
14
|
-
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')
|
|
21
|
-
|
|
22
|
-
if (formattedResults) {
|
|
23
|
-
formattedResults += `\n\n${'='.repeat(50)}\n\n`
|
|
24
|
-
}
|
|
25
|
-
formattedResults += `NEWS RESULTS:\n\n${newsResults}`
|
|
26
|
-
}
|
|
27
|
-
|
|
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,
|
|
67
|
-
},
|
|
68
|
-
fullResponse: response,
|
|
69
|
-
}
|
|
70
|
-
}
|
|
@@ -1,156 +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(result).toHaveProperty('content')
|
|
31
|
-
expect(result).toHaveProperty('structuredContent')
|
|
32
|
-
expect(result).toHaveProperty('fullResponse')
|
|
33
|
-
expect(Array.isArray(result.content)).toBe(true)
|
|
34
|
-
expect(result.content[0]).toHaveProperty('type', 'text')
|
|
35
|
-
expect(result.content[0]).toHaveProperty('text')
|
|
36
|
-
expect(result.content[0]?.text).toContain('WEB RESULTS:')
|
|
37
|
-
expect(result.content[0]?.text).toContain('Test Title')
|
|
38
|
-
// URL and page_age should be in text content
|
|
39
|
-
expect(result.content[0]?.text).toContain('URL: https://example.com')
|
|
40
|
-
expect(result.content[0]?.text).toContain('Published: 2023-01-01T00:00:00')
|
|
41
|
-
expect(result.structuredContent).toHaveProperty('resultCounts')
|
|
42
|
-
expect(result.structuredContent.resultCounts).toHaveProperty('web', 1)
|
|
43
|
-
expect(result.structuredContent.resultCounts).toHaveProperty('news', 0)
|
|
44
|
-
expect(result.structuredContent.resultCounts).toHaveProperty('total', 1)
|
|
45
|
-
// All fields should be in structuredContent.results
|
|
46
|
-
expect(result.structuredContent).toHaveProperty('results')
|
|
47
|
-
expect(result.structuredContent.results?.web).toBeDefined()
|
|
48
|
-
expect(result.structuredContent.results?.web?.length).toBe(1)
|
|
49
|
-
expect(result.structuredContent.results?.web?.[0]).toMatchObject({
|
|
50
|
-
url: 'https://example.com',
|
|
51
|
-
title: 'Test Title',
|
|
52
|
-
page_age: '2023-01-01T00:00:00',
|
|
53
|
-
})
|
|
54
|
-
expect(result.fullResponse).toBe(mockResponse)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test('formats news results correctly', () => {
|
|
58
|
-
const mockResponse: SearchResponse = {
|
|
59
|
-
results: {
|
|
60
|
-
web: [],
|
|
61
|
-
news: [
|
|
62
|
-
{
|
|
63
|
-
title: 'News Title',
|
|
64
|
-
description: 'News description',
|
|
65
|
-
page_age: '2023-01-01T00:00:00',
|
|
66
|
-
url: 'https://news.com/article',
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
},
|
|
70
|
-
metadata: {
|
|
71
|
-
search_uuid: 'test-uuid',
|
|
72
|
-
query: 'test query',
|
|
73
|
-
latency: 0.1,
|
|
74
|
-
},
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const result = formatSearchResults(mockResponse)
|
|
78
|
-
|
|
79
|
-
expect(result.content[0]?.text).toContain('NEWS RESULTS:')
|
|
80
|
-
expect(result.content[0]?.text).toContain('News Title')
|
|
81
|
-
expect(result.content[0]?.text).toContain('Published: 2023-01-01T00:00:00')
|
|
82
|
-
// URL should be in text content
|
|
83
|
-
expect(result.content[0]?.text).toContain('URL: https://news.com/article')
|
|
84
|
-
expect(result.structuredContent).toHaveProperty('resultCounts')
|
|
85
|
-
expect(result.structuredContent.resultCounts).toHaveProperty('web', 0)
|
|
86
|
-
expect(result.structuredContent.resultCounts).toHaveProperty('news', 1)
|
|
87
|
-
expect(result.structuredContent.resultCounts).toHaveProperty('total', 1)
|
|
88
|
-
// All fields should be in structuredContent.results
|
|
89
|
-
expect(result.structuredContent).toHaveProperty('results')
|
|
90
|
-
expect(result.structuredContent.results?.news).toBeDefined()
|
|
91
|
-
expect(result.structuredContent.results?.news?.length).toBe(1)
|
|
92
|
-
expect(result.structuredContent.results?.news?.[0]).toMatchObject({
|
|
93
|
-
url: 'https://news.com/article',
|
|
94
|
-
title: 'News Title',
|
|
95
|
-
page_age: '2023-01-01T00:00:00',
|
|
96
|
-
})
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test('formats both web and news results', () => {
|
|
100
|
-
const mockResponse: SearchResponse = {
|
|
101
|
-
results: {
|
|
102
|
-
web: [
|
|
103
|
-
{
|
|
104
|
-
url: 'https://web.com',
|
|
105
|
-
title: 'Web Title',
|
|
106
|
-
description: 'Web description',
|
|
107
|
-
snippets: ['web snippet'],
|
|
108
|
-
page_age: '2023-01-01T00:00:00',
|
|
109
|
-
authors: ['Web Author'],
|
|
110
|
-
},
|
|
111
|
-
],
|
|
112
|
-
news: [
|
|
113
|
-
{
|
|
114
|
-
title: 'News Title',
|
|
115
|
-
description: 'News description',
|
|
116
|
-
page_age: '2023-01-01T00:00:00',
|
|
117
|
-
url: 'https://news.com/article',
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
},
|
|
121
|
-
metadata: {
|
|
122
|
-
search_uuid: 'test-uuid',
|
|
123
|
-
query: 'test query',
|
|
124
|
-
latency: 0.1,
|
|
125
|
-
},
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const result = formatSearchResults(mockResponse)
|
|
129
|
-
|
|
130
|
-
expect(result.content[0]?.text).toContain('WEB RESULTS:')
|
|
131
|
-
expect(result.content[0]?.text).toContain('NEWS RESULTS:')
|
|
132
|
-
expect(result.content[0]?.text).toContain(`=${'='.repeat(49)}`)
|
|
133
|
-
// URLs should be in text content
|
|
134
|
-
expect(result.content[0]?.text).toContain('URL: https://web.com')
|
|
135
|
-
expect(result.content[0]?.text).toContain('URL: https://news.com/article')
|
|
136
|
-
expect(result.structuredContent.resultCounts).toHaveProperty('web', 1)
|
|
137
|
-
expect(result.structuredContent.resultCounts).toHaveProperty('news', 1)
|
|
138
|
-
expect(result.structuredContent.resultCounts).toHaveProperty('total', 2)
|
|
139
|
-
// All fields should be in structuredContent.results
|
|
140
|
-
expect(result.structuredContent).toHaveProperty('results')
|
|
141
|
-
expect(result.structuredContent.results?.web).toBeDefined()
|
|
142
|
-
expect(result.structuredContent.results?.news).toBeDefined()
|
|
143
|
-
expect(result.structuredContent.results?.web?.length).toBe(1)
|
|
144
|
-
expect(result.structuredContent.results?.news?.length).toBe(1)
|
|
145
|
-
expect(result.structuredContent.results?.web?.[0]).toMatchObject({
|
|
146
|
-
url: 'https://web.com',
|
|
147
|
-
title: 'Web Title',
|
|
148
|
-
page_age: '2023-01-01T00:00:00',
|
|
149
|
-
})
|
|
150
|
-
expect(result.structuredContent.results?.news?.[0]).toMatchObject({
|
|
151
|
-
url: 'https://news.com/article',
|
|
152
|
-
title: 'News Title',
|
|
153
|
-
page_age: '2023-01-01T00:00:00',
|
|
154
|
-
})
|
|
155
|
-
})
|
|
156
|
-
})
|
|
@@ -1,49 +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
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Format array of search results into display text
|
|
16
|
-
* Used by search result formatting
|
|
17
|
-
* @param results - Array of search results to format
|
|
18
|
-
*/
|
|
19
|
-
export const formatSearchResultsText = (results: GenericSearchResult[]): string => {
|
|
20
|
-
return results
|
|
21
|
-
.map((result) => {
|
|
22
|
-
const parts: string[] = [`Title: ${result.title}`]
|
|
23
|
-
|
|
24
|
-
// Add URL
|
|
25
|
-
parts.push(`URL: ${result.url}`)
|
|
26
|
-
|
|
27
|
-
// Add page age if present
|
|
28
|
-
if (result.page_age) {
|
|
29
|
-
parts.push(`Published: ${result.page_age}`)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Add description if present (from Search API)
|
|
33
|
-
if (result.description) {
|
|
34
|
-
parts.push(`Description: ${result.description}`)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Handle snippets array (from Search API)
|
|
38
|
-
if (result.snippets && result.snippets.length > 0) {
|
|
39
|
-
parts.push(`Snippets:\n- ${result.snippets.join('\n- ')}`)
|
|
40
|
-
}
|
|
41
|
-
// Handle single snippet
|
|
42
|
-
else if (result.snippet) {
|
|
43
|
-
parts.push(`Snippet: ${result.snippet}`)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return parts.join('\n')
|
|
47
|
-
})
|
|
48
|
-
.join('\n\n')
|
|
49
|
-
}
|
package/src/shared/get-logger.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
-
import type { LoggingMessageNotification } from '@modelcontextprotocol/sdk/types.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Creates a logger function that sends messages through MCP server
|
|
6
|
-
* Used by tool registration files
|
|
7
|
-
*/
|
|
8
|
-
export const getLogger = (mcp: McpServer) => async (params: LoggingMessageNotification['params']) => {
|
|
9
|
-
await mcp.server.sendLoggingMessage(params)
|
|
10
|
-
}
|
|
@@ -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
|
-
}
|
package/src/stdio.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
|
-
import { registerContentsTool } from './contents/register-contents-tool.ts'
|
|
4
|
-
import { getMCpServer } from './get-mcp-server.ts'
|
|
5
|
-
import { registerResearchTool } from './research/register-research-tool.ts'
|
|
6
|
-
import { registerSearchTool } from './search/register-search-tool.ts'
|
|
7
|
-
import { useGetClientVersion } from './shared/use-client-version.ts'
|
|
8
|
-
|
|
9
|
-
const YDC_API_KEY = process.env.YDC_API_KEY
|
|
10
|
-
|
|
11
|
-
try {
|
|
12
|
-
const mcp = getMCpServer()
|
|
13
|
-
const getUserAgent = useGetClientVersion(mcp)
|
|
14
|
-
|
|
15
|
-
registerSearchTool({ mcp, YDC_API_KEY, getUserAgent })
|
|
16
|
-
registerContentsTool({ mcp, YDC_API_KEY, getUserAgent })
|
|
17
|
-
registerResearchTool({ mcp, YDC_API_KEY, getUserAgent })
|
|
18
|
-
|
|
19
|
-
const transport = new StdioServerTransport()
|
|
20
|
-
await mcp.connect(transport)
|
|
21
|
-
} catch (error) {
|
|
22
|
-
process.stderr.write(`Failed to start server: ${error}\n`)
|
|
23
|
-
process.exit(1)
|
|
24
|
-
}
|