Skip to content

Commit 9b972c0

Browse files
committed
feat: add Wayback Machine MCP server and CLI implementation
1 parent 2d7154e commit 9b972c0

File tree

4 files changed

+338
-618
lines changed

4 files changed

+338
-618
lines changed

src/cli.test.ts

Lines changed: 56 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest';
22
import { createCLI } from './cli.js';
3-
import * as exampleModule from './tools/example.js';
4-
import * as fetchExampleModule from './tools/fetch-example.js';
3+
import * as retrieveModule from './tools/retrieve.js';
4+
import * as saveModule from './tools/save.js';
5+
import * as searchModule from './tools/search.js';
6+
import * as statusModule from './tools/status.js';
57

6-
vi.mock('./tools/example.js');
7-
vi.mock('./tools/fetch-example.js');
8+
vi.mock('./tools/save.js');
9+
vi.mock('./tools/retrieve.js');
10+
vi.mock('./tools/search.js');
11+
vi.mock('./tools/status.js');
812

913
describe('CLI', () => {
1014
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
@@ -18,172 +22,83 @@ describe('CLI', () => {
1822

1923
it('should create CLI program', () => {
2024
const program = createCLI();
21-
expect(program.name()).toBe('mcp-template');
22-
expect(program.description()).toContain('MCP template');
25+
expect(program.name()).toBe('wayback');
26+
expect(program.description()).toContain('Wayback Machine');
2327
});
2428

25-
it('should handle example command', async () => {
26-
vi.spyOn(exampleModule, 'exampleTool').mockResolvedValue({
27-
content: [
28-
{
29-
type: 'text',
30-
text: 'Echo: Hello World',
31-
},
32-
],
29+
it('should handle save command', async () => {
30+
vi.spyOn(saveModule, 'saveUrl').mockResolvedValue({
31+
success: true,
32+
message: 'Saved',
33+
archivedUrl: 'https://web.archive.org/web/123/https://example.com',
34+
timestamp: '123',
3335
});
3436

3537
const program = createCLI();
36-
await program.parseAsync(['node', 'cli', 'example', 'Hello World']);
38+
await program.parseAsync(['node', 'cli', 'save', 'https://example.com']);
3739

38-
expect(exampleModule.exampleTool).toHaveBeenCalledWith({
39-
message: 'Hello World',
40-
uppercase: false,
41-
});
40+
expect(saveModule.saveUrl).toHaveBeenCalledWith({ url: 'https://example.com' });
4241
});
4342

44-
it('should handle example command with uppercase option', async () => {
45-
vi.spyOn(exampleModule, 'exampleTool').mockResolvedValue({
46-
content: [
47-
{
48-
type: 'text',
49-
text: 'Echo: HELLO WORLD',
50-
},
51-
],
43+
it('should handle get command', async () => {
44+
vi.spyOn(retrieveModule, 'getArchivedUrl').mockResolvedValue({
45+
success: true,
46+
message: 'Archive found',
47+
available: true,
48+
archivedUrl: 'https://web.archive.org/web/123/https://example.com',
49+
timestamp: '123',
5250
});
5351

5452
const program = createCLI();
55-
await program.parseAsync(['node', 'cli', 'example', 'Hello World', '--uppercase']);
53+
await program.parseAsync(['node', 'cli', 'get', 'https://example.com']);
5654

57-
expect(exampleModule.exampleTool).toHaveBeenCalledWith({
58-
message: 'Hello World',
59-
uppercase: true,
55+
expect(retrieveModule.getArchivedUrl).toHaveBeenCalledWith({
56+
url: 'https://example.com',
57+
timestamp: undefined,
6058
});
6159
});
6260

63-
it('should handle errors gracefully', async () => {
64-
const mockProcessExit = vi.spyOn(process, 'exit').mockImplementation(() => {
65-
throw new Error('Process exit called');
66-
});
67-
68-
vi.spyOn(exampleModule, 'exampleTool').mockRejectedValue(new Error('Test error'));
69-
70-
const program = createCLI();
71-
72-
try {
73-
await program.parseAsync(['node', 'cli', 'example', 'Hello World']);
74-
} catch (error) {
75-
// Expected to throw due to process.exit mock
76-
}
77-
78-
expect(mockProcessExit).toHaveBeenCalledWith(1);
79-
mockProcessExit.mockRestore();
80-
});
81-
82-
it('should handle fetch-example command', async () => {
83-
vi.spyOn(fetchExampleModule, 'fetchExampleTool').mockResolvedValue({
84-
content: [
61+
it('should handle search command', async () => {
62+
vi.spyOn(searchModule, 'searchArchives').mockResolvedValue({
63+
success: true,
64+
message: 'Found archives',
65+
results: [
8566
{
86-
type: 'text',
87-
text: '# Fetch Example Results\n\nURL: https://httpbin.org/json\nStatus: 200 OK',
67+
url: 'https://example.com',
68+
archivedUrl: 'https://web.archive.org/web/123/https://example.com',
69+
timestamp: '123',
70+
date: '2023-01-01',
71+
statusCode: '200',
72+
mimeType: 'text/html',
8873
},
8974
],
90-
isError: false,
75+
totalResults: 1,
9176
});
9277

9378
const program = createCLI();
94-
await program.parseAsync(['node', 'cli', 'fetch-example', 'https://httpbin.org/json']);
95-
96-
expect(fetchExampleModule.fetchExampleTool).toHaveBeenCalledWith({
97-
url: 'https://httpbin.org/json',
98-
});
99-
});
100-
101-
it('should handle fetch-example command with options', async () => {
102-
vi.spyOn(fetchExampleModule, 'fetchExampleTool').mockResolvedValue({
103-
content: [
104-
{
105-
type: 'text',
106-
text: '# Fetch Example Results\n\nURL: https://httpbin.org/json\nBackend: cache-memory',
107-
},
108-
],
109-
isError: false,
110-
});
79+
await program.parseAsync(['node', 'cli', 'search', 'https://example.com']);
11180

112-
const program = createCLI();
113-
await program.parseAsync([
114-
'node',
115-
'cli',
116-
'fetch-example',
117-
'https://httpbin.org/json',
118-
'--backend',
119-
'cache-memory',
120-
'--no-cache',
121-
'--user-agent',
122-
'Test-Agent/1.0',
123-
]);
124-
125-
expect(fetchExampleModule.fetchExampleTool).toHaveBeenCalledWith({
126-
url: 'https://httpbin.org/json',
127-
backend: 'cache-memory',
128-
no_cache: true,
129-
user_agent: 'Test-Agent/1.0',
81+
expect(searchModule.searchArchives).toHaveBeenCalledWith({
82+
url: 'https://example.com',
83+
limit: 10,
13084
});
13185
});
13286

133-
it('should handle configure-fetch command', async () => {
134-
vi.spyOn(fetchExampleModule, 'configureFetchTool').mockResolvedValue({
135-
content: [
136-
{
137-
type: 'text',
138-
text: '# Fetch Configuration Updated\n\nBackend: cache-disk\nCache TTL: 60000ms',
139-
},
140-
],
141-
isError: false,
87+
it('should handle status command', async () => {
88+
vi.spyOn(statusModule, 'checkArchiveStatus').mockResolvedValue({
89+
success: true,
90+
message: 'Status checked',
91+
isArchived: true,
92+
totalCaptures: 100,
93+
firstCapture: '2020-01-01',
94+
lastCapture: '2023-12-31',
14295
});
14396

14497
const program = createCLI();
145-
await program.parseAsync([
146-
'node',
147-
'cli',
148-
'configure-fetch',
149-
'--backend',
150-
'cache-disk',
151-
'--cache-ttl',
152-
'60000',
153-
'--clear-cache',
154-
]);
155-
156-
expect(fetchExampleModule.configureFetchTool).toHaveBeenCalledWith({
157-
backend: 'cache-disk',
158-
cache_ttl: 60000,
159-
clear_cache: true,
160-
});
161-
});
98+
await program.parseAsync(['node', 'cli', 'status', 'https://example.com']);
16299

163-
it('should handle fetch-example errors', async () => {
164-
const mockProcessExit = vi.spyOn(process, 'exit').mockImplementation(() => {
165-
throw new Error('Process exit called');
100+
expect(statusModule.checkArchiveStatus).toHaveBeenCalledWith({
101+
url: 'https://example.com',
166102
});
167-
168-
vi.spyOn(fetchExampleModule, 'fetchExampleTool').mockResolvedValue({
169-
content: [
170-
{
171-
type: 'text',
172-
text: 'Network error occurred',
173-
},
174-
],
175-
isError: true,
176-
});
177-
178-
const program = createCLI();
179-
180-
try {
181-
await program.parseAsync(['node', 'cli', 'fetch-example', 'https://invalid-url']);
182-
} catch (error) {
183-
// Expected to throw due to process.exit mock
184-
}
185-
186-
expect(mockProcessExit).toHaveBeenCalledWith(1);
187-
mockProcessExit.mockRestore();
188103
});
189104
});

0 commit comments

Comments
 (0)