@youdotcom-oss/mcp 2.0.2 → 2.0.3

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/bin/stdio.js CHANGED
@@ -20252,7 +20252,7 @@ var EMPTY_COMPLETION_RESULT = {
20252
20252
  // package.json
20253
20253
  var package_default = {
20254
20254
  name: "@youdotcom-oss/mcp",
20255
- version: "2.0.2",
20255
+ version: "2.0.3",
20256
20256
  description: "You.com API Model Context Protocol Server - For programmatic API access, use @youdotcom-oss/api",
20257
20257
  license: "MIT",
20258
20258
  engines: {
@@ -20308,7 +20308,7 @@ var package_default = {
20308
20308
  },
20309
20309
  mcpName: "io.github.youdotcom-oss/mcp",
20310
20310
  dependencies: {
20311
- "@youdotcom-oss/api": "0.2.1",
20311
+ "@youdotcom-oss/api": "0.2.2",
20312
20312
  zod: "^4.3.6",
20313
20313
  "@hono/mcp": "^0.2.3",
20314
20314
  "@modelcontextprotocol/sdk": "^1.25.3",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdotcom-oss/mcp",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "You.com API Model Context Protocol Server - For programmatic API access, use @youdotcom-oss/api",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -56,7 +56,7 @@
56
56
  },
57
57
  "mcpName": "io.github.youdotcom-oss/mcp",
58
58
  "dependencies": {
59
- "@youdotcom-oss/api": "0.2.1",
59
+ "@youdotcom-oss/api": "0.2.2",
60
60
  "zod": "^4.3.6",
61
61
  "@hono/mcp": "^0.2.3",
62
62
  "@modelcontextprotocol/sdk": "^1.25.3",
package/src/http.ts CHANGED
@@ -52,6 +52,20 @@ const handleMcpRequest = async (c: Context) => {
52
52
  return response
53
53
  }
54
54
 
55
+ const return405MethodNotAllowed = (c: Context) => {
56
+ c.status(405)
57
+ c.header('Allow', 'POST')
58
+ c.header('Content-Type', 'application/json')
59
+ return c.json({
60
+ jsonrpc: '2.0',
61
+ error: {
62
+ code: -32000,
63
+ message: 'Method Not Allowed: Use POST to send MCP requests',
64
+ },
65
+ id: null,
66
+ })
67
+ }
68
+
55
69
  const app = new Hono()
56
70
  app.use(trimTrailingSlash())
57
71
 
@@ -64,7 +78,14 @@ app.get('/mcp-health', async (c) => {
64
78
  })
65
79
  })
66
80
 
67
- app.all('/mcp', handleMcpRequest)
68
- app.all('/mcp/', handleMcpRequest)
81
+ // POST handler for MCP requests (per MCP Streamable HTTP spec)
82
+ app.post('/mcp', handleMcpRequest)
83
+ app.post('/mcp/', handleMcpRequest)
84
+
85
+ // Fallback for other methods - returns 405 per MCP spec
86
+ // Spec: "The server MUST either return Content-Type: text/event-stream
87
+ // or else return HTTP 405 Method Not Allowed"
88
+ app.all('/mcp', return405MethodNotAllowed)
89
+ app.all('/mcp/', return405MethodNotAllowed)
69
90
 
70
91
  export default app
@@ -1,11 +1,15 @@
1
1
  import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from 'bun:test'
2
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
3
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
2
4
  import httpApp from '../http.ts'
5
+ import type { SearchStructuredContent } from '../search/search.schema.ts'
3
6
 
4
7
  // Increase default timeout for hooks to prevent intermittent failures
5
8
  setDefaultTimeout(15_000)
6
9
 
7
10
  let server: ReturnType<typeof Bun.serve>
8
11
  let baseUrl: string
12
+ let mcpClient: Client
9
13
  const testApiKey = process.env.YDC_API_KEY
10
14
 
11
15
  beforeAll(async () => {
@@ -21,9 +25,29 @@ beforeAll(async () => {
21
25
 
22
26
  // Wait a bit for server to start
23
27
  await new Promise((resolve) => setTimeout(resolve, 500))
28
+
29
+ // Create MCP client with HTTP transport for e2e testing
30
+ const transport = new StreamableHTTPClientTransport(new URL(`${baseUrl}/mcp`), {
31
+ requestInit: {
32
+ headers: {
33
+ Authorization: `Bearer ${testApiKey}`,
34
+ },
35
+ },
36
+ })
37
+
38
+ mcpClient = new Client({
39
+ name: 'test-http-client',
40
+ version: '1.0.0',
41
+ })
42
+
43
+ await mcpClient.connect(transport)
24
44
  })
25
45
 
26
46
  afterAll(async () => {
47
+ if (mcpClient) {
48
+ await mcpClient.close()
49
+ }
50
+
27
51
  if (server) {
28
52
  server.stop()
29
53
  // Wait a bit for server to fully stop
@@ -171,6 +195,75 @@ describe('HTTP Server Endpoints', () => {
171
195
  })
172
196
  })
173
197
 
198
+ describe('HTTP Method Handling', () => {
199
+ test('mcp endpoint returns 405 for GET requests per MCP spec', async () => {
200
+ const response = await fetch(`${baseUrl}/mcp`, {
201
+ method: 'GET',
202
+ headers: {
203
+ Accept: 'text/event-stream',
204
+ Authorization: `Bearer ${testApiKey}`,
205
+ },
206
+ })
207
+
208
+ // Verify 405 status
209
+ expect(response.status).toBe(405)
210
+
211
+ // Verify Allow header (RFC 9110 §15.5.6)
212
+ expect(response.headers.get('allow')).toBe('POST')
213
+
214
+ // Verify JSON-RPC error format
215
+ expect(response.headers.get('content-type')).toContain('application/json')
216
+
217
+ const data = (await response.json()) as {
218
+ jsonrpc: string
219
+ error: { code: number; message: string }
220
+ id: null
221
+ }
222
+ expect(data).toHaveProperty('jsonrpc', '2.0')
223
+ expect(data).toHaveProperty('error')
224
+ expect(data.error).toHaveProperty('code', -32000)
225
+ expect(data.error.message).toContain('Method Not Allowed')
226
+ })
227
+
228
+ test('mcp endpoint returns 405 for DELETE requests', async () => {
229
+ const response = await fetch(`${baseUrl}/mcp`, {
230
+ method: 'DELETE',
231
+ headers: {
232
+ Authorization: `Bearer ${testApiKey}`,
233
+ },
234
+ })
235
+
236
+ expect(response.status).toBe(405)
237
+ expect(response.headers.get('allow')).toBe('POST')
238
+ })
239
+
240
+ test('mcp endpoint returns 405 for PUT requests', async () => {
241
+ const response = await fetch(`${baseUrl}/mcp`, {
242
+ method: 'PUT',
243
+ headers: {
244
+ 'Content-Type': 'application/json',
245
+ Authorization: `Bearer ${testApiKey}`,
246
+ },
247
+ body: JSON.stringify({}),
248
+ })
249
+
250
+ expect(response.status).toBe(405)
251
+ expect(response.headers.get('allow')).toBe('POST')
252
+ })
253
+
254
+ test('mcp endpoint with trailing slash returns 405 for GET', async () => {
255
+ const response = await fetch(`${baseUrl}/mcp/`, {
256
+ method: 'GET',
257
+ headers: {
258
+ Accept: 'text/event-stream',
259
+ },
260
+ })
261
+
262
+ expect(response.status).toBe(405)
263
+ expect(response.headers.get('allow')).toBe('POST')
264
+ })
265
+ })
266
+
174
267
  describe('HTTP MCP Endpoint Basic Functionality', () => {
175
268
  test('mcp endpoint responds to valid Bearer token', async () => {
176
269
  // Test that the endpoint accepts valid Bearer token and doesn't return auth error
@@ -320,3 +413,140 @@ describe('HTTP MCP Endpoint Basic Functionality', () => {
320
413
  { retry: 2 },
321
414
  )
322
415
  })
416
+
417
+ describe('HTTP MCP Client E2E Tests', () => {
418
+ test('mcp client can initialize and list tools', async () => {
419
+ const tools = await mcpClient.listTools()
420
+
421
+ expect(tools.tools).toBeDefined()
422
+ expect(Array.isArray(tools.tools)).toBe(true)
423
+ expect(tools.tools.length).toBeGreaterThan(0)
424
+
425
+ // Verify search tool is available
426
+ const searchTool = tools.tools.find((t) => t.name === 'you-search')
427
+ expect(searchTool).toBeDefined()
428
+ expect(searchTool?.title).toBe('Web Search')
429
+
430
+ // Verify contents tool is available
431
+ const contentsTool = tools.tools.find((t) => t.name === 'you-contents')
432
+ expect(contentsTool).toBeDefined()
433
+ expect(contentsTool?.title).toBe('Extract Web Page Contents')
434
+ })
435
+
436
+ test(
437
+ 'mcp client can call search tool successfully',
438
+ async () => {
439
+ const result = await mcpClient.callTool({
440
+ name: 'you-search',
441
+ arguments: {
442
+ query: 'typescript tutorial',
443
+ count: 2,
444
+ },
445
+ })
446
+
447
+ expect(result).toHaveProperty('content')
448
+ expect(result).toHaveProperty('structuredContent')
449
+
450
+ const content = result.content as { type: string; text: string }[]
451
+ expect(Array.isArray(content)).toBe(true)
452
+ expect(content[0]).toHaveProperty('type', 'text')
453
+ expect(content[0]).toHaveProperty('text')
454
+
455
+ const text = content[0]?.text
456
+ expect(text).toContain('Search Results for')
457
+ expect(text).toContain('typescript tutorial')
458
+
459
+ const structuredContent = result.structuredContent as SearchStructuredContent
460
+ expect(structuredContent).toHaveProperty('resultCounts')
461
+ expect(structuredContent.resultCounts).toHaveProperty('web')
462
+ expect(structuredContent.resultCounts).toHaveProperty('total')
463
+ },
464
+ { retry: 2 },
465
+ )
466
+
467
+ test(
468
+ 'mcp client handles search with multiple results',
469
+ async () => {
470
+ const result = await mcpClient.callTool({
471
+ name: 'you-search',
472
+ arguments: {
473
+ query: 'javascript frameworks',
474
+ count: 3,
475
+ },
476
+ })
477
+
478
+ const content = result.content as { type: string; text: string }[]
479
+ const text = content[0]?.text
480
+
481
+ expect(text).toContain('WEB RESULTS:')
482
+ expect(text).toContain('Title:')
483
+ expect(text).toContain('URL:')
484
+
485
+ const structuredContent = result.structuredContent as SearchStructuredContent
486
+ expect(structuredContent.resultCounts.web).toBeGreaterThan(0)
487
+ expect(structuredContent.results?.web).toBeDefined()
488
+ expect(structuredContent.results?.web?.length).toBeGreaterThanOrEqual(1)
489
+ },
490
+ { retry: 2 },
491
+ )
492
+
493
+ test(
494
+ 'mcp client handles search parameters correctly',
495
+ async () => {
496
+ const result = await mcpClient.callTool({
497
+ name: 'you-search',
498
+ arguments: {
499
+ query: 'programming tutorials',
500
+ count: 2,
501
+ safesearch: 'strict',
502
+ freshness: 'month',
503
+ },
504
+ })
505
+
506
+ const content = result.content as { type: string; text: string }[]
507
+ expect(content[0]?.text).toContain('programming tutorials')
508
+
509
+ const structuredContent = result.structuredContent as SearchStructuredContent
510
+ expect(structuredContent).toHaveProperty('resultCounts')
511
+ },
512
+ { retry: 2 },
513
+ )
514
+
515
+ test('mcp client maintains connection across multiple requests', async () => {
516
+ // First request
517
+ const result1 = await mcpClient.callTool({
518
+ name: 'you-search',
519
+ arguments: {
520
+ query: 'test query 1',
521
+ count: 1,
522
+ },
523
+ })
524
+
525
+ expect(result1).toHaveProperty('content')
526
+
527
+ // Second request - should work without reconnecting
528
+ const result2 = await mcpClient.callTool({
529
+ name: 'you-search',
530
+ arguments: {
531
+ query: 'test query 2',
532
+ count: 1,
533
+ },
534
+ })
535
+
536
+ expect(result2).toHaveProperty('content')
537
+
538
+ // Both should have succeeded
539
+ const content1 = result1.content as { type: string; text: string }[]
540
+ const content2 = result2.content as { type: string; text: string }[]
541
+
542
+ expect(content1[0]?.text).toContain('test query 1')
543
+ expect(content2[0]?.text).toContain('test query 2')
544
+ })
545
+
546
+ test('mcp client can list server capabilities', async () => {
547
+ // The client should have received server info during initialization
548
+ // We can verify this by checking that tools are available
549
+ const tools = await mcpClient.listTools()
550
+ expect(tools.tools.length).toBeGreaterThan(0)
551
+ })
552
+ })