@youdotcom-oss/mcp 3.2.3 → 3.3.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 +36 -159
- package/package.json +3 -2
- package/server.json +2 -2
- package/src/contents/contents.utils.ts +8 -34
- package/src/contents/register-contents-tool.ts +15 -13
- package/src/contents/tests/contents.utils.spec.ts +9 -33
- package/src/main.ts +0 -3
- package/src/research/register-research-tool.ts +5 -6
- package/src/research/research.utils.ts +6 -24
- package/src/research/tests/research.utils.spec.ts +5 -82
- package/src/search/register-search-tool.ts +11 -13
- package/src/search/search.utils.ts +5 -61
- package/src/search/tests/register-search-tool.spec.ts +2 -6
- package/src/search/tests/search.utils.spec.ts +26 -125
- package/src/contents/contents.schemas.ts +0 -30
- package/src/research/research.schemas.ts +0 -19
- package/src/search/search.schemas.ts +0 -53
package/bin/stdio.js
CHANGED
|
@@ -12889,29 +12889,12 @@ var getLogger = (sendNotification) => async (params) => {
|
|
|
12889
12889
|
await sendNotification({ method: "notifications/message", params });
|
|
12890
12890
|
};
|
|
12891
12891
|
|
|
12892
|
-
// src/contents/contents.schemas.ts
|
|
12893
|
-
var ContentsStructuredContentSchema = object({
|
|
12894
|
-
count: number2().describe("URLs processed"),
|
|
12895
|
-
formats: array(string2()).describe("Content formats requested"),
|
|
12896
|
-
items: array(object({
|
|
12897
|
-
url: string2().describe("URL"),
|
|
12898
|
-
title: string2().optional().describe("Title"),
|
|
12899
|
-
markdown: string2().optional().describe("Markdown content"),
|
|
12900
|
-
html: string2().optional().describe("HTML content"),
|
|
12901
|
-
metadata: object({
|
|
12902
|
-
favicon_url: string2().describe("Favicon URL"),
|
|
12903
|
-
site_name: string2().optional().nullable().describe("Site name")
|
|
12904
|
-
}).optional().nullable().describe("Page metadata")
|
|
12905
|
-
})).describe("Extracted items")
|
|
12906
|
-
});
|
|
12907
|
-
|
|
12908
12892
|
// src/contents/contents.utils.ts
|
|
12909
12893
|
var formatContentsResponse = (response, formats) => {
|
|
12910
12894
|
const textParts = [`Successfully extracted content from ${response.length} URL(s):
|
|
12911
12895
|
`];
|
|
12912
12896
|
textParts.push(`Formats: ${formats.join(", ")}
|
|
12913
12897
|
`);
|
|
12914
|
-
const items = [];
|
|
12915
12898
|
for (const item of response) {
|
|
12916
12899
|
textParts.push(`
|
|
12917
12900
|
## ${item.title || "Untitled"}`);
|
|
@@ -12957,28 +12940,14 @@ var formatContentsResponse = (response, formats) => {
|
|
|
12957
12940
|
textParts.push(`
|
|
12958
12941
|
---
|
|
12959
12942
|
`);
|
|
12960
|
-
items.push({
|
|
12961
|
-
url: item.url,
|
|
12962
|
-
title: item.title ?? undefined,
|
|
12963
|
-
markdown: item.markdown ?? undefined,
|
|
12964
|
-
html: item.html ?? undefined,
|
|
12965
|
-
metadata: item.metadata ?? undefined
|
|
12966
|
-
});
|
|
12967
12943
|
}
|
|
12968
|
-
return
|
|
12969
|
-
|
|
12970
|
-
|
|
12971
|
-
|
|
12972
|
-
text: textParts.join(`
|
|
12944
|
+
return [
|
|
12945
|
+
{
|
|
12946
|
+
type: "text",
|
|
12947
|
+
text: textParts.join(`
|
|
12973
12948
|
`)
|
|
12974
|
-
}
|
|
12975
|
-
],
|
|
12976
|
-
structuredContent: {
|
|
12977
|
-
count: response.length,
|
|
12978
|
-
formats,
|
|
12979
|
-
items
|
|
12980
12949
|
}
|
|
12981
|
-
|
|
12950
|
+
];
|
|
12982
12951
|
};
|
|
12983
12952
|
|
|
12984
12953
|
// src/contents/register-contents-tool.ts
|
|
@@ -12990,8 +12959,10 @@ var registerContentsTool = ({
|
|
|
12990
12959
|
mcp.registerTool("you-contents", {
|
|
12991
12960
|
title: "Extract Web Page Contents",
|
|
12992
12961
|
description: "Extract page content in markdown or HTML",
|
|
12993
|
-
inputSchema: ContentsQuerySchema
|
|
12994
|
-
outputSchema:
|
|
12962
|
+
inputSchema: ContentsQuerySchema,
|
|
12963
|
+
outputSchema: object({
|
|
12964
|
+
output: ContentsApiResponseSchema
|
|
12965
|
+
})
|
|
12995
12966
|
}, async (contentsQuery, { sendNotification }) => {
|
|
12996
12967
|
const logger = getLogger(sendNotification);
|
|
12997
12968
|
try {
|
|
@@ -13007,14 +12978,16 @@ var registerContentsTool = ({
|
|
|
13007
12978
|
YDC_API_KEY,
|
|
13008
12979
|
getUserAgent
|
|
13009
12980
|
});
|
|
13010
|
-
const
|
|
12981
|
+
const content = formatContentsResponse(response, requestFormats);
|
|
13011
12982
|
await logger({
|
|
13012
12983
|
level: "info",
|
|
13013
12984
|
data: `Contents API call successful: extracted ${response.length} page(s)`
|
|
13014
12985
|
});
|
|
13015
12986
|
return {
|
|
13016
12987
|
content,
|
|
13017
|
-
structuredContent
|
|
12988
|
+
structuredContent: {
|
|
12989
|
+
output: response
|
|
12990
|
+
}
|
|
13018
12991
|
};
|
|
13019
12992
|
} catch (err) {
|
|
13020
12993
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
@@ -20464,7 +20437,7 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
20464
20437
|
// package.json
|
|
20465
20438
|
var package_default = {
|
|
20466
20439
|
name: "@youdotcom-oss/mcp",
|
|
20467
|
-
version: "3.
|
|
20440
|
+
version: "3.3.0",
|
|
20468
20441
|
description: "You.com MCP server — web search, AI research, and content extraction via You.com APIs",
|
|
20469
20442
|
license: "MIT",
|
|
20470
20443
|
engines: {
|
|
@@ -20521,7 +20494,8 @@ var package_default = {
|
|
|
20521
20494
|
zod: "^4.3.6"
|
|
20522
20495
|
},
|
|
20523
20496
|
devDependencies: {
|
|
20524
|
-
"@modelcontextprotocol/inspector": "0.21.
|
|
20497
|
+
"@modelcontextprotocol/inspector": "0.21.2",
|
|
20498
|
+
"@types/bun": "latest"
|
|
20525
20499
|
}
|
|
20526
20500
|
};
|
|
20527
20501
|
|
|
@@ -20537,37 +20511,15 @@ var getMcpServer = () => new McpServer({
|
|
|
20537
20511
|
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.`
|
|
20538
20512
|
});
|
|
20539
20513
|
|
|
20540
|
-
// src/research/research.schemas.ts
|
|
20541
|
-
var ResearchStructuredContentSchema = object({
|
|
20542
|
-
contentType: string2().describe("Format of the content field"),
|
|
20543
|
-
sourceCount: number2().describe("Number of sources used"),
|
|
20544
|
-
sources: array(object({
|
|
20545
|
-
url: string2().describe("Source URL"),
|
|
20546
|
-
title: string2().optional().describe("Source title"),
|
|
20547
|
-
snippetCount: number2().describe("Number of excerpts from this source")
|
|
20548
|
-
})).describe("Sources used in the research answer")
|
|
20549
|
-
});
|
|
20550
|
-
|
|
20551
20514
|
// src/research/research.utils.ts
|
|
20552
20515
|
var formatResearchResults = (response) => {
|
|
20553
20516
|
const text = formatResearchResponse(response);
|
|
20554
|
-
return
|
|
20555
|
-
|
|
20556
|
-
|
|
20557
|
-
|
|
20558
|
-
text
|
|
20559
|
-
}
|
|
20560
|
-
],
|
|
20561
|
-
structuredContent: {
|
|
20562
|
-
contentType: response.output.content_type,
|
|
20563
|
-
sourceCount: response.output.sources.length,
|
|
20564
|
-
sources: response.output.sources.map((source) => ({
|
|
20565
|
-
url: source.url,
|
|
20566
|
-
title: source.title,
|
|
20567
|
-
snippetCount: source.snippets?.length ?? 0
|
|
20568
|
-
}))
|
|
20517
|
+
return [
|
|
20518
|
+
{
|
|
20519
|
+
type: "text",
|
|
20520
|
+
text
|
|
20569
20521
|
}
|
|
20570
|
-
|
|
20522
|
+
];
|
|
20571
20523
|
};
|
|
20572
20524
|
|
|
20573
20525
|
// src/research/register-research-tool.ts
|
|
@@ -20579,8 +20531,8 @@ var registerResearchTool = ({
|
|
|
20579
20531
|
mcp.registerTool("you-research", {
|
|
20580
20532
|
title: "Research",
|
|
20581
20533
|
description: "Research a topic with comprehensive answers and cited sources. Configurable effort levels (lite, standard, deep, exhaustive).",
|
|
20582
|
-
inputSchema: ResearchQuerySchema
|
|
20583
|
-
outputSchema:
|
|
20534
|
+
inputSchema: ResearchQuerySchema,
|
|
20535
|
+
outputSchema: ResearchResponseSchema
|
|
20584
20536
|
}, async (researchQuery, { sendNotification }) => {
|
|
20585
20537
|
const logger = getLogger(sendNotification);
|
|
20586
20538
|
try {
|
|
@@ -20594,8 +20546,8 @@ var registerResearchTool = ({
|
|
|
20594
20546
|
level: "info",
|
|
20595
20547
|
data: `Research for "${researchQuery.input.substring(0, 100)}" complete: ${sourceCount} source(s)`
|
|
20596
20548
|
});
|
|
20597
|
-
const
|
|
20598
|
-
return { content, structuredContent };
|
|
20549
|
+
const content = formatResearchResults(response);
|
|
20550
|
+
return { content, structuredContent: response };
|
|
20599
20551
|
} catch (err) {
|
|
20600
20552
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
20601
20553
|
const reportLink = generateErrorReportLink({
|
|
@@ -20623,36 +20575,6 @@ Report this issue: ${reportLink}`
|
|
|
20623
20575
|
});
|
|
20624
20576
|
};
|
|
20625
20577
|
|
|
20626
|
-
// src/search/search.schemas.ts
|
|
20627
|
-
var SearchStructuredContentSchema = object({
|
|
20628
|
-
resultCounts: object({
|
|
20629
|
-
web: number2().describe("Web results"),
|
|
20630
|
-
news: number2().describe("News results"),
|
|
20631
|
-
total: number2().describe("Total results")
|
|
20632
|
-
}),
|
|
20633
|
-
results: object({
|
|
20634
|
-
web: array(object({
|
|
20635
|
-
url: string2().describe("URL"),
|
|
20636
|
-
title: string2().describe("Title"),
|
|
20637
|
-
page_age: string2().optional().describe("Publication timestamp"),
|
|
20638
|
-
snippets: array(string2()).optional().describe("Content snippets"),
|
|
20639
|
-
contents: object({
|
|
20640
|
-
html: string2().optional().describe("Full HTML content"),
|
|
20641
|
-
markdown: string2().optional().describe("Full Markdown content")
|
|
20642
|
-
}).optional().describe("Livecrawled page content")
|
|
20643
|
-
})).optional().describe("Web results"),
|
|
20644
|
-
news: array(object({
|
|
20645
|
-
url: string2().describe("URL"),
|
|
20646
|
-
title: string2().describe("Title"),
|
|
20647
|
-
page_age: string2().describe("Publication timestamp"),
|
|
20648
|
-
contents: object({
|
|
20649
|
-
html: string2().optional().describe("Full HTML content"),
|
|
20650
|
-
markdown: string2().optional().describe("Full Markdown content")
|
|
20651
|
-
}).optional().describe("Livecrawled page content")
|
|
20652
|
-
})).optional().describe("News results")
|
|
20653
|
-
}).optional().describe("Search results")
|
|
20654
|
-
});
|
|
20655
|
-
|
|
20656
20578
|
// src/shared/format-search-results-text.ts
|
|
20657
20579
|
var formatCharCount = (count) => count.toLocaleString();
|
|
20658
20580
|
var formatSearchResultsText = (results) => {
|
|
@@ -20713,53 +20635,14 @@ ${"=".repeat(50)}
|
|
|
20713
20635
|
|
|
20714
20636
|
${newsResults}`;
|
|
20715
20637
|
}
|
|
20716
|
-
|
|
20717
|
-
|
|
20718
|
-
|
|
20719
|
-
|
|
20720
|
-
url: result.url,
|
|
20721
|
-
title: result.title
|
|
20722
|
-
};
|
|
20723
|
-
if (result.page_age)
|
|
20724
|
-
item.page_age = result.page_age;
|
|
20725
|
-
if (result.snippets?.length)
|
|
20726
|
-
item.snippets = result.snippets;
|
|
20727
|
-
if (result.contents)
|
|
20728
|
-
item.contents = result.contents ?? undefined;
|
|
20729
|
-
return item;
|
|
20730
|
-
});
|
|
20731
|
-
}
|
|
20732
|
-
if (response.results.news?.length) {
|
|
20733
|
-
structuredResults.news = response.results.news.map((article) => {
|
|
20734
|
-
const item = {
|
|
20735
|
-
url: article.url,
|
|
20736
|
-
title: article.title,
|
|
20737
|
-
page_age: article.page_age
|
|
20738
|
-
};
|
|
20739
|
-
if (article.contents)
|
|
20740
|
-
item.contents = article.contents ?? undefined;
|
|
20741
|
-
return item;
|
|
20742
|
-
});
|
|
20743
|
-
}
|
|
20744
|
-
return {
|
|
20745
|
-
content: [
|
|
20746
|
-
{
|
|
20747
|
-
type: "text",
|
|
20748
|
-
text: `Search Results for "${response.metadata.query}":
|
|
20638
|
+
return [
|
|
20639
|
+
{
|
|
20640
|
+
type: "text",
|
|
20641
|
+
text: `Search Results for "${response.metadata.query}":
|
|
20749
20642
|
|
|
20750
20643
|
${formattedResults}`
|
|
20751
|
-
|
|
20752
|
-
|
|
20753
|
-
structuredContent: {
|
|
20754
|
-
resultCounts: {
|
|
20755
|
-
web: response.results.web?.length || 0,
|
|
20756
|
-
news: response.results.news?.length || 0,
|
|
20757
|
-
total: (response.results.web?.length || 0) + (response.results.news?.length || 0)
|
|
20758
|
-
},
|
|
20759
|
-
results: Object.keys(structuredResults).length > 0 ? structuredResults : undefined
|
|
20760
|
-
},
|
|
20761
|
-
fullResponse: response
|
|
20762
|
-
};
|
|
20644
|
+
}
|
|
20645
|
+
];
|
|
20763
20646
|
};
|
|
20764
20647
|
|
|
20765
20648
|
// src/search/register-search-tool.ts
|
|
@@ -20771,8 +20654,8 @@ var registerSearchTool = ({
|
|
|
20771
20654
|
mcp.registerTool("you-search", {
|
|
20772
20655
|
title: "Web Search",
|
|
20773
20656
|
description: "Web and news search via You.com. Supports domain filtering, language selection, livecrawl for full page content, and date freshness controls.",
|
|
20774
|
-
inputSchema: SearchQuerySchema
|
|
20775
|
-
outputSchema:
|
|
20657
|
+
inputSchema: SearchQuerySchema,
|
|
20658
|
+
outputSchema: SearchResponseSchema
|
|
20776
20659
|
}, async (searchQuery, { sendNotification }) => {
|
|
20777
20660
|
const logger = getLogger(sendNotification);
|
|
20778
20661
|
try {
|
|
@@ -20790,21 +20673,15 @@ var registerSearchTool = ({
|
|
|
20790
20673
|
});
|
|
20791
20674
|
return {
|
|
20792
20675
|
content: [{ type: "text", text: "No results found." }],
|
|
20793
|
-
structuredContent:
|
|
20794
|
-
resultCounts: {
|
|
20795
|
-
web: 0,
|
|
20796
|
-
news: 0,
|
|
20797
|
-
total: 0
|
|
20798
|
-
}
|
|
20799
|
-
}
|
|
20676
|
+
structuredContent: response
|
|
20800
20677
|
};
|
|
20801
20678
|
}
|
|
20802
20679
|
await logger({
|
|
20803
20680
|
level: "info",
|
|
20804
20681
|
data: `Search successful for query: "${searchQuery.query}" - ${webCount} web results, ${newsCount} news results (${webCount + newsCount} total)`
|
|
20805
20682
|
});
|
|
20806
|
-
const
|
|
20807
|
-
return { content, structuredContent };
|
|
20683
|
+
const content = formatSearchResults(response);
|
|
20684
|
+
return { content, structuredContent: response };
|
|
20808
20685
|
} catch (err) {
|
|
20809
20686
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
20810
20687
|
const reportLink = generateErrorReportLink({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youdotcom-oss/mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.0",
|
|
4
4
|
"description": "You.com MCP server — web search, AI research, and content extraction via You.com APIs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"engines": {
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"zod": "^4.3.6"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@modelcontextprotocol/inspector": "0.21.
|
|
60
|
+
"@modelcontextprotocol/inspector": "0.21.2",
|
|
61
|
+
"@types/bun": "latest"
|
|
61
62
|
}
|
|
62
63
|
}
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.youdotcom-oss/mcp",
|
|
4
4
|
"title": "You.com Web Access & AI",
|
|
5
5
|
"description": "Web search, AI agent, and content extraction via You.com APIs",
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.2.3",
|
|
7
7
|
"remotes": [
|
|
8
8
|
{
|
|
9
9
|
"type": "streamable-http",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
{
|
|
23
23
|
"registryType": "npm",
|
|
24
24
|
"identifier": "@youdotcom-oss/mcp",
|
|
25
|
-
"version": "3.
|
|
25
|
+
"version": "3.2.3",
|
|
26
26
|
"transport": {
|
|
27
27
|
"type": "stdio"
|
|
28
28
|
},
|
|
@@ -1,33 +1,23 @@
|
|
|
1
1
|
import type { ContentsApiResponse } from '@youdotcom-oss/api'
|
|
2
|
-
import type { ContentsStructuredContent } from './contents.schemas.ts'
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
4
|
* Format contents API response for MCP output
|
|
6
|
-
* Returns full content in both text and structured formats
|
|
7
5
|
* @param response - Validated API response
|
|
8
6
|
* @param formats - Formats used for extraction
|
|
9
|
-
* @returns
|
|
7
|
+
* @returns Text content blocks for the MCP response
|
|
10
8
|
*/
|
|
11
9
|
export const formatContentsResponse = (
|
|
12
10
|
response: ContentsApiResponse,
|
|
13
11
|
formats: string[],
|
|
14
|
-
): {
|
|
15
|
-
content: Array<{ type: 'text'; text: string }>
|
|
16
|
-
structuredContent: ContentsStructuredContent
|
|
17
|
-
} => {
|
|
18
|
-
// Build text content with full extracted content
|
|
12
|
+
): Array<{ type: 'text'; text: string }> => {
|
|
19
13
|
const textParts: string[] = [`Successfully extracted content from ${response.length} URL(s):\n`]
|
|
20
14
|
textParts.push(`Formats: ${formats.join(', ')}\n`)
|
|
21
15
|
|
|
22
|
-
const items: ContentsStructuredContent['items'] = []
|
|
23
|
-
|
|
24
16
|
for (const item of response) {
|
|
25
|
-
// Add header for this item
|
|
26
17
|
textParts.push(`\n## ${item.title || 'Untitled'}`)
|
|
27
18
|
textParts.push(`URL: ${item.url}\n`)
|
|
28
19
|
textParts.push('---\n')
|
|
29
20
|
|
|
30
|
-
// Add content based on requested formats
|
|
31
21
|
if (formats.includes('markdown') && item.markdown) {
|
|
32
22
|
textParts.push('\n### Markdown Content\n')
|
|
33
23
|
textParts.push(item.markdown)
|
|
@@ -35,7 +25,7 @@ export const formatContentsResponse = (
|
|
|
35
25
|
}
|
|
36
26
|
|
|
37
27
|
if (formats.includes('html') && item.html) {
|
|
38
|
-
// Text output is a brief preview only — full HTML is in structuredContent.
|
|
28
|
+
// Text output is a brief preview only — full HTML is in structuredContent.output[].html
|
|
39
29
|
textParts.push('\n### HTML Content\n')
|
|
40
30
|
textParts.push(`Length: ${item.html.length} characters\n`)
|
|
41
31
|
textParts.push(item.html.substring(0, 500))
|
|
@@ -58,28 +48,12 @@ export const formatContentsResponse = (
|
|
|
58
48
|
}
|
|
59
49
|
|
|
60
50
|
textParts.push('\n---\n')
|
|
61
|
-
|
|
62
|
-
// Add to structured content
|
|
63
|
-
items.push({
|
|
64
|
-
url: item.url,
|
|
65
|
-
title: item.title ?? undefined,
|
|
66
|
-
markdown: item.markdown ?? undefined,
|
|
67
|
-
html: item.html ?? undefined,
|
|
68
|
-
metadata: item.metadata ?? undefined,
|
|
69
|
-
})
|
|
70
51
|
}
|
|
71
52
|
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
text: textParts.join('\n'),
|
|
77
|
-
},
|
|
78
|
-
],
|
|
79
|
-
structuredContent: {
|
|
80
|
-
count: response.length,
|
|
81
|
-
formats,
|
|
82
|
-
items,
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
type: 'text',
|
|
56
|
+
text: textParts.join('\n'),
|
|
83
57
|
},
|
|
84
|
-
|
|
58
|
+
]
|
|
85
59
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ContentsApiResponseSchema,
|
|
4
|
+
ContentsQuerySchema,
|
|
5
|
+
fetchContents,
|
|
6
|
+
generateErrorReportLink,
|
|
7
|
+
} from '@youdotcom-oss/api'
|
|
8
|
+
import * as z from 'zod'
|
|
3
9
|
import { getLogger } from '../shared/get-logger.ts'
|
|
4
|
-
import { ContentsStructuredContentSchema } from './contents.schemas.ts'
|
|
5
10
|
import { formatContentsResponse } from './contents.utils.ts'
|
|
6
11
|
|
|
7
12
|
/**
|
|
@@ -17,43 +22,39 @@ export const registerContentsTool = ({
|
|
|
17
22
|
YDC_API_KEY?: string
|
|
18
23
|
getUserAgent: () => string
|
|
19
24
|
}) => {
|
|
20
|
-
// Register the tool
|
|
21
25
|
mcp.registerTool(
|
|
22
26
|
'you-contents',
|
|
23
27
|
{
|
|
24
28
|
title: 'Extract Web Page Contents',
|
|
25
29
|
description: 'Extract page content in markdown or HTML',
|
|
26
|
-
inputSchema: ContentsQuerySchema
|
|
27
|
-
outputSchema:
|
|
30
|
+
inputSchema: ContentsQuerySchema,
|
|
31
|
+
outputSchema: z.object({
|
|
32
|
+
output: ContentsApiResponseSchema,
|
|
33
|
+
}),
|
|
28
34
|
},
|
|
29
35
|
async (contentsQuery, { sendNotification }) => {
|
|
30
36
|
const logger = getLogger(sendNotification)
|
|
31
37
|
|
|
32
38
|
try {
|
|
33
|
-
// Validate and parse input
|
|
34
39
|
const { urls, formats, format, crawl_timeout } = contentsQuery
|
|
35
40
|
|
|
36
41
|
// Handle backward compatibility: prefer formats array, fallback to format string, default to ['markdown']
|
|
37
42
|
const requestFormats = formats || (format ? [format] : ['markdown'])
|
|
38
43
|
|
|
39
|
-
// Log the request
|
|
40
44
|
const timeoutInfo = crawl_timeout ? ` with timeout: ${crawl_timeout}s` : ''
|
|
41
45
|
await logger({
|
|
42
46
|
level: 'info',
|
|
43
47
|
data: `Contents API call initiated for ${urls.length} URL(s) with formats: ${requestFormats.join(', ')}${timeoutInfo}`,
|
|
44
48
|
})
|
|
45
49
|
|
|
46
|
-
// Fetch contents from API
|
|
47
50
|
const response = await fetchContents({
|
|
48
51
|
contentsQuery,
|
|
49
52
|
YDC_API_KEY,
|
|
50
53
|
getUserAgent,
|
|
51
54
|
})
|
|
52
55
|
|
|
53
|
-
|
|
54
|
-
const { content, structuredContent } = formatContentsResponse(response, requestFormats)
|
|
56
|
+
const content = formatContentsResponse(response, requestFormats)
|
|
55
57
|
|
|
56
|
-
// Log success
|
|
57
58
|
await logger({
|
|
58
59
|
level: 'info',
|
|
59
60
|
data: `Contents API call successful: extracted ${response.length} page(s)`,
|
|
@@ -61,10 +62,11 @@ export const registerContentsTool = ({
|
|
|
61
62
|
|
|
62
63
|
return {
|
|
63
64
|
content,
|
|
64
|
-
structuredContent
|
|
65
|
+
structuredContent: {
|
|
66
|
+
output: response,
|
|
67
|
+
},
|
|
65
68
|
}
|
|
66
69
|
} catch (err: unknown) {
|
|
67
|
-
// Handle and log errors
|
|
68
70
|
const errorMessage = err instanceof Error ? err.message : String(err)
|
|
69
71
|
const reportLink = generateErrorReportLink({
|
|
70
72
|
errorMessage,
|
|
@@ -14,30 +14,16 @@ describe('formatContentsResponse', () => {
|
|
|
14
14
|
|
|
15
15
|
const result = formatContentsResponse(mockResponse, ['markdown'])
|
|
16
16
|
|
|
17
|
-
expect(result).
|
|
18
|
-
expect(result).toHaveProperty('
|
|
19
|
-
expect(
|
|
20
|
-
expect(result.content[0]).toHaveProperty('type', 'text')
|
|
21
|
-
expect(result.content[0]).toHaveProperty('text')
|
|
17
|
+
expect(Array.isArray(result)).toBe(true)
|
|
18
|
+
expect(result[0]).toHaveProperty('type', 'text')
|
|
19
|
+
expect(result[0]).toHaveProperty('text')
|
|
22
20
|
|
|
23
|
-
const text = result
|
|
21
|
+
const text = result[0]?.text
|
|
24
22
|
expect(text).toContain('Example Page')
|
|
25
23
|
expect(text).toContain('https://example.com')
|
|
26
24
|
expect(text).toContain('Formats: markdown')
|
|
27
25
|
expect(text).toContain('# Hello')
|
|
28
26
|
expect(text).toContain('This is a test page with some content.')
|
|
29
|
-
|
|
30
|
-
expect(result.structuredContent).toHaveProperty('count', 1)
|
|
31
|
-
expect(result.structuredContent).toHaveProperty('formats')
|
|
32
|
-
expect(result.structuredContent.formats).toEqual(['markdown'])
|
|
33
|
-
expect(result.structuredContent.items).toHaveLength(1)
|
|
34
|
-
|
|
35
|
-
const item = result.structuredContent.items[0]
|
|
36
|
-
expect(item).toBeDefined()
|
|
37
|
-
|
|
38
|
-
expect(item).toHaveProperty('url', 'https://example.com')
|
|
39
|
-
expect(item).toHaveProperty('title', 'Example Page')
|
|
40
|
-
expect(item).toHaveProperty('markdown', '# Hello\n\nThis is a test page with some content.')
|
|
41
27
|
})
|
|
42
28
|
|
|
43
29
|
test('formats multiple items correctly', () => {
|
|
@@ -56,10 +42,8 @@ describe('formatContentsResponse', () => {
|
|
|
56
42
|
|
|
57
43
|
const result = formatContentsResponse(mockResponse, ['markdown'])
|
|
58
44
|
|
|
59
|
-
|
|
60
|
-
expect(
|
|
61
|
-
|
|
62
|
-
const text = result.content[0]?.text
|
|
45
|
+
const text = result[0]?.text
|
|
46
|
+
expect(text).toContain('Successfully extracted content from 2 URL(s)')
|
|
63
47
|
expect(text).toContain('Page 1')
|
|
64
48
|
expect(text).toContain('Page 2')
|
|
65
49
|
expect(text).toContain('https://example1.com')
|
|
@@ -77,8 +61,7 @@ describe('formatContentsResponse', () => {
|
|
|
77
61
|
|
|
78
62
|
const result = formatContentsResponse(mockResponse, ['html'])
|
|
79
63
|
|
|
80
|
-
|
|
81
|
-
const text = result.content[0]?.text
|
|
64
|
+
const text = result[0]?.text
|
|
82
65
|
expect(text).toContain('Formats: html')
|
|
83
66
|
expect(text).toContain('<html>')
|
|
84
67
|
})
|
|
@@ -95,13 +78,8 @@ describe('formatContentsResponse', () => {
|
|
|
95
78
|
|
|
96
79
|
const result = formatContentsResponse(mockResponse, ['markdown'])
|
|
97
80
|
|
|
98
|
-
const text = result
|
|
99
|
-
// Full content should be included (not truncated)
|
|
81
|
+
const text = result[0]?.text
|
|
100
82
|
expect(text).toContain(longContent)
|
|
101
|
-
|
|
102
|
-
// Structured content should have full markdown content
|
|
103
|
-
const item = result.structuredContent.items[0]
|
|
104
|
-
expect(item?.markdown).toBe(longContent)
|
|
105
83
|
})
|
|
106
84
|
|
|
107
85
|
test('handles empty content gracefully', () => {
|
|
@@ -115,9 +93,7 @@ describe('formatContentsResponse', () => {
|
|
|
115
93
|
|
|
116
94
|
const result = formatContentsResponse(mockResponse, ['markdown'])
|
|
117
95
|
|
|
118
|
-
|
|
119
|
-
const text = result.content[0]?.text
|
|
96
|
+
const text = result[0]?.text
|
|
120
97
|
expect(text).toContain('Empty Page')
|
|
121
|
-
// Empty content should still be handled gracefully
|
|
122
98
|
})
|
|
123
99
|
})
|
package/src/main.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
export type { ContentsStructuredContent } from './contents/contents.schemas.ts'
|
|
2
1
|
export { registerContentsTool } from './contents/register-contents-tool.ts'
|
|
3
2
|
export { getMcpServer } from './get-mcp-server.ts'
|
|
4
3
|
export { registerResearchTool } from './research/register-research-tool.ts'
|
|
5
|
-
export type { ResearchStructuredContent } from './research/research.schemas.ts'
|
|
6
4
|
export { registerSearchTool } from './search/register-search-tool.ts'
|
|
7
|
-
export type { SearchStructuredContent } from './search/search.schemas.ts'
|
|
8
5
|
export { useGetClientVersion } from './shared/use-client-version.ts'
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
-
import { callResearch, generateErrorReportLink, ResearchQuerySchema } from '@youdotcom-oss/api'
|
|
2
|
+
import { callResearch, generateErrorReportLink, ResearchQuerySchema, ResearchResponseSchema } from '@youdotcom-oss/api'
|
|
3
3
|
import { getLogger } from '../shared/get-logger.ts'
|
|
4
|
-
import { ResearchStructuredContentSchema } from './research.schemas.ts'
|
|
5
4
|
import { formatResearchResults } from './research.utils.ts'
|
|
6
5
|
|
|
7
6
|
export const registerResearchTool = ({
|
|
@@ -19,8 +18,8 @@ export const registerResearchTool = ({
|
|
|
19
18
|
title: 'Research',
|
|
20
19
|
description:
|
|
21
20
|
'Research a topic with comprehensive answers and cited sources. Configurable effort levels (lite, standard, deep, exhaustive).',
|
|
22
|
-
inputSchema: ResearchQuerySchema
|
|
23
|
-
outputSchema:
|
|
21
|
+
inputSchema: ResearchQuerySchema,
|
|
22
|
+
outputSchema: ResearchResponseSchema,
|
|
24
23
|
},
|
|
25
24
|
async (researchQuery, { sendNotification }) => {
|
|
26
25
|
const logger = getLogger(sendNotification)
|
|
@@ -38,8 +37,8 @@ export const registerResearchTool = ({
|
|
|
38
37
|
data: `Research for "${researchQuery.input.substring(0, 100)}" complete: ${sourceCount} source(s)`,
|
|
39
38
|
})
|
|
40
39
|
|
|
41
|
-
const
|
|
42
|
-
return { content, structuredContent }
|
|
40
|
+
const content = formatResearchResults(response)
|
|
41
|
+
return { content, structuredContent: response }
|
|
43
42
|
} catch (err: unknown) {
|
|
44
43
|
const errorMessage = err instanceof Error ? err.message : String(err)
|
|
45
44
|
const reportLink = generateErrorReportLink({
|
|
@@ -1,30 +1,12 @@
|
|
|
1
1
|
import type { ResearchResponse } from '@youdotcom-oss/api'
|
|
2
2
|
import { formatResearchResponse } from '@youdotcom-oss/api'
|
|
3
|
-
import type { ResearchStructuredContent } from './research.schemas.ts'
|
|
4
3
|
|
|
5
|
-
export const formatResearchResults = (
|
|
6
|
-
response: ResearchResponse,
|
|
7
|
-
): {
|
|
8
|
-
content: Array<{ type: 'text'; text: string }>
|
|
9
|
-
structuredContent: ResearchStructuredContent
|
|
10
|
-
} => {
|
|
4
|
+
export const formatResearchResults = (response: ResearchResponse): Array<{ type: 'text'; text: string }> => {
|
|
11
5
|
const text = formatResearchResponse(response)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
type: 'text',
|
|
17
|
-
text,
|
|
18
|
-
},
|
|
19
|
-
],
|
|
20
|
-
structuredContent: {
|
|
21
|
-
contentType: response.output.content_type,
|
|
22
|
-
sourceCount: response.output.sources.length,
|
|
23
|
-
sources: response.output.sources.map((source) => ({
|
|
24
|
-
url: source.url,
|
|
25
|
-
title: source.title,
|
|
26
|
-
snippetCount: source.snippets?.length ?? 0,
|
|
27
|
-
})),
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
type: 'text',
|
|
9
|
+
text,
|
|
28
10
|
},
|
|
29
|
-
|
|
11
|
+
]
|
|
30
12
|
}
|
|
@@ -25,89 +25,14 @@ describe('formatResearchResults', () => {
|
|
|
25
25
|
|
|
26
26
|
const result = formatResearchResults(mockResponse)
|
|
27
27
|
|
|
28
|
-
expect(result).
|
|
29
|
-
expect(result).toHaveProperty('
|
|
30
|
-
expect(
|
|
31
|
-
expect(result.content[0]).toHaveProperty('type', 'text')
|
|
32
|
-
expect(result.content[0]).toHaveProperty('text')
|
|
28
|
+
expect(Array.isArray(result)).toBe(true)
|
|
29
|
+
expect(result[0]).toHaveProperty('type', 'text')
|
|
30
|
+
expect(result[0]).toHaveProperty('text')
|
|
33
31
|
|
|
34
|
-
const text = result
|
|
32
|
+
const text = result[0]?.text
|
|
35
33
|
expect(text).toContain('Research Answer')
|
|
36
34
|
expect(text).toContain('Source One')
|
|
37
35
|
expect(text).toContain('https://example.com/source1')
|
|
38
|
-
|
|
39
|
-
expect(result.structuredContent.contentType).toBe('text')
|
|
40
|
-
expect(result.structuredContent.sourceCount).toBe(2)
|
|
41
|
-
expect(result.structuredContent.sources).toHaveLength(2)
|
|
42
|
-
expect(result.structuredContent.sources[0]).toMatchObject({
|
|
43
|
-
url: 'https://example.com/source1',
|
|
44
|
-
title: 'Source One',
|
|
45
|
-
snippetCount: 2,
|
|
46
|
-
})
|
|
47
|
-
expect(result.structuredContent.sources[1]).toMatchObject({
|
|
48
|
-
url: 'https://example.com/source2',
|
|
49
|
-
title: 'Source Two',
|
|
50
|
-
snippetCount: 1,
|
|
51
|
-
})
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('handles source with undefined title', () => {
|
|
55
|
-
const mockResponse: ResearchResponse = {
|
|
56
|
-
output: {
|
|
57
|
-
content: 'Answer text',
|
|
58
|
-
content_type: 'text',
|
|
59
|
-
sources: [
|
|
60
|
-
{
|
|
61
|
-
url: 'https://example.com/no-title',
|
|
62
|
-
snippets: ['A snippet'],
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
},
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const result = formatResearchResults(mockResponse)
|
|
69
|
-
|
|
70
|
-
expect(result.structuredContent.sourceCount).toBe(1)
|
|
71
|
-
expect(result.structuredContent.sources[0]).toMatchObject({
|
|
72
|
-
url: 'https://example.com/no-title',
|
|
73
|
-
title: undefined,
|
|
74
|
-
snippetCount: 1,
|
|
75
|
-
})
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
test('handles source with empty snippets array', () => {
|
|
79
|
-
const mockResponse: ResearchResponse = {
|
|
80
|
-
output: {
|
|
81
|
-
content: 'Answer with no-snippet source',
|
|
82
|
-
content_type: 'text',
|
|
83
|
-
sources: [
|
|
84
|
-
{
|
|
85
|
-
url: 'https://example.com/empty-snippets',
|
|
86
|
-
title: 'Empty Snippets Source',
|
|
87
|
-
snippets: [],
|
|
88
|
-
},
|
|
89
|
-
],
|
|
90
|
-
},
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const result = formatResearchResults(mockResponse)
|
|
94
|
-
|
|
95
|
-
expect(result.structuredContent.sourceCount).toBe(1)
|
|
96
|
-
expect(result.structuredContent.sources[0]?.snippetCount).toBe(0)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test('handles source with undefined snippets', () => {
|
|
100
|
-
const mockResponse: ResearchResponse = {
|
|
101
|
-
output: {
|
|
102
|
-
content: 'Answer',
|
|
103
|
-
content_type: 'text',
|
|
104
|
-
sources: [{ url: 'https://example.com/no-snippets', title: 'No Snippets' }],
|
|
105
|
-
},
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const result = formatResearchResults(mockResponse)
|
|
109
|
-
|
|
110
|
-
expect(result.structuredContent.sources[0]?.snippetCount).toBe(0)
|
|
111
36
|
})
|
|
112
37
|
|
|
113
38
|
test('handles response with zero sources', () => {
|
|
@@ -121,8 +46,6 @@ describe('formatResearchResults', () => {
|
|
|
121
46
|
|
|
122
47
|
const result = formatResearchResults(mockResponse)
|
|
123
48
|
|
|
124
|
-
expect(result
|
|
125
|
-
expect(result.structuredContent.sources).toHaveLength(0)
|
|
126
|
-
expect(result.content[0]?.text).toContain('An answer with no cited sources.')
|
|
49
|
+
expect(result[0]?.text).toContain('An answer with no cited sources.')
|
|
127
50
|
})
|
|
128
51
|
})
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
fetchSearchResults,
|
|
4
|
+
generateErrorReportLink,
|
|
5
|
+
SearchQuerySchema,
|
|
6
|
+
SearchResponseSchema,
|
|
7
|
+
} from '@youdotcom-oss/api'
|
|
3
8
|
import { getLogger } from '../shared/get-logger.ts'
|
|
4
|
-
import { SearchStructuredContentSchema } from './search.schemas.ts'
|
|
5
9
|
import { formatSearchResults } from './search.utils.ts'
|
|
6
10
|
|
|
7
11
|
export const registerSearchTool = ({
|
|
@@ -19,8 +23,8 @@ export const registerSearchTool = ({
|
|
|
19
23
|
title: 'Web Search',
|
|
20
24
|
description:
|
|
21
25
|
'Web and news search via You.com. Supports domain filtering, language selection, livecrawl for full page content, and date freshness controls.',
|
|
22
|
-
inputSchema: SearchQuerySchema
|
|
23
|
-
outputSchema:
|
|
26
|
+
inputSchema: SearchQuerySchema,
|
|
27
|
+
outputSchema: SearchResponseSchema,
|
|
24
28
|
},
|
|
25
29
|
async (searchQuery, { sendNotification }) => {
|
|
26
30
|
const logger = getLogger(sendNotification)
|
|
@@ -42,13 +46,7 @@ export const registerSearchTool = ({
|
|
|
42
46
|
|
|
43
47
|
return {
|
|
44
48
|
content: [{ type: 'text' as const, text: 'No results found.' }],
|
|
45
|
-
structuredContent:
|
|
46
|
-
resultCounts: {
|
|
47
|
-
web: 0,
|
|
48
|
-
news: 0,
|
|
49
|
-
total: 0,
|
|
50
|
-
},
|
|
51
|
-
},
|
|
49
|
+
structuredContent: response,
|
|
52
50
|
}
|
|
53
51
|
}
|
|
54
52
|
|
|
@@ -57,8 +55,8 @@ export const registerSearchTool = ({
|
|
|
57
55
|
data: `Search successful for query: "${searchQuery.query}" - ${webCount} web results, ${newsCount} news results (${webCount + newsCount} total)`,
|
|
58
56
|
})
|
|
59
57
|
|
|
60
|
-
const
|
|
61
|
-
return { content, structuredContent }
|
|
58
|
+
const content = formatSearchResults(response)
|
|
59
|
+
return { content, structuredContent: response }
|
|
62
60
|
} catch (err: unknown) {
|
|
63
61
|
const errorMessage = err instanceof Error ? err.message : String(err)
|
|
64
62
|
const reportLink = generateErrorReportLink({
|
|
@@ -4,13 +4,11 @@ import { formatSearchResultsText } from '../shared/format-search-results-text.ts
|
|
|
4
4
|
export const formatSearchResults = (response: SearchResponse) => {
|
|
5
5
|
let formattedResults = ''
|
|
6
6
|
|
|
7
|
-
// Format web results using shared utility
|
|
8
7
|
if (response.results.web?.length) {
|
|
9
8
|
const webResults = formatSearchResultsText(response.results.web)
|
|
10
9
|
formattedResults += `WEB RESULTS:\n\n${webResults}`
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
// Format news results using shared utility (consistent with web formatting)
|
|
14
12
|
if (response.results.news?.length) {
|
|
15
13
|
const newsResults = formatSearchResultsText(response.results.news)
|
|
16
14
|
|
|
@@ -20,64 +18,10 @@ export const formatSearchResults = (response: SearchResponse) => {
|
|
|
20
18
|
formattedResults += `NEWS RESULTS:\n\n${newsResults}`
|
|
21
19
|
}
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
title: string
|
|
28
|
-
page_age?: string
|
|
29
|
-
snippets?: string[]
|
|
30
|
-
contents?: { html?: string; markdown?: string }
|
|
31
|
-
}>
|
|
32
|
-
news?: Array<{ url: string; title: string; page_age: string; contents?: { html?: string; markdown?: string } }>
|
|
33
|
-
} = {}
|
|
34
|
-
|
|
35
|
-
if (response.results.web?.length) {
|
|
36
|
-
structuredResults.web = response.results.web.map((result) => {
|
|
37
|
-
const item: {
|
|
38
|
-
url: string
|
|
39
|
-
title: string
|
|
40
|
-
page_age?: string
|
|
41
|
-
snippets?: string[]
|
|
42
|
-
contents?: { html?: string; markdown?: string }
|
|
43
|
-
} = {
|
|
44
|
-
url: result.url,
|
|
45
|
-
title: result.title,
|
|
46
|
-
}
|
|
47
|
-
if (result.page_age) item.page_age = result.page_age
|
|
48
|
-
if (result.snippets?.length) item.snippets = result.snippets
|
|
49
|
-
if (result.contents) item.contents = result.contents ?? undefined
|
|
50
|
-
return item
|
|
51
|
-
})
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (response.results.news?.length) {
|
|
55
|
-
structuredResults.news = response.results.news.map((article) => {
|
|
56
|
-
const item: { url: string; title: string; page_age: string; contents?: { html?: string; markdown?: string } } = {
|
|
57
|
-
url: article.url,
|
|
58
|
-
title: article.title,
|
|
59
|
-
page_age: article.page_age,
|
|
60
|
-
}
|
|
61
|
-
if (article.contents) item.contents = article.contents ?? undefined
|
|
62
|
-
return item
|
|
63
|
-
})
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
content: [
|
|
68
|
-
{
|
|
69
|
-
type: 'text' as const,
|
|
70
|
-
text: `Search Results for "${response.metadata.query}":\n\n${formattedResults}`,
|
|
71
|
-
},
|
|
72
|
-
],
|
|
73
|
-
structuredContent: {
|
|
74
|
-
resultCounts: {
|
|
75
|
-
web: response.results.web?.length || 0,
|
|
76
|
-
news: response.results.news?.length || 0,
|
|
77
|
-
total: (response.results.web?.length || 0) + (response.results.news?.length || 0),
|
|
78
|
-
},
|
|
79
|
-
results: Object.keys(structuredResults).length > 0 ? structuredResults : undefined,
|
|
21
|
+
return [
|
|
22
|
+
{
|
|
23
|
+
type: 'text' as const,
|
|
24
|
+
text: `Search Results for "${response.metadata.query}":\n\n${formattedResults}`,
|
|
80
25
|
},
|
|
81
|
-
|
|
82
|
-
}
|
|
26
|
+
]
|
|
83
27
|
}
|
|
@@ -88,9 +88,7 @@ describe('registerSearchTool', () => {
|
|
|
88
88
|
const toolResult = await result.client.callTool({ name: 'you-search', arguments: { query: 'nonexistent' } })
|
|
89
89
|
|
|
90
90
|
expect(toolResult.content).toEqual([{ type: 'text', text: 'No results found.' }])
|
|
91
|
-
expect(toolResult.structuredContent).toEqual(
|
|
92
|
-
resultCounts: { web: 0, news: 0, total: 0 },
|
|
93
|
-
})
|
|
91
|
+
expect(toolResult.structuredContent).toEqual(emptyResponse)
|
|
94
92
|
})
|
|
95
93
|
|
|
96
94
|
test('returns formatted results for successful search', async () => {
|
|
@@ -104,9 +102,7 @@ describe('registerSearchTool', () => {
|
|
|
104
102
|
expect(text).toContain('Example')
|
|
105
103
|
expect(text).toContain('https://example.com')
|
|
106
104
|
|
|
107
|
-
|
|
108
|
-
expect(structured).toHaveProperty('resultCounts')
|
|
109
|
-
expect((structured as { resultCounts: { total: number } }).resultCounts.total).toBe(1)
|
|
105
|
+
expect(toolResult.structuredContent).toEqual(oneResultResponse)
|
|
110
106
|
})
|
|
111
107
|
|
|
112
108
|
test('returns error when API call fails', async () => {
|
|
@@ -27,32 +27,13 @@ describe('formatSearchResults', () => {
|
|
|
27
27
|
|
|
28
28
|
const result = formatSearchResults(mockResponse)
|
|
29
29
|
|
|
30
|
-
expect(result).
|
|
31
|
-
expect(result).toHaveProperty('
|
|
32
|
-
expect(result).toHaveProperty('
|
|
33
|
-
expect(
|
|
34
|
-
expect(result
|
|
35
|
-
expect(result
|
|
36
|
-
expect(result
|
|
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
|
-
snippets: ['snippet 1', 'snippet 2'],
|
|
54
|
-
})
|
|
55
|
-
expect(result.fullResponse).toBe(mockResponse)
|
|
30
|
+
expect(Array.isArray(result)).toBe(true)
|
|
31
|
+
expect(result[0]).toHaveProperty('type', 'text')
|
|
32
|
+
expect(result[0]).toHaveProperty('text')
|
|
33
|
+
expect(result[0]?.text).toContain('WEB RESULTS:')
|
|
34
|
+
expect(result[0]?.text).toContain('Test Title')
|
|
35
|
+
expect(result[0]?.text).toContain('URL: https://example.com')
|
|
36
|
+
expect(result[0]?.text).toContain('Published: 2023-01-01T00:00:00')
|
|
56
37
|
})
|
|
57
38
|
|
|
58
39
|
test('formats news results correctly', () => {
|
|
@@ -77,25 +58,11 @@ describe('formatSearchResults', () => {
|
|
|
77
58
|
|
|
78
59
|
const result = formatSearchResults(mockResponse)
|
|
79
60
|
|
|
80
|
-
expect(result
|
|
81
|
-
expect(result
|
|
82
|
-
expect(result
|
|
83
|
-
|
|
84
|
-
expect(result
|
|
85
|
-
expect(result.content[0]?.text).toContain('Description: News description')
|
|
86
|
-
expect(result.structuredContent).toHaveProperty('resultCounts')
|
|
87
|
-
expect(result.structuredContent.resultCounts).toHaveProperty('web', 0)
|
|
88
|
-
expect(result.structuredContent.resultCounts).toHaveProperty('news', 1)
|
|
89
|
-
expect(result.structuredContent.resultCounts).toHaveProperty('total', 1)
|
|
90
|
-
// All fields should be in structuredContent.results
|
|
91
|
-
expect(result.structuredContent).toHaveProperty('results')
|
|
92
|
-
expect(result.structuredContent.results?.news).toBeDefined()
|
|
93
|
-
expect(result.structuredContent.results?.news?.length).toBe(1)
|
|
94
|
-
expect(result.structuredContent.results?.news?.[0]).toMatchObject({
|
|
95
|
-
url: 'https://news.com/article',
|
|
96
|
-
title: 'News Title',
|
|
97
|
-
page_age: '2023-01-01T00:00:00',
|
|
98
|
-
})
|
|
61
|
+
expect(result[0]?.text).toContain('NEWS RESULTS:')
|
|
62
|
+
expect(result[0]?.text).toContain('News Title')
|
|
63
|
+
expect(result[0]?.text).toContain('Published: 2023-01-01T00:00:00')
|
|
64
|
+
expect(result[0]?.text).toContain('URL: https://news.com/article')
|
|
65
|
+
expect(result[0]?.text).toContain('Description: News description')
|
|
99
66
|
})
|
|
100
67
|
|
|
101
68
|
test('formats both web and news results', () => {
|
|
@@ -129,35 +96,14 @@ describe('formatSearchResults', () => {
|
|
|
129
96
|
|
|
130
97
|
const result = formatSearchResults(mockResponse)
|
|
131
98
|
|
|
132
|
-
expect(result
|
|
133
|
-
expect(result
|
|
134
|
-
expect(result
|
|
135
|
-
|
|
136
|
-
expect(result
|
|
137
|
-
expect(result.content[0]?.text).toContain('URL: https://news.com/article')
|
|
138
|
-
expect(result.structuredContent.resultCounts).toHaveProperty('web', 1)
|
|
139
|
-
expect(result.structuredContent.resultCounts).toHaveProperty('news', 1)
|
|
140
|
-
expect(result.structuredContent.resultCounts).toHaveProperty('total', 2)
|
|
141
|
-
// All fields should be in structuredContent.results
|
|
142
|
-
expect(result.structuredContent).toHaveProperty('results')
|
|
143
|
-
expect(result.structuredContent.results?.web).toBeDefined()
|
|
144
|
-
expect(result.structuredContent.results?.news).toBeDefined()
|
|
145
|
-
expect(result.structuredContent.results?.web?.length).toBe(1)
|
|
146
|
-
expect(result.structuredContent.results?.news?.length).toBe(1)
|
|
147
|
-
expect(result.structuredContent.results?.web?.[0]).toMatchObject({
|
|
148
|
-
url: 'https://web.com',
|
|
149
|
-
title: 'Web Title',
|
|
150
|
-
page_age: '2023-01-01T00:00:00',
|
|
151
|
-
snippets: ['web snippet'],
|
|
152
|
-
})
|
|
153
|
-
expect(result.structuredContent.results?.news?.[0]).toMatchObject({
|
|
154
|
-
url: 'https://news.com/article',
|
|
155
|
-
title: 'News Title',
|
|
156
|
-
page_age: '2023-01-01T00:00:00',
|
|
157
|
-
})
|
|
99
|
+
expect(result[0]?.text).toContain('WEB RESULTS:')
|
|
100
|
+
expect(result[0]?.text).toContain('NEWS RESULTS:')
|
|
101
|
+
expect(result[0]?.text).toContain(`=${'='.repeat(49)}`)
|
|
102
|
+
expect(result[0]?.text).toContain('URL: https://web.com')
|
|
103
|
+
expect(result[0]?.text).toContain('URL: https://news.com/article')
|
|
158
104
|
})
|
|
159
105
|
|
|
160
|
-
test('includes
|
|
106
|
+
test('includes page content indicator when livecrawl returns contents', () => {
|
|
161
107
|
const mockResponse: SearchResponse = {
|
|
162
108
|
results: {
|
|
163
109
|
web: [
|
|
@@ -185,23 +131,12 @@ describe('formatSearchResults', () => {
|
|
|
185
131
|
|
|
186
132
|
const result = formatSearchResults(mockResponse)
|
|
187
133
|
|
|
188
|
-
|
|
189
|
-
expect(result
|
|
190
|
-
expect(result
|
|
191
|
-
expect(result.content[0]?.text).toContain('chars (html)')
|
|
192
|
-
|
|
193
|
-
// structuredContent should include contents
|
|
194
|
-
expect(result.structuredContent.results?.web?.[0]).toMatchObject({
|
|
195
|
-
url: 'https://example.com',
|
|
196
|
-
title: 'Livecrawl Title',
|
|
197
|
-
contents: {
|
|
198
|
-
markdown: 'Full page content in markdown format.',
|
|
199
|
-
html: '<p>Full page content in HTML format.</p>',
|
|
200
|
-
},
|
|
201
|
-
})
|
|
134
|
+
expect(result[0]?.text).toContain('Page content available:')
|
|
135
|
+
expect(result[0]?.text).toContain('chars (markdown)')
|
|
136
|
+
expect(result[0]?.text).toContain('chars (html)')
|
|
202
137
|
})
|
|
203
138
|
|
|
204
|
-
test('omits
|
|
139
|
+
test('omits content indicator when livecrawl contents absent', () => {
|
|
205
140
|
const mockResponse: SearchResponse = {
|
|
206
141
|
results: {
|
|
207
142
|
web: [
|
|
@@ -223,11 +158,10 @@ describe('formatSearchResults', () => {
|
|
|
223
158
|
|
|
224
159
|
const result = formatSearchResults(mockResponse)
|
|
225
160
|
|
|
226
|
-
expect(result
|
|
227
|
-
expect(result.structuredContent.results?.web?.[0]?.contents).toBeUndefined()
|
|
161
|
+
expect(result[0]?.text).not.toContain('Page content available:')
|
|
228
162
|
})
|
|
229
163
|
|
|
230
|
-
test('includes
|
|
164
|
+
test('includes content indicator for news results with livecrawl', () => {
|
|
231
165
|
const mockResponse: SearchResponse = {
|
|
232
166
|
results: {
|
|
233
167
|
web: [],
|
|
@@ -252,40 +186,7 @@ describe('formatSearchResults', () => {
|
|
|
252
186
|
|
|
253
187
|
const result = formatSearchResults(mockResponse)
|
|
254
188
|
|
|
255
|
-
|
|
256
|
-
expect(result
|
|
257
|
-
expect(result.content[0]?.text).toContain('chars (markdown)')
|
|
258
|
-
|
|
259
|
-
// structuredContent should include contents for news
|
|
260
|
-
expect(result.structuredContent.results?.news?.[0]).toMatchObject({
|
|
261
|
-
url: 'https://news.com/article',
|
|
262
|
-
title: 'News with Content',
|
|
263
|
-
contents: { markdown: 'Full news article content in markdown.' },
|
|
264
|
-
})
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
test('includes snippets in structuredContent for web results', () => {
|
|
268
|
-
const mockResponse: SearchResponse = {
|
|
269
|
-
results: {
|
|
270
|
-
web: [
|
|
271
|
-
{
|
|
272
|
-
url: 'https://example.com',
|
|
273
|
-
title: 'With Snippets',
|
|
274
|
-
description: 'Has snippets',
|
|
275
|
-
snippets: ['first snippet', 'second snippet'],
|
|
276
|
-
},
|
|
277
|
-
],
|
|
278
|
-
news: [],
|
|
279
|
-
},
|
|
280
|
-
metadata: {
|
|
281
|
-
search_uuid: 'test-uuid',
|
|
282
|
-
query: 'test',
|
|
283
|
-
latency: 0.1,
|
|
284
|
-
},
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const result = formatSearchResults(mockResponse)
|
|
288
|
-
|
|
289
|
-
expect(result.structuredContent.results?.web?.[0]?.snippets).toEqual(['first snippet', 'second snippet'])
|
|
189
|
+
expect(result[0]?.text).toContain('Page content available:')
|
|
190
|
+
expect(result[0]?.text).toContain('chars (markdown)')
|
|
290
191
|
})
|
|
291
192
|
})
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import * as z from 'zod'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Structured content schema for MCP response
|
|
5
|
-
* Includes full content and metadata for each URL
|
|
6
|
-
*/
|
|
7
|
-
export const ContentsStructuredContentSchema = z.object({
|
|
8
|
-
count: z.number().describe('URLs processed'),
|
|
9
|
-
formats: z.array(z.string()).describe('Content formats requested'),
|
|
10
|
-
items: z
|
|
11
|
-
.array(
|
|
12
|
-
z.object({
|
|
13
|
-
url: z.string().describe('URL'),
|
|
14
|
-
title: z.string().optional().describe('Title'),
|
|
15
|
-
markdown: z.string().optional().describe('Markdown content'),
|
|
16
|
-
html: z.string().optional().describe('HTML content'),
|
|
17
|
-
metadata: z
|
|
18
|
-
.object({
|
|
19
|
-
favicon_url: z.string().describe('Favicon URL'),
|
|
20
|
-
site_name: z.string().optional().nullable().describe('Site name'),
|
|
21
|
-
})
|
|
22
|
-
.optional()
|
|
23
|
-
.nullable()
|
|
24
|
-
.describe('Page metadata'),
|
|
25
|
-
}),
|
|
26
|
-
)
|
|
27
|
-
.describe('Extracted items'),
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
export type ContentsStructuredContent = z.infer<typeof ContentsStructuredContentSchema>
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import * as z from 'zod'
|
|
2
|
-
|
|
3
|
-
// Minimal schema for structuredContent (reduces payload duplication)
|
|
4
|
-
// Full research content is in the text content field
|
|
5
|
-
export const ResearchStructuredContentSchema = z.object({
|
|
6
|
-
contentType: z.string().describe('Format of the content field'),
|
|
7
|
-
sourceCount: z.number().describe('Number of sources used'),
|
|
8
|
-
sources: z
|
|
9
|
-
.array(
|
|
10
|
-
z.object({
|
|
11
|
-
url: z.string().describe('Source URL'),
|
|
12
|
-
title: z.string().optional().describe('Source title'),
|
|
13
|
-
snippetCount: z.number().describe('Number of excerpts from this source'),
|
|
14
|
-
}),
|
|
15
|
-
)
|
|
16
|
-
.describe('Sources used in the research answer'),
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
export type ResearchStructuredContent = z.infer<typeof ResearchStructuredContentSchema>
|
|
@@ -1,53 +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
|
-
snippets: z.array(z.string()).optional().describe('Content snippets'),
|
|
20
|
-
contents: z
|
|
21
|
-
.object({
|
|
22
|
-
html: z.string().optional().describe('Full HTML content'),
|
|
23
|
-
markdown: z.string().optional().describe('Full Markdown content'),
|
|
24
|
-
})
|
|
25
|
-
.optional()
|
|
26
|
-
.describe('Livecrawled page content'),
|
|
27
|
-
}),
|
|
28
|
-
)
|
|
29
|
-
.optional()
|
|
30
|
-
.describe('Web results'),
|
|
31
|
-
news: z
|
|
32
|
-
.array(
|
|
33
|
-
z.object({
|
|
34
|
-
url: z.string().describe('URL'),
|
|
35
|
-
title: z.string().describe('Title'),
|
|
36
|
-
page_age: z.string().describe('Publication timestamp'),
|
|
37
|
-
contents: z
|
|
38
|
-
.object({
|
|
39
|
-
html: z.string().optional().describe('Full HTML content'),
|
|
40
|
-
markdown: z.string().optional().describe('Full Markdown content'),
|
|
41
|
-
})
|
|
42
|
-
.optional()
|
|
43
|
-
.describe('Livecrawled page content'),
|
|
44
|
-
}),
|
|
45
|
-
)
|
|
46
|
-
.optional()
|
|
47
|
-
.describe('News results'),
|
|
48
|
-
})
|
|
49
|
-
.optional()
|
|
50
|
-
.describe('Search results'),
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
export type SearchStructuredContent = z.infer<typeof SearchStructuredContentSchema>
|