@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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdotcom-oss/mcp",
3
- "version": "1.3.4",
3
+ "version": "1.3.5-next.12",
4
4
  "description": "You.com API Model Context Protocol Server",
5
5
  "license": "MIT",
6
6
  "engines": {
@@ -9,12 +9,13 @@
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "git+https://github.com/youdotcom-oss/youdotcom-mcp-server.git"
12
+ "url": "git+https://github.com/youdotcom-oss/dx-toolkit.git",
13
+ "directory": "packages/mcp"
13
14
  },
14
15
  "bugs": {
15
- "url": "https://github.com/youdotcom-oss/youdotcom-mcp-server/issues"
16
+ "url": "https://github.com/youdotcom-oss/dx-toolkit/issues"
16
17
  },
17
- "homepage": "https://github.com/youdotcom-oss/youdotcom-mcp-server/tree/main#readme",
18
+ "homepage": "https://github.com/youdotcom-oss/dx-toolkit/tree/main/packages/mcp#readme",
18
19
  "author": "You.com (https://you.com)",
19
20
  "keywords": [
20
21
  "mcp",
@@ -23,19 +24,17 @@
23
24
  ],
24
25
  "bin": "bin/stdio.js",
25
26
  "type": "module",
26
- "main": "./src/main.ts",
27
+ "main": "./src/utils.ts",
27
28
  "exports": {
28
- ".": "./src/main.ts"
29
+ ".": "./src/utils.ts",
30
+ "./http": "./src/http.ts",
31
+ "./stdio": "./src/stdio.ts"
29
32
  },
30
33
  "files": [
31
34
  "bin/stdio.js",
32
- "src/**/*.schemas.ts",
33
- "src/**/*.utils.ts",
34
- "src/shared/check-response-for-errors.ts",
35
- "src/shared/format-search-results-text.ts",
36
- "AGENTS.md",
37
- "CONTRIBUTING.md",
38
- "docs/API.md"
35
+ "./src/**",
36
+ "!./src/**/tests/*",
37
+ "!./src/**/*.spec.@(tsx|ts)"
39
38
  ],
40
39
  "publishConfig": {
41
40
  "access": "public"
@@ -54,41 +53,19 @@
54
53
  "inspect": "bash -c 'source .env 2>/dev/null || true; bunx @modelcontextprotocol/inspector -e YDC_API_KEY=$YDC_API_KEY bun dev'",
55
54
  "lint": "biome lint",
56
55
  "lint:fix": "biome lint --write",
57
- "prepare": "git config core.hooksPath .hooks",
58
56
  "start": "bun run bin/http",
59
57
  "test": "bun test",
60
58
  "test:coverage": "bun test --coverage",
61
59
  "test:coverage:watch": "bun test --coverage --watch",
62
60
  "test:watch": "bun test --watch"
63
61
  },
64
- "lint-staged": {
65
- "*.{ts,tsx}": [
66
- "biome check --write --no-errors-on-unmatched"
67
- ],
68
- "*.{json,md}": [
69
- "biome format --write --no-errors-on-unmatched"
70
- ],
71
- "package.json": [
72
- "format-package --write"
73
- ]
74
- },
75
- "peerDependencies": {
76
- "zod": "^3.25.76"
62
+ "dependencies": {
63
+ "zod": "^4.1.13",
64
+ "@hono/mcp": "^0.2.0",
65
+ "@modelcontextprotocol/sdk": "^1.24.3",
66
+ "hono": "^4.10.7"
77
67
  },
78
68
  "devDependencies": {
79
- "@biomejs/biome": "2.3.8",
80
- "@commitlint/cli": "^20.1.0",
81
- "@commitlint/config-conventional": "^20.0.0",
82
- "@eslint/js": "9.39.1",
83
- "@modelcontextprotocol/inspector": "0.17.2",
84
- "@types/bun": "latest",
85
- "typescript": "5.9.3",
86
- "lint-staged": "^16.2.7",
87
- "format-package": "^7.0.0",
88
- "@hono/bun-compress": "0.1.0",
89
- "@hono/mcp": "0.2.0",
90
- "@modelcontextprotocol/sdk": "1.24.2",
91
- "hono": "^4.10.7",
92
- "zod": "4.1.13"
69
+ "@modelcontextprotocol/inspector": "0.17.5"
93
70
  }
94
71
  }
@@ -0,0 +1,90 @@
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 { ContentsQuerySchema, ContentsStructuredContentSchema } from './contents.schemas.ts';
5
+ import { fetchContents, formatContentsResponse } from './contents.utils.ts';
6
+
7
+ /**
8
+ * Register the you-contents tool with the MCP server
9
+ * Extracts and returns full content from multiple URLs in markdown or HTML format
10
+ */
11
+ export const registerContentsTool = ({
12
+ mcp,
13
+ YDC_API_KEY,
14
+ getUserAgent,
15
+ }: {
16
+ mcp: McpServer;
17
+ YDC_API_KEY?: string;
18
+ getUserAgent: () => string;
19
+ }) => {
20
+ // Register the tool
21
+ mcp.registerTool(
22
+ 'you-contents',
23
+ {
24
+ title: 'Extract Web Page Contents',
25
+ description: 'Extract page content in markdown or HTML',
26
+ inputSchema: ContentsQuerySchema.shape,
27
+ outputSchema: ContentsStructuredContentSchema.shape,
28
+ },
29
+ async (toolInput) => {
30
+ const logger = getLogger(mcp);
31
+
32
+ try {
33
+ // Validate and parse input
34
+ const contentsQuery = ContentsQuerySchema.parse(toolInput);
35
+ const { urls, format = 'markdown' } = contentsQuery;
36
+
37
+ // Log the request
38
+ await logger({
39
+ level: 'info',
40
+ data: `Contents API call initiated for ${urls.length} URL(s) with format: ${format}`,
41
+ });
42
+
43
+ // Fetch contents from API
44
+ const response = await fetchContents({
45
+ contentsQuery,
46
+ YDC_API_KEY,
47
+ getUserAgent,
48
+ });
49
+
50
+ // Format response with full content
51
+ const { content, structuredContent } = formatContentsResponse(response, format);
52
+
53
+ // Log success
54
+ await logger({
55
+ level: 'info',
56
+ data: `Contents API call successful: extracted ${response.length} page(s)`,
57
+ });
58
+
59
+ return {
60
+ content,
61
+ structuredContent,
62
+ };
63
+ } catch (err: unknown) {
64
+ // Handle and log errors
65
+ const errorMessage = err instanceof Error ? err.message : String(err);
66
+ const reportLink = generateErrorReportLink({
67
+ errorMessage,
68
+ tool: 'you-contents',
69
+ clientInfo: getUserAgent(),
70
+ });
71
+
72
+ await logger({
73
+ level: 'error',
74
+ data: `Contents API call failed: ${errorMessage}\n\nReport this issue: ${reportLink}`,
75
+ });
76
+
77
+ return {
78
+ content: [
79
+ {
80
+ type: 'text' as const,
81
+ text: `Error extracting contents: ${errorMessage}`,
82
+ },
83
+ ],
84
+ structuredContent: undefined,
85
+ isError: true,
86
+ };
87
+ }
88
+ },
89
+ );
90
+ };
@@ -0,0 +1,188 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import type { ContentsApiResponse } from '../contents.schemas.ts';
3
+ import { fetchContents, formatContentsResponse } from '../contents.utils.ts';
4
+
5
+ const getUserAgent = () => 'MCP/test (You.com; test-client)';
6
+
7
+ // NOTE: The following tests require a You.com API key with access to the Contents API
8
+ // Using example.com/example.org as test URLs since You.com blocks self-scraping
9
+ describe('fetchContents', () => {
10
+ test('returns valid response structure for single URL', async () => {
11
+ const result = await fetchContents({
12
+ contentsQuery: {
13
+ urls: ['https://documentation.you.com/developer-resources/mcp-server'],
14
+ format: 'markdown',
15
+ },
16
+ getUserAgent,
17
+ });
18
+
19
+ expect(Array.isArray(result)).toBe(true);
20
+ expect(result.length).toBeGreaterThan(0);
21
+
22
+ const firstItem = result[0];
23
+ expect(firstItem).toBeDefined();
24
+
25
+ // Should have markdown content
26
+ expect(firstItem?.markdown).toBeDefined();
27
+ expect(typeof firstItem?.markdown).toBe('string');
28
+ });
29
+
30
+ test('handles multiple URLs', async () => {
31
+ const result = await fetchContents({
32
+ contentsQuery: {
33
+ urls: [
34
+ 'https://documentation.you.com/developer-resources/mcp-server',
35
+ 'https://documentation.you.com/developer-resources/python-sdk',
36
+ ],
37
+ format: 'markdown',
38
+ },
39
+ getUserAgent,
40
+ });
41
+
42
+ expect(Array.isArray(result)).toBe(true);
43
+ expect(result.length).toBe(2);
44
+
45
+ for (const item of result) {
46
+ expect(item).toHaveProperty('url');
47
+ expect(item.markdown).toBeDefined();
48
+ }
49
+ });
50
+
51
+ test('handles html format', async () => {
52
+ const result = await fetchContents({
53
+ contentsQuery: {
54
+ urls: ['https://documentation.you.com/developer-resources/mcp-server'],
55
+ format: 'html',
56
+ },
57
+ getUserAgent,
58
+ });
59
+
60
+ expect(Array.isArray(result)).toBe(true);
61
+ const firstItem = result[0];
62
+ expect(firstItem).toBeDefined();
63
+
64
+ expect(firstItem?.html).toBeDefined();
65
+ expect(typeof firstItem?.html).toBe('string');
66
+ });
67
+ });
68
+
69
+ describe('formatContentsResponse', () => {
70
+ test('formats single markdown content correctly', () => {
71
+ const mockResponse: ContentsApiResponse = [
72
+ {
73
+ url: 'https://example.com',
74
+ title: 'Example Page',
75
+ markdown: '# Hello\n\nThis is a test page with some content.',
76
+ },
77
+ ];
78
+
79
+ const result = formatContentsResponse(mockResponse, 'markdown');
80
+
81
+ expect(result).toHaveProperty('content');
82
+ expect(result).toHaveProperty('structuredContent');
83
+ expect(Array.isArray(result.content)).toBe(true);
84
+ expect(result.content[0]).toHaveProperty('type', 'text');
85
+ expect(result.content[0]).toHaveProperty('text');
86
+
87
+ const text = result.content[0]?.text;
88
+ expect(text).toContain('Example Page');
89
+ expect(text).toContain('https://example.com');
90
+ expect(text).toContain('Format: markdown');
91
+ expect(text).toContain('# Hello');
92
+ expect(text).toContain('This is a test page with some content.');
93
+
94
+ expect(result.structuredContent).toHaveProperty('count', 1);
95
+ expect(result.structuredContent).toHaveProperty('format', 'markdown');
96
+ expect(result.structuredContent.items).toHaveLength(1);
97
+
98
+ const item = result.structuredContent.items[0];
99
+ expect(item).toBeDefined();
100
+
101
+ expect(item).toHaveProperty('url', 'https://example.com');
102
+ expect(item).toHaveProperty('title', 'Example Page');
103
+ expect(item).toHaveProperty('content', '# Hello\n\nThis is a test page with some content.');
104
+ expect(item?.contentLength).toBe('# Hello\n\nThis is a test page with some content.'.length);
105
+ });
106
+
107
+ test('formats multiple items correctly', () => {
108
+ const mockResponse: ContentsApiResponse = [
109
+ {
110
+ url: 'https://example1.com',
111
+ title: 'Page 1',
112
+ markdown: 'Content 1',
113
+ },
114
+ {
115
+ url: 'https://example2.com',
116
+ title: 'Page 2',
117
+ markdown: 'Content 2',
118
+ },
119
+ ];
120
+
121
+ const result = formatContentsResponse(mockResponse, 'markdown');
122
+
123
+ expect(result.structuredContent.count).toBe(2);
124
+ expect(result.structuredContent.items).toHaveLength(2);
125
+
126
+ const text = result.content[0]?.text;
127
+ expect(text).toContain('Page 1');
128
+ expect(text).toContain('Page 2');
129
+ expect(text).toContain('https://example1.com');
130
+ expect(text).toContain('https://example2.com');
131
+ });
132
+
133
+ test('handles html format', () => {
134
+ const mockResponse: ContentsApiResponse = [
135
+ {
136
+ url: 'https://example.com',
137
+ title: 'HTML Page',
138
+ html: '<html><body><h1>Hello</h1></body></html>',
139
+ },
140
+ ];
141
+
142
+ const result = formatContentsResponse(mockResponse, 'html');
143
+
144
+ expect(result.structuredContent.format).toBe('html');
145
+ const text = result.content[0]?.text;
146
+ expect(text).toContain('Format: html');
147
+ expect(text).toContain('<html>');
148
+ });
149
+
150
+ test('includes full content for long text', () => {
151
+ const longContent = 'a'.repeat(1000);
152
+ const mockResponse: ContentsApiResponse = [
153
+ {
154
+ url: 'https://example.com',
155
+ title: 'Long Page',
156
+ markdown: longContent,
157
+ },
158
+ ];
159
+
160
+ const result = formatContentsResponse(mockResponse, 'markdown');
161
+
162
+ const text = result.content[0]?.text;
163
+ // Full content should be included (not truncated)
164
+ expect(text).toContain(longContent);
165
+
166
+ // Structured content should have full content and correct length
167
+ const item = result.structuredContent.items[0];
168
+ expect(item?.content).toBe(longContent);
169
+ expect(item?.contentLength).toBe(1000);
170
+ });
171
+
172
+ test('handles empty content gracefully', () => {
173
+ const mockResponse: ContentsApiResponse = [
174
+ {
175
+ url: 'https://example.com',
176
+ title: 'Empty Page',
177
+ markdown: '',
178
+ },
179
+ ];
180
+
181
+ const result = formatContentsResponse(mockResponse, 'markdown');
182
+
183
+ expect(result.structuredContent.items[0]?.contentLength).toBe(0);
184
+ const text = result.content[0]?.text;
185
+ expect(text).toContain('Empty Page');
186
+ expect(text).toContain('Content Length: 0 characters');
187
+ });
188
+ });
@@ -0,0 +1,67 @@
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 { ExpressAgentInputSchema, ExpressStructuredContentSchema } from './express.schemas.ts';
5
+ import { callExpressAgent, formatExpressAgentResponse } from './express.utils.ts';
6
+
7
+ export const registerExpressTool = ({
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-express',
18
+ {
19
+ title: 'Express Agent',
20
+ description: 'Fast AI answers with web search',
21
+ inputSchema: ExpressAgentInputSchema.shape,
22
+ outputSchema: ExpressStructuredContentSchema.shape,
23
+ },
24
+ async (agentInput) => {
25
+ const logger = getLogger(mcp);
26
+
27
+ try {
28
+ const response = await callExpressAgent({
29
+ agentInput,
30
+ YDC_API_KEY,
31
+ getUserAgent,
32
+ });
33
+
34
+ await logger({
35
+ level: 'info',
36
+ data: `Express agent call successful for input: "${agentInput.input}"`,
37
+ });
38
+
39
+ const { content, structuredContent } = formatExpressAgentResponse(response);
40
+ return { content, structuredContent };
41
+ } catch (err: unknown) {
42
+ const errorMessage = err instanceof Error ? err.message : String(err);
43
+ const reportLink = generateErrorReportLink({
44
+ errorMessage,
45
+ tool: 'you-express',
46
+ clientInfo: getUserAgent(),
47
+ });
48
+
49
+ await logger({
50
+ level: 'error',
51
+ data: `Express agent call failed: ${errorMessage}\n\nReport this issue: ${reportLink}`,
52
+ });
53
+
54
+ return {
55
+ content: [
56
+ {
57
+ type: 'text' as const,
58
+ text: `Error: ${errorMessage}`,
59
+ },
60
+ ],
61
+ structuredContent: undefined,
62
+ isError: true,
63
+ };
64
+ }
65
+ },
66
+ );
67
+ };
@@ -0,0 +1,244 @@
1
+ import { describe, expect, setDefaultTimeout, test } from 'bun:test';
2
+ import type { ExpressAgentMcpResponse } from '../express.schemas.ts';
3
+ import { callExpressAgent, formatExpressAgentResponse } from '../express.utils.ts';
4
+
5
+ const getUserAgent = () => 'MCP/test (You.com; test-client)';
6
+
7
+ setDefaultTimeout(20_000);
8
+
9
+ describe('callExpressAgent', () => {
10
+ test('returns answer only (WITHOUT web_search tools)', async () => {
11
+ const result = await callExpressAgent({
12
+ agentInput: { input: 'What is machine learning?' },
13
+ getUserAgent,
14
+ });
15
+
16
+ // Verify MCP response structure
17
+ expect(result).toHaveProperty('answer');
18
+ expect(typeof result.answer).toBe('string');
19
+ expect(result.answer.length).toBeGreaterThan(0);
20
+
21
+ // Should NOT have results when web_search is not used
22
+ expect(result.results).toBeUndefined();
23
+
24
+ expect(result.agent).toBe('express');
25
+ });
26
+
27
+ test('returns answer and search results (WITH web_search tools)', async () => {
28
+ const result = await callExpressAgent({
29
+ agentInput: {
30
+ input: 'Latest developments in quantum computing',
31
+ tools: [{ type: 'web_search' }],
32
+ },
33
+ getUserAgent,
34
+ });
35
+
36
+ // Verify MCP response has both answer and results
37
+ expect(result).toHaveProperty('answer');
38
+ expect(typeof result.answer).toBe('string');
39
+ expect(result.answer.length).toBeGreaterThan(0);
40
+
41
+ expect(result).toHaveProperty('results');
42
+ expect(result.results).toHaveProperty('web');
43
+ expect(Array.isArray(result.results?.web)).toBe(true);
44
+ expect(result.results?.web.length).toBeGreaterThan(0);
45
+
46
+ // Verify each search result has required fields
47
+ const firstResult = result.results?.web[0];
48
+ expect(firstResult).toHaveProperty('url');
49
+ expect(firstResult).toHaveProperty('title');
50
+ expect(firstResult).toHaveProperty('snippet');
51
+ expect(typeof firstResult?.url).toBe('string');
52
+ expect(typeof firstResult?.title).toBe('string');
53
+ expect(typeof firstResult?.snippet).toBe('string');
54
+ expect(firstResult?.url.length).toBeGreaterThan(0);
55
+ expect(firstResult?.title.length).toBeGreaterThan(0);
56
+
57
+ expect(result.agent).toBe('express');
58
+ }, 30000);
59
+
60
+ test('works without optional parameters', async () => {
61
+ const result = await callExpressAgent({
62
+ agentInput: { input: 'What is the capital of France?' },
63
+ getUserAgent,
64
+ // No progressToken or sendProgress provided
65
+ });
66
+
67
+ // Should work normally without progress tracking
68
+ expect(result).toHaveProperty('answer');
69
+ expect(result.answer.length).toBeGreaterThan(0);
70
+ expect(result.agent).toBe('express');
71
+ });
72
+ });
73
+
74
+ describe('formatExpressAgentResponse', () => {
75
+ test('formats response with answer only (no search results)', () => {
76
+ const mockResponse: ExpressAgentMcpResponse = {
77
+ answer: 'The capital of France is Paris.',
78
+ agent: 'express',
79
+ };
80
+
81
+ const result = formatExpressAgentResponse(mockResponse);
82
+
83
+ // Verify content array has 1 item (answer only)
84
+ expect(result).toHaveProperty('content');
85
+ expect(Array.isArray(result.content)).toBe(true);
86
+ expect(result.content.length).toBe(1);
87
+
88
+ // Verify answer content
89
+ expect(result.content[0]).toHaveProperty('type', 'text');
90
+ expect(result.content[0]).toHaveProperty('text');
91
+ expect(result.content[0]?.text).toContain('Express Agent Answer');
92
+ expect(result.content[0]?.text).toContain('The capital of France is Paris.');
93
+
94
+ // Verify structuredContent is minimal (not full response)
95
+ expect(result).toHaveProperty('structuredContent');
96
+ expect(result).toHaveProperty('fullResponse');
97
+ expect(result.structuredContent).toHaveProperty('answer');
98
+ expect(result.structuredContent).toHaveProperty('hasResults');
99
+ expect(result.structuredContent).toHaveProperty('resultCount');
100
+ expect(result.structuredContent).toHaveProperty('agent');
101
+ expect(result.structuredContent.answer).toBe(mockResponse.answer);
102
+ expect(result.structuredContent.hasResults).toBe(false);
103
+ expect(result.structuredContent.resultCount).toBe(0);
104
+ // No results, so results field should be undefined
105
+ expect(result.structuredContent.results).toBeUndefined();
106
+ expect(result.fullResponse).toEqual(mockResponse);
107
+ });
108
+
109
+ test('formats response with answer and search results', () => {
110
+ const mockResponse: ExpressAgentMcpResponse = {
111
+ answer: 'Quantum computing is advancing rapidly with recent breakthroughs in error correction.',
112
+ results: {
113
+ web: [
114
+ {
115
+ url: 'https://example.com/quantum1',
116
+ title: 'Quantum Computing Breakthrough',
117
+ snippet: 'Scientists achieve quantum error correction milestone.',
118
+ },
119
+ {
120
+ url: 'https://example.com/quantum2',
121
+ title: 'Latest in Quantum Research',
122
+ snippet: 'New quantum processor demonstrates superiority.',
123
+ },
124
+ ],
125
+ },
126
+ agent: 'express',
127
+ };
128
+
129
+ const result = formatExpressAgentResponse(mockResponse);
130
+
131
+ // Verify content array has 2 items (answer + search results)
132
+ expect(result.content.length).toBe(2);
133
+
134
+ // Verify answer comes FIRST
135
+ expect(result.content[0]?.type).toBe('text');
136
+ expect(result.content[0]?.text).toContain('Express Agent Answer');
137
+ expect(result.content[0]?.text).toContain('Quantum computing is advancing rapidly');
138
+
139
+ // Verify search results come SECOND (without URLs in text)
140
+ expect(result.content[1]?.type).toBe('text');
141
+ expect(result.content[1]?.text).toContain('Search Results');
142
+ expect(result.content[1]?.text).toContain('Quantum Computing Breakthrough');
143
+ expect(result.content[1]?.text).toContain('Latest in Quantum Research');
144
+ // URLs should NOT be in text content
145
+ expect(result.content[1]?.text).not.toContain('https://example.com/quantum1');
146
+ expect(result.content[1]?.text).not.toContain('https://example.com/quantum2');
147
+
148
+ // Verify structuredContent is minimal with counts
149
+ expect(result.structuredContent).toHaveProperty('answer');
150
+ expect(result.structuredContent).toHaveProperty('hasResults');
151
+ expect(result.structuredContent).toHaveProperty('resultCount');
152
+ expect(result.structuredContent.answer).toBe(mockResponse.answer);
153
+ expect(result.structuredContent.hasResults).toBe(true);
154
+ expect(result.structuredContent.resultCount).toBe(2);
155
+
156
+ // URLs should be in structuredContent.results
157
+ expect(result.structuredContent).toHaveProperty('results');
158
+ expect(result.structuredContent.results?.web).toBeDefined();
159
+ expect(result.structuredContent.results?.web?.length).toBe(2);
160
+ expect(result.structuredContent.results?.web?.[0]).toEqual({
161
+ url: 'https://example.com/quantum1',
162
+ title: 'Quantum Computing Breakthrough',
163
+ });
164
+ expect(result.structuredContent.results?.web?.[1]).toEqual({
165
+ url: 'https://example.com/quantum2',
166
+ title: 'Latest in Quantum Research',
167
+ });
168
+
169
+ // Verify fullResponse has complete data
170
+ expect(result.fullResponse).toEqual(mockResponse);
171
+ expect(result.fullResponse.results?.web).toHaveLength(2);
172
+ });
173
+
174
+ test('structuredContent validation for answer only', () => {
175
+ const mockResponse: ExpressAgentMcpResponse = {
176
+ answer: 'Neural networks are computational models inspired by biological neurons.',
177
+ agent: 'express',
178
+ };
179
+
180
+ const result = formatExpressAgentResponse(mockResponse);
181
+
182
+ // Verify structure matches minimal schema
183
+ expect(result.structuredContent).toMatchObject({
184
+ answer: expect.any(String),
185
+ hasResults: false,
186
+ resultCount: 0,
187
+ agent: 'express',
188
+ });
189
+
190
+ // Verify fullResponse has complete data
191
+ expect(result.fullResponse.results).toBeUndefined();
192
+ });
193
+
194
+ test('structuredContent validation for answer with results', () => {
195
+ const mockResponse: ExpressAgentMcpResponse = {
196
+ answer: 'Recent AI breakthroughs include advances in language models and computer vision.',
197
+ results: {
198
+ web: [
199
+ {
200
+ url: 'https://example.com/ai-breakthrough',
201
+ title: 'AI Breakthrough 2025',
202
+ snippet: 'Major advances in artificial intelligence.',
203
+ },
204
+ ],
205
+ },
206
+ agent: 'express',
207
+ };
208
+
209
+ const result = formatExpressAgentResponse(mockResponse);
210
+
211
+ // Verify structuredContent is minimal with counts
212
+ expect(result.structuredContent).toHaveProperty('answer');
213
+ expect(result.structuredContent).toHaveProperty('hasResults');
214
+ expect(result.structuredContent).toHaveProperty('resultCount');
215
+ expect(result.structuredContent).toHaveProperty('agent');
216
+ expect(result.structuredContent.answer).toBe(
217
+ 'Recent AI breakthroughs include advances in language models and computer vision.',
218
+ );
219
+ expect(result.structuredContent.agent).toBe('express');
220
+ expect(result.structuredContent.hasResults).toBe(true);
221
+ expect(result.structuredContent.resultCount).toBe(1);
222
+
223
+ // URLs should be in structuredContent.results
224
+ expect(result.structuredContent).toHaveProperty('results');
225
+ expect(result.structuredContent.results?.web).toBeDefined();
226
+ expect(result.structuredContent.results?.web?.length).toBe(1);
227
+ expect(result.structuredContent.results?.web?.[0]).toEqual({
228
+ url: 'https://example.com/ai-breakthrough',
229
+ title: 'AI Breakthrough 2025',
230
+ });
231
+
232
+ // Verify fullResponse has complete search results
233
+ expect(result.fullResponse).toEqual(mockResponse);
234
+ expect(result.fullResponse.results).toBeDefined();
235
+ expect(Array.isArray(result.fullResponse.results?.web)).toBe(true);
236
+ expect(result.fullResponse.results?.web.length).toBe(1);
237
+
238
+ // Verify search result fields in fullResponse
239
+ const searchResult = result.fullResponse.results?.web[0];
240
+ expect(searchResult?.url).toBe('https://example.com/ai-breakthrough');
241
+ expect(searchResult?.title).toBe('AI Breakthrough 2025');
242
+ expect(searchResult?.snippet).toBe('Major advances in artificial intelligence.');
243
+ });
244
+ });