@youdotcom-oss/mcp 1.5.0 → 1.6.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 +394 -415
- package/package.json +12 -23
- package/src/contents/contents.schemas.ts +3 -48
- package/src/contents/contents.utils.ts +33 -133
- package/src/contents/register-contents-tool.ts +24 -24
- package/src/contents/tests/contents.utils.spec.ts +58 -134
- package/src/express/express.schema.ts +23 -0
- package/src/express/express.utils.ts +10 -121
- package/src/express/register-express-tool.ts +19 -19
- package/src/express/tests/express.utils.spec.ts +80 -159
- package/src/get-mcp-server.ts +3 -3
- package/src/http.ts +38 -38
- package/src/search/register-search-tool.ts +23 -23
- package/src/search/search.schema.ts +38 -0
- package/src/search/search.utils.ts +18 -96
- package/src/search/tests/search.utils.spec.ts +61 -194
- package/src/shared/format-search-results-text.ts +16 -16
- package/src/shared/get-logger.ts +4 -4
- package/src/shared/tests/shared.utils.spec.ts +56 -56
- package/src/shared/use-client-version.ts +8 -8
- package/src/stdio.ts +16 -16
- package/src/tests/http.spec.ts +105 -105
- package/src/tests/tool.spec.ts +212 -212
- package/dist/contents/contents.schemas.d.ts +0 -55
- package/dist/contents/contents.utils.d.ts +0 -28
- package/dist/contents/register-contents-tool.d.ts +0 -10
- package/dist/express/express.schemas.d.ts +0 -56
- package/dist/express/express.utils.d.ts +0 -45
- package/dist/express/register-express-tool.d.ts +0 -6
- package/dist/get-mcp-server.d.ts +0 -2
- package/dist/http.d.ts +0 -3
- package/dist/main.d.ts +0 -9
- package/dist/search/register-search-tool.d.ts +0 -6
- package/dist/search/search.schemas.d.ts +0 -133
- package/dist/search/search.utils.d.ts +0 -98
- package/dist/shared/api-constants.d.ts +0 -9
- package/dist/shared/check-response-for-errors.d.ts +0 -6
- package/dist/shared/format-search-results-text.d.ts +0 -19
- package/dist/shared/generate-error-report-link.d.ts +0 -9
- package/dist/shared/get-logger.d.ts +0 -7
- package/dist/shared/use-client-version.d.ts +0 -6
- package/dist/stdio.d.ts +0 -2
- package/src/express/express.schemas.ts +0 -99
- package/src/main.ts +0 -9
- package/src/search/search.schemas.ts +0 -147
- package/src/shared/api-constants.ts +0 -10
- package/src/shared/check-response-for-errors.ts +0 -13
- package/src/shared/generate-error-report-link.ts +0 -37
- package/src/tests/exports.spec.ts +0 -24
|
@@ -1,122 +1,43 @@
|
|
|
1
|
-
import { describe, expect, setDefaultTimeout, test } from 'bun:test'
|
|
2
|
-
import type { ExpressAgentMcpResponse } from '
|
|
3
|
-
import {
|
|
1
|
+
import { describe, expect, setDefaultTimeout, test } from 'bun:test'
|
|
2
|
+
import type { ExpressAgentMcpResponse } from '@youdotcom-oss/api'
|
|
3
|
+
import { formatExpressAgentResponse } from '../express.utils.ts'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
setDefaultTimeout(20_000);
|
|
8
|
-
|
|
9
|
-
describe('callExpressAgent', () => {
|
|
10
|
-
test(
|
|
11
|
-
'returns answer only (WITHOUT web_search tools)',
|
|
12
|
-
async () => {
|
|
13
|
-
const result = await callExpressAgent({
|
|
14
|
-
agentInput: { input: 'What is machine learning?' },
|
|
15
|
-
getUserAgent,
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
// Verify MCP response structure
|
|
19
|
-
expect(result).toHaveProperty('answer');
|
|
20
|
-
expect(typeof result.answer).toBe('string');
|
|
21
|
-
expect(result.answer.length).toBeGreaterThan(0);
|
|
22
|
-
|
|
23
|
-
// Should NOT have results when web_search is not used
|
|
24
|
-
expect(result.results).toBeUndefined();
|
|
25
|
-
|
|
26
|
-
expect(result.agent).toBe('express');
|
|
27
|
-
},
|
|
28
|
-
{ retry: 2 },
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
test(
|
|
32
|
-
'returns answer and search results (WITH web_search tools)',
|
|
33
|
-
async () => {
|
|
34
|
-
const result = await callExpressAgent({
|
|
35
|
-
agentInput: {
|
|
36
|
-
input: 'Latest developments in quantum computing',
|
|
37
|
-
tools: [{ type: 'web_search' }],
|
|
38
|
-
},
|
|
39
|
-
getUserAgent,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
// Verify MCP response has both answer and results
|
|
43
|
-
expect(result).toHaveProperty('answer');
|
|
44
|
-
expect(typeof result.answer).toBe('string');
|
|
45
|
-
expect(result.answer.length).toBeGreaterThan(0);
|
|
46
|
-
|
|
47
|
-
expect(result).toHaveProperty('results');
|
|
48
|
-
expect(result.results).toHaveProperty('web');
|
|
49
|
-
expect(Array.isArray(result.results?.web)).toBe(true);
|
|
50
|
-
expect(result.results?.web.length).toBeGreaterThan(0);
|
|
51
|
-
|
|
52
|
-
// Verify each search result has required fields
|
|
53
|
-
const firstResult = result.results?.web[0];
|
|
54
|
-
expect(firstResult).toHaveProperty('url');
|
|
55
|
-
expect(firstResult).toHaveProperty('title');
|
|
56
|
-
expect(firstResult).toHaveProperty('snippet');
|
|
57
|
-
expect(typeof firstResult?.url).toBe('string');
|
|
58
|
-
expect(typeof firstResult?.title).toBe('string');
|
|
59
|
-
expect(typeof firstResult?.snippet).toBe('string');
|
|
60
|
-
expect(firstResult?.url.length).toBeGreaterThan(0);
|
|
61
|
-
expect(firstResult?.title.length).toBeGreaterThan(0);
|
|
62
|
-
|
|
63
|
-
expect(result.agent).toBe('express');
|
|
64
|
-
},
|
|
65
|
-
{ timeout: 30_000, retry: 2 },
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
test(
|
|
69
|
-
'works without optional parameters',
|
|
70
|
-
async () => {
|
|
71
|
-
const result = await callExpressAgent({
|
|
72
|
-
agentInput: { input: 'What is the capital of France?' },
|
|
73
|
-
getUserAgent,
|
|
74
|
-
// No progressToken or sendProgress provided
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Should work normally without progress tracking
|
|
78
|
-
expect(result).toHaveProperty('answer');
|
|
79
|
-
expect(result.answer.length).toBeGreaterThan(0);
|
|
80
|
-
expect(result.agent).toBe('express');
|
|
81
|
-
},
|
|
82
|
-
{ retry: 2 },
|
|
83
|
-
);
|
|
84
|
-
});
|
|
5
|
+
setDefaultTimeout(20_000)
|
|
85
6
|
|
|
86
7
|
describe('formatExpressAgentResponse', () => {
|
|
87
8
|
test('formats response with answer only (no search results)', () => {
|
|
88
9
|
const mockResponse: ExpressAgentMcpResponse = {
|
|
89
10
|
answer: 'The capital of France is Paris.',
|
|
90
11
|
agent: 'express',
|
|
91
|
-
}
|
|
12
|
+
}
|
|
92
13
|
|
|
93
|
-
const result = formatExpressAgentResponse(mockResponse)
|
|
14
|
+
const result = formatExpressAgentResponse(mockResponse)
|
|
94
15
|
|
|
95
16
|
// Verify content array has 1 item (answer only)
|
|
96
|
-
expect(result).toHaveProperty('content')
|
|
97
|
-
expect(Array.isArray(result.content)).toBe(true)
|
|
98
|
-
expect(result.content.length).toBe(1)
|
|
17
|
+
expect(result).toHaveProperty('content')
|
|
18
|
+
expect(Array.isArray(result.content)).toBe(true)
|
|
19
|
+
expect(result.content.length).toBe(1)
|
|
99
20
|
|
|
100
21
|
// Verify answer content
|
|
101
|
-
expect(result.content[0]).toHaveProperty('type', 'text')
|
|
102
|
-
expect(result.content[0]).toHaveProperty('text')
|
|
103
|
-
expect(result.content[0]?.text).toContain('Express Agent Answer')
|
|
104
|
-
expect(result.content[0]?.text).toContain('The capital of France is Paris.')
|
|
22
|
+
expect(result.content[0]).toHaveProperty('type', 'text')
|
|
23
|
+
expect(result.content[0]).toHaveProperty('text')
|
|
24
|
+
expect(result.content[0]?.text).toContain('Express Agent Answer')
|
|
25
|
+
expect(result.content[0]?.text).toContain('The capital of France is Paris.')
|
|
105
26
|
|
|
106
27
|
// Verify structuredContent is minimal (not full response)
|
|
107
|
-
expect(result).toHaveProperty('structuredContent')
|
|
108
|
-
expect(result).toHaveProperty('fullResponse')
|
|
109
|
-
expect(result.structuredContent).toHaveProperty('answer')
|
|
110
|
-
expect(result.structuredContent).toHaveProperty('hasResults')
|
|
111
|
-
expect(result.structuredContent).toHaveProperty('resultCount')
|
|
112
|
-
expect(result.structuredContent).toHaveProperty('agent')
|
|
113
|
-
expect(result.structuredContent.answer).toBe(mockResponse.answer)
|
|
114
|
-
expect(result.structuredContent.hasResults).toBe(false)
|
|
115
|
-
expect(result.structuredContent.resultCount).toBe(0)
|
|
28
|
+
expect(result).toHaveProperty('structuredContent')
|
|
29
|
+
expect(result).toHaveProperty('fullResponse')
|
|
30
|
+
expect(result.structuredContent).toHaveProperty('answer')
|
|
31
|
+
expect(result.structuredContent).toHaveProperty('hasResults')
|
|
32
|
+
expect(result.structuredContent).toHaveProperty('resultCount')
|
|
33
|
+
expect(result.structuredContent).toHaveProperty('agent')
|
|
34
|
+
expect(result.structuredContent.answer).toBe(mockResponse.answer)
|
|
35
|
+
expect(result.structuredContent.hasResults).toBe(false)
|
|
36
|
+
expect(result.structuredContent.resultCount).toBe(0)
|
|
116
37
|
// No results, so results field should be undefined
|
|
117
|
-
expect(result.structuredContent.results).toBeUndefined()
|
|
118
|
-
expect(result.fullResponse).toEqual(mockResponse)
|
|
119
|
-
})
|
|
38
|
+
expect(result.structuredContent.results).toBeUndefined()
|
|
39
|
+
expect(result.fullResponse).toEqual(mockResponse)
|
|
40
|
+
})
|
|
120
41
|
|
|
121
42
|
test('formats response with answer and search results', () => {
|
|
122
43
|
const mockResponse: ExpressAgentMcpResponse = {
|
|
@@ -136,60 +57,60 @@ describe('formatExpressAgentResponse', () => {
|
|
|
136
57
|
],
|
|
137
58
|
},
|
|
138
59
|
agent: 'express',
|
|
139
|
-
}
|
|
60
|
+
}
|
|
140
61
|
|
|
141
|
-
const result = formatExpressAgentResponse(mockResponse)
|
|
62
|
+
const result = formatExpressAgentResponse(mockResponse)
|
|
142
63
|
|
|
143
64
|
// Verify content array has 2 items (answer + search results)
|
|
144
|
-
expect(result.content.length).toBe(2)
|
|
65
|
+
expect(result.content.length).toBe(2)
|
|
145
66
|
|
|
146
67
|
// Verify answer comes FIRST
|
|
147
|
-
expect(result.content[0]?.type).toBe('text')
|
|
148
|
-
expect(result.content[0]?.text).toContain('Express Agent Answer')
|
|
149
|
-
expect(result.content[0]?.text).toContain('Quantum computing is advancing rapidly')
|
|
68
|
+
expect(result.content[0]?.type).toBe('text')
|
|
69
|
+
expect(result.content[0]?.text).toContain('Express Agent Answer')
|
|
70
|
+
expect(result.content[0]?.text).toContain('Quantum computing is advancing rapidly')
|
|
150
71
|
|
|
151
72
|
// Verify search results come SECOND
|
|
152
|
-
expect(result.content[1]?.type).toBe('text')
|
|
153
|
-
expect(result.content[1]?.text).toContain('Search Results')
|
|
154
|
-
expect(result.content[1]?.text).toContain('Quantum Computing Breakthrough')
|
|
155
|
-
expect(result.content[1]?.text).toContain('Latest in Quantum Research')
|
|
73
|
+
expect(result.content[1]?.type).toBe('text')
|
|
74
|
+
expect(result.content[1]?.text).toContain('Search Results')
|
|
75
|
+
expect(result.content[1]?.text).toContain('Quantum Computing Breakthrough')
|
|
76
|
+
expect(result.content[1]?.text).toContain('Latest in Quantum Research')
|
|
156
77
|
// URLs should be in text content
|
|
157
|
-
expect(result.content[1]?.text).toContain('https://example.com/quantum1')
|
|
158
|
-
expect(result.content[1]?.text).toContain('https://example.com/quantum2')
|
|
78
|
+
expect(result.content[1]?.text).toContain('https://example.com/quantum1')
|
|
79
|
+
expect(result.content[1]?.text).toContain('https://example.com/quantum2')
|
|
159
80
|
|
|
160
81
|
// Verify structuredContent is minimal with counts
|
|
161
|
-
expect(result.structuredContent).toHaveProperty('answer')
|
|
162
|
-
expect(result.structuredContent).toHaveProperty('hasResults')
|
|
163
|
-
expect(result.structuredContent).toHaveProperty('resultCount')
|
|
164
|
-
expect(result.structuredContent.answer).toBe(mockResponse.answer)
|
|
165
|
-
expect(result.structuredContent.hasResults).toBe(true)
|
|
166
|
-
expect(result.structuredContent.resultCount).toBe(2)
|
|
82
|
+
expect(result.structuredContent).toHaveProperty('answer')
|
|
83
|
+
expect(result.structuredContent).toHaveProperty('hasResults')
|
|
84
|
+
expect(result.structuredContent).toHaveProperty('resultCount')
|
|
85
|
+
expect(result.structuredContent.answer).toBe(mockResponse.answer)
|
|
86
|
+
expect(result.structuredContent.hasResults).toBe(true)
|
|
87
|
+
expect(result.structuredContent.resultCount).toBe(2)
|
|
167
88
|
|
|
168
89
|
// URLs should be in structuredContent.results
|
|
169
|
-
expect(result.structuredContent).toHaveProperty('results')
|
|
170
|
-
expect(result.structuredContent.results?.web).toBeDefined()
|
|
171
|
-
expect(result.structuredContent.results?.web?.length).toBe(2)
|
|
90
|
+
expect(result.structuredContent).toHaveProperty('results')
|
|
91
|
+
expect(result.structuredContent.results?.web).toBeDefined()
|
|
92
|
+
expect(result.structuredContent.results?.web?.length).toBe(2)
|
|
172
93
|
expect(result.structuredContent.results?.web?.[0]).toEqual({
|
|
173
94
|
url: 'https://example.com/quantum1',
|
|
174
95
|
title: 'Quantum Computing Breakthrough',
|
|
175
|
-
})
|
|
96
|
+
})
|
|
176
97
|
expect(result.structuredContent.results?.web?.[1]).toEqual({
|
|
177
98
|
url: 'https://example.com/quantum2',
|
|
178
99
|
title: 'Latest in Quantum Research',
|
|
179
|
-
})
|
|
100
|
+
})
|
|
180
101
|
|
|
181
102
|
// Verify fullResponse has complete data
|
|
182
|
-
expect(result.fullResponse).toEqual(mockResponse)
|
|
183
|
-
expect(result.fullResponse.results?.web).toHaveLength(2)
|
|
184
|
-
})
|
|
103
|
+
expect(result.fullResponse).toEqual(mockResponse)
|
|
104
|
+
expect(result.fullResponse.results?.web).toHaveLength(2)
|
|
105
|
+
})
|
|
185
106
|
|
|
186
107
|
test('structuredContent validation for answer only', () => {
|
|
187
108
|
const mockResponse: ExpressAgentMcpResponse = {
|
|
188
109
|
answer: 'Neural networks are computational models inspired by biological neurons.',
|
|
189
110
|
agent: 'express',
|
|
190
|
-
}
|
|
111
|
+
}
|
|
191
112
|
|
|
192
|
-
const result = formatExpressAgentResponse(mockResponse)
|
|
113
|
+
const result = formatExpressAgentResponse(mockResponse)
|
|
193
114
|
|
|
194
115
|
// Verify structure matches minimal schema
|
|
195
116
|
expect(result.structuredContent).toMatchObject({
|
|
@@ -197,11 +118,11 @@ describe('formatExpressAgentResponse', () => {
|
|
|
197
118
|
hasResults: false,
|
|
198
119
|
resultCount: 0,
|
|
199
120
|
agent: 'express',
|
|
200
|
-
})
|
|
121
|
+
})
|
|
201
122
|
|
|
202
123
|
// Verify fullResponse has complete data
|
|
203
|
-
expect(result.fullResponse.results).toBeUndefined()
|
|
204
|
-
})
|
|
124
|
+
expect(result.fullResponse.results).toBeUndefined()
|
|
125
|
+
})
|
|
205
126
|
|
|
206
127
|
test('structuredContent validation for answer with results', () => {
|
|
207
128
|
const mockResponse: ExpressAgentMcpResponse = {
|
|
@@ -216,41 +137,41 @@ describe('formatExpressAgentResponse', () => {
|
|
|
216
137
|
],
|
|
217
138
|
},
|
|
218
139
|
agent: 'express',
|
|
219
|
-
}
|
|
140
|
+
}
|
|
220
141
|
|
|
221
|
-
const result = formatExpressAgentResponse(mockResponse)
|
|
142
|
+
const result = formatExpressAgentResponse(mockResponse)
|
|
222
143
|
|
|
223
144
|
// Verify structuredContent is minimal with counts
|
|
224
|
-
expect(result.structuredContent).toHaveProperty('answer')
|
|
225
|
-
expect(result.structuredContent).toHaveProperty('hasResults')
|
|
226
|
-
expect(result.structuredContent).toHaveProperty('resultCount')
|
|
227
|
-
expect(result.structuredContent).toHaveProperty('agent')
|
|
145
|
+
expect(result.structuredContent).toHaveProperty('answer')
|
|
146
|
+
expect(result.structuredContent).toHaveProperty('hasResults')
|
|
147
|
+
expect(result.structuredContent).toHaveProperty('resultCount')
|
|
148
|
+
expect(result.structuredContent).toHaveProperty('agent')
|
|
228
149
|
expect(result.structuredContent.answer).toBe(
|
|
229
150
|
'Recent AI breakthroughs include advances in language models and computer vision.',
|
|
230
|
-
)
|
|
231
|
-
expect(result.structuredContent.agent).toBe('express')
|
|
232
|
-
expect(result.structuredContent.hasResults).toBe(true)
|
|
233
|
-
expect(result.structuredContent.resultCount).toBe(1)
|
|
151
|
+
)
|
|
152
|
+
expect(result.structuredContent.agent).toBe('express')
|
|
153
|
+
expect(result.structuredContent.hasResults).toBe(true)
|
|
154
|
+
expect(result.structuredContent.resultCount).toBe(1)
|
|
234
155
|
|
|
235
156
|
// URLs should be in structuredContent.results
|
|
236
|
-
expect(result.structuredContent).toHaveProperty('results')
|
|
237
|
-
expect(result.structuredContent.results?.web).toBeDefined()
|
|
238
|
-
expect(result.structuredContent.results?.web?.length).toBe(1)
|
|
157
|
+
expect(result.structuredContent).toHaveProperty('results')
|
|
158
|
+
expect(result.structuredContent.results?.web).toBeDefined()
|
|
159
|
+
expect(result.structuredContent.results?.web?.length).toBe(1)
|
|
239
160
|
expect(result.structuredContent.results?.web?.[0]).toEqual({
|
|
240
161
|
url: 'https://example.com/ai-breakthrough',
|
|
241
162
|
title: 'AI Breakthrough 2025',
|
|
242
|
-
})
|
|
163
|
+
})
|
|
243
164
|
|
|
244
165
|
// Verify fullResponse has complete search results
|
|
245
|
-
expect(result.fullResponse).toEqual(mockResponse)
|
|
246
|
-
expect(result.fullResponse.results).toBeDefined()
|
|
247
|
-
expect(Array.isArray(result.fullResponse.results?.web)).toBe(true)
|
|
248
|
-
expect(result.fullResponse.results?.web.length).toBe(1)
|
|
166
|
+
expect(result.fullResponse).toEqual(mockResponse)
|
|
167
|
+
expect(result.fullResponse.results).toBeDefined()
|
|
168
|
+
expect(Array.isArray(result.fullResponse.results?.web)).toBe(true)
|
|
169
|
+
expect(result.fullResponse.results?.web.length).toBe(1)
|
|
249
170
|
|
|
250
171
|
// Verify search result fields in fullResponse
|
|
251
|
-
const searchResult = result.fullResponse.results?.web[0]
|
|
252
|
-
expect(searchResult?.url).toBe('https://example.com/ai-breakthrough')
|
|
253
|
-
expect(searchResult?.title).toBe('AI Breakthrough 2025')
|
|
254
|
-
expect(searchResult?.snippet).toBe('Major advances in artificial intelligence.')
|
|
255
|
-
})
|
|
256
|
-
})
|
|
172
|
+
const searchResult = result.fullResponse.results?.web[0]
|
|
173
|
+
expect(searchResult?.url).toBe('https://example.com/ai-breakthrough')
|
|
174
|
+
expect(searchResult?.title).toBe('AI Breakthrough 2025')
|
|
175
|
+
expect(searchResult?.snippet).toBe('Major advances in artificial intelligence.')
|
|
176
|
+
})
|
|
177
|
+
})
|
package/src/get-mcp-server.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
-
import packageJson from '../package.json' with { type: 'json' }
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
+
import packageJson from '../package.json' with { type: 'json' }
|
|
3
3
|
|
|
4
4
|
export const getMCpServer = () =>
|
|
5
5
|
new McpServer(
|
|
@@ -14,4 +14,4 @@ export const getMCpServer = () =>
|
|
|
14
14
|
},
|
|
15
15
|
instructions: `Use this server to search the web, get AI-powered answers with web context, and extract content from web pages using You.com. The you-contents tool extracts page content and returns it in markdown or HTML format. Use HTML format for layout preservation, interactive content, and visual fidelity; use markdown for text extraction and simpler consumption.`,
|
|
16
16
|
},
|
|
17
|
-
)
|
|
17
|
+
)
|
package/src/http.ts
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
import { StreamableHTTPTransport } from '@hono/mcp'
|
|
2
|
-
import { type Context, Hono } from 'hono'
|
|
3
|
-
import { trimTrailingSlash } from 'hono/trailing-slash'
|
|
4
|
-
import packageJson from '../package.json' with { type: 'json' }
|
|
5
|
-
import { registerContentsTool } from './contents/register-contents-tool.ts'
|
|
6
|
-
import { registerExpressTool } from './express/register-express-tool.ts'
|
|
7
|
-
import { getMCpServer } from './get-mcp-server.ts'
|
|
8
|
-
import { registerSearchTool } from './search/register-search-tool.ts'
|
|
9
|
-
import { useGetClientVersion } from './shared/use-client-version.ts'
|
|
1
|
+
import { StreamableHTTPTransport } from '@hono/mcp'
|
|
2
|
+
import { type Context, Hono } from 'hono'
|
|
3
|
+
import { trimTrailingSlash } from 'hono/trailing-slash'
|
|
4
|
+
import packageJson from '../package.json' with { type: 'json' }
|
|
5
|
+
import { registerContentsTool } from './contents/register-contents-tool.ts'
|
|
6
|
+
import { registerExpressTool } from './express/register-express-tool.ts'
|
|
7
|
+
import { getMCpServer } from './get-mcp-server.ts'
|
|
8
|
+
import { registerSearchTool } from './search/register-search-tool.ts'
|
|
9
|
+
import { useGetClientVersion } from './shared/use-client-version.ts'
|
|
10
10
|
|
|
11
11
|
const extractBearerToken = (authHeader: string | null): string | null => {
|
|
12
12
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
13
|
-
return null
|
|
13
|
+
return null
|
|
14
14
|
}
|
|
15
|
-
return authHeader.slice(7)
|
|
16
|
-
}
|
|
15
|
+
return authHeader.slice(7)
|
|
16
|
+
}
|
|
17
17
|
|
|
18
18
|
const handleMcpRequest = async (c: Context) => {
|
|
19
|
-
const authHeader = c.req.header('Authorization')
|
|
19
|
+
const authHeader = c.req.header('Authorization')
|
|
20
20
|
|
|
21
21
|
if (!authHeader) {
|
|
22
|
-
c.status(401)
|
|
23
|
-
c.header('Content-Type', 'text/plain')
|
|
24
|
-
return c.text('Unauthorized: Authorization header required')
|
|
22
|
+
c.status(401)
|
|
23
|
+
c.header('Content-Type', 'text/plain')
|
|
24
|
+
return c.text('Unauthorized: Authorization header required')
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
const YDC_API_KEY = extractBearerToken(authHeader)
|
|
27
|
+
const YDC_API_KEY = extractBearerToken(authHeader)
|
|
28
28
|
|
|
29
29
|
if (!YDC_API_KEY) {
|
|
30
|
-
c.status(401)
|
|
31
|
-
c.header('Content-Type', 'text/plain')
|
|
32
|
-
return c.text('Unauthorized: Bearer token required')
|
|
30
|
+
c.status(401)
|
|
31
|
+
c.header('Content-Type', 'text/plain')
|
|
32
|
+
return c.text('Unauthorized: Bearer token required')
|
|
33
33
|
}
|
|
34
|
-
const mcp = getMCpServer()
|
|
35
|
-
const getUserAgent = useGetClientVersion(mcp)
|
|
34
|
+
const mcp = getMCpServer()
|
|
35
|
+
const getUserAgent = useGetClientVersion(mcp)
|
|
36
36
|
|
|
37
37
|
registerSearchTool({
|
|
38
38
|
mcp,
|
|
39
39
|
YDC_API_KEY,
|
|
40
40
|
getUserAgent,
|
|
41
|
-
})
|
|
42
|
-
registerExpressTool({ mcp, YDC_API_KEY, getUserAgent })
|
|
43
|
-
registerContentsTool({ mcp, YDC_API_KEY, getUserAgent })
|
|
41
|
+
})
|
|
42
|
+
registerExpressTool({ mcp, YDC_API_KEY, getUserAgent })
|
|
43
|
+
registerContentsTool({ mcp, YDC_API_KEY, getUserAgent })
|
|
44
44
|
|
|
45
|
-
const transport = new StreamableHTTPTransport()
|
|
46
|
-
await mcp.connect(transport)
|
|
47
|
-
const response = await transport.handleRequest(c)
|
|
45
|
+
const transport = new StreamableHTTPTransport()
|
|
46
|
+
await mcp.connect(transport)
|
|
47
|
+
const response = await transport.handleRequest(c)
|
|
48
48
|
|
|
49
49
|
// Explicitly set Content-Encoding to 'identity' to prevent httpx auto-decompression issues
|
|
50
50
|
// httpx by default sends Accept-Encoding and attempts decompression, but MCP SSE streams
|
|
51
51
|
// are not compressed. Setting 'identity' tells clients the response is uncompressed.
|
|
52
|
-
response?.headers.set('Content-Encoding', 'identity')
|
|
52
|
+
response?.headers.set('Content-Encoding', 'identity')
|
|
53
53
|
|
|
54
|
-
return response
|
|
55
|
-
}
|
|
54
|
+
return response
|
|
55
|
+
}
|
|
56
56
|
|
|
57
|
-
const app = new Hono()
|
|
58
|
-
app.use(trimTrailingSlash())
|
|
57
|
+
const app = new Hono()
|
|
58
|
+
app.use(trimTrailingSlash())
|
|
59
59
|
|
|
60
60
|
app.get('/mcp-health', async (c) => {
|
|
61
61
|
return c.json({
|
|
@@ -63,10 +63,10 @@ app.get('/mcp-health', async (c) => {
|
|
|
63
63
|
timestamp: new Date().toISOString(),
|
|
64
64
|
version: packageJson.version,
|
|
65
65
|
service: 'youdotcom-mcp-server',
|
|
66
|
-
})
|
|
67
|
-
})
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
68
|
|
|
69
|
-
app.all('/mcp', handleMcpRequest)
|
|
70
|
-
app.all('/mcp/', handleMcpRequest)
|
|
69
|
+
app.all('/mcp', handleMcpRequest)
|
|
70
|
+
app.all('/mcp/', handleMcpRequest)
|
|
71
71
|
|
|
72
|
-
export default app
|
|
72
|
+
export default app
|
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
-
import { generateErrorReportLink } from '
|
|
3
|
-
import { getLogger } from '../shared/get-logger.ts'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
6
|
|
|
7
7
|
export const registerSearchTool = ({
|
|
8
8
|
mcp,
|
|
9
9
|
YDC_API_KEY,
|
|
10
10
|
getUserAgent,
|
|
11
11
|
}: {
|
|
12
|
-
mcp: McpServer
|
|
13
|
-
YDC_API_KEY?: string
|
|
14
|
-
getUserAgent: () => string
|
|
12
|
+
mcp: McpServer
|
|
13
|
+
YDC_API_KEY?: string
|
|
14
|
+
getUserAgent: () => string
|
|
15
15
|
}) => {
|
|
16
16
|
mcp.registerTool(
|
|
17
17
|
'you-search',
|
|
@@ -22,22 +22,22 @@ export const registerSearchTool = ({
|
|
|
22
22
|
outputSchema: SearchStructuredContentSchema.shape,
|
|
23
23
|
},
|
|
24
24
|
async (searchQuery) => {
|
|
25
|
-
const logger = getLogger(mcp)
|
|
25
|
+
const logger = getLogger(mcp)
|
|
26
26
|
try {
|
|
27
27
|
const response = await fetchSearchResults({
|
|
28
28
|
searchQuery,
|
|
29
29
|
YDC_API_KEY,
|
|
30
30
|
getUserAgent,
|
|
31
|
-
})
|
|
31
|
+
})
|
|
32
32
|
|
|
33
|
-
const webCount = response.results.web?.length ?? 0
|
|
34
|
-
const newsCount = response.results.news?.length ?? 0
|
|
33
|
+
const webCount = response.results.web?.length ?? 0
|
|
34
|
+
const newsCount = response.results.news?.length ?? 0
|
|
35
35
|
|
|
36
36
|
if (!webCount && !newsCount) {
|
|
37
37
|
await logger({
|
|
38
38
|
level: 'info',
|
|
39
39
|
data: `No results found for query: "${searchQuery.query}"`,
|
|
40
|
-
})
|
|
40
|
+
})
|
|
41
41
|
|
|
42
42
|
return {
|
|
43
43
|
content: [{ type: 'text' as const, text: 'No results found.' }],
|
|
@@ -48,28 +48,28 @@ export const registerSearchTool = ({
|
|
|
48
48
|
total: 0,
|
|
49
49
|
},
|
|
50
50
|
},
|
|
51
|
-
}
|
|
51
|
+
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
await logger({
|
|
55
55
|
level: 'info',
|
|
56
56
|
data: `Search successful for query: "${searchQuery.query}" - ${webCount} web results, ${newsCount} news results (${webCount + newsCount} total)`,
|
|
57
|
-
})
|
|
57
|
+
})
|
|
58
58
|
|
|
59
|
-
const { content, structuredContent } = formatSearchResults(response)
|
|
60
|
-
return { content, structuredContent }
|
|
59
|
+
const { content, structuredContent } = formatSearchResults(response)
|
|
60
|
+
return { content, structuredContent }
|
|
61
61
|
} catch (err: unknown) {
|
|
62
|
-
const errorMessage = err instanceof Error ? err.message : String(err)
|
|
62
|
+
const errorMessage = err instanceof Error ? err.message : String(err)
|
|
63
63
|
const reportLink = generateErrorReportLink({
|
|
64
64
|
errorMessage,
|
|
65
65
|
tool: 'you-search',
|
|
66
66
|
clientInfo: getUserAgent(),
|
|
67
|
-
})
|
|
67
|
+
})
|
|
68
68
|
|
|
69
69
|
await logger({
|
|
70
70
|
level: 'error',
|
|
71
71
|
data: `Search API call failed: ${errorMessage}\n\nReport this issue: ${reportLink}`,
|
|
72
|
-
})
|
|
72
|
+
})
|
|
73
73
|
|
|
74
74
|
return {
|
|
75
75
|
content: [
|
|
@@ -80,8 +80,8 @@ export const registerSearchTool = ({
|
|
|
80
80
|
],
|
|
81
81
|
structuredContent: undefined,
|
|
82
82
|
isError: true,
|
|
83
|
-
}
|
|
83
|
+
}
|
|
84
84
|
}
|
|
85
85
|
},
|
|
86
|
-
)
|
|
87
|
-
}
|
|
86
|
+
)
|
|
87
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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>
|