Skip to content

Commit a102800

Browse files
committed
feat: add MCP tools demonstrating configurable fetch pattern
- Add fetch_example tool for demonstrating different backends and caching - Add configure_fetch tool for runtime configuration management - Include comprehensive input validation with Zod schemas - Support per-request backend overrides, cache bypass, and custom headers - Add 17 tests covering tool functionality and error handling - Provide clear Markdown output with fetch details and configuration status
1 parent 47b34f7 commit a102800

File tree

2 files changed

+517
-0
lines changed

2 files changed

+517
-0
lines changed

src/tools/fetch-example.test.ts

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
/**
2+
* @fileoverview Tests for the fetch example tools
3+
* @module tools/fetch-example.test
4+
*/
5+
6+
import { beforeEach, describe, expect, it, vi } from 'vitest';
7+
import { FetchBackend, configurableFetch } from '../utils/fetch.js';
8+
import {
9+
ConfigureFetchSchema,
10+
FetchExampleSchema,
11+
configureFetchTool,
12+
fetchExampleTool,
13+
} from './fetch-example.js';
14+
15+
// Mock the fetch utility
16+
vi.mock('../utils/fetch.js', () => ({
17+
FetchBackend: {
18+
BUILT_IN: 'built-in',
19+
CACHE_MEMORY: 'cache-memory',
20+
CACHE_DISK: 'cache-disk',
21+
},
22+
configurableFetch: {
23+
fetch: vi.fn(),
24+
getConfig: vi.fn(),
25+
updateConfig: vi.fn(),
26+
clearCaches: vi.fn(),
27+
getCacheStats: vi.fn(),
28+
},
29+
}));
30+
31+
describe('FetchExampleSchema', () => {
32+
it('should validate valid fetch example parameters', () => {
33+
const validParams = {
34+
url: 'https://httpbin.org/json',
35+
backend: FetchBackend.CACHE_MEMORY,
36+
no_cache: false,
37+
user_agent: 'Test-Agent/1.0',
38+
};
39+
40+
const result = FetchExampleSchema.parse(validParams);
41+
expect(result).toEqual(validParams);
42+
});
43+
44+
it('should apply default values', () => {
45+
const minimalParams = { url: 'https://example.com' };
46+
const result = FetchExampleSchema.parse(minimalParams);
47+
48+
expect(result.no_cache).toBe(false);
49+
expect(result.url).toBe('https://example.com');
50+
});
51+
52+
it('should reject invalid URL', () => {
53+
expect(() =>
54+
FetchExampleSchema.parse({
55+
url: 'not-a-url',
56+
}),
57+
).toThrow();
58+
});
59+
60+
it('should reject invalid backend', () => {
61+
expect(() =>
62+
FetchExampleSchema.parse({
63+
url: 'https://example.com',
64+
backend: 'invalid-backend',
65+
}),
66+
).toThrow();
67+
});
68+
});
69+
70+
describe('ConfigureFetchSchema', () => {
71+
it('should validate valid configuration parameters', () => {
72+
const validParams = {
73+
backend: FetchBackend.CACHE_DISK,
74+
cache_ttl: 30000,
75+
cache_dir: '/tmp/cache',
76+
user_agent: 'Custom-Agent/1.0',
77+
clear_cache: true,
78+
};
79+
80+
const result = ConfigureFetchSchema.parse(validParams);
81+
expect(result).toEqual(validParams);
82+
});
83+
84+
it('should apply default values', () => {
85+
const result = ConfigureFetchSchema.parse({});
86+
expect(result.clear_cache).toBe(false);
87+
});
88+
89+
it('should reject negative cache_ttl', () => {
90+
expect(() =>
91+
ConfigureFetchSchema.parse({
92+
cache_ttl: -1000,
93+
}),
94+
).toThrow();
95+
});
96+
});
97+
98+
describe('fetchExampleTool', () => {
99+
beforeEach(() => {
100+
vi.clearAllMocks();
101+
102+
// Mock default config
103+
vi.mocked(configurableFetch.getConfig).mockReturnValue({
104+
backend: FetchBackend.BUILT_IN,
105+
cacheTtl: 300000,
106+
cacheDir: '.cache',
107+
maxCacheSize: 104857600,
108+
userAgent: undefined,
109+
defaultHeaders: {},
110+
});
111+
});
112+
113+
it('should fetch and display JSON data successfully', async () => {
114+
const mockData = { message: 'Hello, World!' };
115+
const mockResponse = {
116+
status: 200,
117+
statusText: 'OK',
118+
headers: {
119+
get: vi.fn().mockReturnValue('application/json'),
120+
},
121+
text: vi.fn().mockResolvedValue(JSON.stringify(mockData)),
122+
};
123+
124+
vi.mocked(configurableFetch.fetch).mockResolvedValue(mockResponse as any);
125+
126+
const result = await fetchExampleTool({
127+
url: 'https://httpbin.org/json',
128+
backend: FetchBackend.CACHE_MEMORY,
129+
});
130+
131+
expect(configurableFetch.fetch).toHaveBeenCalledWith('https://httpbin.org/json', {
132+
backend: FetchBackend.CACHE_MEMORY,
133+
noCache: false,
134+
});
135+
136+
expect(result.content[0].text).toContain('Fetch Example Results');
137+
expect(result.content[0].text).toContain('https://httpbin.org/json');
138+
expect(result.content[0].text).toContain('cache-memory');
139+
expect(result.content[0].text).toContain('200 OK');
140+
expect(result.content[0].text).toContain(JSON.stringify(mockData, null, 2));
141+
});
142+
143+
it('should handle text data', async () => {
144+
const mockText = 'Hello, World!';
145+
const mockResponse = {
146+
status: 200,
147+
statusText: 'OK',
148+
headers: {
149+
get: vi.fn().mockReturnValue('text/plain'),
150+
},
151+
text: vi.fn().mockResolvedValue(mockText),
152+
};
153+
154+
vi.mocked(configurableFetch.fetch)
155+
.mockResolvedValueOnce(mockResponse as any)
156+
.mockResolvedValueOnce(mockResponse as any);
157+
158+
const result = await fetchExampleTool({
159+
url: 'https://example.com',
160+
});
161+
162+
expect(result.content[0].text).toContain('**Parsed As**: text');
163+
expect(result.content[0].text).toContain(mockText);
164+
});
165+
166+
it('should handle custom headers', async () => {
167+
const mockResponse = {
168+
status: 200,
169+
statusText: 'OK',
170+
headers: {
171+
get: vi.fn().mockReturnValue('text/plain'),
172+
},
173+
text: vi.fn().mockResolvedValue('test'),
174+
};
175+
176+
vi.mocked(configurableFetch.fetch).mockResolvedValue(mockResponse as any);
177+
178+
await fetchExampleTool({
179+
url: 'https://example.com',
180+
user_agent: 'Custom-Agent/1.0',
181+
no_cache: true,
182+
});
183+
184+
expect(configurableFetch.fetch).toHaveBeenCalledWith('https://example.com', {
185+
backend: undefined,
186+
noCache: true,
187+
headers: {
188+
'User-Agent': 'Custom-Agent/1.0',
189+
},
190+
});
191+
});
192+
193+
it('should handle fetch errors', async () => {
194+
vi.mocked(configurableFetch.fetch).mockRejectedValue(new Error('Network error'));
195+
196+
const result = await fetchExampleTool({
197+
url: 'https://example.com',
198+
});
199+
200+
expect(result.isError).toBe(true);
201+
expect(result.content[0].text).toContain('Fetch Example Error');
202+
expect(result.content[0].text).toContain('Network error');
203+
});
204+
205+
it('should handle validation errors', async () => {
206+
const result = await fetchExampleTool({
207+
url: 'invalid-url',
208+
});
209+
210+
expect(result.isError).toBe(true);
211+
expect(result.content[0].text).toContain('Fetch Example Error');
212+
});
213+
});
214+
215+
describe('configureFetchTool', () => {
216+
beforeEach(() => {
217+
vi.clearAllMocks();
218+
219+
// Mock config and stats
220+
vi.mocked(configurableFetch.getConfig).mockReturnValue({
221+
backend: FetchBackend.CACHE_MEMORY,
222+
cacheTtl: 60000,
223+
cacheDir: '.cache',
224+
maxCacheSize: 104857600,
225+
userAgent: 'Test-Agent/1.0',
226+
defaultHeaders: {},
227+
});
228+
229+
vi.mocked(configurableFetch.getCacheStats).mockReturnValue({
230+
memory: { enabled: true },
231+
disk: { enabled: true },
232+
});
233+
});
234+
235+
it('should update configuration successfully', async () => {
236+
const result = await configureFetchTool({
237+
backend: FetchBackend.CACHE_DISK,
238+
cache_ttl: 120000,
239+
user_agent: 'Updated-Agent/2.0',
240+
});
241+
242+
expect(configurableFetch.updateConfig).toHaveBeenCalledWith({
243+
backend: FetchBackend.CACHE_DISK,
244+
cacheTtl: 120000,
245+
userAgent: 'Updated-Agent/2.0',
246+
});
247+
248+
expect(result.content[0].text).toContain('Fetch Configuration Updated');
249+
expect(result.content[0].text).toContain('cache-memory');
250+
expect(result.content[0].text).toContain('Test-Agent/1.0');
251+
});
252+
253+
it('should clear caches when requested', async () => {
254+
const result = await configureFetchTool({
255+
clear_cache: true,
256+
});
257+
258+
expect(configurableFetch.clearCaches).toHaveBeenCalled();
259+
expect(result.content[0].text).toContain('Caches cleared');
260+
});
261+
262+
it('should handle empty configuration', async () => {
263+
const result = await configureFetchTool({});
264+
265+
// Should not call updateConfig with empty object
266+
expect(configurableFetch.updateConfig).not.toHaveBeenCalled();
267+
expect(result.content[0].text).toContain('Current Configuration');
268+
});
269+
270+
it('should handle configuration errors', async () => {
271+
vi.mocked(configurableFetch.updateConfig).mockImplementation(() => {
272+
throw new Error('Configuration error');
273+
});
274+
275+
const result = await configureFetchTool({
276+
backend: FetchBackend.CACHE_DISK,
277+
});
278+
279+
expect(result.isError).toBe(true);
280+
expect(result.content[0].text).toContain('Configuration Error');
281+
expect(result.content[0].text).toContain('Configuration error');
282+
});
283+
284+
it('should handle validation errors', async () => {
285+
const result = await configureFetchTool({
286+
cache_ttl: -1000, // Invalid negative value
287+
});
288+
289+
expect(result.isError).toBe(true);
290+
expect(result.content[0].text).toContain('Configuration Error');
291+
});
292+
});

0 commit comments

Comments
 (0)