@youdotcom-oss/mcp 1.3.2 → 1.3.3
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/AGENTS.md +149 -33
- package/README.md +60 -323
- package/bin/stdio.js +59 -41
- package/package.json +14 -4
- package/src/contents/contents.schemas.ts +55 -0
- package/src/contents/contents.utils.ts +145 -0
- package/src/express/express.schemas.ts +99 -0
- package/src/express/express.utils.ts +157 -0
- package/src/search/search.schemas.ts +126 -0
- package/src/search/search.utils.ts +142 -0
- package/src/shared/check-response-for-errors.ts +13 -0
- package/src/shared/format-search-results-text.ts +41 -0
package/bin/stdio.js
CHANGED
|
@@ -11154,7 +11154,7 @@ class StdioServerTransport {
|
|
|
11154
11154
|
// package.json
|
|
11155
11155
|
var package_default = {
|
|
11156
11156
|
name: "@youdotcom-oss/mcp",
|
|
11157
|
-
version: "1.3.
|
|
11157
|
+
version: "1.3.3",
|
|
11158
11158
|
description: "You.com API Model Context Protocol Server",
|
|
11159
11159
|
license: "MIT",
|
|
11160
11160
|
engines: {
|
|
@@ -11177,8 +11177,16 @@ var package_default = {
|
|
|
11177
11177
|
],
|
|
11178
11178
|
bin: "bin/stdio.js",
|
|
11179
11179
|
type: "module",
|
|
11180
|
+
main: "./src/main.ts",
|
|
11181
|
+
exports: {
|
|
11182
|
+
".": "./src/main.ts"
|
|
11183
|
+
},
|
|
11180
11184
|
files: [
|
|
11181
11185
|
"bin/stdio.js",
|
|
11186
|
+
"src/**/*.schemas.ts",
|
|
11187
|
+
"src/**/*.utils.ts",
|
|
11188
|
+
"src/shared/check-response-for-errors.ts",
|
|
11189
|
+
"src/shared/format-search-results-text.ts",
|
|
11182
11190
|
"AGENTS.md",
|
|
11183
11191
|
"CONTRIBUTING.md",
|
|
11184
11192
|
"docs/API.md"
|
|
@@ -11218,8 +11226,11 @@ var package_default = {
|
|
|
11218
11226
|
"format-package --write"
|
|
11219
11227
|
]
|
|
11220
11228
|
},
|
|
11229
|
+
peerDependencies: {
|
|
11230
|
+
zod: "^3.25.76"
|
|
11231
|
+
},
|
|
11221
11232
|
devDependencies: {
|
|
11222
|
-
"@biomejs/biome": "2.3.
|
|
11233
|
+
"@biomejs/biome": "2.3.7",
|
|
11223
11234
|
"@commitlint/cli": "^20.1.0",
|
|
11224
11235
|
"@commitlint/config-conventional": "^20.0.0",
|
|
11225
11236
|
"@eslint/js": "9.39.1",
|
|
@@ -11229,34 +11240,14 @@ var package_default = {
|
|
|
11229
11240
|
"lint-staged": "^16.2.7",
|
|
11230
11241
|
"format-package": "^7.0.0",
|
|
11231
11242
|
"@hono/bun-compress": "0.1.0",
|
|
11232
|
-
"@hono/mcp": "0.
|
|
11243
|
+
"@hono/mcp": "0.2.0",
|
|
11233
11244
|
"@modelcontextprotocol/sdk": "1.22.0",
|
|
11234
11245
|
hono: "^4.10.6",
|
|
11235
|
-
neverthrow: "8.2.0",
|
|
11236
11246
|
zod: "3.25.76"
|
|
11237
11247
|
}
|
|
11238
11248
|
};
|
|
11239
11249
|
|
|
11240
|
-
// src/shared/
|
|
11241
|
-
var setUserAgent = (client) => `MCP/${package_default.version} (You.com; ${client})`;
|
|
11242
|
-
var useGetClientVersion = (mcp) => () => {
|
|
11243
|
-
const clientVersion = mcp.server.getClientVersion();
|
|
11244
|
-
if (clientVersion) {
|
|
11245
|
-
const { name, version, title, websiteUrl } = clientVersion;
|
|
11246
|
-
return setUserAgent([name, version, title, websiteUrl].filter(Boolean).join("; "));
|
|
11247
|
-
}
|
|
11248
|
-
return setUserAgent("UNKNOWN");
|
|
11249
|
-
};
|
|
11250
|
-
var getLogger = (mcp) => async (params, _sessionId) => {
|
|
11251
|
-
await mcp.server.sendLoggingMessage(params);
|
|
11252
|
-
};
|
|
11253
|
-
var checkResponseForErrors = (responseData) => {
|
|
11254
|
-
if (typeof responseData === "object" && responseData !== null && "error" in responseData) {
|
|
11255
|
-
const errorMessage = typeof responseData.error === "string" ? responseData.error : JSON.stringify(responseData.error);
|
|
11256
|
-
throw new Error(`You.com API Error: ${errorMessage}`);
|
|
11257
|
-
}
|
|
11258
|
-
return responseData;
|
|
11259
|
-
};
|
|
11250
|
+
// src/shared/generate-error-report-link.ts
|
|
11260
11251
|
var generateErrorReportLink = ({
|
|
11261
11252
|
errorMessage,
|
|
11262
11253
|
tool,
|
|
@@ -11283,24 +11274,10 @@ Additional Context:
|
|
|
11283
11274
|
});
|
|
11284
11275
|
return `mailto:support@you.com?${params.toString()}`;
|
|
11285
11276
|
};
|
|
11286
|
-
var formatSearchResultsText = (results) => {
|
|
11287
|
-
return results.map((result) => {
|
|
11288
|
-
const parts = [`Title: ${result.title}`];
|
|
11289
|
-
if (result.description) {
|
|
11290
|
-
parts.push(`Description: ${result.description}`);
|
|
11291
|
-
}
|
|
11292
|
-
if (result.snippets && result.snippets.length > 0) {
|
|
11293
|
-
parts.push(`Snippets:
|
|
11294
|
-
- ${result.snippets.join(`
|
|
11295
|
-
- `)}`);
|
|
11296
|
-
} else if (result.snippet) {
|
|
11297
|
-
parts.push(`Snippet: ${result.snippet}`);
|
|
11298
|
-
}
|
|
11299
|
-
return parts.join(`
|
|
11300
|
-
`);
|
|
11301
|
-
}).join(`
|
|
11302
11277
|
|
|
11303
|
-
|
|
11278
|
+
// src/shared/get-logger.ts
|
|
11279
|
+
var getLogger = (mcp) => async (params) => {
|
|
11280
|
+
await mcp.server.sendLoggingMessage(params);
|
|
11304
11281
|
};
|
|
11305
11282
|
|
|
11306
11283
|
// src/contents/contents.schemas.ts
|
|
@@ -11326,6 +11303,15 @@ var ContentsStructuredContentSchema = objectType({
|
|
|
11326
11303
|
})).describe("Extracted items")
|
|
11327
11304
|
});
|
|
11328
11305
|
|
|
11306
|
+
// src/shared/check-response-for-errors.ts
|
|
11307
|
+
var checkResponseForErrors = (responseData) => {
|
|
11308
|
+
if (typeof responseData === "object" && responseData !== null && "error" in responseData) {
|
|
11309
|
+
const errorMessage = typeof responseData.error === "string" ? responseData.error : JSON.stringify(responseData.error);
|
|
11310
|
+
throw new Error(`You.com API Error: ${errorMessage}`);
|
|
11311
|
+
}
|
|
11312
|
+
return responseData;
|
|
11313
|
+
};
|
|
11314
|
+
|
|
11329
11315
|
// src/contents/contents.utils.ts
|
|
11330
11316
|
var CONTENTS_API_URL = "https://ydc-index.io/v1/contents";
|
|
11331
11317
|
var fetchContents = async ({
|
|
@@ -11536,6 +11522,27 @@ var ExpressStructuredContentSchema = objectType({
|
|
|
11536
11522
|
}).optional().describe("Search results")
|
|
11537
11523
|
});
|
|
11538
11524
|
|
|
11525
|
+
// src/shared/format-search-results-text.ts
|
|
11526
|
+
var formatSearchResultsText = (results) => {
|
|
11527
|
+
return results.map((result) => {
|
|
11528
|
+
const parts = [`Title: ${result.title}`];
|
|
11529
|
+
if (result.description) {
|
|
11530
|
+
parts.push(`Description: ${result.description}`);
|
|
11531
|
+
}
|
|
11532
|
+
if (result.snippets && result.snippets.length > 0) {
|
|
11533
|
+
parts.push(`Snippets:
|
|
11534
|
+
- ${result.snippets.join(`
|
|
11535
|
+
- `)}`);
|
|
11536
|
+
} else if (result.snippet) {
|
|
11537
|
+
parts.push(`Snippet: ${result.snippet}`);
|
|
11538
|
+
}
|
|
11539
|
+
return parts.join(`
|
|
11540
|
+
`);
|
|
11541
|
+
}).join(`
|
|
11542
|
+
|
|
11543
|
+
`);
|
|
11544
|
+
};
|
|
11545
|
+
|
|
11539
11546
|
// src/express/express.utils.ts
|
|
11540
11547
|
var AGENTS_RUN_URL = "https://api.you.com/v1/agents/runs";
|
|
11541
11548
|
var agentThrowOnFailedStatus = async (response) => {
|
|
@@ -14488,6 +14495,17 @@ Report this issue: ${reportLink}`
|
|
|
14488
14495
|
});
|
|
14489
14496
|
};
|
|
14490
14497
|
|
|
14498
|
+
// src/shared/use-client-version.ts
|
|
14499
|
+
var setUserAgent = (client) => `MCP/${package_default.version} (You.com; ${client})`;
|
|
14500
|
+
var useGetClientVersion = (mcp) => () => {
|
|
14501
|
+
const clientVersion = mcp.server.getClientVersion();
|
|
14502
|
+
if (clientVersion) {
|
|
14503
|
+
const { name, version, title, websiteUrl } = clientVersion;
|
|
14504
|
+
return setUserAgent([name, version, title, websiteUrl].filter(Boolean).join("; "));
|
|
14505
|
+
}
|
|
14506
|
+
return setUserAgent("UNKNOWN");
|
|
14507
|
+
};
|
|
14508
|
+
|
|
14491
14509
|
// src/stdio.ts
|
|
14492
14510
|
var YDC_API_KEY = process.env.YDC_API_KEY;
|
|
14493
14511
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youdotcom-oss/mcp",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "You.com API Model Context Protocol Server",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"engines": {
|
|
@@ -23,8 +23,16 @@
|
|
|
23
23
|
],
|
|
24
24
|
"bin": "bin/stdio.js",
|
|
25
25
|
"type": "module",
|
|
26
|
+
"main": "./src/main.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./src/main.ts"
|
|
29
|
+
},
|
|
26
30
|
"files": [
|
|
27
31
|
"bin/stdio.js",
|
|
32
|
+
"src/**/*.schemas.ts",
|
|
33
|
+
"src/**/*.utils.ts",
|
|
34
|
+
"src/shared/check-response-for-errors.ts",
|
|
35
|
+
"src/shared/format-search-results-text.ts",
|
|
28
36
|
"AGENTS.md",
|
|
29
37
|
"CONTRIBUTING.md",
|
|
30
38
|
"docs/API.md"
|
|
@@ -64,8 +72,11 @@
|
|
|
64
72
|
"format-package --write"
|
|
65
73
|
]
|
|
66
74
|
},
|
|
75
|
+
"peerDependencies": {
|
|
76
|
+
"zod": "^3.25.76"
|
|
77
|
+
},
|
|
67
78
|
"devDependencies": {
|
|
68
|
-
"@biomejs/biome": "2.3.
|
|
79
|
+
"@biomejs/biome": "2.3.7",
|
|
69
80
|
"@commitlint/cli": "^20.1.0",
|
|
70
81
|
"@commitlint/config-conventional": "^20.0.0",
|
|
71
82
|
"@eslint/js": "9.39.1",
|
|
@@ -75,10 +86,9 @@
|
|
|
75
86
|
"lint-staged": "^16.2.7",
|
|
76
87
|
"format-package": "^7.0.0",
|
|
77
88
|
"@hono/bun-compress": "0.1.0",
|
|
78
|
-
"@hono/mcp": "0.
|
|
89
|
+
"@hono/mcp": "0.2.0",
|
|
79
90
|
"@modelcontextprotocol/sdk": "1.22.0",
|
|
80
91
|
"hono": "^4.10.6",
|
|
81
|
-
"neverthrow": "8.2.0",
|
|
82
92
|
"zod": "3.25.76"
|
|
83
93
|
}
|
|
84
94
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Input schema for the you-contents tool
|
|
5
|
+
* Accepts an array of URLs and optional format
|
|
6
|
+
*/
|
|
7
|
+
export const ContentsQuerySchema = z.object({
|
|
8
|
+
urls: z.array(z.string().url()).min(1).describe('URLs to extract content from'),
|
|
9
|
+
format: z
|
|
10
|
+
.enum(['markdown', 'html'])
|
|
11
|
+
.optional()
|
|
12
|
+
.default('markdown')
|
|
13
|
+
.describe('Output format: markdown (text) or html (layout)'),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type ContentsQuery = z.infer<typeof ContentsQuerySchema>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Schema for a single content item in the API response
|
|
20
|
+
*/
|
|
21
|
+
const ContentsItemSchema = z.object({
|
|
22
|
+
url: z.string().describe('URL'),
|
|
23
|
+
title: z.string().describe('Title'),
|
|
24
|
+
html: z.string().optional().describe('HTML content'),
|
|
25
|
+
markdown: z.string().optional().describe('Markdown content'),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* API response schema from You.com Contents API
|
|
30
|
+
* Validates the full response array
|
|
31
|
+
*/
|
|
32
|
+
export const ContentsApiResponseSchema = z.array(ContentsItemSchema);
|
|
33
|
+
|
|
34
|
+
export type ContentsApiResponse = z.infer<typeof ContentsApiResponseSchema>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Structured content schema for MCP response
|
|
38
|
+
* Includes full content and metadata for each URL
|
|
39
|
+
*/
|
|
40
|
+
export const ContentsStructuredContentSchema = z.object({
|
|
41
|
+
count: z.number().describe('URLs processed'),
|
|
42
|
+
format: z.string().describe('Content format'),
|
|
43
|
+
items: z
|
|
44
|
+
.array(
|
|
45
|
+
z.object({
|
|
46
|
+
url: z.string().describe('URL'),
|
|
47
|
+
title: z.string().describe('Title'),
|
|
48
|
+
content: z.string().describe('Extracted content'),
|
|
49
|
+
contentLength: z.number().describe('Content length'),
|
|
50
|
+
}),
|
|
51
|
+
)
|
|
52
|
+
.describe('Extracted items'),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export type ContentsStructuredContent = z.infer<typeof ContentsStructuredContentSchema>;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { checkResponseForErrors } from '../shared/check-response-for-errors.ts';
|
|
2
|
+
import {
|
|
3
|
+
type ContentsApiResponse,
|
|
4
|
+
ContentsApiResponseSchema,
|
|
5
|
+
type ContentsQuery,
|
|
6
|
+
type ContentsStructuredContent,
|
|
7
|
+
} from './contents.schemas.ts';
|
|
8
|
+
|
|
9
|
+
const CONTENTS_API_URL = 'https://ydc-index.io/v1/contents';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Fetch content from You.com Contents API
|
|
13
|
+
* The API accepts multiple URLs in a single request and returns all results
|
|
14
|
+
* @param contentsQuery - Query parameters including URLs and format
|
|
15
|
+
* @param YDC_API_KEY - You.com API key
|
|
16
|
+
* @param getUserAgent - Function to get User-Agent string
|
|
17
|
+
* @returns Parsed and validated API response
|
|
18
|
+
*/
|
|
19
|
+
export const fetchContents = async ({
|
|
20
|
+
contentsQuery: { urls, format = 'markdown' },
|
|
21
|
+
YDC_API_KEY = process.env.YDC_API_KEY,
|
|
22
|
+
getUserAgent,
|
|
23
|
+
}: {
|
|
24
|
+
contentsQuery: ContentsQuery;
|
|
25
|
+
YDC_API_KEY?: string;
|
|
26
|
+
getUserAgent: () => string;
|
|
27
|
+
}): Promise<ContentsApiResponse> => {
|
|
28
|
+
if (!YDC_API_KEY) {
|
|
29
|
+
throw new Error('YDC_API_KEY is required for Contents API');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Make single API call with all URLs
|
|
33
|
+
const options = {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: new Headers({
|
|
36
|
+
'X-API-Key': YDC_API_KEY,
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
'User-Agent': getUserAgent(),
|
|
39
|
+
}),
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
urls,
|
|
42
|
+
format,
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const response = await fetch(CONTENTS_API_URL, options);
|
|
47
|
+
|
|
48
|
+
// Handle HTTP errors
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
const errorCode = response.status;
|
|
51
|
+
|
|
52
|
+
// Try to parse error response body
|
|
53
|
+
let errorDetail = `Failed to fetch contents. HTTP ${errorCode}`;
|
|
54
|
+
try {
|
|
55
|
+
const errorBody = await response.json();
|
|
56
|
+
if (errorBody && typeof errorBody === 'object' && 'detail' in errorBody) {
|
|
57
|
+
errorDetail = String(errorBody.detail);
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// If parsing fails, use default error message
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Handle specific error codes
|
|
64
|
+
if (errorCode === 401) {
|
|
65
|
+
throw new Error(`Authentication failed: ${errorDetail}. Please check your You.com API key.`);
|
|
66
|
+
}
|
|
67
|
+
if (errorCode === 403) {
|
|
68
|
+
throw new Error(`Forbidden: ${errorDetail}. Your API key may not have access to the Contents API.`);
|
|
69
|
+
}
|
|
70
|
+
if (errorCode === 429) {
|
|
71
|
+
throw new Error('Rate limited by You.com API. Please try again later.');
|
|
72
|
+
}
|
|
73
|
+
if (errorCode >= 500) {
|
|
74
|
+
throw new Error(`You.com API server error: ${errorDetail}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
throw new Error(errorDetail);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const results = await response.json();
|
|
81
|
+
|
|
82
|
+
// Check for error field in 200 responses
|
|
83
|
+
checkResponseForErrors(results);
|
|
84
|
+
|
|
85
|
+
// Validate schema
|
|
86
|
+
const parsedResults = ContentsApiResponseSchema.parse(results);
|
|
87
|
+
|
|
88
|
+
return parsedResults;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Format contents API response for MCP output
|
|
93
|
+
* Returns full content in both text and structured formats
|
|
94
|
+
* @param response - Validated API response
|
|
95
|
+
* @param format - Format used for extraction
|
|
96
|
+
* @returns Formatted response with content and structuredContent
|
|
97
|
+
*/
|
|
98
|
+
export const formatContentsResponse = (
|
|
99
|
+
response: ContentsApiResponse,
|
|
100
|
+
format: string,
|
|
101
|
+
): {
|
|
102
|
+
content: Array<{ type: 'text'; text: string }>;
|
|
103
|
+
structuredContent: ContentsStructuredContent;
|
|
104
|
+
} => {
|
|
105
|
+
// Build text content with full extracted content
|
|
106
|
+
const textParts: string[] = [`Successfully extracted content from ${response.length} URL(s):\n`];
|
|
107
|
+
|
|
108
|
+
const items: ContentsStructuredContent['items'] = [];
|
|
109
|
+
|
|
110
|
+
for (const item of response) {
|
|
111
|
+
const contentField = format === 'html' ? item.html : item.markdown;
|
|
112
|
+
const content = contentField || '';
|
|
113
|
+
|
|
114
|
+
// Add full content for this item
|
|
115
|
+
textParts.push(`\n## ${item.title}`);
|
|
116
|
+
textParts.push(`URL: ${item.url}`);
|
|
117
|
+
textParts.push(`Format: ${format}`);
|
|
118
|
+
textParts.push(`Content Length: ${content.length} characters\n`);
|
|
119
|
+
textParts.push('---\n');
|
|
120
|
+
textParts.push(content);
|
|
121
|
+
textParts.push('\n---\n');
|
|
122
|
+
|
|
123
|
+
// Add to structured content with full content
|
|
124
|
+
items.push({
|
|
125
|
+
url: item.url,
|
|
126
|
+
title: item.title,
|
|
127
|
+
content,
|
|
128
|
+
contentLength: content.length,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
content: [
|
|
134
|
+
{
|
|
135
|
+
type: 'text',
|
|
136
|
+
text: textParts.join('\n'),
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
structuredContent: {
|
|
140
|
+
count: response.length,
|
|
141
|
+
format,
|
|
142
|
+
items,
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as z from 'zod';
|
|
2
|
+
|
|
3
|
+
export const ExpressAgentInputSchema = z.object({
|
|
4
|
+
input: z.string().min(1, 'Input is required').describe('Query or prompt'),
|
|
5
|
+
tools: z
|
|
6
|
+
.array(
|
|
7
|
+
z.object({
|
|
8
|
+
type: z.enum(['web_search']).describe('Tool type'),
|
|
9
|
+
}),
|
|
10
|
+
)
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('Tools (web search only)'),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export type ExpressAgentInput = z.infer<typeof ExpressAgentInputSchema>;
|
|
16
|
+
|
|
17
|
+
// API Response Schema - Validates the full response from You.com API
|
|
18
|
+
|
|
19
|
+
// Search result content item from web_search.results
|
|
20
|
+
// Note: thumbnail_url, source_type, and provider are API-only pass-through fields not used in MCP output
|
|
21
|
+
const ApiSearchResultItemSchema = z.object({
|
|
22
|
+
source_type: z.string().optional(),
|
|
23
|
+
citation_uri: z.string().optional(), // Used as fallback for url in transformation
|
|
24
|
+
url: z.string(),
|
|
25
|
+
title: z.string(),
|
|
26
|
+
snippet: z.string(),
|
|
27
|
+
thumbnail_url: z.string().optional(), // API-only, not transformed to MCP output
|
|
28
|
+
provider: z.any().optional(), // API-only, not transformed to MCP output
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Union of possible output item types from API
|
|
32
|
+
const ExpressAgentApiOutputItemSchema = z.union([
|
|
33
|
+
// web_search.results type - has content array, no text
|
|
34
|
+
z.object({
|
|
35
|
+
type: z.literal('web_search.results'),
|
|
36
|
+
content: z.array(ApiSearchResultItemSchema),
|
|
37
|
+
}),
|
|
38
|
+
// message.answer type - has text, no content
|
|
39
|
+
z.object({
|
|
40
|
+
type: z.literal('message.answer'),
|
|
41
|
+
text: z.string(),
|
|
42
|
+
}),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export const ExpressAgentApiResponseSchema = z
|
|
46
|
+
.object({
|
|
47
|
+
output: z.array(ExpressAgentApiOutputItemSchema),
|
|
48
|
+
agent: z.string().optional().describe('Agent identifier'),
|
|
49
|
+
mode: z.string().optional().describe('Agent mode'),
|
|
50
|
+
input: z.array(z.any()).optional().describe('Input messages'),
|
|
51
|
+
})
|
|
52
|
+
.passthrough();
|
|
53
|
+
|
|
54
|
+
export type ExpressAgentApiResponse = z.infer<typeof ExpressAgentApiResponseSchema>;
|
|
55
|
+
|
|
56
|
+
// MCP Output Schema - Defines what we return to the MCP client (answer + optional search results, token efficient)
|
|
57
|
+
|
|
58
|
+
// Search result item for MCP output
|
|
59
|
+
const McpSearchResultItemSchema = z.object({
|
|
60
|
+
url: z.string().describe('URL'),
|
|
61
|
+
title: z.string().describe('Title'),
|
|
62
|
+
snippet: z.string().describe('Snippet'),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// MCP response structure: answer (always) + results (optional when web_search used)
|
|
66
|
+
const ExpressAgentMcpResponseSchema = z.object({
|
|
67
|
+
answer: z.string().describe('AI answer'),
|
|
68
|
+
results: z
|
|
69
|
+
.object({
|
|
70
|
+
web: z.array(McpSearchResultItemSchema).describe('Web results'),
|
|
71
|
+
})
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('Search results'),
|
|
74
|
+
agent: z.string().optional().describe('Agent ID'),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export type ExpressAgentMcpResponse = z.infer<typeof ExpressAgentMcpResponseSchema>;
|
|
78
|
+
|
|
79
|
+
// Minimal schema for structuredContent (reduces payload duplication)
|
|
80
|
+
export const ExpressStructuredContentSchema = z.object({
|
|
81
|
+
answer: z.string().describe('AI answer'),
|
|
82
|
+
hasResults: z.boolean().describe('Has web results'),
|
|
83
|
+
resultCount: z.number().describe('Result count'),
|
|
84
|
+
agent: z.string().optional().describe('Agent ID'),
|
|
85
|
+
results: z
|
|
86
|
+
.object({
|
|
87
|
+
web: z
|
|
88
|
+
.array(
|
|
89
|
+
z.object({
|
|
90
|
+
url: z.string().describe('URL'),
|
|
91
|
+
title: z.string().describe('Title'),
|
|
92
|
+
}),
|
|
93
|
+
)
|
|
94
|
+
.optional()
|
|
95
|
+
.describe('Web results'),
|
|
96
|
+
})
|
|
97
|
+
.optional()
|
|
98
|
+
.describe('Search results'),
|
|
99
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { checkResponseForErrors } from '../shared/check-response-for-errors.ts';
|
|
2
|
+
import { formatSearchResultsText } from '../shared/format-search-results-text.ts';
|
|
3
|
+
import {
|
|
4
|
+
type ExpressAgentApiResponse,
|
|
5
|
+
ExpressAgentApiResponseSchema,
|
|
6
|
+
type ExpressAgentInput,
|
|
7
|
+
type ExpressAgentMcpResponse,
|
|
8
|
+
} from './express.schemas.ts';
|
|
9
|
+
|
|
10
|
+
// Express Agent Constants
|
|
11
|
+
const AGENTS_RUN_URL = 'https://api.you.com/v1/agents/runs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Checks response status and throws appropriate errors for agent API calls
|
|
15
|
+
*/
|
|
16
|
+
const agentThrowOnFailedStatus = async (response: Response) => {
|
|
17
|
+
const errorCode = response.status;
|
|
18
|
+
|
|
19
|
+
const errorData = (await response.json()) as {
|
|
20
|
+
errors?: Array<{ detail?: string }>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
if (errorCode === 400) {
|
|
24
|
+
throw new Error(`Bad Request:\n${JSON.stringify(errorData)}`);
|
|
25
|
+
} else if (errorCode === 401) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Unauthorized: The Agent APIs require a valid You.com API key with agent access. Ensure your YDC_API_KEY has permissions for agent endpoints.`,
|
|
28
|
+
);
|
|
29
|
+
} else if (errorCode === 403) {
|
|
30
|
+
throw new Error(`Forbidden: You are not allowed to use the requested tool for this agent or tenant`);
|
|
31
|
+
} else if (errorCode === 429) {
|
|
32
|
+
throw new Error('Rate limited by You.com API. Please try again later.');
|
|
33
|
+
}
|
|
34
|
+
throw new Error(`Failed to call agent. Error code: ${errorCode}`);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const callExpressAgent = async ({
|
|
38
|
+
YDC_API_KEY = process.env.YDC_API_KEY,
|
|
39
|
+
agentInput: { input, tools },
|
|
40
|
+
getUserAgent,
|
|
41
|
+
}: {
|
|
42
|
+
agentInput: ExpressAgentInput;
|
|
43
|
+
YDC_API_KEY?: string;
|
|
44
|
+
getUserAgent: () => string;
|
|
45
|
+
}) => {
|
|
46
|
+
const requestBody: {
|
|
47
|
+
agent: string;
|
|
48
|
+
input: string;
|
|
49
|
+
stream: boolean;
|
|
50
|
+
tools?: Array<{ type: 'web_search' }>;
|
|
51
|
+
} = {
|
|
52
|
+
agent: 'express',
|
|
53
|
+
input,
|
|
54
|
+
stream: false, // Use non-streaming JSON response
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Only include tools if provided
|
|
58
|
+
if (tools) {
|
|
59
|
+
requestBody.tools = tools;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const options = {
|
|
63
|
+
method: 'POST',
|
|
64
|
+
headers: new Headers({
|
|
65
|
+
Authorization: `Bearer ${YDC_API_KEY || ''}`,
|
|
66
|
+
'Content-Type': 'application/json',
|
|
67
|
+
Accept: 'application/json',
|
|
68
|
+
'User-Agent': getUserAgent(),
|
|
69
|
+
}),
|
|
70
|
+
body: JSON.stringify(requestBody),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const response = await fetch(AGENTS_RUN_URL, options);
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
await agentThrowOnFailedStatus(response);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Parse JSON response directly
|
|
80
|
+
const jsonResponse = await response.json();
|
|
81
|
+
|
|
82
|
+
// Check for error field in response
|
|
83
|
+
checkResponseForErrors(jsonResponse);
|
|
84
|
+
|
|
85
|
+
// Validate API response schema (full response with all fields)
|
|
86
|
+
const apiResponse: ExpressAgentApiResponse = ExpressAgentApiResponseSchema.parse(jsonResponse);
|
|
87
|
+
|
|
88
|
+
// Find the answer (always present as message.answer, validated by Zod)
|
|
89
|
+
const answerItem = apiResponse.output.find((item) => item.type === 'message.answer');
|
|
90
|
+
if (!answerItem) {
|
|
91
|
+
throw new Error('Express API response missing required message.answer item');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Find search results (optional, present when web_search tool is used)
|
|
95
|
+
const searchItem = apiResponse.output.find((item) => item.type === 'web_search.results');
|
|
96
|
+
|
|
97
|
+
// Transform API response to MCP output format (answer + optional search results, token efficient)
|
|
98
|
+
const mcpResponse: ExpressAgentMcpResponse = {
|
|
99
|
+
answer: answerItem.text,
|
|
100
|
+
agent: apiResponse.agent,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Transform search results if present
|
|
104
|
+
if (searchItem && 'content' in searchItem && Array.isArray(searchItem.content)) {
|
|
105
|
+
mcpResponse.results = {
|
|
106
|
+
web: searchItem.content.map((item) => ({
|
|
107
|
+
url: item.url || item.citation_uri || '',
|
|
108
|
+
title: item.title || '',
|
|
109
|
+
snippet: item.snippet || '',
|
|
110
|
+
})),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return mcpResponse;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export const formatExpressAgentResponse = (response: ExpressAgentMcpResponse) => {
|
|
118
|
+
const _agentId = response.agent || 'express';
|
|
119
|
+
const content: Array<{ type: 'text'; text: string }> = [];
|
|
120
|
+
|
|
121
|
+
// 1. Answer first (always present)
|
|
122
|
+
content.push({
|
|
123
|
+
type: 'text',
|
|
124
|
+
text: `Express Agent Answer:\n\n${response.answer}`,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// 2. Search results second (if present when web_search tool was used) - without URLs in text
|
|
128
|
+
if (response.results?.web?.length) {
|
|
129
|
+
const formattedResults = formatSearchResultsText(response.results.web);
|
|
130
|
+
content.push({
|
|
131
|
+
type: 'text',
|
|
132
|
+
text: `\nSearch Results:\n\n${formattedResults}`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Extract URLs and titles for structuredContent
|
|
137
|
+
const structuredResults = response.results?.web?.length
|
|
138
|
+
? {
|
|
139
|
+
web: response.results.web.map((result) => ({
|
|
140
|
+
url: result.url,
|
|
141
|
+
title: result.title,
|
|
142
|
+
})),
|
|
143
|
+
}
|
|
144
|
+
: undefined;
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
content,
|
|
148
|
+
structuredContent: {
|
|
149
|
+
answer: response.answer,
|
|
150
|
+
hasResults: !!response.results?.web?.length,
|
|
151
|
+
resultCount: response.results?.web?.length || 0,
|
|
152
|
+
agent: response.agent,
|
|
153
|
+
results: structuredResults,
|
|
154
|
+
},
|
|
155
|
+
fullResponse: response,
|
|
156
|
+
};
|
|
157
|
+
};
|