@youdotcom-oss/mcp 1.3.4 → 1.3.5-next.12

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.
@@ -0,0 +1,17 @@
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 ADDED
@@ -0,0 +1,72 @@
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 { registerExpressTool } from './express/register-express-tool.ts';
7
+ import { getMCpServer } from './get-mcp-server.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
+ if (!authHeader) {
22
+ c.status(401);
23
+ c.header('Content-Type', 'text/plain');
24
+ return c.text('Unauthorized: Authorization header required');
25
+ }
26
+
27
+ const YDC_API_KEY = extractBearerToken(authHeader);
28
+
29
+ if (!YDC_API_KEY) {
30
+ c.status(401);
31
+ c.header('Content-Type', 'text/plain');
32
+ return c.text('Unauthorized: Bearer token required');
33
+ }
34
+ const mcp = getMCpServer();
35
+ const getUserAgent = useGetClientVersion(mcp);
36
+
37
+ registerSearchTool({
38
+ mcp,
39
+ YDC_API_KEY,
40
+ getUserAgent,
41
+ });
42
+ registerExpressTool({ mcp, YDC_API_KEY, getUserAgent });
43
+ registerContentsTool({ mcp, YDC_API_KEY, getUserAgent });
44
+
45
+ const transport = new StreamableHTTPTransport();
46
+ await mcp.connect(transport);
47
+ const response = await transport.handleRequest(c);
48
+
49
+ // Explicitly set Content-Encoding to 'identity' to prevent httpx auto-decompression issues
50
+ // httpx by default sends Accept-Encoding and attempts decompression, but MCP SSE streams
51
+ // are not compressed. Setting 'identity' tells clients the response is uncompressed.
52
+ response?.headers.set('Content-Encoding', 'identity');
53
+
54
+ return response;
55
+ };
56
+
57
+ const app = new Hono();
58
+ app.use(trimTrailingSlash());
59
+
60
+ app.get('/mcp-health', async (c) => {
61
+ return c.json({
62
+ status: 'healthy',
63
+ timestamp: new Date().toISOString(),
64
+ version: packageJson.version,
65
+ service: 'youdotcom-mcp-server',
66
+ });
67
+ });
68
+
69
+ app.all('/mcp', handleMcpRequest);
70
+ app.all('/mcp/', handleMcpRequest);
71
+
72
+ export default app;
@@ -0,0 +1,87 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { generateErrorReportLink } from '../shared/generate-error-report-link.ts';
3
+ import { getLogger } from '../shared/get-logger.ts';
4
+ import { SearchQuerySchema, SearchStructuredContentSchema } from './search.schemas.ts';
5
+ import { fetchSearchResults, formatSearchResults } from './search.utils.ts';
6
+
7
+ export const registerSearchTool = ({
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-search',
18
+ {
19
+ title: 'Web Search',
20
+ description: 'Web and news search via You.com',
21
+ inputSchema: SearchQuerySchema.shape,
22
+ outputSchema: SearchStructuredContentSchema.shape,
23
+ },
24
+ async (searchQuery) => {
25
+ const logger = getLogger(mcp);
26
+ try {
27
+ const response = await fetchSearchResults({
28
+ searchQuery,
29
+ YDC_API_KEY,
30
+ getUserAgent,
31
+ });
32
+
33
+ const webCount = response.results.web?.length ?? 0;
34
+ const newsCount = response.results.news?.length ?? 0;
35
+
36
+ if (!webCount && !newsCount) {
37
+ await logger({
38
+ level: 'info',
39
+ data: `No results found for query: "${searchQuery.query}"`,
40
+ });
41
+
42
+ return {
43
+ content: [{ type: 'text' as const, text: 'No results found.' }],
44
+ structuredContent: {
45
+ resultCounts: {
46
+ web: 0,
47
+ news: 0,
48
+ total: 0,
49
+ },
50
+ },
51
+ };
52
+ }
53
+
54
+ await logger({
55
+ level: 'info',
56
+ data: `Search successful for query: "${searchQuery.query}" - ${webCount} web results, ${newsCount} news results (${webCount + newsCount} total)`,
57
+ });
58
+
59
+ const { content, structuredContent } = formatSearchResults(response);
60
+ return { content, structuredContent };
61
+ } catch (err: unknown) {
62
+ const errorMessage = err instanceof Error ? err.message : String(err);
63
+ const reportLink = generateErrorReportLink({
64
+ errorMessage,
65
+ tool: 'you-search',
66
+ clientInfo: getUserAgent(),
67
+ });
68
+
69
+ await logger({
70
+ level: 'error',
71
+ data: `Search API call failed: ${errorMessage}\n\nReport this issue: ${reportLink}`,
72
+ });
73
+
74
+ return {
75
+ content: [
76
+ {
77
+ type: 'text' as const,
78
+ text: `Error: ${errorMessage}`,
79
+ },
80
+ ],
81
+ structuredContent: undefined,
82
+ isError: true,
83
+ };
84
+ }
85
+ },
86
+ );
87
+ };
@@ -0,0 +1,217 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import type { SearchResponse } from '../search.schemas.ts';
3
+ import { fetchSearchResults, formatSearchResults } from '../search.utils.ts';
4
+
5
+ const getUserAgent = () => 'MCP/test (You.com; test-client)';
6
+
7
+ describe('fetchSearchResults', () => {
8
+ test('returns valid response structure for basic query', async () => {
9
+ const result = await fetchSearchResults({
10
+ searchQuery: { query: 'latest stock news' },
11
+ getUserAgent,
12
+ });
13
+
14
+ expect(result).toHaveProperty('results');
15
+ expect(result).toHaveProperty('metadata');
16
+ expect(result.results).toHaveProperty('web');
17
+ expect(result.results).toHaveProperty('news');
18
+ expect(Array.isArray(result.results.web)).toBe(true);
19
+ expect(Array.isArray(result.results.news)).toBe(true);
20
+
21
+ // Assert required metadata fields
22
+ expect(typeof result.metadata?.query).toBe('string');
23
+
24
+ // Optional fields: only assert type if present
25
+ if (result.metadata?.request_uuid !== undefined) {
26
+ expect(typeof result.metadata.request_uuid).toBe('string');
27
+ }
28
+ });
29
+
30
+ test('handles search with filters', async () => {
31
+ const result = await fetchSearchResults({
32
+ searchQuery: {
33
+ query: 'javascript tutorial',
34
+ count: 3,
35
+ freshness: 'week',
36
+ country: 'US',
37
+ },
38
+ getUserAgent,
39
+ });
40
+
41
+ expect(result.results.web?.length).toBeLessThanOrEqual(3);
42
+ expect(result.metadata?.query).toContain('javascript tutorial');
43
+ });
44
+
45
+ test('validates response schema', async () => {
46
+ const result = await fetchSearchResults({
47
+ searchQuery: { query: 'latest technology news' },
48
+ getUserAgent,
49
+ });
50
+
51
+ // Test that web results have required properties
52
+ // biome-ignore lint/style/noNonNullAssertion: Test
53
+ const webResult = result.results.web![0];
54
+
55
+ expect(webResult).toHaveProperty('url');
56
+ expect(webResult).toHaveProperty('title');
57
+ expect(webResult).toHaveProperty('description');
58
+ expect(webResult).toHaveProperty('snippets');
59
+ expect(Array.isArray(webResult?.snippets)).toBe(true);
60
+
61
+ // Test that news results have required properties
62
+ // biome-ignore lint/style/noNonNullAssertion: Test
63
+ const newsResult = result.results.news![0];
64
+ expect(newsResult).toHaveProperty('url');
65
+ expect(newsResult).toHaveProperty('title');
66
+ expect(newsResult).toHaveProperty('description');
67
+ expect(newsResult).toHaveProperty('page_age');
68
+ });
69
+ });
70
+
71
+ describe('formatSearchResults', () => {
72
+ test('formats web results correctly', () => {
73
+ const mockResponse: SearchResponse = {
74
+ results: {
75
+ web: [
76
+ {
77
+ url: 'https://example.com',
78
+ title: 'Test Title',
79
+ description: 'Test description',
80
+ snippets: ['snippet 1', 'snippet 2'],
81
+ page_age: '2023-01-01T00:00:00',
82
+ authors: ['Author Name'],
83
+ },
84
+ ],
85
+ news: [],
86
+ },
87
+ metadata: {
88
+ request_uuid: 'test-uuid',
89
+ query: 'test query',
90
+ latency: 0.1,
91
+ },
92
+ };
93
+
94
+ const result = formatSearchResults(mockResponse);
95
+
96
+ expect(result).toHaveProperty('content');
97
+ expect(result).toHaveProperty('structuredContent');
98
+ expect(result).toHaveProperty('fullResponse');
99
+ expect(Array.isArray(result.content)).toBe(true);
100
+ expect(result.content[0]).toHaveProperty('type', 'text');
101
+ expect(result.content[0]).toHaveProperty('text');
102
+ expect(result.content[0]?.text).toContain('WEB RESULTS:');
103
+ expect(result.content[0]?.text).toContain('Test Title');
104
+ // URLs should NOT be in text content
105
+ expect(result.content[0]?.text).not.toContain('https://example.com');
106
+ expect(result.structuredContent).toHaveProperty('resultCounts');
107
+ expect(result.structuredContent.resultCounts).toHaveProperty('web', 1);
108
+ expect(result.structuredContent.resultCounts).toHaveProperty('news', 0);
109
+ expect(result.structuredContent.resultCounts).toHaveProperty('total', 1);
110
+ // URLs should be in structuredContent.results
111
+ expect(result.structuredContent).toHaveProperty('results');
112
+ expect(result.structuredContent.results?.web).toBeDefined();
113
+ expect(result.structuredContent.results?.web?.length).toBe(1);
114
+ expect(result.structuredContent.results?.web?.[0]).toEqual({
115
+ url: 'https://example.com',
116
+ title: 'Test Title',
117
+ });
118
+ expect(result.fullResponse).toBe(mockResponse);
119
+ });
120
+
121
+ test('formats news results correctly', () => {
122
+ const mockResponse: SearchResponse = {
123
+ results: {
124
+ web: [],
125
+ news: [
126
+ {
127
+ title: 'News Title',
128
+ description: 'News description',
129
+ page_age: '2023-01-01T00:00:00',
130
+ url: 'https://news.com/article',
131
+ },
132
+ ],
133
+ },
134
+ metadata: {
135
+ request_uuid: 'test-uuid',
136
+ query: 'test query',
137
+ latency: 0.1,
138
+ },
139
+ };
140
+
141
+ const result = formatSearchResults(mockResponse);
142
+
143
+ expect(result.content[0]?.text).toContain('NEWS RESULTS:');
144
+ expect(result.content[0]?.text).toContain('News Title');
145
+ expect(result.content[0]?.text).toContain('Published: 2023-01-01T00:00:00');
146
+ // URLs should NOT be in text content
147
+ expect(result.content[0]?.text).not.toContain('https://news.com/article');
148
+ expect(result.structuredContent).toHaveProperty('resultCounts');
149
+ expect(result.structuredContent.resultCounts).toHaveProperty('web', 0);
150
+ expect(result.structuredContent.resultCounts).toHaveProperty('news', 1);
151
+ expect(result.structuredContent.resultCounts).toHaveProperty('total', 1);
152
+ // URLs should be in structuredContent.results
153
+ expect(result.structuredContent).toHaveProperty('results');
154
+ expect(result.structuredContent.results?.news).toBeDefined();
155
+ expect(result.structuredContent.results?.news?.length).toBe(1);
156
+ expect(result.structuredContent.results?.news?.[0]).toEqual({
157
+ url: 'https://news.com/article',
158
+ title: 'News Title',
159
+ });
160
+ });
161
+
162
+ test('formats both web and news results', () => {
163
+ const mockResponse: SearchResponse = {
164
+ results: {
165
+ web: [
166
+ {
167
+ url: 'https://web.com',
168
+ title: 'Web Title',
169
+ description: 'Web description',
170
+ snippets: ['web snippet'],
171
+ page_age: '2023-01-01T00:00:00',
172
+ authors: ['Web Author'],
173
+ },
174
+ ],
175
+ news: [
176
+ {
177
+ title: 'News Title',
178
+ description: 'News description',
179
+ page_age: '2023-01-01T00:00:00',
180
+ url: 'https://news.com/article',
181
+ },
182
+ ],
183
+ },
184
+ metadata: {
185
+ request_uuid: 'test-uuid',
186
+ query: 'test query',
187
+ latency: 0.1,
188
+ },
189
+ };
190
+
191
+ const result = formatSearchResults(mockResponse);
192
+
193
+ expect(result.content[0]?.text).toContain('WEB RESULTS:');
194
+ expect(result.content[0]?.text).toContain('NEWS RESULTS:');
195
+ expect(result.content[0]?.text).toContain(`=${'='.repeat(49)}`);
196
+ // URLs should NOT be in text content
197
+ expect(result.content[0]?.text).not.toContain('https://web.com');
198
+ expect(result.content[0]?.text).not.toContain('https://news.com/article');
199
+ expect(result.structuredContent.resultCounts).toHaveProperty('web', 1);
200
+ expect(result.structuredContent.resultCounts).toHaveProperty('news', 1);
201
+ expect(result.structuredContent.resultCounts).toHaveProperty('total', 2);
202
+ // URLs should be in structuredContent.results
203
+ expect(result.structuredContent).toHaveProperty('results');
204
+ expect(result.structuredContent.results?.web).toBeDefined();
205
+ expect(result.structuredContent.results?.news).toBeDefined();
206
+ expect(result.structuredContent.results?.web?.length).toBe(1);
207
+ expect(result.structuredContent.results?.news?.length).toBe(1);
208
+ expect(result.structuredContent.results?.web?.[0]).toEqual({
209
+ url: 'https://web.com',
210
+ title: 'Web Title',
211
+ });
212
+ expect(result.structuredContent.results?.news?.[0]).toEqual({
213
+ url: 'https://news.com/article',
214
+ title: 'News Title',
215
+ });
216
+ });
217
+ });
@@ -0,0 +1,37 @@
1
+ import packageJson from '../../package.json' with { type: 'json' };
2
+ /**
3
+ * Generates a mailto link for error reporting with pre-filled context
4
+ * Used by tool error handlers to provide easy error reporting
5
+ */
6
+ export const generateErrorReportLink = ({
7
+ errorMessage,
8
+ tool,
9
+ clientInfo,
10
+ }: {
11
+ errorMessage: string;
12
+ tool: string;
13
+ clientInfo: string;
14
+ }): string => {
15
+ const subject = `MCP Server Issue v${packageJson.version}`;
16
+ const body = `Server Version: v${packageJson.version}
17
+ Client: ${clientInfo}
18
+ Tool: ${tool}
19
+
20
+ Error Message:
21
+ ${errorMessage}
22
+
23
+ Steps to Reproduce:
24
+ 1.
25
+ 2.
26
+ 3.
27
+
28
+ Additional Context:
29
+ `;
30
+
31
+ const params = new URLSearchParams({
32
+ subject,
33
+ body,
34
+ });
35
+
36
+ return `mailto:support@you.com?${params.toString()}`;
37
+ };
@@ -0,0 +1,10 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { LoggingMessageNotification } from '@modelcontextprotocol/sdk/types.js';
3
+
4
+ /**
5
+ * Creates a logger function that sends messages through MCP server
6
+ * Used by tool registration files
7
+ */
8
+ export const getLogger = (mcp: McpServer) => async (params: LoggingMessageNotification['params']) => {
9
+ await mcp.server.sendLoggingMessage(params);
10
+ };
@@ -0,0 +1,160 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { useGetClientVersion } from '../use-client-version.ts';
4
+
5
+ describe('useGetClientVersion', () => {
6
+ test('returns formatted string with all fields present', () => {
7
+ const mockMcp = {
8
+ server: {
9
+ getClientVersion: () => ({
10
+ name: 'test-client',
11
+ version: '1.0.0',
12
+ title: 'Test Client',
13
+ websiteUrl: 'https://example.com',
14
+ }),
15
+ },
16
+ } as unknown as McpServer;
17
+
18
+ const getUserAgent = useGetClientVersion(mockMcp);
19
+ const result = getUserAgent();
20
+
21
+ expect(result).toMatch(
22
+ /^MCP\/[\d.]+(-[\w.]+)? \(You\.com; test-client; 1\.0\.0; Test Client; https:\/\/example\.com\)$/,
23
+ );
24
+ });
25
+
26
+ test('returns formatted string with name and version only', () => {
27
+ const mockMcp = {
28
+ server: {
29
+ getClientVersion: () => ({
30
+ name: 'test-client',
31
+ version: '1.0.0',
32
+ }),
33
+ },
34
+ } as unknown as McpServer;
35
+
36
+ const getUserAgent = useGetClientVersion(mockMcp);
37
+ const result = getUserAgent();
38
+
39
+ expect(result).toMatch(/^MCP\/[\d.]+(-[\w.]+)? \(You\.com; test-client; 1\.0\.0\)$/);
40
+ });
41
+
42
+ test('returns UNKNOWN when no client version available', () => {
43
+ const mockMcp = {
44
+ server: {
45
+ getClientVersion: () => null,
46
+ },
47
+ } as unknown as McpServer;
48
+
49
+ const getUserAgent = useGetClientVersion(mockMcp);
50
+ const result = getUserAgent();
51
+
52
+ expect(result).toMatch(/^MCP\/[\d.]+(-[\w.]+)? \(You\.com; UNKNOWN\)$/);
53
+ });
54
+
55
+ test('returns UNKNOWN when getClientVersion returns undefined', () => {
56
+ const mockMcp = {
57
+ server: {
58
+ getClientVersion: () => undefined,
59
+ },
60
+ } as unknown as McpServer;
61
+
62
+ const getUserAgent = useGetClientVersion(mockMcp);
63
+ const result = getUserAgent();
64
+
65
+ expect(result).toMatch(/^MCP\/[\d.]+(-[\w.]+)? \(You\.com; UNKNOWN\)$/);
66
+ });
67
+
68
+ test('filters out empty strings from fields', () => {
69
+ const mockMcp = {
70
+ server: {
71
+ getClientVersion: () => ({
72
+ name: 'test-client',
73
+ version: '1.0.0',
74
+ title: '', // Empty string should be filtered out
75
+ websiteUrl: 'https://example.com',
76
+ }),
77
+ },
78
+ } as unknown as McpServer;
79
+
80
+ const getUserAgent = useGetClientVersion(mockMcp);
81
+ const result = getUserAgent();
82
+
83
+ expect(result).toMatch(/^MCP\/[\d.]+(-[\w.]+)? \(You\.com; test-client; 1\.0\.0; https:\/\/example\.com\)$/);
84
+ expect(result).not.toContain(';;'); // No double semicolons
85
+ });
86
+
87
+ test('filters out null values from fields', () => {
88
+ const mockMcp = {
89
+ server: {
90
+ getClientVersion: () => ({
91
+ name: 'test-client',
92
+ version: '1.0.0',
93
+ title: null, // Null should be filtered out
94
+ websiteUrl: 'https://example.com',
95
+ }),
96
+ },
97
+ } as unknown as McpServer;
98
+
99
+ const getUserAgent = useGetClientVersion(mockMcp);
100
+ const result = getUserAgent();
101
+
102
+ expect(result).toMatch(/^MCP\/[\d.]+(-[\w.]+)? \(You\.com; test-client; 1\.0\.0; https:\/\/example\.com\)$/);
103
+ });
104
+
105
+ test('handles partial fields - name, version, and title only', () => {
106
+ const mockMcp = {
107
+ server: {
108
+ getClientVersion: () => ({
109
+ name: 'Claude Desktop',
110
+ version: '0.7.6',
111
+ title: 'Claude Desktop App',
112
+ }),
113
+ },
114
+ } as unknown as McpServer;
115
+
116
+ const getUserAgent = useGetClientVersion(mockMcp);
117
+ const result = getUserAgent();
118
+
119
+ expect(result).toMatch(/^MCP\/[\d.]+(-[\w.]+)? \(You\.com; Claude Desktop; 0\.7\.6; Claude Desktop App\)$/);
120
+ });
121
+
122
+ test('handles Claude Desktop client info format', () => {
123
+ const mockMcp = {
124
+ server: {
125
+ getClientVersion: () => ({
126
+ name: 'Claude Desktop',
127
+ version: '0.7.6',
128
+ }),
129
+ },
130
+ } as unknown as McpServer;
131
+
132
+ const getUserAgent = useGetClientVersion(mockMcp);
133
+ const result = getUserAgent();
134
+
135
+ expect(result).toMatch(/^MCP\/[\d.]+(-[\w.]+)? \(You\.com; Claude Desktop; 0\.7\.6\)$/);
136
+ });
137
+
138
+ test('returns a function that can be called multiple times', () => {
139
+ const mockMcp = {
140
+ server: {
141
+ getClientVersion: () => ({
142
+ name: 'test-client',
143
+ version: '1.0.0',
144
+ }),
145
+ },
146
+ } as unknown as McpServer;
147
+
148
+ const getUserAgent = useGetClientVersion(mockMcp);
149
+
150
+ // Call multiple times to ensure consistent results
151
+ const result1 = getUserAgent();
152
+ const result2 = getUserAgent();
153
+ const result3 = getUserAgent();
154
+
155
+ const pattern = /^MCP\/[\d.]+(-[\w.]+)? \(You\.com; test-client; 1\.0\.0\)$/;
156
+ expect(result1).toMatch(pattern);
157
+ expect(result2).toMatch(pattern);
158
+ expect(result3).toMatch(pattern);
159
+ });
160
+ });
@@ -0,0 +1,21 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import packageJson from '../../package.json' with { type: 'json' };
3
+
4
+ /**
5
+ * Creates User-Agent string for API requests
6
+ * Used by search and express agent API calls
7
+ */
8
+ const setUserAgent = (client: string) => `MCP/${packageJson.version} (You.com; ${client})`;
9
+
10
+ /**
11
+ * Get's function that returns a formatted client version information into a string
12
+ * Used by stdio.ts and http.ts for logging/debugging
13
+ */
14
+ export const useGetClientVersion = (mcp: McpServer) => () => {
15
+ const clientVersion = mcp.server.getClientVersion();
16
+ if (clientVersion) {
17
+ const { name, version, title, websiteUrl } = clientVersion;
18
+ return setUserAgent([name, version, title, websiteUrl].filter(Boolean).join('; '));
19
+ }
20
+ return setUserAgent('UNKNOWN');
21
+ };
package/src/stdio.ts ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { registerContentsTool } from './contents/register-contents-tool.ts';
4
+ import { registerExpressTool } from './express/register-express-tool.ts';
5
+ import { getMCpServer } from './get-mcp-server.ts';
6
+ import { registerSearchTool } from './search/register-search-tool.ts';
7
+ import { useGetClientVersion } from './shared/use-client-version.ts';
8
+
9
+ const YDC_API_KEY = process.env.YDC_API_KEY;
10
+
11
+ try {
12
+ const mcp = getMCpServer();
13
+ const getUserAgent = useGetClientVersion(mcp);
14
+
15
+ registerSearchTool({ mcp, YDC_API_KEY, getUserAgent });
16
+ registerExpressTool({ mcp, YDC_API_KEY, getUserAgent });
17
+ registerContentsTool({ mcp, YDC_API_KEY, getUserAgent });
18
+
19
+ const transport = new StdioServerTransport();
20
+ await mcp.connect(transport);
21
+ } catch (error) {
22
+ process.stderr.write(`Failed to start server: ${error}\n`);
23
+ process.exit(1);
24
+ }
@@ -0,0 +1,24 @@
1
+ // src/tests/exports.spec.ts
2
+ import { describe, expect, test } from 'bun:test';
3
+ import * as exports from '@youdotcom-oss/mcp';
4
+
5
+ describe('Package exports', () => {
6
+ test('should export all required schemas', async () => {
7
+ // Schemas
8
+ expect(exports.ContentsQuerySchema).toBeDefined();
9
+ expect(exports.ExpressAgentInputSchema).toBeDefined();
10
+ expect(exports.SearchQuerySchema).toBeDefined();
11
+
12
+ // Utilities
13
+ expect(exports.fetchContents).toBeDefined();
14
+ expect(exports.formatContentsResponse).toBeDefined();
15
+ expect(exports.callExpressAgent).toBeDefined();
16
+ expect(exports.formatExpressAgentResponse).toBeDefined();
17
+ expect(exports.fetchSearchResults).toBeDefined();
18
+ expect(exports.formatSearchResults).toBeDefined();
19
+
20
+ // Shared utilities
21
+ expect(exports.checkResponseForErrors).toBeDefined();
22
+ expect(exports.formatSearchResultsText).toBeDefined();
23
+ });
24
+ });