@youdotcom-oss/mcp 1.3.5-next.5 → 1.3.6

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,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
+ });
@@ -0,0 +1,318 @@
1
+ import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from 'bun:test';
2
+ import httpApp from '../http.ts';
3
+
4
+ // Increase default timeout for hooks to prevent intermittent failures
5
+ setDefaultTimeout(15_000);
6
+
7
+ let server: ReturnType<typeof Bun.serve>;
8
+ let baseUrl: string;
9
+ const testApiKey = process.env.YDC_API_KEY;
10
+
11
+ beforeAll(async () => {
12
+ // Start HTTP server on random port
13
+ const port = Math.floor(Math.random() * 10000) + 20000;
14
+ baseUrl = `http://localhost:${port}`;
15
+
16
+ // Start actual HTTP server using Bun
17
+ server = Bun.serve({
18
+ port,
19
+ fetch: httpApp.fetch.bind(httpApp),
20
+ });
21
+
22
+ // Wait a bit for server to start
23
+ await new Promise((resolve) => setTimeout(resolve, 500));
24
+ });
25
+
26
+ afterAll(async () => {
27
+ if (server) {
28
+ server.stop();
29
+ // Wait a bit for server to fully stop
30
+ await new Promise((resolve) => setTimeout(resolve, 100));
31
+ }
32
+ });
33
+
34
+ describe('HTTP Server Endpoints', () => {
35
+ test('health endpoint returns service status', async () => {
36
+ const response = await fetch(`${baseUrl}/mcp-health`);
37
+
38
+ expect(response.status).toBe(200);
39
+ expect(response.headers.get('content-type')).toContain('application/json');
40
+
41
+ const data = (await response.json()) as {
42
+ status: string;
43
+ timestamp: string;
44
+ version: string;
45
+ service: string;
46
+ };
47
+ expect(data).toHaveProperty('status', 'healthy');
48
+ expect(data).toHaveProperty('timestamp');
49
+ expect(data).toHaveProperty('version');
50
+ expect(data).toHaveProperty('service', 'youdotcom-mcp-server');
51
+ expect(typeof data.timestamp).toBe('string');
52
+ expect(typeof data.version).toBe('string');
53
+ });
54
+
55
+ test('mcp endpoint requires authorization header', async () => {
56
+ const response = await fetch(`${baseUrl}/mcp`, {
57
+ method: 'POST',
58
+ headers: {
59
+ 'Content-Type': 'application/json',
60
+ },
61
+ body: JSON.stringify({}),
62
+ });
63
+
64
+ expect(response.status).toBe(401);
65
+ expect(response.headers.get('content-type')).toContain('text/plain');
66
+
67
+ const text = await response.text();
68
+ expect(text).toBe('Unauthorized: Authorization header required');
69
+ });
70
+
71
+ test('mcp endpoint requires Bearer token format', async () => {
72
+ const response = await fetch(`${baseUrl}/mcp`, {
73
+ method: 'POST',
74
+ headers: {
75
+ 'Content-Type': 'application/json',
76
+ Authorization: 'InvalidFormat token123',
77
+ },
78
+ body: JSON.stringify({}),
79
+ });
80
+
81
+ expect(response.status).toBe(401);
82
+ expect(response.headers.get('content-type')).toContain('text/plain');
83
+
84
+ const text = await response.text();
85
+ expect(text).toBe('Unauthorized: Bearer token required');
86
+ });
87
+
88
+ test('mcp endpoint accepts valid Bearer token', async () => {
89
+ const response = await fetch(`${baseUrl}/mcp`, {
90
+ method: 'POST',
91
+ headers: {
92
+ 'Content-Type': 'application/json',
93
+ Accept: 'application/json, text/event-stream',
94
+ Authorization: `Bearer ${testApiKey}`,
95
+ },
96
+ body: JSON.stringify({
97
+ jsonrpc: '2.0',
98
+ method: 'initialize',
99
+ id: 1,
100
+ params: {
101
+ protocolVersion: '2024-11-05',
102
+ capabilities: {},
103
+ clientInfo: {
104
+ name: 'test-client',
105
+ version: '1.0.0',
106
+ },
107
+ },
108
+ }),
109
+ });
110
+
111
+ expect(response.status).toBe(200);
112
+ expect(response.headers.get('content-type')).toContain('text/event-stream');
113
+
114
+ // StreamableHTTPTransport uses SSE format, so response will be streaming
115
+ const text = await response.text();
116
+ expect(text).toContain('data:');
117
+ expect(text).toContain('jsonrpc');
118
+ expect(text).toContain('result');
119
+ expect(text).toContain('protocolVersion');
120
+ expect(text).toContain('capabilities');
121
+ });
122
+
123
+ test('mcp endpoint with trailing slash works identically', async () => {
124
+ const response = await fetch(`${baseUrl}/mcp/`, {
125
+ method: 'POST',
126
+ headers: {
127
+ 'Content-Type': 'application/json',
128
+ Accept: 'application/json, text/event-stream',
129
+ Authorization: `Bearer ${testApiKey}`,
130
+ },
131
+ body: JSON.stringify({
132
+ jsonrpc: '2.0',
133
+ method: 'initialize',
134
+ id: 1,
135
+ params: {
136
+ protocolVersion: '2024-11-05',
137
+ capabilities: {},
138
+ clientInfo: {
139
+ name: 'test-client',
140
+ version: '1.0.0',
141
+ },
142
+ },
143
+ }),
144
+ });
145
+
146
+ expect(response.status).toBe(200);
147
+ expect(response.headers.get('content-type')).toContain('text/event-stream');
148
+
149
+ const text = await response.text();
150
+ expect(text).toContain('data:');
151
+ expect(text).toContain('jsonrpc');
152
+ expect(text).toContain('result');
153
+ expect(text).toContain('protocolVersion');
154
+ expect(text).toContain('capabilities');
155
+ });
156
+
157
+ test('mcp endpoint with trailing slash requires authorization', async () => {
158
+ const response = await fetch(`${baseUrl}/mcp/`, {
159
+ method: 'POST',
160
+ headers: {
161
+ 'Content-Type': 'application/json',
162
+ },
163
+ body: JSON.stringify({}),
164
+ });
165
+
166
+ expect(response.status).toBe(401);
167
+ expect(response.headers.get('content-type')).toContain('text/plain');
168
+
169
+ const text = await response.text();
170
+ expect(text).toBe('Unauthorized: Authorization header required');
171
+ });
172
+ });
173
+
174
+ describe('HTTP MCP Endpoint Basic Functionality', () => {
175
+ test('mcp endpoint responds to valid Bearer token', async () => {
176
+ // Test that the endpoint accepts valid Bearer token and doesn't return auth error
177
+ const response = await fetch(`${baseUrl}/mcp`, {
178
+ method: 'POST',
179
+ headers: {
180
+ 'Content-Type': 'application/json',
181
+ Accept: 'application/json, text/event-stream',
182
+ Authorization: `Bearer ${testApiKey}`,
183
+ },
184
+ body: JSON.stringify({
185
+ jsonrpc: '2.0',
186
+ method: 'ping',
187
+ id: 1,
188
+ }),
189
+ });
190
+
191
+ // Should get a response (not 401/403), even if the method isn't supported
192
+ expect(response.status).not.toBe(401);
193
+ expect(response.status).not.toBe(403);
194
+
195
+ // Should be SSE response for StreamableHTTPTransport
196
+ expect(response.headers.get('content-type')).toContain('text/event-stream');
197
+ });
198
+
199
+ test('mcp endpoint processes JSON-RPC requests', async () => {
200
+ // Test basic JSON-RPC structure handling
201
+ const response = await fetch(`${baseUrl}/mcp`, {
202
+ method: 'POST',
203
+ headers: {
204
+ 'Content-Type': 'application/json',
205
+ Accept: 'application/json, text/event-stream',
206
+ Authorization: `Bearer ${testApiKey}`,
207
+ },
208
+ body: JSON.stringify({
209
+ jsonrpc: '2.0',
210
+ method: 'unknown-method',
211
+ id: 123,
212
+ }),
213
+ });
214
+
215
+ expect(response.status).toBe(200);
216
+ expect(response.headers.get('content-type')).toContain('text/event-stream');
217
+
218
+ // StreamableHTTPTransport uses SSE format
219
+ const text = await response.text();
220
+ expect(text).toContain('data:');
221
+ expect(text).toContain('jsonrpc');
222
+ expect(text).toContain('123');
223
+ });
224
+
225
+ test('mcp endpoint extracts Bearer token correctly', async () => {
226
+ // Test that different tokens are processed
227
+ const response1 = await fetch(`${baseUrl}/mcp`, {
228
+ method: 'POST',
229
+ headers: {
230
+ 'Content-Type': 'application/json',
231
+ Accept: 'application/json, text/event-stream',
232
+ Authorization: `Bearer token123`,
233
+ },
234
+ body: JSON.stringify({
235
+ jsonrpc: '2.0',
236
+ method: 'test',
237
+ id: 1,
238
+ }),
239
+ });
240
+
241
+ const response2 = await fetch(`${baseUrl}/mcp`, {
242
+ method: 'POST',
243
+ headers: {
244
+ 'Content-Type': 'application/json',
245
+ Accept: 'application/json, text/event-stream',
246
+ Authorization: `Bearer different-token`,
247
+ },
248
+ body: JSON.stringify({
249
+ jsonrpc: '2.0',
250
+ method: 'test',
251
+ id: 2,
252
+ }),
253
+ });
254
+
255
+ // Both should be processed (not authentication errors)
256
+ expect(response1.status).not.toBe(401);
257
+ expect(response2.status).not.toBe(401);
258
+ });
259
+
260
+ test('mcp endpoint uses StreamableHTTPTransport', async () => {
261
+ // Test that the transport is properly handling requests
262
+ const response = await fetch(`${baseUrl}/mcp`, {
263
+ method: 'POST',
264
+ headers: {
265
+ 'Content-Type': 'application/json',
266
+ Accept: 'application/json, text/event-stream',
267
+ Authorization: `Bearer ${testApiKey}`,
268
+ },
269
+ body: JSON.stringify({
270
+ jsonrpc: '2.0',
271
+ method: 'test',
272
+ id: 42,
273
+ }),
274
+ });
275
+
276
+ expect(response.status).toBe(200);
277
+ expect(response.headers.get('content-type')).toContain('text/event-stream');
278
+
279
+ // StreamableHTTPTransport uses SSE format
280
+ const text = await response.text();
281
+ expect(text).toContain('data:');
282
+ expect(text).toContain('jsonrpc');
283
+ expect(text).toContain('42');
284
+ });
285
+
286
+ test('mcp server handles search tool request for latest tech news', async () => {
287
+ const response = await fetch(`${baseUrl}/mcp`, {
288
+ method: 'POST',
289
+ headers: {
290
+ 'Content-Type': 'application/json',
291
+ Accept: 'application/json, text/event-stream',
292
+ Authorization: `Bearer ${testApiKey}`,
293
+ },
294
+ body: JSON.stringify({
295
+ jsonrpc: '2.0',
296
+ method: 'tools/call',
297
+ id: 100,
298
+ params: {
299
+ name: 'you-search',
300
+ arguments: {
301
+ query: 'latest tech news',
302
+ count: 3,
303
+ },
304
+ },
305
+ }),
306
+ });
307
+
308
+ expect(response.status).toBe(200);
309
+ expect(response.headers.get('content-type')).toContain('text/event-stream');
310
+
311
+ const text = await response.text();
312
+ expect(text).toContain('data:');
313
+ expect(text).toContain('jsonrpc');
314
+ expect(text).toContain('result');
315
+ expect(text).toContain('latest tech news');
316
+ expect(text).toContain('Search Results for');
317
+ });
318
+ });