@youdotcom-oss/mcp 2.1.1 → 3.2.0-next.1
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 +114 -100
- package/package.json +14 -20
- package/server.json +51 -0
- package/src/contents/contents.utils.ts +2 -1
- package/src/contents/register-contents-tool.ts +2 -3
- package/src/get-mcp-server.ts +1 -1
- package/src/main.ts +8 -0
- package/src/research/register-research-tool.ts +3 -3
- package/src/research/research.schemas.ts +1 -1
- package/src/research/research.utils.ts +2 -2
- package/src/research/tests/research.utils.spec.ts +114 -0
- package/src/search/register-search-tool.ts +3 -3
- package/src/shared/get-logger.ts +16 -7
- package/src/{stdio.ts → stdio-server.ts} +4 -4
- package/src/http.ts +0 -94
- package/src/tests/http.spec.ts +0 -583
- package/src/tests/tool.spec.ts +0 -649
- /package/src/search/{search.schema.ts → search.schemas.ts} +0 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youdotcom-oss/mcp",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "You.com
|
|
3
|
+
"version": "3.2.0-next.1",
|
|
4
|
+
"description": "You.com MCP server — web search, AI research, and content extraction via You.com APIs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=18",
|
|
@@ -20,49 +20,43 @@
|
|
|
20
20
|
"keywords": [
|
|
21
21
|
"mcp",
|
|
22
22
|
"web search",
|
|
23
|
-
"model context protocol"
|
|
23
|
+
"model context protocol",
|
|
24
|
+
"ai research"
|
|
24
25
|
],
|
|
25
26
|
"bin": "bin/stdio.js",
|
|
26
27
|
"type": "module",
|
|
27
28
|
"exports": {
|
|
28
|
-
"
|
|
29
|
+
".": "./src/main.ts"
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
31
32
|
"bin/stdio.js",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"!./src/**/tests/*",
|
|
35
|
-
"!./src/**/*.spec.@(tsx|ts)"
|
|
33
|
+
"src",
|
|
34
|
+
"server.json"
|
|
36
35
|
],
|
|
37
36
|
"publishConfig": {
|
|
38
37
|
"access": "public"
|
|
39
38
|
},
|
|
40
39
|
"scripts": {
|
|
41
|
-
"build": "bun build ./src/stdio.ts --outfile ./bin/stdio.js --target=node",
|
|
40
|
+
"build": "bun build ./src/stdio-server.ts --outfile ./bin/stdio.js --target=node",
|
|
42
41
|
"check": "bun run check:biome && bun run check:types && bun run check:package",
|
|
43
42
|
"check:biome": "biome check",
|
|
44
43
|
"check:package": "format-package --check",
|
|
45
44
|
"check:types": "tsc --noEmit",
|
|
46
45
|
"check:write": "biome check --write && bun run format:package",
|
|
47
|
-
"dev": "bun src/stdio.ts",
|
|
46
|
+
"dev": "bun src/stdio-server.ts",
|
|
48
47
|
"format:package": "format-package --write",
|
|
49
|
-
"
|
|
48
|
+
"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
49
|
"prepublishOnly": "bun run build",
|
|
51
|
-
"start": "bun run src/http.ts",
|
|
52
50
|
"test": "bun test",
|
|
53
|
-
"test:coverage": "bun test --coverage",
|
|
54
|
-
"test:coverage:watch": "bun test --coverage --watch",
|
|
55
51
|
"test:watch": "bun test --watch"
|
|
56
52
|
},
|
|
57
53
|
"mcpName": "io.github.youdotcom-oss/mcp",
|
|
58
54
|
"dependencies": {
|
|
59
|
-
"@
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
63
|
-
"hono": "^4.11.7"
|
|
55
|
+
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
56
|
+
"@youdotcom-oss/api": "0.5.1",
|
|
57
|
+
"zod": "^4.3.6"
|
|
64
58
|
},
|
|
65
59
|
"devDependencies": {
|
|
66
|
-
"@modelcontextprotocol/inspector": "0.
|
|
60
|
+
"@modelcontextprotocol/inspector": "0.21.1"
|
|
67
61
|
}
|
|
68
62
|
}
|
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
|
+
}
|
|
@@ -35,11 +35,12 @@ export const formatContentsResponse = (
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
if (formats.includes('html') && item.html) {
|
|
38
|
+
// Text output is a brief preview only — full HTML is in structuredContent.items[].html
|
|
38
39
|
textParts.push('\n### HTML Content\n')
|
|
39
40
|
textParts.push(`Length: ${item.html.length} characters\n`)
|
|
40
41
|
textParts.push(item.html.substring(0, 500))
|
|
41
42
|
if (item.html.length > 500) {
|
|
42
|
-
textParts.push('...\n(truncated for display)')
|
|
43
|
+
textParts.push('...\n(truncated for display — full HTML available in structuredContent)')
|
|
43
44
|
}
|
|
44
45
|
textParts.push('\n')
|
|
45
46
|
}
|
|
@@ -26,12 +26,11 @@ export const registerContentsTool = ({
|
|
|
26
26
|
inputSchema: ContentsQuerySchema.shape,
|
|
27
27
|
outputSchema: ContentsStructuredContentSchema.shape,
|
|
28
28
|
},
|
|
29
|
-
async (
|
|
30
|
-
const logger = getLogger(
|
|
29
|
+
async (contentsQuery, { sendNotification }) => {
|
|
30
|
+
const logger = getLogger(sendNotification)
|
|
31
31
|
|
|
32
32
|
try {
|
|
33
33
|
// Validate and parse input
|
|
34
|
-
const contentsQuery = ContentsQuerySchema.parse(toolInput)
|
|
35
34
|
const { urls, formats, format, crawl_timeout } = contentsQuery
|
|
36
35
|
|
|
37
36
|
// Handle backward compatibility: prefer formats array, fallback to format string, default to ['markdown']
|
package/src/get-mcp-server.ts
CHANGED
package/src/main.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { ContentsStructuredContent } from './contents/contents.schemas.ts'
|
|
2
|
+
export { registerContentsTool } from './contents/register-contents-tool.ts'
|
|
3
|
+
export { getMcpServer } from './get-mcp-server.ts'
|
|
4
|
+
export { registerResearchTool } from './research/register-research-tool.ts'
|
|
5
|
+
export type { ResearchStructuredContent } from './research/research.schemas.ts'
|
|
6
|
+
export { registerSearchTool } from './search/register-search-tool.ts'
|
|
7
|
+
export type { SearchStructuredContent } from './search/search.schemas.ts'
|
|
8
|
+
export { useGetClientVersion } from './shared/use-client-version.ts'
|
|
@@ -22,8 +22,8 @@ export const registerResearchTool = ({
|
|
|
22
22
|
inputSchema: ResearchQuerySchema.shape,
|
|
23
23
|
outputSchema: ResearchStructuredContentSchema.shape,
|
|
24
24
|
},
|
|
25
|
-
async (researchQuery) => {
|
|
26
|
-
const logger = getLogger(
|
|
25
|
+
async (researchQuery, { sendNotification }) => {
|
|
26
|
+
const logger = getLogger(sendNotification)
|
|
27
27
|
try {
|
|
28
28
|
const response = await callResearch({
|
|
29
29
|
researchQuery,
|
|
@@ -35,7 +35,7 @@ export const registerResearchTool = ({
|
|
|
35
35
|
|
|
36
36
|
await logger({
|
|
37
37
|
level: 'info',
|
|
38
|
-
data: `Research
|
|
38
|
+
data: `Research for "${researchQuery.input.substring(0, 100)}" complete: ${sourceCount} source(s)`,
|
|
39
39
|
})
|
|
40
40
|
|
|
41
41
|
const { content, structuredContent } = formatResearchResults(response)
|
|
@@ -3,7 +3,7 @@ import * as z from 'zod'
|
|
|
3
3
|
// Minimal schema for structuredContent (reduces payload duplication)
|
|
4
4
|
// Full research content is in the text content field
|
|
5
5
|
export const ResearchStructuredContentSchema = z.object({
|
|
6
|
-
|
|
6
|
+
contentType: z.string().describe('Format of the content field'),
|
|
7
7
|
sourceCount: z.number().describe('Number of sources used'),
|
|
8
8
|
sources: z
|
|
9
9
|
.array(
|
|
@@ -18,12 +18,12 @@ export const formatResearchResults = (
|
|
|
18
18
|
},
|
|
19
19
|
],
|
|
20
20
|
structuredContent: {
|
|
21
|
-
|
|
21
|
+
contentType: response.output.content_type,
|
|
22
22
|
sourceCount: response.output.sources.length,
|
|
23
23
|
sources: response.output.sources.map((source) => ({
|
|
24
24
|
url: source.url,
|
|
25
25
|
title: source.title,
|
|
26
|
-
snippetCount: source.snippets
|
|
26
|
+
snippetCount: source.snippets?.length ?? 0,
|
|
27
27
|
})),
|
|
28
28
|
},
|
|
29
29
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { ResearchResponse } from '@youdotcom-oss/api'
|
|
3
|
+
import { formatResearchResults } from '../research.utils.ts'
|
|
4
|
+
|
|
5
|
+
describe('formatResearchResults', () => {
|
|
6
|
+
test('formats research response with sources correctly', () => {
|
|
7
|
+
const mockResponse: ResearchResponse = {
|
|
8
|
+
output: {
|
|
9
|
+
content: '# Research Answer\n\nThis is a comprehensive answer about the topic.',
|
|
10
|
+
content_type: 'text',
|
|
11
|
+
sources: [
|
|
12
|
+
{
|
|
13
|
+
url: 'https://example.com/source1',
|
|
14
|
+
title: 'Source One',
|
|
15
|
+
snippets: ['First snippet', 'Second snippet'],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
url: 'https://example.com/source2',
|
|
19
|
+
title: 'Source Two',
|
|
20
|
+
snippets: ['Another snippet'],
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
},
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = formatResearchResults(mockResponse)
|
|
27
|
+
|
|
28
|
+
expect(result).toHaveProperty('content')
|
|
29
|
+
expect(result).toHaveProperty('structuredContent')
|
|
30
|
+
expect(Array.isArray(result.content)).toBe(true)
|
|
31
|
+
expect(result.content[0]).toHaveProperty('type', 'text')
|
|
32
|
+
expect(result.content[0]).toHaveProperty('text')
|
|
33
|
+
|
|
34
|
+
const text = result.content[0]?.text
|
|
35
|
+
expect(text).toContain('Research Answer')
|
|
36
|
+
expect(text).toContain('Source One')
|
|
37
|
+
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 response with zero sources', () => {
|
|
100
|
+
const mockResponse: ResearchResponse = {
|
|
101
|
+
output: {
|
|
102
|
+
content: 'An answer with no cited sources.',
|
|
103
|
+
content_type: 'text',
|
|
104
|
+
sources: [],
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = formatResearchResults(mockResponse)
|
|
109
|
+
|
|
110
|
+
expect(result.structuredContent.sourceCount).toBe(0)
|
|
111
|
+
expect(result.structuredContent.sources).toHaveLength(0)
|
|
112
|
+
expect(result.content[0]?.text).toContain('An answer with no cited sources.')
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
2
2
|
import { fetchSearchResults, generateErrorReportLink, SearchQuerySchema } from '@youdotcom-oss/api'
|
|
3
3
|
import { getLogger } from '../shared/get-logger.ts'
|
|
4
|
-
import { SearchStructuredContentSchema } from './search.
|
|
4
|
+
import { SearchStructuredContentSchema } from './search.schemas.ts'
|
|
5
5
|
import { formatSearchResults } from './search.utils.ts'
|
|
6
6
|
|
|
7
7
|
export const registerSearchTool = ({
|
|
@@ -21,8 +21,8 @@ export const registerSearchTool = ({
|
|
|
21
21
|
inputSchema: SearchQuerySchema.shape,
|
|
22
22
|
outputSchema: SearchStructuredContentSchema.shape,
|
|
23
23
|
},
|
|
24
|
-
async (searchQuery) => {
|
|
25
|
-
const logger = getLogger(
|
|
24
|
+
async (searchQuery, { sendNotification }) => {
|
|
25
|
+
const logger = getLogger(sendNotification)
|
|
26
26
|
try {
|
|
27
27
|
const response = await fetchSearchResults({
|
|
28
28
|
searchQuery,
|
package/src/shared/get-logger.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { LoggingMessageNotification } from '@modelcontextprotocol/sdk/types.js'
|
|
1
|
+
import type { LoggingMessageNotification, ServerNotification } from '@modelcontextprotocol/sdk/types.js'
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
* Creates a logger
|
|
6
|
-
*
|
|
4
|
+
* Creates a logger that routes notifications through the request's SSE stream
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Uses `extra.sendNotification` from the tool callback, which attaches `relatedRequestId`
|
|
8
|
+
* so the transport routes the message to the POST SSE stream (not the standalone GET stream).
|
|
9
|
+
*
|
|
10
|
+
* @param sendNotification - From the tool callback's `extra` parameter
|
|
11
|
+
* @returns Async function that sends logging notifications
|
|
12
|
+
*
|
|
13
|
+
* @internal
|
|
7
14
|
*/
|
|
8
|
-
export const getLogger =
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
export const getLogger =
|
|
16
|
+
(sendNotification: (notification: ServerNotification) => Promise<void>) =>
|
|
17
|
+
async (params: LoggingMessageNotification['params']) => {
|
|
18
|
+
await sendNotification({ method: 'notifications/message', params })
|
|
19
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
3
3
|
import { registerContentsTool } from './contents/register-contents-tool.ts'
|
|
4
|
-
import {
|
|
4
|
+
import { getMcpServer } from './get-mcp-server.ts'
|
|
5
5
|
import { registerResearchTool } from './research/register-research-tool.ts'
|
|
6
6
|
import { registerSearchTool } from './search/register-search-tool.ts'
|
|
7
7
|
import { useGetClientVersion } from './shared/use-client-version.ts'
|
|
8
8
|
|
|
9
|
-
const YDC_API_KEY = process.env.YDC_API_KEY
|
|
9
|
+
const YDC_API_KEY = process.env.YDC_API_KEY || undefined
|
|
10
10
|
|
|
11
11
|
try {
|
|
12
|
-
const mcp =
|
|
12
|
+
const mcp = getMcpServer()
|
|
13
13
|
const getUserAgent = useGetClientVersion(mcp)
|
|
14
14
|
|
|
15
15
|
registerSearchTool({ mcp, YDC_API_KEY, getUserAgent })
|
|
@@ -19,6 +19,6 @@ try {
|
|
|
19
19
|
const transport = new StdioServerTransport()
|
|
20
20
|
await mcp.connect(transport)
|
|
21
21
|
} catch (error) {
|
|
22
|
-
process.stderr.write(`Failed to start server: ${error}\n`)
|
|
22
|
+
process.stderr.write(`Failed to start MCP server: ${error}\n`)
|
|
23
23
|
process.exit(1)
|
|
24
24
|
}
|
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
|