@youdotcom-oss/mcp 1.6.0 → 2.0.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 +1 -13
- package/bin/stdio.js +145 -325
- package/package.json +2 -2
- package/src/contents/contents.schemas.ts +4 -4
- package/src/contents/contents.utils.ts +8 -26
- package/src/http.ts +0 -2
- package/src/shared/format-search-results-text.ts +4 -4
- package/src/shared/use-client-version.ts +1 -1
- package/src/stdio.ts +0 -2
- package/src/tests/tool.spec.ts +12 -73
- package/src/express/express.schema.ts +0 -23
- package/src/express/express.utils.ts +0 -44
- package/src/express/register-express-tool.ts +0 -67
- package/src/express/tests/express.utils.spec.ts +0 -177
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youdotcom-oss/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "You.com API Model Context Protocol Server - For programmatic API access, use @youdotcom-oss/api",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"engines": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"mcpName": "io.github.youdotcom-oss/mcp",
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@youdotcom-oss/api": "0.
|
|
59
|
+
"@youdotcom-oss/api": "0.2.0",
|
|
60
60
|
"zod": "^4.3.6",
|
|
61
61
|
"@hono/mcp": "^0.2.3",
|
|
62
62
|
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
@@ -16,12 +16,12 @@ export const ContentsStructuredContentSchema = z.object({
|
|
|
16
16
|
html: z.string().optional().describe('HTML content'),
|
|
17
17
|
metadata: z
|
|
18
18
|
.object({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
twitter: z.record(z.string(), z.string()).optional(),
|
|
19
|
+
favicon_url: z.string().describe('Favicon URL'),
|
|
20
|
+
site_name: z.string().optional().nullable().describe('Site name'),
|
|
22
21
|
})
|
|
23
22
|
.optional()
|
|
24
|
-
.
|
|
23
|
+
.nullable()
|
|
24
|
+
.describe('Page metadata'),
|
|
25
25
|
}),
|
|
26
26
|
)
|
|
27
27
|
.describe('Extracted items'),
|
|
@@ -47,30 +47,12 @@ export const formatContentsResponse = (
|
|
|
47
47
|
if (formats.includes('metadata') && item.metadata) {
|
|
48
48
|
textParts.push('\n### Metadata\n')
|
|
49
49
|
|
|
50
|
-
if (item.metadata.
|
|
51
|
-
textParts.push(
|
|
52
|
-
const jsonldStr = JSON.stringify(item.metadata.jsonld, null, 2)
|
|
53
|
-
if (jsonldStr.length > 2000) {
|
|
54
|
-
textParts.push(jsonldStr.substring(0, 2000))
|
|
55
|
-
textParts.push('\n...(truncated for display, see structuredContent for full data)')
|
|
56
|
-
} else {
|
|
57
|
-
textParts.push(jsonldStr)
|
|
58
|
-
}
|
|
59
|
-
textParts.push('\n')
|
|
50
|
+
if (item.metadata.site_name) {
|
|
51
|
+
textParts.push(`**Site Name:** ${item.metadata.site_name}\n`)
|
|
60
52
|
}
|
|
61
53
|
|
|
62
|
-
if (item.metadata.
|
|
63
|
-
textParts.push(
|
|
64
|
-
for (const [key, value] of Object.entries(item.metadata.opengraph)) {
|
|
65
|
-
textParts.push(`- ${key}: ${value}\n`)
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (item.metadata.twitter) {
|
|
70
|
-
textParts.push('\n**Twitter:**\n')
|
|
71
|
-
for (const [key, value] of Object.entries(item.metadata.twitter)) {
|
|
72
|
-
textParts.push(`- ${key}: ${value}\n`)
|
|
73
|
-
}
|
|
54
|
+
if (item.metadata.favicon_url) {
|
|
55
|
+
textParts.push(`**Favicon:** ${item.metadata.favicon_url}\n`)
|
|
74
56
|
}
|
|
75
57
|
}
|
|
76
58
|
|
|
@@ -79,10 +61,10 @@ export const formatContentsResponse = (
|
|
|
79
61
|
// Add to structured content
|
|
80
62
|
items.push({
|
|
81
63
|
url: item.url,
|
|
82
|
-
title: item.title,
|
|
83
|
-
markdown: item.markdown,
|
|
84
|
-
html: item.html,
|
|
85
|
-
metadata: item.metadata,
|
|
64
|
+
title: item.title ?? undefined,
|
|
65
|
+
markdown: item.markdown ?? undefined,
|
|
66
|
+
html: item.html ?? undefined,
|
|
67
|
+
metadata: item.metadata ?? undefined,
|
|
86
68
|
})
|
|
87
69
|
}
|
|
88
70
|
|
package/src/http.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { type Context, Hono } from 'hono'
|
|
|
3
3
|
import { trimTrailingSlash } from 'hono/trailing-slash'
|
|
4
4
|
import packageJson from '../package.json' with { type: 'json' }
|
|
5
5
|
import { registerContentsTool } from './contents/register-contents-tool.ts'
|
|
6
|
-
import { registerExpressTool } from './express/register-express-tool.ts'
|
|
7
6
|
import { getMCpServer } from './get-mcp-server.ts'
|
|
8
7
|
import { registerSearchTool } from './search/register-search-tool.ts'
|
|
9
8
|
import { useGetClientVersion } from './shared/use-client-version.ts'
|
|
@@ -39,7 +38,6 @@ const handleMcpRequest = async (c: Context) => {
|
|
|
39
38
|
YDC_API_KEY,
|
|
40
39
|
getUserAgent,
|
|
41
40
|
})
|
|
42
|
-
registerExpressTool({ mcp, YDC_API_KEY, getUserAgent })
|
|
43
41
|
registerContentsTool({ mcp, YDC_API_KEY, getUserAgent })
|
|
44
42
|
|
|
45
43
|
const transport = new StreamableHTTPTransport()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Generic search result type
|
|
3
|
-
* Used by
|
|
2
|
+
* Generic search result type for Search API results
|
|
3
|
+
* Used by search.utils.ts
|
|
4
4
|
*/
|
|
5
5
|
type GenericSearchResult = {
|
|
6
6
|
url: string
|
|
@@ -13,7 +13,7 @@ type GenericSearchResult = {
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Format array of search results into display text
|
|
16
|
-
* Used by
|
|
16
|
+
* Used by search result formatting
|
|
17
17
|
* @param results - Array of search results to format
|
|
18
18
|
*/
|
|
19
19
|
export const formatSearchResultsText = (results: GenericSearchResult[]): string => {
|
|
@@ -38,7 +38,7 @@ export const formatSearchResultsText = (results: GenericSearchResult[]): string
|
|
|
38
38
|
if (result.snippets && result.snippets.length > 0) {
|
|
39
39
|
parts.push(`Snippets:\n- ${result.snippets.join('\n- ')}`)
|
|
40
40
|
}
|
|
41
|
-
// Handle single snippet
|
|
41
|
+
// Handle single snippet
|
|
42
42
|
else if (result.snippet) {
|
|
43
43
|
parts.push(`Snippet: ${result.snippet}`)
|
|
44
44
|
}
|
|
@@ -3,7 +3,7 @@ import packageJson from '../../package.json' with { type: 'json' }
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Creates User-Agent string for API requests
|
|
6
|
-
* Used by search and
|
|
6
|
+
* Used by search and contents API calls
|
|
7
7
|
*/
|
|
8
8
|
const setUserAgent = (client: string) => `MCP/${packageJson.version} (You.com; ${client})`
|
|
9
9
|
|
package/src/stdio.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
3
|
import { registerContentsTool } from './contents/register-contents-tool.ts'
|
|
4
|
-
import { registerExpressTool } from './express/register-express-tool.ts'
|
|
5
4
|
import { getMCpServer } from './get-mcp-server.ts'
|
|
6
5
|
import { registerSearchTool } from './search/register-search-tool.ts'
|
|
7
6
|
import { useGetClientVersion } from './shared/use-client-version.ts'
|
|
@@ -13,7 +12,6 @@ try {
|
|
|
13
12
|
const getUserAgent = useGetClientVersion(mcp)
|
|
14
13
|
|
|
15
14
|
registerSearchTool({ mcp, YDC_API_KEY, getUserAgent })
|
|
16
|
-
registerExpressTool({ mcp, YDC_API_KEY, getUserAgent })
|
|
17
15
|
registerContentsTool({ mcp, YDC_API_KEY, getUserAgent })
|
|
18
16
|
|
|
19
17
|
const transport = new StdioServerTransport()
|
package/src/tests/tool.spec.ts
CHANGED
|
@@ -212,13 +212,12 @@ describe('registerSearchTool', () => {
|
|
|
212
212
|
)
|
|
213
213
|
|
|
214
214
|
test(
|
|
215
|
-
'handles site
|
|
215
|
+
'handles site: operator in query',
|
|
216
216
|
async () => {
|
|
217
217
|
const result = await client.callTool({
|
|
218
218
|
name: 'you-search',
|
|
219
219
|
arguments: {
|
|
220
|
-
query: 'react components',
|
|
221
|
-
site: 'github.com',
|
|
220
|
+
query: 'react components site:github.com',
|
|
222
221
|
},
|
|
223
222
|
})
|
|
224
223
|
|
|
@@ -229,13 +228,12 @@ describe('registerSearchTool', () => {
|
|
|
229
228
|
)
|
|
230
229
|
|
|
231
230
|
test(
|
|
232
|
-
'handles
|
|
231
|
+
'handles filetype: operator in query',
|
|
233
232
|
async () => {
|
|
234
233
|
const result = await client.callTool({
|
|
235
234
|
name: 'you-search',
|
|
236
235
|
arguments: {
|
|
237
|
-
query: 'documentation',
|
|
238
|
-
fileType: 'pdf',
|
|
236
|
+
query: 'documentation filetype:pdf',
|
|
239
237
|
},
|
|
240
238
|
})
|
|
241
239
|
|
|
@@ -246,13 +244,12 @@ describe('registerSearchTool', () => {
|
|
|
246
244
|
)
|
|
247
245
|
|
|
248
246
|
test(
|
|
249
|
-
'handles
|
|
247
|
+
'handles lang: operator in query',
|
|
250
248
|
async () => {
|
|
251
249
|
const result = await client.callTool({
|
|
252
250
|
name: 'you-search',
|
|
253
251
|
arguments: {
|
|
254
|
-
query: 'tutorial',
|
|
255
|
-
language: 'es',
|
|
252
|
+
query: 'tutorial lang:es',
|
|
256
253
|
},
|
|
257
254
|
})
|
|
258
255
|
|
|
@@ -263,13 +260,12 @@ describe('registerSearchTool', () => {
|
|
|
263
260
|
)
|
|
264
261
|
|
|
265
262
|
test(
|
|
266
|
-
'handles
|
|
263
|
+
'handles + operator for required terms in query',
|
|
267
264
|
async () => {
|
|
268
265
|
const result = await client.callTool({
|
|
269
266
|
name: 'you-search',
|
|
270
267
|
arguments: {
|
|
271
|
-
query: 'programming',
|
|
272
|
-
exactTerms: 'javascript|typescript',
|
|
268
|
+
query: 'programming +javascript +typescript',
|
|
273
269
|
},
|
|
274
270
|
})
|
|
275
271
|
|
|
@@ -280,13 +276,12 @@ describe('registerSearchTool', () => {
|
|
|
280
276
|
)
|
|
281
277
|
|
|
282
278
|
test(
|
|
283
|
-
'handles
|
|
279
|
+
'handles - operator for excluded terms in query',
|
|
284
280
|
async () => {
|
|
285
281
|
const result = await client.callTool({
|
|
286
282
|
name: 'you-search',
|
|
287
283
|
arguments: {
|
|
288
|
-
query: 'tutorial',
|
|
289
|
-
excludeTerms: 'beginner|basic',
|
|
284
|
+
query: 'tutorial -beginner -basic',
|
|
290
285
|
},
|
|
291
286
|
})
|
|
292
287
|
|
|
@@ -297,54 +292,17 @@ describe('registerSearchTool', () => {
|
|
|
297
292
|
)
|
|
298
293
|
|
|
299
294
|
test(
|
|
300
|
-
'handles
|
|
301
|
-
async () => {
|
|
302
|
-
const result = await client.callTool({
|
|
303
|
-
name: 'you-search',
|
|
304
|
-
arguments: {
|
|
305
|
-
query: 'programming',
|
|
306
|
-
exactTerms: '(machine learning)|typescript',
|
|
307
|
-
},
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
const content = result.content as { type: string; text: string }[]
|
|
311
|
-
expect(content[0]?.text).toContain('programming')
|
|
312
|
-
},
|
|
313
|
-
{ retry: 2 },
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
test(
|
|
317
|
-
'handles multi-word phrases with parentheses in excludeTerms',
|
|
318
|
-
async () => {
|
|
319
|
-
const result = await client.callTool({
|
|
320
|
-
name: 'you-search',
|
|
321
|
-
arguments: {
|
|
322
|
-
query: 'programming',
|
|
323
|
-
excludeTerms: '(social media)|ads',
|
|
324
|
-
},
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
const content = result.content as { type: string; text: string }[]
|
|
328
|
-
expect(content[0]?.text).toContain('programming')
|
|
329
|
-
},
|
|
330
|
-
{ retry: 2 },
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
test(
|
|
334
|
-
'handles complex search with multiple parameters',
|
|
295
|
+
'handles complex search with multiple operators in query',
|
|
335
296
|
async () => {
|
|
336
297
|
const result = await client.callTool({
|
|
337
298
|
name: 'you-search',
|
|
338
299
|
arguments: {
|
|
339
|
-
query: 'machine learning tutorial',
|
|
300
|
+
query: 'machine learning tutorial site:github.com filetype:md lang:en',
|
|
340
301
|
count: 5,
|
|
341
302
|
offset: 1,
|
|
342
303
|
freshness: 'month',
|
|
343
304
|
country: 'US',
|
|
344
305
|
safesearch: 'moderate',
|
|
345
|
-
site: 'github.com',
|
|
346
|
-
fileType: 'md',
|
|
347
|
-
language: 'en',
|
|
348
306
|
},
|
|
349
307
|
})
|
|
350
308
|
|
|
@@ -431,25 +389,6 @@ describe('registerSearchTool', () => {
|
|
|
431
389
|
{ retry: 2 },
|
|
432
390
|
)
|
|
433
391
|
|
|
434
|
-
test(
|
|
435
|
-
'returns error when both exactTerms and excludeTerms are provided',
|
|
436
|
-
async () => {
|
|
437
|
-
const result = await client.callTool({
|
|
438
|
-
name: 'you-search',
|
|
439
|
-
arguments: {
|
|
440
|
-
query: 'programming',
|
|
441
|
-
exactTerms: 'javascript',
|
|
442
|
-
excludeTerms: 'beginner',
|
|
443
|
-
},
|
|
444
|
-
})
|
|
445
|
-
|
|
446
|
-
expect(result.isError).toBe(true)
|
|
447
|
-
const content = result.content as { type: string; text: string }[]
|
|
448
|
-
expect(content[0]?.text).toContain('Cannot specify both exactTerms and excludeTerms - please use only one')
|
|
449
|
-
},
|
|
450
|
-
{ retry: 2 },
|
|
451
|
-
)
|
|
452
|
-
|
|
453
392
|
test('handles API errors gracefully', async () => {
|
|
454
393
|
try {
|
|
455
394
|
await client.callTool({
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import * as z from 'zod'
|
|
2
|
-
|
|
3
|
-
// Minimal schema for structuredContent (reduces payload duplication)
|
|
4
|
-
export const ExpressStructuredContentSchema = z.object({
|
|
5
|
-
answer: z.string().describe('AI answer'),
|
|
6
|
-
hasResults: z.boolean().describe('Has web results'),
|
|
7
|
-
resultCount: z.number().describe('Result count'),
|
|
8
|
-
agent: z.string().optional().describe('Agent ID'),
|
|
9
|
-
results: z
|
|
10
|
-
.object({
|
|
11
|
-
web: z
|
|
12
|
-
.array(
|
|
13
|
-
z.object({
|
|
14
|
-
url: z.string().describe('URL'),
|
|
15
|
-
title: z.string().describe('Title'),
|
|
16
|
-
}),
|
|
17
|
-
)
|
|
18
|
-
.optional()
|
|
19
|
-
.describe('Web results'),
|
|
20
|
-
})
|
|
21
|
-
.optional()
|
|
22
|
-
.describe('Search results'),
|
|
23
|
-
})
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { ExpressAgentMcpResponse } from '@youdotcom-oss/api'
|
|
2
|
-
import { formatSearchResultsText } from '../shared/format-search-results-text.ts'
|
|
3
|
-
|
|
4
|
-
export const formatExpressAgentResponse = (response: ExpressAgentMcpResponse) => {
|
|
5
|
-
const _agentId = response.agent || 'express'
|
|
6
|
-
const content: Array<{ type: 'text'; text: string }> = []
|
|
7
|
-
|
|
8
|
-
// 1. Answer first (always present)
|
|
9
|
-
content.push({
|
|
10
|
-
type: 'text',
|
|
11
|
-
text: `Express Agent Answer:\n\n${response.answer}`,
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
// 2. Search results second (if present when web_search tool was used) - without URLs in text
|
|
15
|
-
if (response.results?.web?.length) {
|
|
16
|
-
const formattedResults = formatSearchResultsText(response.results.web)
|
|
17
|
-
content.push({
|
|
18
|
-
type: 'text',
|
|
19
|
-
text: `\nSearch Results:\n\n${formattedResults}`,
|
|
20
|
-
})
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// Extract URLs and titles for structuredContent
|
|
24
|
-
const structuredResults = response.results?.web?.length
|
|
25
|
-
? {
|
|
26
|
-
web: response.results.web.map((result) => ({
|
|
27
|
-
url: result.url,
|
|
28
|
-
title: result.title,
|
|
29
|
-
})),
|
|
30
|
-
}
|
|
31
|
-
: undefined
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
content,
|
|
35
|
-
structuredContent: {
|
|
36
|
-
answer: response.answer,
|
|
37
|
-
hasResults: !!response.results?.web?.length,
|
|
38
|
-
resultCount: response.results?.web?.length || 0,
|
|
39
|
-
agent: response.agent,
|
|
40
|
-
results: structuredResults,
|
|
41
|
-
},
|
|
42
|
-
fullResponse: response,
|
|
43
|
-
}
|
|
44
|
-
}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
-
import { callExpressAgent, ExpressAgentInputSchema, generateErrorReportLink } from '@youdotcom-oss/api'
|
|
3
|
-
import { getLogger } from '../shared/get-logger.ts'
|
|
4
|
-
import { ExpressStructuredContentSchema } from './express.schema.ts'
|
|
5
|
-
import { formatExpressAgentResponse } from './express.utils.ts'
|
|
6
|
-
|
|
7
|
-
export const registerExpressTool = ({
|
|
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-express',
|
|
18
|
-
{
|
|
19
|
-
title: 'Express Agent',
|
|
20
|
-
description: 'Fast AI answers with web search',
|
|
21
|
-
inputSchema: ExpressAgentInputSchema.shape,
|
|
22
|
-
outputSchema: ExpressStructuredContentSchema.shape,
|
|
23
|
-
},
|
|
24
|
-
async (agentInput) => {
|
|
25
|
-
const logger = getLogger(mcp)
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
const response = await callExpressAgent({
|
|
29
|
-
agentInput,
|
|
30
|
-
YDC_API_KEY,
|
|
31
|
-
getUserAgent,
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
await logger({
|
|
35
|
-
level: 'info',
|
|
36
|
-
data: `Express agent call successful for input: "${agentInput.input}"`,
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
const { content, structuredContent } = formatExpressAgentResponse(response)
|
|
40
|
-
return { content, structuredContent }
|
|
41
|
-
} catch (err: unknown) {
|
|
42
|
-
const errorMessage = err instanceof Error ? err.message : String(err)
|
|
43
|
-
const reportLink = generateErrorReportLink({
|
|
44
|
-
errorMessage,
|
|
45
|
-
tool: 'you-express',
|
|
46
|
-
clientInfo: getUserAgent(),
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
await logger({
|
|
50
|
-
level: 'error',
|
|
51
|
-
data: `Express agent call failed: ${errorMessage}\n\nReport this issue: ${reportLink}`,
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
content: [
|
|
56
|
-
{
|
|
57
|
-
type: 'text' as const,
|
|
58
|
-
text: `Error: ${errorMessage}`,
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
structuredContent: undefined,
|
|
62
|
-
isError: true,
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
},
|
|
66
|
-
)
|
|
67
|
-
}
|
|
@@ -1,177 +0,0 @@
|
|
|
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
|
-
|
|
5
|
-
setDefaultTimeout(20_000)
|
|
6
|
-
|
|
7
|
-
describe('formatExpressAgentResponse', () => {
|
|
8
|
-
test('formats response with answer only (no search results)', () => {
|
|
9
|
-
const mockResponse: ExpressAgentMcpResponse = {
|
|
10
|
-
answer: 'The capital of France is Paris.',
|
|
11
|
-
agent: 'express',
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const result = formatExpressAgentResponse(mockResponse)
|
|
15
|
-
|
|
16
|
-
// Verify content array has 1 item (answer only)
|
|
17
|
-
expect(result).toHaveProperty('content')
|
|
18
|
-
expect(Array.isArray(result.content)).toBe(true)
|
|
19
|
-
expect(result.content.length).toBe(1)
|
|
20
|
-
|
|
21
|
-
// Verify answer content
|
|
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.')
|
|
26
|
-
|
|
27
|
-
// Verify structuredContent is minimal (not full response)
|
|
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)
|
|
37
|
-
// No results, so results field should be undefined
|
|
38
|
-
expect(result.structuredContent.results).toBeUndefined()
|
|
39
|
-
expect(result.fullResponse).toEqual(mockResponse)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('formats response with answer and search results', () => {
|
|
43
|
-
const mockResponse: ExpressAgentMcpResponse = {
|
|
44
|
-
answer: 'Quantum computing is advancing rapidly with recent breakthroughs in error correction.',
|
|
45
|
-
results: {
|
|
46
|
-
web: [
|
|
47
|
-
{
|
|
48
|
-
url: 'https://example.com/quantum1',
|
|
49
|
-
title: 'Quantum Computing Breakthrough',
|
|
50
|
-
snippet: 'Scientists achieve quantum error correction milestone.',
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
url: 'https://example.com/quantum2',
|
|
54
|
-
title: 'Latest in Quantum Research',
|
|
55
|
-
snippet: 'New quantum processor demonstrates superiority.',
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
},
|
|
59
|
-
agent: 'express',
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const result = formatExpressAgentResponse(mockResponse)
|
|
63
|
-
|
|
64
|
-
// Verify content array has 2 items (answer + search results)
|
|
65
|
-
expect(result.content.length).toBe(2)
|
|
66
|
-
|
|
67
|
-
// Verify answer comes FIRST
|
|
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')
|
|
71
|
-
|
|
72
|
-
// Verify search results come SECOND
|
|
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')
|
|
77
|
-
// URLs should be in text content
|
|
78
|
-
expect(result.content[1]?.text).toContain('https://example.com/quantum1')
|
|
79
|
-
expect(result.content[1]?.text).toContain('https://example.com/quantum2')
|
|
80
|
-
|
|
81
|
-
// Verify structuredContent is minimal with counts
|
|
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)
|
|
88
|
-
|
|
89
|
-
// URLs should be in structuredContent.results
|
|
90
|
-
expect(result.structuredContent).toHaveProperty('results')
|
|
91
|
-
expect(result.structuredContent.results?.web).toBeDefined()
|
|
92
|
-
expect(result.structuredContent.results?.web?.length).toBe(2)
|
|
93
|
-
expect(result.structuredContent.results?.web?.[0]).toEqual({
|
|
94
|
-
url: 'https://example.com/quantum1',
|
|
95
|
-
title: 'Quantum Computing Breakthrough',
|
|
96
|
-
})
|
|
97
|
-
expect(result.structuredContent.results?.web?.[1]).toEqual({
|
|
98
|
-
url: 'https://example.com/quantum2',
|
|
99
|
-
title: 'Latest in Quantum Research',
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
// Verify fullResponse has complete data
|
|
103
|
-
expect(result.fullResponse).toEqual(mockResponse)
|
|
104
|
-
expect(result.fullResponse.results?.web).toHaveLength(2)
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
test('structuredContent validation for answer only', () => {
|
|
108
|
-
const mockResponse: ExpressAgentMcpResponse = {
|
|
109
|
-
answer: 'Neural networks are computational models inspired by biological neurons.',
|
|
110
|
-
agent: 'express',
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const result = formatExpressAgentResponse(mockResponse)
|
|
114
|
-
|
|
115
|
-
// Verify structure matches minimal schema
|
|
116
|
-
expect(result.structuredContent).toMatchObject({
|
|
117
|
-
answer: expect.any(String),
|
|
118
|
-
hasResults: false,
|
|
119
|
-
resultCount: 0,
|
|
120
|
-
agent: 'express',
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
// Verify fullResponse has complete data
|
|
124
|
-
expect(result.fullResponse.results).toBeUndefined()
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
test('structuredContent validation for answer with results', () => {
|
|
128
|
-
const mockResponse: ExpressAgentMcpResponse = {
|
|
129
|
-
answer: 'Recent AI breakthroughs include advances in language models and computer vision.',
|
|
130
|
-
results: {
|
|
131
|
-
web: [
|
|
132
|
-
{
|
|
133
|
-
url: 'https://example.com/ai-breakthrough',
|
|
134
|
-
title: 'AI Breakthrough 2025',
|
|
135
|
-
snippet: 'Major advances in artificial intelligence.',
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
},
|
|
139
|
-
agent: 'express',
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const result = formatExpressAgentResponse(mockResponse)
|
|
143
|
-
|
|
144
|
-
// Verify structuredContent is minimal with counts
|
|
145
|
-
expect(result.structuredContent).toHaveProperty('answer')
|
|
146
|
-
expect(result.structuredContent).toHaveProperty('hasResults')
|
|
147
|
-
expect(result.structuredContent).toHaveProperty('resultCount')
|
|
148
|
-
expect(result.structuredContent).toHaveProperty('agent')
|
|
149
|
-
expect(result.structuredContent.answer).toBe(
|
|
150
|
-
'Recent AI breakthroughs include advances in language models and computer vision.',
|
|
151
|
-
)
|
|
152
|
-
expect(result.structuredContent.agent).toBe('express')
|
|
153
|
-
expect(result.structuredContent.hasResults).toBe(true)
|
|
154
|
-
expect(result.structuredContent.resultCount).toBe(1)
|
|
155
|
-
|
|
156
|
-
// URLs should be in structuredContent.results
|
|
157
|
-
expect(result.structuredContent).toHaveProperty('results')
|
|
158
|
-
expect(result.structuredContent.results?.web).toBeDefined()
|
|
159
|
-
expect(result.structuredContent.results?.web?.length).toBe(1)
|
|
160
|
-
expect(result.structuredContent.results?.web?.[0]).toEqual({
|
|
161
|
-
url: 'https://example.com/ai-breakthrough',
|
|
162
|
-
title: 'AI Breakthrough 2025',
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
// Verify fullResponse has complete search results
|
|
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)
|
|
170
|
-
|
|
171
|
-
// Verify search result fields in fullResponse
|
|
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
|
-
})
|