@youdotcom-oss/mcp 2.1.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -72
- package/bin/stdio.js +6699 -28740
- package/package.json +10 -22
- package/server.json +51 -0
- package/src/contents/contents.schemas.ts +0 -30
- package/src/contents/contents.utils.ts +0 -84
- package/src/contents/register-contents-tool.ts +0 -94
- package/src/contents/tests/contents.utils.spec.ts +0 -123
- package/src/get-mcp-server.ts +0 -17
- package/src/http.ts +0 -94
- package/src/research/register-research-tool.ts +0 -69
- package/src/research/research.schemas.ts +0 -19
- package/src/research/research.utils.ts +0 -30
- package/src/search/register-search-tool.ts +0 -87
- package/src/search/search.schema.ts +0 -38
- package/src/search/search.utils.ts +0 -70
- package/src/search/tests/search.utils.spec.ts +0 -156
- package/src/shared/format-search-results-text.ts +0 -49
- package/src/shared/get-logger.ts +0 -10
- package/src/shared/tests/shared.utils.spec.ts +0 -160
- package/src/shared/use-client-version.ts +0 -21
- package/src/stdio.ts +0 -24
- package/src/tests/http.spec.ts +0 -583
- package/src/tests/tool.spec.ts +0 -649
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youdotcom-oss/mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "You.com
|
|
3
|
+
"version": "3.1.0",
|
|
4
|
+
"description": "You.com MCP STDIO bridge — proxies STDIO to the remote You.com MCP server at api.you.com/mcp",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=18",
|
|
@@ -20,49 +20,37 @@
|
|
|
20
20
|
"keywords": [
|
|
21
21
|
"mcp",
|
|
22
22
|
"web search",
|
|
23
|
-
"model context protocol"
|
|
23
|
+
"model context protocol",
|
|
24
|
+
"stdio bridge"
|
|
24
25
|
],
|
|
25
26
|
"bin": "bin/stdio.js",
|
|
26
27
|
"type": "module",
|
|
27
|
-
"exports": {
|
|
28
|
-
"./http": "./src/http.ts"
|
|
29
|
-
},
|
|
30
28
|
"files": [
|
|
31
29
|
"bin/stdio.js",
|
|
32
|
-
"
|
|
33
|
-
"./src/**",
|
|
34
|
-
"!./src/**/tests/*",
|
|
35
|
-
"!./src/**/*.spec.@(tsx|ts)"
|
|
30
|
+
"server.json"
|
|
36
31
|
],
|
|
37
32
|
"publishConfig": {
|
|
38
33
|
"access": "public"
|
|
39
34
|
},
|
|
40
35
|
"scripts": {
|
|
41
|
-
"build": "bun build ./src/stdio.ts --outfile ./bin/stdio.js --target=node",
|
|
36
|
+
"build": "bun build ./src/stdio-bridge.ts --outfile ./bin/stdio.js --target=node",
|
|
42
37
|
"check": "bun run check:biome && bun run check:types && bun run check:package",
|
|
43
38
|
"check:biome": "biome check",
|
|
44
39
|
"check:package": "format-package --check",
|
|
45
40
|
"check:types": "tsc --noEmit",
|
|
46
41
|
"check:write": "biome check --write && bun run format:package",
|
|
47
|
-
"dev": "bun src/stdio.ts",
|
|
42
|
+
"dev": "bun src/stdio-bridge.ts",
|
|
48
43
|
"format:package": "format-package --write",
|
|
49
|
-
"
|
|
44
|
+
"inspector": "test -n \"$YDC_API_KEY\" || (echo 'YDC_API_KEY is not set' && exit 1); mcp-inspector --config mcp-inspector.json -e YDC_API_KEY=$YDC_API_KEY",
|
|
50
45
|
"prepublishOnly": "bun run build",
|
|
51
|
-
"start": "bun run src/http.ts",
|
|
52
46
|
"test": "bun test",
|
|
53
|
-
"test:coverage": "bun test --coverage",
|
|
54
|
-
"test:coverage:watch": "bun test --coverage --watch",
|
|
55
47
|
"test:watch": "bun test --watch"
|
|
56
48
|
},
|
|
57
49
|
"mcpName": "io.github.youdotcom-oss/mcp",
|
|
58
50
|
"dependencies": {
|
|
59
|
-
"@
|
|
60
|
-
"zod": "^4.3.6",
|
|
61
|
-
"@hono/mcp": "^0.2.3",
|
|
62
|
-
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
63
|
-
"hono": "^4.11.7"
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.25.3"
|
|
64
52
|
},
|
|
65
53
|
"devDependencies": {
|
|
66
|
-
"@modelcontextprotocol/inspector": "0.
|
|
54
|
+
"@modelcontextprotocol/inspector": "0.21.1"
|
|
67
55
|
}
|
|
68
56
|
}
|
package/server.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
|
+
"name": "io.github.youdotcom-oss/mcp",
|
|
4
|
+
"title": "You.com Web Access & AI",
|
|
5
|
+
"description": "Web search, AI agent, and content extraction via You.com APIs",
|
|
6
|
+
"version": "3.0.0",
|
|
7
|
+
"remotes": [
|
|
8
|
+
{
|
|
9
|
+
"type": "streamable-http",
|
|
10
|
+
"url": "https://api.you.com/mcp",
|
|
11
|
+
"headers": [
|
|
12
|
+
{
|
|
13
|
+
"name": "Authorization",
|
|
14
|
+
"description": "Bearer (You.com API key)",
|
|
15
|
+
"isRequired": false,
|
|
16
|
+
"isSecret": true
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"packages": [
|
|
22
|
+
{
|
|
23
|
+
"registryType": "npm",
|
|
24
|
+
"identifier": "@youdotcom-oss/mcp",
|
|
25
|
+
"version": "3.0.0",
|
|
26
|
+
"transport": {
|
|
27
|
+
"type": "stdio"
|
|
28
|
+
},
|
|
29
|
+
"environmentVariables": [
|
|
30
|
+
{
|
|
31
|
+
"name": "YDC_API_KEY",
|
|
32
|
+
"description": "You.com API key (optional — omit for free tier)",
|
|
33
|
+
"isRequired": false,
|
|
34
|
+
"isSecret": true,
|
|
35
|
+
"format": "string"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "MCP_SERVER_URL",
|
|
39
|
+
"description": "Remote MCP server URL (defaults to https://api.you.com/mcp)",
|
|
40
|
+
"isRequired": false,
|
|
41
|
+
"isSecret": false,
|
|
42
|
+
"format": "uri"
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"repository": {
|
|
48
|
+
"url": "https://github.com/youdotcom-oss/dx-toolkit",
|
|
49
|
+
"source": "github"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -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,84 +0,0 @@
|
|
|
1
|
-
import type { ContentsApiResponse } from '@youdotcom-oss/api'
|
|
2
|
-
import type { ContentsStructuredContent } from './contents.schemas.ts'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Format contents API response for MCP output
|
|
6
|
-
* Returns full content in both text and structured formats
|
|
7
|
-
* @param response - Validated API response
|
|
8
|
-
* @param formats - Formats used for extraction
|
|
9
|
-
* @returns Formatted response with content and structuredContent
|
|
10
|
-
*/
|
|
11
|
-
export const formatContentsResponse = (
|
|
12
|
-
response: ContentsApiResponse,
|
|
13
|
-
formats: string[],
|
|
14
|
-
): {
|
|
15
|
-
content: Array<{ type: 'text'; text: string }>
|
|
16
|
-
structuredContent: ContentsStructuredContent
|
|
17
|
-
} => {
|
|
18
|
-
// Build text content with full extracted content
|
|
19
|
-
const textParts: string[] = [`Successfully extracted content from ${response.length} URL(s):\n`]
|
|
20
|
-
textParts.push(`Formats: ${formats.join(', ')}\n`)
|
|
21
|
-
|
|
22
|
-
const items: ContentsStructuredContent['items'] = []
|
|
23
|
-
|
|
24
|
-
for (const item of response) {
|
|
25
|
-
// Add header for this item
|
|
26
|
-
textParts.push(`\n## ${item.title || 'Untitled'}`)
|
|
27
|
-
textParts.push(`URL: ${item.url}\n`)
|
|
28
|
-
textParts.push('---\n')
|
|
29
|
-
|
|
30
|
-
// Add content based on requested formats
|
|
31
|
-
if (formats.includes('markdown') && item.markdown) {
|
|
32
|
-
textParts.push('\n### Markdown Content\n')
|
|
33
|
-
textParts.push(item.markdown)
|
|
34
|
-
textParts.push('\n')
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (formats.includes('html') && item.html) {
|
|
38
|
-
textParts.push('\n### HTML Content\n')
|
|
39
|
-
textParts.push(`Length: ${item.html.length} characters\n`)
|
|
40
|
-
textParts.push(item.html.substring(0, 500))
|
|
41
|
-
if (item.html.length > 500) {
|
|
42
|
-
textParts.push('...\n(truncated for display)')
|
|
43
|
-
}
|
|
44
|
-
textParts.push('\n')
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (formats.includes('metadata') && item.metadata) {
|
|
48
|
-
textParts.push('\n### Metadata\n')
|
|
49
|
-
|
|
50
|
-
if (item.metadata.site_name) {
|
|
51
|
-
textParts.push(`**Site Name:** ${item.metadata.site_name}\n`)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (item.metadata.favicon_url) {
|
|
55
|
-
textParts.push(`**Favicon:** ${item.metadata.favicon_url}\n`)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
textParts.push('\n---\n')
|
|
60
|
-
|
|
61
|
-
// Add to structured content
|
|
62
|
-
items.push({
|
|
63
|
-
url: item.url,
|
|
64
|
-
title: item.title ?? undefined,
|
|
65
|
-
markdown: item.markdown ?? undefined,
|
|
66
|
-
html: item.html ?? undefined,
|
|
67
|
-
metadata: item.metadata ?? undefined,
|
|
68
|
-
})
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
content: [
|
|
73
|
-
{
|
|
74
|
-
type: 'text',
|
|
75
|
-
text: textParts.join('\n'),
|
|
76
|
-
},
|
|
77
|
-
],
|
|
78
|
-
structuredContent: {
|
|
79
|
-
count: response.length,
|
|
80
|
-
formats,
|
|
81
|
-
items,
|
|
82
|
-
},
|
|
83
|
-
}
|
|
84
|
-
}
|
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
-
import { ContentsQuerySchema, fetchContents, generateErrorReportLink } from '@youdotcom-oss/api'
|
|
3
|
-
import { getLogger } from '../shared/get-logger.ts'
|
|
4
|
-
import { ContentsStructuredContentSchema } from './contents.schemas.ts'
|
|
5
|
-
import { formatContentsResponse } from './contents.utils.ts'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Register the you-contents tool with the MCP server
|
|
9
|
-
* Extracts and returns full content from multiple URLs in markdown or HTML format
|
|
10
|
-
*/
|
|
11
|
-
export const registerContentsTool = ({
|
|
12
|
-
mcp,
|
|
13
|
-
YDC_API_KEY,
|
|
14
|
-
getUserAgent,
|
|
15
|
-
}: {
|
|
16
|
-
mcp: McpServer
|
|
17
|
-
YDC_API_KEY?: string
|
|
18
|
-
getUserAgent: () => string
|
|
19
|
-
}) => {
|
|
20
|
-
// Register the tool
|
|
21
|
-
mcp.registerTool(
|
|
22
|
-
'you-contents',
|
|
23
|
-
{
|
|
24
|
-
title: 'Extract Web Page Contents',
|
|
25
|
-
description: 'Extract page content in markdown or HTML',
|
|
26
|
-
inputSchema: ContentsQuerySchema.shape,
|
|
27
|
-
outputSchema: ContentsStructuredContentSchema.shape,
|
|
28
|
-
},
|
|
29
|
-
async (toolInput) => {
|
|
30
|
-
const logger = getLogger(mcp)
|
|
31
|
-
|
|
32
|
-
try {
|
|
33
|
-
// Validate and parse input
|
|
34
|
-
const contentsQuery = ContentsQuerySchema.parse(toolInput)
|
|
35
|
-
const { urls, formats, format, crawl_timeout } = contentsQuery
|
|
36
|
-
|
|
37
|
-
// Handle backward compatibility: prefer formats array, fallback to format string, default to ['markdown']
|
|
38
|
-
const requestFormats = formats || (format ? [format] : ['markdown'])
|
|
39
|
-
|
|
40
|
-
// Log the request
|
|
41
|
-
const timeoutInfo = crawl_timeout ? ` with timeout: ${crawl_timeout}s` : ''
|
|
42
|
-
await logger({
|
|
43
|
-
level: 'info',
|
|
44
|
-
data: `Contents API call initiated for ${urls.length} URL(s) with formats: ${requestFormats.join(', ')}${timeoutInfo}`,
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
// Fetch contents from API
|
|
48
|
-
const response = await fetchContents({
|
|
49
|
-
contentsQuery,
|
|
50
|
-
YDC_API_KEY,
|
|
51
|
-
getUserAgent,
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
// Format response with full content
|
|
55
|
-
const { content, structuredContent } = formatContentsResponse(response, requestFormats)
|
|
56
|
-
|
|
57
|
-
// Log success
|
|
58
|
-
await logger({
|
|
59
|
-
level: 'info',
|
|
60
|
-
data: `Contents API call successful: extracted ${response.length} page(s)`,
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
content,
|
|
65
|
-
structuredContent,
|
|
66
|
-
}
|
|
67
|
-
} catch (err: unknown) {
|
|
68
|
-
// Handle and log errors
|
|
69
|
-
const errorMessage = err instanceof Error ? err.message : String(err)
|
|
70
|
-
const reportLink = generateErrorReportLink({
|
|
71
|
-
errorMessage,
|
|
72
|
-
tool: 'you-contents',
|
|
73
|
-
clientInfo: getUserAgent(),
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
await logger({
|
|
77
|
-
level: 'error',
|
|
78
|
-
data: `Contents API call failed: ${errorMessage}\n\nReport this issue: ${reportLink}`,
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
content: [
|
|
83
|
-
{
|
|
84
|
-
type: 'text' as const,
|
|
85
|
-
text: `Error extracting contents: ${errorMessage}`,
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
structuredContent: undefined,
|
|
89
|
-
isError: true,
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
)
|
|
94
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import type { ContentsApiResponse } from '@youdotcom-oss/api'
|
|
3
|
-
import { formatContentsResponse } from '../contents.utils.ts'
|
|
4
|
-
|
|
5
|
-
describe('formatContentsResponse', () => {
|
|
6
|
-
test('formats single markdown content correctly', () => {
|
|
7
|
-
const mockResponse: ContentsApiResponse = [
|
|
8
|
-
{
|
|
9
|
-
url: 'https://example.com',
|
|
10
|
-
title: 'Example Page',
|
|
11
|
-
markdown: '# Hello\n\nThis is a test page with some content.',
|
|
12
|
-
},
|
|
13
|
-
]
|
|
14
|
-
|
|
15
|
-
const result = formatContentsResponse(mockResponse, ['markdown'])
|
|
16
|
-
|
|
17
|
-
expect(result).toHaveProperty('content')
|
|
18
|
-
expect(result).toHaveProperty('structuredContent')
|
|
19
|
-
expect(Array.isArray(result.content)).toBe(true)
|
|
20
|
-
expect(result.content[0]).toHaveProperty('type', 'text')
|
|
21
|
-
expect(result.content[0]).toHaveProperty('text')
|
|
22
|
-
|
|
23
|
-
const text = result.content[0]?.text
|
|
24
|
-
expect(text).toContain('Example Page')
|
|
25
|
-
expect(text).toContain('https://example.com')
|
|
26
|
-
expect(text).toContain('Formats: markdown')
|
|
27
|
-
expect(text).toContain('# Hello')
|
|
28
|
-
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
|
-
})
|
|
42
|
-
|
|
43
|
-
test('formats multiple items correctly', () => {
|
|
44
|
-
const mockResponse: ContentsApiResponse = [
|
|
45
|
-
{
|
|
46
|
-
url: 'https://example1.com',
|
|
47
|
-
title: 'Page 1',
|
|
48
|
-
markdown: 'Content 1',
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
url: 'https://example2.com',
|
|
52
|
-
title: 'Page 2',
|
|
53
|
-
markdown: 'Content 2',
|
|
54
|
-
},
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
const result = formatContentsResponse(mockResponse, ['markdown'])
|
|
58
|
-
|
|
59
|
-
expect(result.structuredContent.count).toBe(2)
|
|
60
|
-
expect(result.structuredContent.items).toHaveLength(2)
|
|
61
|
-
|
|
62
|
-
const text = result.content[0]?.text
|
|
63
|
-
expect(text).toContain('Page 1')
|
|
64
|
-
expect(text).toContain('Page 2')
|
|
65
|
-
expect(text).toContain('https://example1.com')
|
|
66
|
-
expect(text).toContain('https://example2.com')
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test('handles html format', () => {
|
|
70
|
-
const mockResponse: ContentsApiResponse = [
|
|
71
|
-
{
|
|
72
|
-
url: 'https://example.com',
|
|
73
|
-
title: 'HTML Page',
|
|
74
|
-
html: '<html><body><h1>Hello</h1></body></html>',
|
|
75
|
-
},
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
const result = formatContentsResponse(mockResponse, ['html'])
|
|
79
|
-
|
|
80
|
-
expect(result.structuredContent.formats).toEqual(['html'])
|
|
81
|
-
const text = result.content[0]?.text
|
|
82
|
-
expect(text).toContain('Formats: html')
|
|
83
|
-
expect(text).toContain('<html>')
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
test('includes full content for long text', () => {
|
|
87
|
-
const longContent = 'a'.repeat(1000)
|
|
88
|
-
const mockResponse: ContentsApiResponse = [
|
|
89
|
-
{
|
|
90
|
-
url: 'https://example.com',
|
|
91
|
-
title: 'Long Page',
|
|
92
|
-
markdown: longContent,
|
|
93
|
-
},
|
|
94
|
-
]
|
|
95
|
-
|
|
96
|
-
const result = formatContentsResponse(mockResponse, ['markdown'])
|
|
97
|
-
|
|
98
|
-
const text = result.content[0]?.text
|
|
99
|
-
// Full content should be included (not truncated)
|
|
100
|
-
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
|
-
})
|
|
106
|
-
|
|
107
|
-
test('handles empty content gracefully', () => {
|
|
108
|
-
const mockResponse: ContentsApiResponse = [
|
|
109
|
-
{
|
|
110
|
-
url: 'https://example.com',
|
|
111
|
-
title: 'Empty Page',
|
|
112
|
-
markdown: '',
|
|
113
|
-
},
|
|
114
|
-
]
|
|
115
|
-
|
|
116
|
-
const result = formatContentsResponse(mockResponse, ['markdown'])
|
|
117
|
-
|
|
118
|
-
expect(result.structuredContent.items[0]?.markdown).toBe('')
|
|
119
|
-
const text = result.content[0]?.text
|
|
120
|
-
expect(text).toContain('Empty Page')
|
|
121
|
-
// Empty content should still be handled gracefully
|
|
122
|
-
})
|
|
123
|
-
})
|
package/src/get-mcp-server.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
-
import packageJson from '../package.json' with { type: 'json' }
|
|
3
|
-
|
|
4
|
-
export const getMCpServer = () =>
|
|
5
|
-
new McpServer(
|
|
6
|
-
{
|
|
7
|
-
name: 'You.com',
|
|
8
|
-
version: packageJson.version,
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
capabilities: {
|
|
12
|
-
logging: {},
|
|
13
|
-
tools: { listChanged: true },
|
|
14
|
-
},
|
|
15
|
-
instructions: `Use this server to search the web, get AI-powered answers with web context, and extract content from web pages using You.com. The you-contents tool extracts page content and returns it in markdown or HTML format. Use HTML format for layout preservation, interactive content, and visual fidelity; use markdown for text extraction and simpler consumption.`,
|
|
16
|
-
},
|
|
17
|
-
)
|
package/src/http.ts
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { StreamableHTTPTransport } from '@hono/mcp'
|
|
2
|
-
import { type Context, Hono } from 'hono'
|
|
3
|
-
import { trimTrailingSlash } from 'hono/trailing-slash'
|
|
4
|
-
import packageJson from '../package.json' with { type: 'json' }
|
|
5
|
-
import { registerContentsTool } from './contents/register-contents-tool.ts'
|
|
6
|
-
import { getMCpServer } from './get-mcp-server.ts'
|
|
7
|
-
import { registerResearchTool } from './research/register-research-tool.ts'
|
|
8
|
-
import { registerSearchTool } from './search/register-search-tool.ts'
|
|
9
|
-
import { useGetClientVersion } from './shared/use-client-version.ts'
|
|
10
|
-
|
|
11
|
-
const extractBearerToken = (authHeader: string | null): string | null => {
|
|
12
|
-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
13
|
-
return null
|
|
14
|
-
}
|
|
15
|
-
return authHeader.slice(7)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const handleMcpRequest = async (c: Context) => {
|
|
19
|
-
const authHeader = c.req.header('Authorization')
|
|
20
|
-
|
|
21
|
-
let YDC_API_KEY: string | undefined
|
|
22
|
-
|
|
23
|
-
if (authHeader) {
|
|
24
|
-
const token = extractBearerToken(authHeader)
|
|
25
|
-
|
|
26
|
-
if (!token) {
|
|
27
|
-
c.status(401)
|
|
28
|
-
c.header('Content-Type', 'text/plain')
|
|
29
|
-
return c.text('Unauthorized: Invalid Bearer token format')
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
YDC_API_KEY = token
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const mcp = getMCpServer()
|
|
36
|
-
const getUserAgent = useGetClientVersion(mcp)
|
|
37
|
-
|
|
38
|
-
registerSearchTool({
|
|
39
|
-
mcp,
|
|
40
|
-
YDC_API_KEY,
|
|
41
|
-
getUserAgent,
|
|
42
|
-
})
|
|
43
|
-
registerContentsTool({ mcp, YDC_API_KEY, getUserAgent })
|
|
44
|
-
registerResearchTool({ mcp, YDC_API_KEY, getUserAgent })
|
|
45
|
-
|
|
46
|
-
const transport = new StreamableHTTPTransport()
|
|
47
|
-
await mcp.connect(transport)
|
|
48
|
-
const response = await transport.handleRequest(c)
|
|
49
|
-
|
|
50
|
-
// Explicitly set Content-Encoding to 'identity' to prevent httpx auto-decompression issues
|
|
51
|
-
// httpx by default sends Accept-Encoding and attempts decompression, but MCP SSE streams
|
|
52
|
-
// are not compressed. Setting 'identity' tells clients the response is uncompressed.
|
|
53
|
-
response?.headers.set('Content-Encoding', 'identity')
|
|
54
|
-
|
|
55
|
-
return response
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const return405MethodNotAllowed = (c: Context) => {
|
|
59
|
-
c.status(405)
|
|
60
|
-
c.header('Allow', 'POST')
|
|
61
|
-
c.header('Content-Type', 'application/json')
|
|
62
|
-
return c.json({
|
|
63
|
-
jsonrpc: '2.0',
|
|
64
|
-
error: {
|
|
65
|
-
code: -32000,
|
|
66
|
-
message: 'Method Not Allowed: Use POST to send MCP requests',
|
|
67
|
-
},
|
|
68
|
-
id: null,
|
|
69
|
-
})
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const app = new Hono()
|
|
73
|
-
app.use(trimTrailingSlash())
|
|
74
|
-
|
|
75
|
-
app.get('/mcp-health', async (c) => {
|
|
76
|
-
return c.json({
|
|
77
|
-
status: 'healthy',
|
|
78
|
-
timestamp: new Date().toISOString(),
|
|
79
|
-
version: packageJson.version,
|
|
80
|
-
service: 'youdotcom-mcp-server',
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
// POST handler for MCP requests (per MCP Streamable HTTP spec)
|
|
85
|
-
app.post('/mcp', handleMcpRequest)
|
|
86
|
-
app.post('/mcp/', handleMcpRequest)
|
|
87
|
-
|
|
88
|
-
// Fallback for other methods - returns 405 per MCP spec
|
|
89
|
-
// Spec: "The server MUST either return Content-Type: text/event-stream
|
|
90
|
-
// or else return HTTP 405 Method Not Allowed"
|
|
91
|
-
app.all('/mcp', return405MethodNotAllowed)
|
|
92
|
-
app.all('/mcp/', return405MethodNotAllowed)
|
|
93
|
-
|
|
94
|
-
export default app
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
|
-
import { callResearch, generateErrorReportLink, ResearchQuerySchema } from '@youdotcom-oss/api'
|
|
3
|
-
import { getLogger } from '../shared/get-logger.ts'
|
|
4
|
-
import { ResearchStructuredContentSchema } from './research.schemas.ts'
|
|
5
|
-
import { formatResearchResults } from './research.utils.ts'
|
|
6
|
-
|
|
7
|
-
export const registerResearchTool = ({
|
|
8
|
-
mcp,
|
|
9
|
-
YDC_API_KEY,
|
|
10
|
-
getUserAgent,
|
|
11
|
-
}: {
|
|
12
|
-
mcp: McpServer
|
|
13
|
-
YDC_API_KEY?: string
|
|
14
|
-
getUserAgent: () => string
|
|
15
|
-
}) => {
|
|
16
|
-
mcp.registerTool(
|
|
17
|
-
'you-research',
|
|
18
|
-
{
|
|
19
|
-
title: 'Research',
|
|
20
|
-
description:
|
|
21
|
-
'Research a topic with comprehensive answers and cited sources. Configurable effort levels (lite, standard, deep, exhaustive).',
|
|
22
|
-
inputSchema: ResearchQuerySchema.shape,
|
|
23
|
-
outputSchema: ResearchStructuredContentSchema.shape,
|
|
24
|
-
},
|
|
25
|
-
async (researchQuery) => {
|
|
26
|
-
const logger = getLogger(mcp)
|
|
27
|
-
try {
|
|
28
|
-
const response = await callResearch({
|
|
29
|
-
researchQuery,
|
|
30
|
-
YDC_API_KEY,
|
|
31
|
-
getUserAgent,
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const sourceCount = response.output.sources.length
|
|
35
|
-
|
|
36
|
-
await logger({
|
|
37
|
-
level: 'info',
|
|
38
|
-
data: `Research successful for input: "${researchQuery.input.substring(0, 100)}" - ${sourceCount} source(s)`,
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
const { content, structuredContent } = formatResearchResults(response)
|
|
42
|
-
return { content, structuredContent }
|
|
43
|
-
} catch (err: unknown) {
|
|
44
|
-
const errorMessage = err instanceof Error ? err.message : String(err)
|
|
45
|
-
const reportLink = generateErrorReportLink({
|
|
46
|
-
errorMessage,
|
|
47
|
-
tool: 'you-research',
|
|
48
|
-
clientInfo: getUserAgent(),
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
await logger({
|
|
52
|
-
level: 'error',
|
|
53
|
-
data: `Research API call failed: ${errorMessage}\n\nReport this issue: ${reportLink}`,
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
content: [
|
|
58
|
-
{
|
|
59
|
-
type: 'text' as const,
|
|
60
|
-
text: `Error: ${errorMessage}`,
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
structuredContent: undefined,
|
|
64
|
-
isError: true,
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
)
|
|
69
|
-
}
|
|
@@ -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
|
-
content_type: 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,30 +0,0 @@
|
|
|
1
|
-
import type { ResearchResponse } from '@youdotcom-oss/api'
|
|
2
|
-
import { formatResearchResponse } from '@youdotcom-oss/api'
|
|
3
|
-
import type { ResearchStructuredContent } from './research.schemas.ts'
|
|
4
|
-
|
|
5
|
-
export const formatResearchResults = (
|
|
6
|
-
response: ResearchResponse,
|
|
7
|
-
): {
|
|
8
|
-
content: Array<{ type: 'text'; text: string }>
|
|
9
|
-
structuredContent: ResearchStructuredContent
|
|
10
|
-
} => {
|
|
11
|
-
const text = formatResearchResponse(response)
|
|
12
|
-
|
|
13
|
-
return {
|
|
14
|
-
content: [
|
|
15
|
-
{
|
|
16
|
-
type: 'text',
|
|
17
|
-
text,
|
|
18
|
-
},
|
|
19
|
-
],
|
|
20
|
-
structuredContent: {
|
|
21
|
-
content_type: 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,
|
|
27
|
-
})),
|
|
28
|
-
},
|
|
29
|
-
}
|
|
30
|
-
}
|