@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/README.md +2 -2
- package/bin/stdio.js +168 -191
- package/package.json +18 -41
- package/src/contents/register-contents-tool.ts +90 -0
- package/src/contents/tests/contents.utils.spec.ts +188 -0
- package/src/express/register-express-tool.ts +67 -0
- package/src/express/tests/express.utils.spec.ts +244 -0
- package/src/get-mcp-server.ts +17 -0
- package/src/http.ts +72 -0
- package/src/search/register-search-tool.ts +87 -0
- package/src/search/tests/search.utils.spec.ts +217 -0
- package/src/shared/generate-error-report-link.ts +37 -0
- package/src/shared/get-logger.ts +10 -0
- package/src/shared/tests/shared.utils.spec.ts +160 -0
- package/src/shared/use-client-version.ts +21 -0
- package/src/stdio.ts +24 -0
- package/src/tests/exports.spec.ts +24 -0
- package/src/tests/http.spec.ts +318 -0
- package/src/tests/tool.spec.ts +496 -0
- package/src/utils.ts +8 -0
- package/AGENTS.md +0 -730
- package/CONTRIBUTING.md +0 -246
- package/docs/API.md +0 -319
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@youdotcom-oss/mcp",
|
|
3
|
-
"version": "1.3.
|
|
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/
|
|
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/
|
|
16
|
+
"url": "https://github.com/youdotcom-oss/dx-toolkit/issues"
|
|
16
17
|
},
|
|
17
|
-
"homepage": "https://github.com/youdotcom-oss/
|
|
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/
|
|
27
|
+
"main": "./src/utils.ts",
|
|
27
28
|
"exports": {
|
|
28
|
-
".": "./src/
|
|
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
|
|
33
|
-
"src
|
|
34
|
-
"src
|
|
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
|
-
"
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
"
|
|
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
|
-
"@
|
|
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
|
+
});
|