@youdotcom-oss/mcp 2.0.8 → 2.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdotcom-oss/mcp",
3
- "version": "2.0.8",
3
+ "version": "2.1.1",
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.3.4",
59
+ "@youdotcom-oss/api": "0.4.1",
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
@@ -4,6 +4,7 @@ import { trimTrailingSlash } from 'hono/trailing-slash'
4
4
  import packageJson from '../package.json' with { type: 'json' }
5
5
  import { registerContentsTool } from './contents/register-contents-tool.ts'
6
6
  import { getMCpServer } from './get-mcp-server.ts'
7
+ import { registerResearchTool } from './research/register-research-tool.ts'
7
8
  import { registerSearchTool } from './search/register-search-tool.ts'
8
9
  import { useGetClientVersion } from './shared/use-client-version.ts'
9
10
 
@@ -40,6 +41,7 @@ const handleMcpRequest = async (c: Context) => {
40
41
  getUserAgent,
41
42
  })
42
43
  registerContentsTool({ mcp, YDC_API_KEY, getUserAgent })
44
+ registerResearchTool({ mcp, YDC_API_KEY, getUserAgent })
43
45
 
44
46
  const transport = new StreamableHTTPTransport()
45
47
  await mcp.connect(transport)
@@ -0,0 +1,69 @@
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
+ }
@@ -0,0 +1,19 @@
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>
@@ -0,0 +1,30 @@
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
+ }
package/src/stdio.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
3
  import { registerContentsTool } from './contents/register-contents-tool.ts'
4
4
  import { getMCpServer } from './get-mcp-server.ts'
5
+ import { registerResearchTool } from './research/register-research-tool.ts'
5
6
  import { registerSearchTool } from './search/register-search-tool.ts'
6
7
  import { useGetClientVersion } from './shared/use-client-version.ts'
7
8
 
@@ -13,6 +14,7 @@ try {
13
14
 
14
15
  registerSearchTool({ mcp, YDC_API_KEY, getUserAgent })
15
16
  registerContentsTool({ mcp, YDC_API_KEY, getUserAgent })
17
+ registerResearchTool({ mcp, YDC_API_KEY, getUserAgent })
16
18
 
17
19
  const transport = new StdioServerTransport()
18
20
  await mcp.connect(transport)
@@ -3,6 +3,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
3
3
  import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
4
4
  import { $ } from 'bun'
5
5
  import type { ContentsStructuredContent } from '../contents/contents.schemas.ts'
6
+ import type { ResearchStructuredContent } from '../research/research.schemas.ts'
6
7
  import type { SearchStructuredContent } from '../search/search.schema.ts'
7
8
 
8
9
  let client: Client
@@ -521,3 +522,128 @@ describe('registerContentsTool', () => {
521
522
  { retry: 2 },
522
523
  )
523
524
  })
525
+
526
+ describe('registerResearchTool', () => {
527
+ test('tool is registered and available', async () => {
528
+ const tools = await client.listTools()
529
+
530
+ const researchTool = tools.tools.find((t) => t.name === 'you-research')
531
+
532
+ expect(researchTool).toBeDefined()
533
+ expect(researchTool?.title).toBe('Research')
534
+ expect(researchTool?.description).toContain('Research a topic')
535
+ })
536
+
537
+ test(
538
+ 'performs basic research successfully',
539
+ async () => {
540
+ const result = await client.callTool({
541
+ name: 'you-research',
542
+ arguments: {
543
+ input: 'What is TypeScript?',
544
+ research_effort: 'lite',
545
+ },
546
+ })
547
+
548
+ expect(result).toHaveProperty('content')
549
+ expect(result).toHaveProperty('structuredContent')
550
+
551
+ const content = result.content as { type: string; text: string }[]
552
+ expect(Array.isArray(content)).toBe(true)
553
+ expect(content[0]).toHaveProperty('type', 'text')
554
+ expect(content[0]).toHaveProperty('text')
555
+
556
+ const text = content[0]?.text
557
+ expect(typeof text).toBe('string')
558
+ expect(text?.length).toBeGreaterThan(0)
559
+ },
560
+ { retry: 2 },
561
+ )
562
+
563
+ test(
564
+ 'returns structured content with source information',
565
+ async () => {
566
+ const result = await client.callTool({
567
+ name: 'you-research',
568
+ arguments: {
569
+ input: 'What are the benefits of REST APIs?',
570
+ research_effort: 'lite',
571
+ },
572
+ })
573
+
574
+ const structuredContent = result.structuredContent as ResearchStructuredContent
575
+ expect(structuredContent).toHaveProperty('content_type', 'text')
576
+ expect(structuredContent).toHaveProperty('sourceCount')
577
+ expect(typeof structuredContent.sourceCount).toBe('number')
578
+ expect(structuredContent.sourceCount).toBeGreaterThan(0)
579
+
580
+ expect(structuredContent).toHaveProperty('sources')
581
+ expect(Array.isArray(structuredContent.sources)).toBe(true)
582
+ expect(structuredContent.sources.length).toBeGreaterThan(0)
583
+
584
+ const source = structuredContent.sources[0]
585
+ expect(source).toBeDefined()
586
+ expect(source).toHaveProperty('url')
587
+ expect(typeof source?.url).toBe('string')
588
+ expect(source).toHaveProperty('snippetCount')
589
+ expect(typeof source?.snippetCount).toBe('number')
590
+ },
591
+ { retry: 2 },
592
+ )
593
+
594
+ test(
595
+ 'text content contains markdown with answer and sources',
596
+ async () => {
597
+ const result = await client.callTool({
598
+ name: 'you-research',
599
+ arguments: {
600
+ input: 'What is JWT authentication?',
601
+ research_effort: 'lite',
602
+ },
603
+ })
604
+
605
+ const content = result.content as { type: string; text: string }[]
606
+ const text = content[0]?.text
607
+ expect(text).toContain('# Answer')
608
+ expect(text).toContain('## Sources')
609
+ },
610
+ { retry: 2 },
611
+ )
612
+
613
+ test(
614
+ 'handles research_effort parameter',
615
+ async () => {
616
+ const transport = new StdioClientTransport({
617
+ command: 'npx',
618
+ args: [Bun.resolveSync('../../bin/stdio', import.meta.dir)],
619
+ env: {
620
+ YDC_API_KEY: process.env.YDC_API_KEY ?? '',
621
+ },
622
+ })
623
+
624
+ const dedicatedClient = new Client({
625
+ name: 'test-client-research',
626
+ version: '0.0.1',
627
+ })
628
+
629
+ await dedicatedClient.connect(transport)
630
+
631
+ try {
632
+ const result = await dedicatedClient.callTool({
633
+ name: 'you-research',
634
+ arguments: {
635
+ input: 'Explain microservices architecture',
636
+ research_effort: 'standard',
637
+ },
638
+ })
639
+
640
+ const content = result.content as { type: string; text: string }[]
641
+ expect(content[0]).toHaveProperty('text')
642
+ expect(content[0]?.text?.length).toBeGreaterThan(0)
643
+ } finally {
644
+ await dedicatedClient.close()
645
+ }
646
+ },
647
+ { retry: 2, timeout: 120_000 },
648
+ )
649
+ })