Skip to content

Commit 782c5f1

Browse files
committed
test: add very basic test suite for fetch functionality
1 parent fc2b310 commit 782c5f1

File tree

9 files changed

+153
-30
lines changed

9 files changed

+153
-30
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
"lint:fix": "eslint src/** --fix",
1515
"format": "prettier --write \"**/*.{ts,json,md,yml,js}\"",
1616
"format:check": "prettier --check .",
17-
"test": "node --import tsx --test tests/**",
17+
"test": "node --import tsx --test tests/**/*.test.ts",
18+
"test:watch": "node --import tsx --test --watch tests/**/*.test.ts",
1819
"inspect": "pnpx @modelcontextprotocol/inspector",
1920
"fix": "pnpm format && pnpm lint:fix"
2021
},

src/fetch.prompt.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { PromptCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
2-
import { z } from 'zod';
1+
import { z, ZodTypeAny } from 'zod';
2+
import { config } from './config/config.js';
33
import { DEFAULT_USER_AGENT_MANUAL } from './constants.js';
44
import { cache } from './utils/lru-cache.js';
55
import { processURL } from './utils/process-url.js';
6-
import { config } from './config/config.js';
76

87
const name = 'fetch';
98

@@ -13,7 +12,10 @@ const parameters = {
1312
url: z.string().describe('URL to fetch.'),
1413
};
1514

16-
const execute: PromptCallback<typeof parameters> = async ({ url }) => {
15+
type Args = z.objectOutputType<typeof parameters, ZodTypeAny>;
16+
17+
// PromptCallback<typeof parameters>
18+
const execute = async ({ url }: Args) => {
1719
const userAgent = config['user-agent'] ?? DEFAULT_USER_AGENT_MANUAL;
1820

1921
const cacheKey = `${url}||${userAgent}||false`;
@@ -37,7 +39,7 @@ const execute: PromptCallback<typeof parameters> = async ({ url }) => {
3739
{
3840
role: 'user',
3941
content: { type: 'text', text: result },
40-
},
42+
} as const,
4143
],
4244
};
4345
};

src/fetch.tool.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js';
2-
import { z } from 'zod';
1+
import { z, ZodTypeAny } from 'zod';
32
import { config } from './config/config.js';
43
import { DEFAULT_USER_AGENT_AUTONOMOUS } from './constants.js';
54
import { checkRobotsTxt } from './utils/check-robots-txt.js';
@@ -37,9 +36,12 @@ const parameters = {
3736
),
3837
};
3938

40-
const execute: ToolCallback<typeof parameters> =
39+
type Args = z.objectOutputType<typeof parameters, ZodTypeAny>;
40+
41+
// ToolCallback<typeof parameters>
42+
const execute =
4143
// TODO: use signal to handle cancellation
42-
async ({ url, max_length, start_index, raw }) => {
44+
async ({ url, max_length, start_index, raw }: Args) => {
4345
const userAgent = config['user-agent'] ?? DEFAULT_USER_AGENT_AUTONOMOUS;
4446

4547
const cacheKey = `${url}||${userAgent}||${raw.toString()}`;
@@ -61,7 +63,7 @@ const execute: ToolCallback<typeof parameters> =
6163
const result = paginate(url, content, prefix, start_index, max_length);
6264

6365
return {
64-
content: [{ type: 'text', text: result }],
66+
content: [{ type: 'text' as const, text: result }],
6567
};
6668
};
6769

src/main.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@ import { config } from './config/config.js';
1010
import { fetchPrompt } from './fetch.prompt.js';
1111
import { fetchTool } from './fetch.tool.js';
1212

13-
const server = new McpServer({
13+
const mcp = new McpServer({
1414
name: 'mcp-fetch-node',
1515
version: '1.x.x',
1616
});
1717

18-
server.tool(
18+
mcp.tool(
1919
fetchTool.name,
2020
fetchTool.description,
2121
fetchTool.parameters,
2222
fetchTool.execute,
2323
);
2424

25-
server.prompt(
25+
mcp.prompt(
2626
fetchPrompt.name,
2727
fetchPrompt.description,
2828
fetchPrompt.parameters,
@@ -38,7 +38,7 @@ app.get('/sse', async (_req, res) => {
3838

3939
transports.set(transport.sessionId, transport);
4040

41-
await server.connect(transport);
41+
await mcp.connect(transport);
4242

4343
res.on('close', () => {
4444
transport.close().catch((err: unknown) => {
@@ -66,7 +66,7 @@ app.post('/messages', async (req, res) => {
6666
await transport.handlePostMessage(req, res);
6767
});
6868

69-
const expressServer = app.listen(config.port);
69+
const server = app.listen(config.port);
7070

7171
console.log(`Server is running on port ${config.port.toString()}`);
7272

@@ -76,8 +76,8 @@ await readline.question('Press enter to exit...\n');
7676

7777
readline.close();
7878

79-
expressServer.closeAllConnections();
79+
server.closeAllConnections();
8080

81-
await promisify(expressServer.close)();
81+
await promisify(server.close.bind(server))();
8282

83-
await server.close();
83+
await mcp.close();

src/utils/extract.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export function extract(html: string) {
4848
const { document } = parseHTML(result);
4949

5050
// Remove unwanted elements
51-
document.body
51+
document
5252
.querySelectorAll(
5353
[
5454
'[hidden]',
@@ -67,7 +67,7 @@ export function extract(html: string) {
6767
?.forEach((a: any) => a.remove());
6868

6969
// Remove nav-liked lists
70-
document.querySelectorAll('ul, table, section').forEach((node: any) => {
70+
document.querySelectorAll('ul, table').forEach((node: any) => {
7171
const list = node.cloneNode(true);
7272
list.querySelectorAll('a').forEach((a: any) => {
7373
a.innerHTML = '';
@@ -85,7 +85,7 @@ export function extract(html: string) {
8585
});
8686

8787
// Sanitize again
88-
result = sanitizeHtml(document.body.innerHTML as string, {
88+
result = sanitizeHtml(document.documentElement.innerHTML as string, {
8989
allowedAttributes: { a: ['href'] },
9090
});
9191

tests/fetch.prompt.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import assert from 'node:assert';
2+
import { describe, it } from 'node:test';
3+
import { fetchPrompt } from '../src/fetch.prompt.js';
4+
5+
describe('Fetch Prompt', () => {
6+
it('should fetch content via prompt', async () => {
7+
const result = await fetchPrompt.execute({
8+
url: 'https://example.com',
9+
});
10+
11+
assert.ok(result.messages);
12+
assert.equal(result.messages[0].role, 'user');
13+
assert.ok(
14+
(result.messages[0].content.text as string).includes('Example Domain'),
15+
);
16+
});
17+
18+
it('should handle invalid URLs in prompt', async () => {
19+
try {
20+
await fetchPrompt.execute({
21+
url: 'not-a-valid-url',
22+
});
23+
assert.fail('Should have thrown an error');
24+
} catch (error) {
25+
assert.ok(error instanceof Error);
26+
}
27+
});
28+
});

tests/fetch.tool.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import assert from 'node:assert';
2+
import { describe, it } from 'node:test';
3+
import { fetchTool } from '../src/fetch.tool.js';
4+
5+
describe('Fetch Tool', () => {
6+
it('should fetch and return content from a valid URL', async () => {
7+
const result = await fetchTool.execute({
8+
url: 'https://example.com',
9+
max_length: 1000,
10+
start_index: 0,
11+
raw: false,
12+
});
13+
14+
assert.ok(result.content);
15+
assert.equal(result.content[0].type, 'text');
16+
assert.ok(result.content[0].text.includes('Example Domain'));
17+
});
18+
19+
it('should respect max_length parameter', async () => {
20+
const maxLength = 100;
21+
const result = await fetchTool.execute({
22+
url: 'https://example.com',
23+
max_length: maxLength,
24+
start_index: 0,
25+
raw: false,
26+
});
27+
28+
assert.ok(result.content[0].text.length <= maxLength + 200); // Adding buffer for prefix text
29+
});
30+
31+
it('should handle invalid URLs', async () => {
32+
try {
33+
await fetchTool.execute({
34+
url: 'not-a-valid-url',
35+
max_length: 1000,
36+
start_index: 0,
37+
raw: false,
38+
});
39+
assert.fail('Should have thrown an error');
40+
} catch (error) {
41+
assert.ok(error instanceof Error);
42+
}
43+
});
44+
});

tests/index.test.ts

Lines changed: 0 additions & 8 deletions
This file was deleted.

tests/utils.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import assert from 'node:assert';
2+
import { describe, it } from 'node:test';
3+
import { paginate } from '../src/utils/paginate.js';
4+
import { extract } from '../src/utils/extract.js';
5+
import { processURL } from '../src/utils/process-url.js';
6+
7+
describe('Utility Functions', () => {
8+
describe('paginate', () => {
9+
it('should correctly paginate content', () => {
10+
const content = 'Hello World!';
11+
const result = paginate('test-url', content, '', 0, 5);
12+
assert.ok(result.includes('Hello'));
13+
assert.ok(result.includes('truncated'));
14+
});
15+
16+
it('should handle start index beyond content length', () => {
17+
const content = 'Hello World!';
18+
const result = paginate('test-url', content, '', 20, 5);
19+
assert.ok(result.includes('No more content available'));
20+
});
21+
});
22+
23+
describe('extract', () => {
24+
it('should extract content from HTML', () => {
25+
const html = '<div><h1>Title</h1><p>Content</p><nav>Menu</nav></div>';
26+
const result = extract(html);
27+
assert.ok(result.includes('Title'));
28+
assert.ok(result.includes('Content'));
29+
assert.ok(!result.includes('Menu')); // nav should be removed
30+
});
31+
});
32+
33+
describe('processURL', () => {
34+
it('should process HTML content', async () => {
35+
const [content, prefix] = await processURL(
36+
'https://example.com',
37+
'test-user-agent',
38+
false,
39+
);
40+
assert.ok(content.length > 0);
41+
assert.equal(prefix, '');
42+
});
43+
44+
it('should handle raw content request', async () => {
45+
const [content, prefix] = await processURL(
46+
'https://example.com',
47+
'test-user-agent',
48+
true,
49+
);
50+
assert.ok(content.includes('<html'));
51+
assert.ok(prefix.includes('raw'));
52+
});
53+
});
54+
});

0 commit comments

Comments
 (0)