@youdotcom-oss/mcp 1.3.2 → 1.3.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdotcom-oss/mcp",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
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.6",
79
+ "@biomejs/biome": "2.3.8",
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.1.5",
79
- "@modelcontextprotocol/sdk": "1.22.0",
80
- "hono": "^4.10.6",
81
- "neverthrow": "8.2.0",
82
- "zod": "3.25.76"
89
+ "@hono/mcp": "0.2.0",
90
+ "@modelcontextprotocol/sdk": "1.24.2",
91
+ "hono": "^4.10.7",
92
+ "zod": "4.1.13"
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().optional().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().optional().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
+ };
@@ -0,0 +1,126 @@
1
+ import * as z from 'zod';
2
+
3
+ export const SearchQuerySchema = z.object({
4
+ query: z.string().min(1, 'Query is required').describe('Search query (supports +, -, site:, filetype:, lang:)'),
5
+ count: z.number().int().min(1).max(20).optional().describe('Max results per section'),
6
+ freshness: z.enum(['day', 'week', 'month', 'year']).optional().describe('Filter by freshness'),
7
+ offset: z.number().int().min(0).max(9).optional().describe('Pagination offset'),
8
+ country: z
9
+ .enum([
10
+ 'AR',
11
+ 'AU',
12
+ 'AT',
13
+ 'BE',
14
+ 'BR',
15
+ 'CA',
16
+ 'CL',
17
+ 'DK',
18
+ 'FI',
19
+ 'FR',
20
+ 'DE',
21
+ 'HK',
22
+ 'IN',
23
+ 'ID',
24
+ 'IT',
25
+ 'JP',
26
+ 'KR',
27
+ 'MY',
28
+ 'MX',
29
+ 'NL',
30
+ 'NZ',
31
+ 'NO',
32
+ 'CN',
33
+ 'PL',
34
+ 'PT',
35
+ 'PH',
36
+ 'RU',
37
+ 'SA',
38
+ 'ZA',
39
+ 'ES',
40
+ 'SE',
41
+ 'CH',
42
+ 'TW',
43
+ 'TR',
44
+ 'GB',
45
+ 'US',
46
+ ])
47
+ .optional()
48
+ .describe('Country code'),
49
+ safesearch: z.enum(['off', 'moderate', 'strict']).optional().describe('Filter level'),
50
+ site: z.string().optional().describe('Specific domain'),
51
+ fileType: z.string().optional().describe('File type'),
52
+ language: z.string().optional().describe('ISO 639-1 language code'),
53
+ excludeTerms: z.string().optional().describe('Terms to exclude (pipe-separated)'),
54
+ exactTerms: z.string().optional().describe('Exact terms (pipe-separated)'),
55
+ });
56
+
57
+ export type SearchQuery = z.infer<typeof SearchQuerySchema>;
58
+
59
+ const WebResultSchema = z.object({
60
+ url: z.string().describe('URL'),
61
+ title: z.string().describe('Title'),
62
+ description: z.string().describe('Description'),
63
+ snippets: z.array(z.string()).describe('Content snippets'),
64
+ page_age: z.string().optional().describe('Publication timestamp'),
65
+ authors: z.array(z.string()).optional().describe('Authors'),
66
+ });
67
+
68
+ const NewsResultSchema = z.object({
69
+ title: z.string().describe('Title'),
70
+ description: z.string().describe('Description'),
71
+ page_age: z.string().describe('Publication timestamp'),
72
+ url: z.string().describe('URL'),
73
+ });
74
+
75
+ export type NewsResult = z.infer<typeof NewsResultSchema>;
76
+
77
+ const MetadataSchema = z.object({
78
+ request_uuid: z.string().optional().describe('Request ID'),
79
+ query: z.string().describe('Query'),
80
+ latency: z.number().describe('Latency in seconds'),
81
+ });
82
+
83
+ export const SearchResponseSchema = z.object({
84
+ results: z.object({
85
+ web: z.array(WebResultSchema).optional(),
86
+ news: z.array(NewsResultSchema).optional(),
87
+ }),
88
+ metadata: MetadataSchema.partial(),
89
+ });
90
+
91
+ export type SearchResponse = z.infer<typeof SearchResponseSchema>;
92
+
93
+ // Minimal schema for structuredContent (reduces payload duplication)
94
+ // Excludes metadata (query, request_uuid, latency) as these are not actionable by LLM
95
+ export const SearchStructuredContentSchema = z.object({
96
+ resultCounts: z.object({
97
+ web: z.number().describe('Web results'),
98
+ news: z.number().describe('News results'),
99
+ total: z.number().describe('Total results'),
100
+ }),
101
+ results: z
102
+ .object({
103
+ web: z
104
+ .array(
105
+ z.object({
106
+ url: z.string().describe('URL'),
107
+ title: z.string().describe('Title'),
108
+ }),
109
+ )
110
+ .optional()
111
+ .describe('Web results'),
112
+ news: z
113
+ .array(
114
+ z.object({
115
+ url: z.string().describe('URL'),
116
+ title: z.string().describe('Title'),
117
+ }),
118
+ )
119
+ .optional()
120
+ .describe('News results'),
121
+ })
122
+ .optional()
123
+ .describe('Search results'),
124
+ });
125
+
126
+ export type SearchStructuredContent = z.infer<typeof SearchStructuredContentSchema>;