@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.
Files changed (49) hide show
  1. package/bin/stdio.js +394 -415
  2. package/package.json +12 -23
  3. package/src/contents/contents.schemas.ts +3 -48
  4. package/src/contents/contents.utils.ts +33 -133
  5. package/src/contents/register-contents-tool.ts +24 -24
  6. package/src/contents/tests/contents.utils.spec.ts +58 -134
  7. package/src/express/express.schema.ts +23 -0
  8. package/src/express/express.utils.ts +10 -121
  9. package/src/express/register-express-tool.ts +19 -19
  10. package/src/express/tests/express.utils.spec.ts +80 -159
  11. package/src/get-mcp-server.ts +3 -3
  12. package/src/http.ts +38 -38
  13. package/src/search/register-search-tool.ts +23 -23
  14. package/src/search/search.schema.ts +38 -0
  15. package/src/search/search.utils.ts +18 -96
  16. package/src/search/tests/search.utils.spec.ts +61 -194
  17. package/src/shared/format-search-results-text.ts +16 -16
  18. package/src/shared/get-logger.ts +4 -4
  19. package/src/shared/tests/shared.utils.spec.ts +56 -56
  20. package/src/shared/use-client-version.ts +8 -8
  21. package/src/stdio.ts +16 -16
  22. package/src/tests/http.spec.ts +105 -105
  23. package/src/tests/tool.spec.ts +212 -212
  24. package/dist/contents/contents.schemas.d.ts +0 -55
  25. package/dist/contents/contents.utils.d.ts +0 -28
  26. package/dist/contents/register-contents-tool.d.ts +0 -10
  27. package/dist/express/express.schemas.d.ts +0 -56
  28. package/dist/express/express.utils.d.ts +0 -45
  29. package/dist/express/register-express-tool.d.ts +0 -6
  30. package/dist/get-mcp-server.d.ts +0 -2
  31. package/dist/http.d.ts +0 -3
  32. package/dist/main.d.ts +0 -9
  33. package/dist/search/register-search-tool.d.ts +0 -6
  34. package/dist/search/search.schemas.d.ts +0 -133
  35. package/dist/search/search.utils.d.ts +0 -98
  36. package/dist/shared/api-constants.d.ts +0 -9
  37. package/dist/shared/check-response-for-errors.d.ts +0 -6
  38. package/dist/shared/format-search-results-text.d.ts +0 -19
  39. package/dist/shared/generate-error-report-link.d.ts +0 -9
  40. package/dist/shared/get-logger.d.ts +0 -7
  41. package/dist/shared/use-client-version.d.ts +0 -6
  42. package/dist/stdio.d.ts +0 -2
  43. package/src/express/express.schemas.ts +0 -99
  44. package/src/main.ts +0 -9
  45. package/src/search/search.schemas.ts +0 -147
  46. package/src/shared/api-constants.ts +0 -10
  47. package/src/shared/check-response-for-errors.ts +0 -13
  48. package/src/shared/generate-error-report-link.ts +0 -37
  49. 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 '../express.schemas.ts';
3
- import { callExpressAgent, formatExpressAgentResponse } from '../express.utils.ts';
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
- const getUserAgent = () => 'MCP/test (You.com; test-client)';
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
+ })
@@ -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 '../shared/generate-error-report-link.ts';
3
- import { getLogger } from '../shared/get-logger.ts';
4
- import { SearchQuerySchema, SearchStructuredContentSchema } from './search.schemas.ts';
5
- import { fetchSearchResults, formatSearchResults } from './search.utils.ts';
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>